Merge branch 'develop' of https://github.com/hcengineering/platform into staging-new

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artem Savchenko
2026-04-20 13:10:34 +07:00
11 changed files with 150 additions and 56 deletions
+3
View File
@@ -9666,6 +9666,9 @@ importers:
'@hcengineering/model-core':
specifier: workspace:^0.7.0
version: link:../core
'@hcengineering/model-print':
specifier: workspace:^0.7.0
version: link:../print
'@hcengineering/model-tracker':
specifier: workspace:^0.7.0
version: link:../tracker
@@ -22,6 +22,7 @@ import {
isEmptyMarkup,
isEmptyNode,
jsonToMarkup,
type MarkupMark,
MarkupMarkType,
MarkupNode,
MarkupNodeType,
@@ -300,6 +301,32 @@ describe('jsonToPmNode', () => {
expect(node.content.child(0).type.name).toEqual('text')
expect(node.content.child(0).text).toEqual('Hello, world!')
})
it('ServerKit schema includes node-uuid mark and parses text carrying it (QMS anchors)', () => {
const schema = getSchema(extensions)
expect(schema.marks['node-uuid']).toBeDefined()
const json: MarkupNode = {
type: MarkupNodeType.doc,
content: [
{
type: MarkupNodeType.paragraph,
attrs: { textAlign: null as any },
content: [
{
type: MarkupNodeType.text,
text: 'hello',
marks: [{ type: 'node-uuid' } as unknown as MarkupMark]
}
]
}
]
}
const node = jsonToPmNode(json, schema)
expect(node.type.name).toEqual('doc')
const para = node.content.child(0)
const text = para.content.child(0)
expect(text.marks.some((m) => m.type.name === 'node-uuid')).toBeTruthy()
})
})
describe('htmlToMarkup', () => {
+2 -2
View File
@@ -428,9 +428,9 @@ export default function buildModel (): Builder {
questionsId,
{
label: setting.string.Configure,
enabled: false,
enabled: true,
beta: false,
hidden: true,
hidden: false,
classFilter: defaultFilter
}
],
+1
View File
@@ -44,6 +44,7 @@
"@hcengineering/model-core": "workspace:^0.7.0",
"@hcengineering/model-tracker": "workspace:^0.7.0",
"@hcengineering/model-view": "workspace:^0.7.0",
"@hcengineering/model-print": "workspace:^0.7.0",
"@hcengineering/platform": "workspace:^0.7.20",
"@hcengineering/view": "workspace:^0.7.0"
}
+10 -1
View File
@@ -24,6 +24,7 @@ import { type Builder } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import tracker from '@hcengineering/model-tracker'
import view, { createAction } from '@hcengineering/model-view'
import print from '@hcengineering/model-print'
import {
TAnswer,
TAssessment,
@@ -215,7 +216,15 @@ function defineActions (builder: Builder): void {
)
builder.mixin(questions.class.Question, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Open, view.action.OpenInNewTab, view.action.Delete, tracker.action.NewRelatedIssue]
actions: [
view.action.Open,
view.action.OpenInNewTab,
view.action.Delete,
tracker.action.NewRelatedIssue,
view.action.CopyLink,
view.action.AddRelation,
print.action.Print
]
})
createAction(
+20 -16
View File
@@ -890,24 +890,28 @@ export function createModel (builder: Builder): void {
view.action.Join
)
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: view.component.AddRelationPopup,
fillProps: {
_objects: 'value'
createAction(
builder,
{
action: view.actionImpl.ShowPopup as ViewAction<Record<string, any>>,
actionProps: {
component: view.component.AddRelationPopup,
fillProps: {
_objects: 'value'
}
},
label: core.string.AddRelation,
input: 'any',
icon: view.icon.CopyLink,
category: view.category.Editor,
target: core.class.Doc,
context: {
mode: ['context', 'browser'],
group: 'associate'
}
},
label: core.string.AddRelation,
input: 'any',
icon: view.icon.CopyLink,
category: view.category.Editor,
target: core.class.Doc,
context: {
mode: ['context', 'browser'],
group: 'associate'
}
})
view.action.AddRelation
)
createAction(
builder,
@@ -134,10 +134,3 @@
{/await}
{/if}
</form>
<style lang="scss">
.actions {
height: 0.8rem;
overflow: visible;
}
</style>
@@ -15,8 +15,11 @@
//
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { Class, Doc, Ref } from '@hcengineering/core'
import { jsonToPmNode, MarkupNode } from '@hcengineering/text'
import presentation from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import { Editor, Extension, mergeAttributes } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { DecorationSet } from '@tiptap/pm/view'
@@ -31,21 +34,29 @@
export let objectClass: Ref<Class<Doc>> | undefined = undefined
let element: HTMLElement
let editor: Editor
let editor: Editor | undefined
let _decoration = DecorationSet.empty
let oldContent: MarkupNode | undefined
let decorateFailed = false
let editorMountFailed = false
function updateEditor (editor: Editor, comparedVersion?: MarkupNode): void {
if (comparedVersion === undefined) {
return
}
const node = jsonToPmNode(comparedVersion, editor.schema)
const r = calculateDecorations(editor, oldContent, node)
if (r !== undefined) {
oldContent = r.oldContent
_decoration = r.decorations
try {
const node = jsonToPmNode(comparedVersion, editor.schema)
const r = calculateDecorations(editor, oldContent, node)
if (r !== undefined) {
oldContent = r.oldContent
_decoration = r.decorations
}
decorateFailed = false
} catch (err: unknown) {
decorateFailed = true
Analytics.handleError(err instanceof Error ? err : new Error(String(err)))
}
}
@@ -77,33 +88,66 @@
}
onMount(async () => {
const kit = await getEditorKit({
objectClass,
commentNode: true
})
try {
const kit = await getEditorKit({
objectClass,
commentNode: true,
qms: {
qmsInlineComment: {
isHighlightModeOn: () => false,
getNodeHighlight: () => null
}
}
})
editor = new Editor({
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, { class: 'flex-grow' }) },
element,
content,
editable: false,
extensions: [kit, DecorationExtension],
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor
}
})
editor = new Editor({
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, { class: 'flex-grow' }) },
element,
content,
editable: false,
extensions: [kit, DecorationExtension],
onContentError: ({ error, disableCollaboration }) => {
disableCollaboration()
Analytics.handleError(error)
editorMountFailed = true
},
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor
}
})
} catch (err: unknown) {
editorMountFailed = true
Analytics.handleError(err instanceof Error ? err : new Error(String(err)))
}
})
onDestroy(() => {
if (editor !== undefined) {
editor.destroy()
}
editor?.destroy()
editor = undefined
})
</script>
<div class="ref-container">
<div class="textInput">
<div class="textInput" hidden={editorMountFailed}>
<div class="select-text" style="width: 100%;" bind:this={element} />
</div>
{#if editorMountFailed}
<div class="error-fallback">
<Label label={presentation.string.FailedToPreview} />
</div>
{/if}
{#if decorateFailed && !editorMountFailed}
<div class="error-fallback">
<Label label={presentation.string.FailedToPreview} />
</div>
{/if}
</div>
<style lang="scss">
.error-fallback {
padding: 0.5rem 0;
color: var(--theme-caption-color);
font-size: 0.875rem;
}
</style>
+14 -2
View File
@@ -41,6 +41,18 @@ import view from './plugin'
import { type FocusSelection, type SelectionStore } from './selection'
import { restrictionStore } from './utils'
/**
* Collapses a single-element array to the element, otherwise returns the original value.
*
* @public
*/
export function normalizeActionContext<T extends Doc> (doc: T | T[] | undefined): T | T[] | undefined {
if (Array.isArray(doc) && doc.length === 1) {
return doc[0]
}
return doc
}
/**
* @public
*/
@@ -114,7 +126,7 @@ export async function filterAvailableActions (
} else {
const visibilityTester = await getResource(action.visibilityTester)
if (await visibilityTester(doc)) {
if (await visibilityTester(normalizeActionContext(doc))) {
result.push(action)
}
}
@@ -132,7 +144,7 @@ export async function invokeAction (
): Promise<void> {
const impl = await getResource(action.action)
Analytics.handleEvent(action.analyticsEvent ?? action._id)
await impl(Array.isArray(object) && object.length === 1 ? object[0] : object, evt, {
await impl(normalizeActionContext(object), evt, {
...action.actionProps,
...props
})
@@ -51,7 +51,7 @@
} from '@hcengineering/ui'
import { Action, ActionCategory, ViewContext } from '@hcengineering/view'
import { createEventDispatcher, onMount, tick } from 'svelte'
import { filterActions, getSelection } from '../actions'
import { filterActions, getSelection, normalizeActionContext } from '../actions'
import view from '../plugin'
import { focusStore, selectionStore } from '../selection'
import { openDoc } from '../utils'
@@ -99,7 +99,7 @@
} else {
const visibilityTester = await getResource(action.visibilityTester)
if (await visibilityTester(docs)) {
if (await visibilityTester(normalizeActionContext(docs))) {
resultActions.push(action)
}
}
+2 -1
View File
@@ -163,7 +163,8 @@ const view = plugin(viewId, {
RemoveRelation: '' as Ref<Action>,
CopyLink: '' as Ref<Action<Doc, any>>,
CopyDocumentMarkdown: '' as Ref<Action<Doc, any>>
CopyDocumentMarkdown: '' as Ref<Action<Doc, any>>,
AddRelation: '' as Ref<Action<Doc, any>>
},
viewlet: {
Table: '' as Ref<ViewletDescriptor>,