From 2fe638297c20eabb1dd4662e53a12d789c4af20a Mon Sep 17 00:00:00 2001 From: Victor Ilyushchenko Date: Mon, 17 Feb 2025 06:47:55 +0300 Subject: [PATCH] Live-sync for reference label (#8016) Signed-off-by: Victor Ilyushchenko --- models/controlled-documents/src/index.ts | 66 ++-- .../text/src/markup/__tests__/dsl.test.ts | 2 +- packages/text/src/nodes/reference.ts | 37 +- .../src/index.ts | 2 + .../src/plugin.ts | 3 +- .../src/utils.ts | 12 + .../text-editor-resources/src/Completion.ts | 179 --------- .../components/CollaborativeTextEditor.svelte | 8 +- .../src/components/MentionPopup.svelte | 28 +- .../src/components/ReferenceInput.svelte | 21 +- .../src/components/StyledTextArea.svelte | 9 +- .../src/components/StyledTextBox.svelte | 8 +- .../src/components/extension/link.ts | 159 +------- .../src/components/extension/reference.ts | 374 ++++++++++++++++++ .../src/components/extensions.ts | 54 +-- .../src/components/ObjectMention.svelte | 4 +- .../sanity/tests/documents/documents.spec.ts | 50 ++- 17 files changed, 523 insertions(+), 493 deletions(-) delete mode 100644 plugins/text-editor-resources/src/Completion.ts create mode 100644 plugins/text-editor-resources/src/components/extension/reference.ts diff --git a/models/controlled-documents/src/index.ts b/models/controlled-documents/src/index.ts index 0885da5aee..1b15efadc0 100644 --- a/models/controlled-documents/src/index.ts +++ b/models/controlled-documents/src/index.ts @@ -13,60 +13,60 @@ // limitations under the License. // -import documentsPlugin, { - documentsId, - type Document, - type DocumentSpace, - DocumentState -} from '@hcengineering/controlled-documents' import activity from '@hcengineering/activity' import contact from '@hcengineering/contact' +import documentsPlugin, { + documentsId, + DocumentState, + type Document, + type DocumentSpace +} from '@hcengineering/controlled-documents' import { type Builder } from '@hcengineering/model' import chunter from '@hcengineering/model-chunter' import core from '@hcengineering/model-core' +import { generateClassNotificationTypes } from '@hcengineering/model-notification' +import presentation from '@hcengineering/model-presentation' +import print from '@hcengineering/model-print' import request from '@hcengineering/model-request' import tracker from '@hcengineering/model-tracker' -import { generateClassNotificationTypes } from '@hcengineering/model-notification' import view, { classPresenter, createAction } from '@hcengineering/model-view' -import presentation from '@hcengineering/model-presentation' import workbench from '@hcengineering/model-workbench' import notification from '@hcengineering/notification' import setting from '@hcengineering/setting' import tags from '@hcengineering/tags' -import print from '@hcengineering/model-print' import textEditor from '@hcengineering/text-editor' -import documents from './plugin' +import { type Class, type Doc, type Ref } from '@hcengineering/core' +import { type Action } from '@hcengineering/view' import { definePermissions } from './permissions' +import documents from './plugin' +import { defineSpaceType } from './spaceType' import { + TChangeControl, + TControlledDocument, + TControlledDocumentSnapshot, + TDocument, + TDocumentApprovalRequest, + TDocumentCategory, + TDocumentComment, + TDocumentMeta, + TDocumentRequest, + TDocumentReviewRequest, + TDocumentSnapshot, TDocumentSpace, TDocumentSpaceType, TDocumentSpaceTypeDescriptor, - TExternalSpace, - TOrgSpace, - TDocumentMeta, - TProjectDocument, - TProjectMeta, - TProject, - TDocument, - TDocumentSnapshot, - TControlledDocumentSnapshot, - THierarchyDocument, TDocumentTemplate, TDocumentTraining, - TDocumentCategory, - TControlledDocument, - TChangeControl, - TDocumentRequest, - TDocumentReviewRequest, - TDocumentApprovalRequest, - TTypeDocumentState, + TExternalSpace, + THierarchyDocument, + TOrgSpace, + TProject, + TProjectDocument, + TProjectMeta, TTypeControlledDocumentState, - TDocumentComment + TTypeDocumentState } from './types' -import { defineSpaceType } from './spaceType' -import { type Class, type Doc, type Ref } from '@hcengineering/core' -import { type Action } from '@hcengineering/view' export { documentsId } from '@hcengineering/controlled-documents/src/index' export * from './types' @@ -105,6 +105,10 @@ export function createModel (builder: Builder): void { titleProvider: documents.function.ControlledDocumentTitleProvider }) + builder.mixin(documents.class.DocumentMeta, core.class.Class, view.mixin.ObjectTitle, { + titleProvider: documents.function.ControlledDocumentTitleProvider + }) + builder.mixin(documents.class.DocumentApprovalRequest, core.class.Class, view.mixin.ObjectPresenter, { presenter: documents.component.DocumentApprovalRequestPresenter }) diff --git a/packages/text/src/markup/__tests__/dsl.test.ts b/packages/text/src/markup/__tests__/dsl.test.ts index 2458a48b36..69762fa291 100644 --- a/packages/text/src/markup/__tests__/dsl.test.ts +++ b/packages/text/src/markup/__tests__/dsl.test.ts @@ -21,7 +21,7 @@ describe('dsl', () => { ) ) expect(jsonToHTML(doc)).toEqual( - '

Hello, @World

Check out this link.

' + '

Hello, @World

Check out this link.

' ) }) }) diff --git a/packages/text/src/nodes/reference.ts b/packages/text/src/nodes/reference.ts index 6e75cfcffc..c333371095 100644 --- a/packages/text/src/nodes/reference.ts +++ b/packages/text/src/nodes/reference.ts @@ -15,10 +15,18 @@ import { Node, mergeAttributes } from '@tiptap/core' import { getDataAttribute } from './utils' +import { Class, Doc, Ref } from '@hcengineering/core' + +export interface ReferenceNodeProps { + id: Ref + objectclass: Ref> + label: string +} export interface ReferenceOptions { - renderLabel: (props: { options: ReferenceOptions, node: any }) => string - suggestion: { char: string } + renderLabel: (props: { options: ReferenceOptions, props: ReferenceNodeProps }) => string + suggestion: { char?: string } + HTMLAttributes: Record } /** @@ -28,23 +36,26 @@ export const ReferenceNode = Node.create({ name: 'reference', group: 'inline', inline: true, + selectable: true, + atom: true, + draggable: true, addAttributes () { return { id: getDataAttribute('id'), objectclass: getDataAttribute('objectclass'), - label: getDataAttribute('label'), - class: { default: null } + label: getDataAttribute('label') } }, addOptions () { return { - renderLabel ({ options, node }) { + renderLabel ({ options, props }) { // eslint-disable-next-line - return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` + return `${options.suggestion.char}${props.label ?? props.id}` }, - suggestion: { char: '@' } + suggestion: { char: '@' }, + HTMLAttributes: {} } }, @@ -72,21 +83,25 @@ export const ReferenceNode = Node.create({ }, renderHTML ({ node, HTMLAttributes }) { - const options = this.options return [ 'span', mergeAttributes( { - 'data-type': this.name + 'data-type': this.name, + class: 'antiMention' }, + this.options.HTMLAttributes, HTMLAttributes ), - this.options.renderLabel({ options, node }) + this.options.renderLabel({ + options: this.options, + props: node.attrs as ReferenceNodeProps + }) ] }, renderText ({ node }) { const options = this.options - return options.renderLabel({ options, node }) + return options.renderLabel({ options, props: node.attrs as ReferenceNodeProps }) } }) diff --git a/plugins/controlled-documents-resources/src/index.ts b/plugins/controlled-documents-resources/src/index.ts index e0b32883c5..40d379be40 100644 --- a/plugins/controlled-documents-resources/src/index.ts +++ b/plugins/controlled-documents-resources/src/index.ts @@ -109,6 +109,7 @@ import { getAllDocumentStates, getControlledDocumentTitle, getDocumentMetaLinkFragment, + getDocumentMetaTitle, getVisibleFilters, isFolder, renameFolder, @@ -457,6 +458,7 @@ export default async (): Promise => ({ CanPrintDocument: canPrintDocument, DocumentIdentifierProvider: documentIdentifierProvider, ControlledDocumentTitleProvider: getControlledDocumentTitle, + DocumentMetaTitleProvider: getDocumentMetaTitle, Comment: comment, IsCommentVisible: isCommentVisible }, diff --git a/plugins/controlled-documents-resources/src/plugin.ts b/plugins/controlled-documents-resources/src/plugin.ts index 2c139b36c7..9e14aaa0cb 100644 --- a/plugins/controlled-documents-resources/src/plugin.ts +++ b/plugins/controlled-documents-resources/src/plugin.ts @@ -246,6 +246,7 @@ export default mergeIds(documentsId, documents, { CanOpenDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise>, CanPrintDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise>, CanTransferDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise>, - ControlledDocumentTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise> + ControlledDocumentTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise>, + DocumentMetaTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise> } }) diff --git a/plugins/controlled-documents-resources/src/utils.ts b/plugins/controlled-documents-resources/src/utils.ts index bc3122a6d7..d238e18a3b 100644 --- a/plugins/controlled-documents-resources/src/utils.ts +++ b/plugins/controlled-documents-resources/src/utils.ts @@ -727,6 +727,18 @@ export async function getControlledDocumentTitle ( return object.title } +export async function getDocumentMetaTitle ( + client: Client, + ref: Ref, + doc?: DocumentMeta +): Promise { + const object = doc ?? (await client.findOne(documents.class.DocumentMeta, { _id: ref })) + + if (object === undefined) return '' + + return object.title +} + export const getCurrentEmployee = (): Ref | undefined => { const currentAccount = getCurrentAccount() const person = (currentAccount as PersonAccount)?.person diff --git a/plugins/text-editor-resources/src/Completion.ts b/plugins/text-editor-resources/src/Completion.ts deleted file mode 100644 index 061c752f46..0000000000 --- a/plugins/text-editor-resources/src/Completion.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { Node, mergeAttributes } from '@tiptap/core' -import { Plugin, PluginKey } from '@tiptap/pm/state' -import { getDataAttribute } from './utils' - -import Suggestion, { type SuggestionOptions } from './components/extension/suggestion' - -export interface CompletionOptions { - HTMLAttributes: Record - renderLabel: (props: { options: CompletionOptions, node: any }) => string - suggestion: Omit - showDoc?: (event: MouseEvent, _id: string, _class: string) => void -} - -export function clickHandler (opt: CompletionOptions): Plugin { - return new Plugin({ - key: new PluginKey('completion-handleClickLink'), - props: { - handleClick: (view, pos, event) => { - if (event.button !== 0) { - return false - } - - const link = (event.target as HTMLElement)?.closest('span') - if (link != null) { - const _class = link.getAttribute('data-objectclass') - const _id = link.getAttribute('data-id') - if (_id != null && _class != null) { - opt.showDoc?.(event, _id, _class) - } - } - - return false - } - } - }) -} - -export const Completion = Node.create({ - name: 'reference', - - addOptions () { - return { - HTMLAttributes: {}, - renderLabel ({ options, node }) { - // eslint-disable-next-line - return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` - }, - suggestion: { - char: '@', - allowSpaces: true, - // pluginKey: CompletionPluginKey, - command: ({ editor, range, props }) => { - // increase range.to by one when the next node is of type "text" - // and starts with a space character - const nodeAfter = editor.view.state.selection.$to.nodeAfter - const overrideSpace = nodeAfter?.text?.startsWith(' ') - - if (overrideSpace !== undefined && overrideSpace) { - // eslint-disable-next-line - range.to += 1 - } - - if (props !== null) { - editor - .chain() - .focus() - .insertContentAt(range, [ - { - type: this.name, - attrs: props - }, - { - type: 'text', - text: ' ' - } - ]) - .run() - } - }, - allow: ({ editor, range }) => { - if (range.from > editor.state.doc.content.size) return false - const $from = editor.state.doc.resolve(range.from) - const type = editor.schema.nodes[this.name] - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - return !!$from.parent.type.contentMatch.matchType(type) - } - } - } - }, - - group: 'inline', - - inline: true, - - selectable: true, - - atom: true, - - draggable: true, - - addAttributes () { - return { - id: getDataAttribute('id'), - label: getDataAttribute('label'), - objectclass: getDataAttribute('objectclass') - } - }, - - parseHTML () { - return [ - { - tag: `span[data-type="${this.name}"]` - } - ] - }, - - renderHTML ({ node, HTMLAttributes }) { - return [ - 'span', - mergeAttributes( - { - 'data-type': this.name, - class: 'antiMention' - }, - this.options.HTMLAttributes, - HTMLAttributes - ), - this.options.renderLabel({ - options: this.options, - node - }) - ] - }, - - renderText ({ node }) { - return this.options.renderLabel({ - options: this.options, - node - }) - }, - - addKeyboardShortcuts () { - return { - Backspace: () => - this.editor.commands.command(({ tr, state }) => { - let isMention = false - const { selection } = state - const { empty, anchor } = selection - - if (!empty) { - return false - } - - state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => { - if (node.type.name === this.name) { - isMention = true - - // eslint-disable-next-line - tr.insertText(this.options.suggestion.char || '', pos, pos + node.nodeSize) - - return false - } - }) - - return isMention - }) - } - }, - - addProseMirrorPlugins () { - return [ - Suggestion({ - editor: this.editor, - ...this.options.suggestion - }), - clickHandler(this.options) - ] - } -}) diff --git a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte index f58f3936b5..b247fe201d 100644 --- a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte +++ b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte @@ -66,7 +66,6 @@ TextEditorHandler } from '@hcengineering/text-editor' import { EditorKitOptions, getEditorKit } from '../../src/kits/editor-kit' - import { Completion } from '../Completion' import { deleteAttachment } from '../command/deleteAttachment' import { textEditorCommandHandler } from '../commands' import { Provider } from '../provider/types' @@ -84,8 +83,9 @@ import { InlineCommentCollaborationExtension } from './extension/inlineComment' import { LeftMenuExtension } from './extension/leftMenu' import { mermaidOptions } from './extension/mermaid' + import { ReferenceExtension, referenceConfig } from './extension/reference' import { type FileAttachFunction } from './extension/types' - import { completionConfig, inlineCommandsConfig } from './extensions' + import { inlineCommandsConfig } from './extensions' export let object: Doc export let attribute: KeyedAttribute @@ -495,8 +495,8 @@ render: renderCursor, selectionRender: noSelectionRender }), - Completion.configure({ - ...completionConfig, + ReferenceExtension.configure({ + ...referenceConfig, showDoc (event: MouseEvent, _id: string, _class: string) { dispatch('open-document', { event, _id, _class }) } diff --git a/plugins/text-editor-resources/src/components/MentionPopup.svelte b/plugins/text-editor-resources/src/components/MentionPopup.svelte index 6b9632b418..c43b03241d 100644 --- a/plugins/text-editor-resources/src/components/MentionPopup.svelte +++ b/plugins/text-editor-resources/src/components/MentionPopup.svelte @@ -15,23 +15,13 @@ -->