mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-03 05:43:24 +00:00
Live-sync for reference label (#8016)
Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
parent
69384ad4a1
commit
2fe638297c
@ -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
|
||||
})
|
||||
|
@ -21,7 +21,7 @@ describe('dsl', () => {
|
||||
)
|
||||
)
|
||||
expect(jsonToHTML(doc)).toEqual(
|
||||
'<p>Hello, <span data-type="reference" data-id="123" data-objectclass="world" data-label="World">@World</span></p><p>Check out <a target="_blank" rel="noopener noreferrer" class="cursor-pointer" href="https://example.com"><u>this link</u></a>.</p>'
|
||||
'<p>Hello, <span data-type="reference" class="antiMention" data-id="123" data-objectclass="world" data-label="World">@World</span></p><p>Check out <a target="_blank" rel="noopener noreferrer" class="cursor-pointer" href="https://example.com"><u>this link</u></a>.</p>'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -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<Doc>
|
||||
objectclass: Ref<Class<Doc>>
|
||||
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<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -28,23 +36,26 @@ export const ReferenceNode = Node.create<ReferenceOptions>({
|
||||
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<ReferenceOptions>({
|
||||
},
|
||||
|
||||
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 })
|
||||
}
|
||||
})
|
||||
|
@ -109,6 +109,7 @@ import {
|
||||
getAllDocumentStates,
|
||||
getControlledDocumentTitle,
|
||||
getDocumentMetaLinkFragment,
|
||||
getDocumentMetaTitle,
|
||||
getVisibleFilters,
|
||||
isFolder,
|
||||
renameFolder,
|
||||
@ -457,6 +458,7 @@ export default async (): Promise<Resources> => ({
|
||||
CanPrintDocument: canPrintDocument,
|
||||
DocumentIdentifierProvider: documentIdentifierProvider,
|
||||
ControlledDocumentTitleProvider: getControlledDocumentTitle,
|
||||
DocumentMetaTitleProvider: getDocumentMetaTitle,
|
||||
Comment: comment,
|
||||
IsCommentVisible: isCommentVisible
|
||||
},
|
||||
|
@ -246,6 +246,7 @@ export default mergeIds(documentsId, documents, {
|
||||
CanOpenDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
CanPrintDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
CanTransferDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
ControlledDocumentTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
|
||||
ControlledDocumentTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
|
||||
DocumentMetaTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
|
||||
}
|
||||
})
|
||||
|
@ -727,6 +727,18 @@ export async function getControlledDocumentTitle (
|
||||
return object.title
|
||||
}
|
||||
|
||||
export async function getDocumentMetaTitle (
|
||||
client: Client,
|
||||
ref: Ref<DocumentMeta>,
|
||||
doc?: DocumentMeta
|
||||
): Promise<string> {
|
||||
const object = doc ?? (await client.findOne(documents.class.DocumentMeta, { _id: ref }))
|
||||
|
||||
if (object === undefined) return ''
|
||||
|
||||
return object.title
|
||||
}
|
||||
|
||||
export const getCurrentEmployee = (): Ref<Employee> | undefined => {
|
||||
const currentAccount = getCurrentAccount()
|
||||
const person = (currentAccount as PersonAccount)?.person
|
||||
|
@ -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<string, any>
|
||||
renderLabel: (props: { options: CompletionOptions, node: any }) => string
|
||||
suggestion: Omit<SuggestionOptions, 'editor'>
|
||||
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<CompletionOptions>({
|
||||
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)
|
||||
]
|
||||
}
|
||||
})
|
@ -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 })
|
||||
}
|
||||
|
@ -15,23 +15,13 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { SearchResultDoc } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import presentation, {
|
||||
SearchResult,
|
||||
getClient,
|
||||
reduceCalls,
|
||||
searchFor,
|
||||
type SearchItem
|
||||
} from '@hcengineering/presentation'
|
||||
import presentation, { SearchResult, reduceCalls, searchFor, type SearchItem } from '@hcengineering/presentation'
|
||||
import { Label, ListView, resizeObserver } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getReferenceLabel } from './extension/reference'
|
||||
|
||||
export let query: string = ''
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
let items: SearchItem[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -40,21 +30,11 @@
|
||||
let scrollContainer: HTMLElement
|
||||
let selection = 0
|
||||
|
||||
async function getIdentifier (item: SearchResultDoc): Promise<string | undefined> {
|
||||
const identifierProvider = hierarchy.classHierarchyMixin(item.doc._class, view.mixin.ObjectIdentifier)
|
||||
if (identifierProvider === undefined) {
|
||||
return item.shortTitle ?? item.title
|
||||
}
|
||||
|
||||
const resource = await getResource(identifierProvider.provider)
|
||||
return await resource(client, item.id)
|
||||
}
|
||||
|
||||
async function handleSelectItem (item: SearchResultDoc): Promise<void> {
|
||||
const identifier = await getIdentifier(item)
|
||||
const label = await getReferenceLabel(item.doc._class, item.doc._id)
|
||||
dispatch('close', {
|
||||
id: item.id,
|
||||
label: identifier,
|
||||
label,
|
||||
objectclass: item.doc._class
|
||||
})
|
||||
}
|
||||
|
@ -16,28 +16,27 @@
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { Asset, IntlString } from '@hcengineering/platform'
|
||||
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
|
||||
import textEditor, { RefAction, TextEditorHandler } from '@hcengineering/text-editor'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
Button,
|
||||
ButtonKind,
|
||||
handler,
|
||||
registerFocus,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
checkAdaptiveMatching,
|
||||
IconClose
|
||||
deviceOptionsStore as deviceInfo,
|
||||
handler,
|
||||
IconClose,
|
||||
registerFocus
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { FocusPosition } from '@tiptap/core'
|
||||
import { EditorView } from '@tiptap/pm/view'
|
||||
import textEditor, { RefAction, TextEditorHandler } from '@hcengineering/text-editor'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import { Completion } from '../Completion'
|
||||
import view from '@hcengineering/view'
|
||||
import TextEditor from './TextEditor.svelte'
|
||||
import { defaultRefActions, getModelRefActions } from './editor/actions'
|
||||
import { completionConfig } from './extensions'
|
||||
import { EmojiExtension } from './extension/emoji'
|
||||
import { IsEmptyContentExtension } from './extension/isEmptyContent'
|
||||
import view from '@hcengineering/view'
|
||||
import { referenceConfig, ReferenceExtension } from './extension/reference'
|
||||
import Send from './icons/Send.svelte'
|
||||
|
||||
export let content: Markup = EmptyMarkup
|
||||
@ -141,8 +140,8 @@
|
||||
focusManager?.setFocus(idx)
|
||||
}
|
||||
}
|
||||
const completionPlugin = Completion.configure({
|
||||
...completionConfig,
|
||||
const completionPlugin = ReferenceExtension.configure({
|
||||
...referenceConfig,
|
||||
showDoc (event: MouseEvent, _id: string, _class: string) {
|
||||
dispatch('open-document', { event, _id, _class })
|
||||
}
|
||||
|
@ -2,14 +2,13 @@
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { EmptyMarkup } from '@hcengineering/text'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
import { ButtonSize, Label } from '@hcengineering/ui'
|
||||
import { AnyExtension } from '@tiptap/core'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
|
||||
import { referenceConfig, ReferenceExtension } from './extension/reference'
|
||||
import StyledTextEditor from './StyledTextEditor.svelte'
|
||||
import { Completion } from '../Completion'
|
||||
import { completionConfig } from './extensions'
|
||||
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let content: Markup | undefined
|
||||
@ -53,8 +52,8 @@
|
||||
}
|
||||
|
||||
function configureExtensions (): AnyExtension[] {
|
||||
const completionPlugin = Completion.configure({
|
||||
...completionConfig,
|
||||
const completionPlugin = ReferenceExtension.configure({
|
||||
...referenceConfig,
|
||||
showDoc (event: MouseEvent, _id: string, _class: string) {
|
||||
dispatch('open-document', { event, _id, _class })
|
||||
}
|
||||
|
@ -21,7 +21,6 @@
|
||||
import type { AnyExtension } from '@tiptap/core'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import { Completion } from '../Completion'
|
||||
import StyledTextEditor from './StyledTextEditor.svelte'
|
||||
|
||||
import { addTableHandler } from '../utils'
|
||||
@ -29,8 +28,9 @@
|
||||
import { FocusExtension } from './extension/focus'
|
||||
import { ImageUploadExtension } from './extension/imageUploadExt'
|
||||
import { InlineCommandsExtension } from './extension/inlineCommands'
|
||||
import { ReferenceExtension, referenceConfig } from './extension/reference'
|
||||
import { type FileAttachFunction } from './extension/types'
|
||||
import { completionConfig, InlineCommandId, inlineCommandsConfig } from './extensions'
|
||||
import { InlineCommandId, inlineCommandsConfig } from './extensions'
|
||||
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let content: Markup
|
||||
@ -179,8 +179,8 @@
|
||||
getFileUrl
|
||||
})
|
||||
|
||||
const completionPlugin = Completion.configure({
|
||||
...completionConfig,
|
||||
const completionPlugin = ReferenceExtension.configure({
|
||||
...referenceConfig,
|
||||
showDoc (event: MouseEvent, _id: string, _class: string) {
|
||||
dispatch('open-document', { event, _id, _class })
|
||||
}
|
||||
|
@ -13,14 +13,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { getMetadata, getResource } from '@hcengineering/platform'
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import { parseLocation, showPopup } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import workbench, { type Application } from '@hcengineering/workbench'
|
||||
import { type Editor, Extension } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { showPopup } from '@hcengineering/ui'
|
||||
import { Extension } from '@tiptap/core'
|
||||
import LinkPopup from '../LinkPopup.svelte'
|
||||
|
||||
export const LinkUtilsExtension = Extension.create<any>({
|
||||
@ -48,153 +42,6 @@ export const LinkUtilsExtension = Extension.create<any>({
|
||||
},
|
||||
|
||||
addProseMirrorPlugins () {
|
||||
return [ResolveReferenceUrlsPlugin(this.editor)]
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
export interface LinkToReferencePluginState {
|
||||
references: Map<string, { id: Ref<Doc>, objectclass: Ref<Class<Doc>>, label: string }>
|
||||
queue: Set<string>
|
||||
}
|
||||
|
||||
const resolveReferencePluginKey = new PluginKey<LinkToReferencePluginState>('linkToReference')
|
||||
|
||||
interface ReferenceProps {
|
||||
id: Ref<Doc>
|
||||
objectclass: Ref<Class<Doc>>
|
||||
label: string
|
||||
}
|
||||
|
||||
export function ResolveReferenceUrlsPlugin (editor: Editor): Plugin<LinkToReferencePluginState> {
|
||||
return new Plugin<LinkToReferencePluginState>({
|
||||
key: resolveReferencePluginKey,
|
||||
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
if (transactions[0]?.getMeta('linkToReference') === undefined) return undefined
|
||||
if (editor.schema.nodes.reference === undefined) return
|
||||
|
||||
const references = resolveReferencePluginKey.getState(newState)?.references ?? new Map()
|
||||
|
||||
const { tr } = newState
|
||||
tr.doc.descendants((node, pos) => {
|
||||
if (!node.isText || !node.marks.some((m) => m.type.name === 'link')) return
|
||||
|
||||
const url = node.textContent
|
||||
const mapping = references.get(url)
|
||||
if (mapping === undefined) return
|
||||
|
||||
const replacementNode = editor.schema.nodes.reference.create(mapping)
|
||||
const mpos = tr.mapping.map(pos)
|
||||
tr.replaceWith(mpos, mpos + node.nodeSize, replacementNode)
|
||||
})
|
||||
|
||||
if (tr.steps.length > 0) return tr
|
||||
},
|
||||
|
||||
state: {
|
||||
init () {
|
||||
return {
|
||||
references: new Map(),
|
||||
queue: new Set()
|
||||
}
|
||||
},
|
||||
apply (tr, prev, oldState, newState) {
|
||||
if (tr.getMeta('linkToReference') !== undefined) {
|
||||
const references = tr.getMeta('linkToReference').references as LinkToReferencePluginState['references']
|
||||
const urls = new Set(references.keys())
|
||||
return {
|
||||
queue: new Set(Array.from(prev.queue).filter((url) => !urls.has(url))),
|
||||
references: new Map([...prev.references, ...references])
|
||||
}
|
||||
}
|
||||
|
||||
if (!tr.docChanged || oldState.doc.eq(newState.doc)) return prev
|
||||
|
||||
const urls: string[] = []
|
||||
tr.doc.descendants((node) => {
|
||||
if (!node.isText || !node.marks.some((m) => m.type.name === 'link')) return
|
||||
const url = node.textContent
|
||||
|
||||
const hasNoMapping = prev.references.has(url) && prev.references.get(url) === undefined
|
||||
if (prev.queue.has(url) || hasNoMapping) return
|
||||
|
||||
urls.push(url)
|
||||
})
|
||||
|
||||
const promises = urls.map(async (url) => {
|
||||
try {
|
||||
return [url, await getReferenceFromUrl(url)] as const
|
||||
} catch {
|
||||
return [url, undefined] as const
|
||||
}
|
||||
})
|
||||
|
||||
if (promises.length > 0) {
|
||||
void Promise.all(promises).then((references) => {
|
||||
editor.view.dispatch(editor.state.tr.setMeta('linkToReference', { references: new Map(references) }))
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
references: prev.references,
|
||||
queue: new Set([...prev.queue, ...urls])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function getReferenceFromUrl (text: string): Promise<ReferenceProps | undefined> {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const url = new URL(text)
|
||||
|
||||
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
|
||||
if (url.origin !== frontUrl) return
|
||||
|
||||
const location = parseLocation(url)
|
||||
|
||||
const appAlias = (location.path[2] ?? '').trim()
|
||||
if (!(appAlias.length > 0)) return
|
||||
|
||||
const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
|
||||
const apps: Application[] = client
|
||||
.getModel()
|
||||
.findAllSync<Application>(workbench.class.Application, { hidden: false, _id: { $nin: excludedApps } })
|
||||
|
||||
const app = apps.find((p) => p.alias === appAlias)
|
||||
|
||||
if (app?.locationResolver === undefined) return
|
||||
const locationResolverFn = await getResource(app.locationResolver)
|
||||
const resolvedLocation = await locationResolverFn(location)
|
||||
|
||||
const locationParts = decodeURIComponent(resolvedLocation?.loc?.fragment ?? '').split('|')
|
||||
const id = locationParts[1] as Ref<Doc>
|
||||
const objectclass = locationParts[2] as Ref<Class<Doc>>
|
||||
if (id === undefined || objectclass === undefined) return
|
||||
|
||||
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
|
||||
const linkProvider = linkProviders.find(({ _id }) => hierarchy.isDerived(objectclass, _id))
|
||||
const _id: Ref<Doc> | undefined =
|
||||
linkProvider !== undefined ? (await (await getResource(linkProvider.decode))(id)) ?? id : id
|
||||
|
||||
let label = ''
|
||||
const labelProvider = hierarchy.classHierarchyMixin(objectclass, view.mixin.ObjectIdentifier)
|
||||
if (labelProvider !== undefined) {
|
||||
const resource = await getResource(labelProvider.provider)
|
||||
label = await resource(client, _id)
|
||||
} else {
|
||||
const titleMixin = hierarchy.classHierarchyMixin(objectclass, view.mixin.ObjectTitle)
|
||||
if (titleMixin === undefined) return
|
||||
|
||||
const titleProviderFn = await getResource(titleMixin.titleProvider)
|
||||
label = await titleProviderFn(client, _id)
|
||||
}
|
||||
|
||||
return {
|
||||
id: _id,
|
||||
objectclass,
|
||||
label
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,374 @@
|
||||
import { mergeAttributes, type Editor } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import MentionList from '../MentionList.svelte'
|
||||
import { SvelteRenderer } from '../node-view'
|
||||
|
||||
import { ReferenceNode, type ReferenceNodeProps, type ReferenceOptions } from '@hcengineering/text'
|
||||
import Suggestion, { type SuggestionKeyDownProps, type SuggestionOptions, type SuggestionProps } from './suggestion'
|
||||
|
||||
import { type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { getMetadata, getResource } from '@hcengineering/platform'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { parseLocation } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import workbench, { type Application } from '@hcengineering/workbench'
|
||||
|
||||
export interface ReferenceExtensionOptions extends ReferenceOptions {
|
||||
suggestion: Omit<SuggestionOptions, 'editor'>
|
||||
showDoc?: (event: MouseEvent, _id: string, _class: string) => void
|
||||
}
|
||||
|
||||
export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions>({
|
||||
addOptions () {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
renderLabel ({ options, props }) {
|
||||
// eslint-disable-next-line
|
||||
return `${options.suggestion.char}${props.label ?? props.id}`
|
||||
},
|
||||
suggestion: {
|
||||
char: '@',
|
||||
allowSpaces: true,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addNodeView () {
|
||||
return ({ node, HTMLAttributes }) => {
|
||||
const span = document.createElement('span')
|
||||
span.setAttribute('data-type', this.name)
|
||||
span.className = 'antimention'
|
||||
const attributes = mergeAttributes(
|
||||
{
|
||||
'data-type': this.name,
|
||||
class: 'antiMention'
|
||||
},
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes
|
||||
)
|
||||
|
||||
span.addEventListener('click', (event) => {
|
||||
if (event.button !== 0) return
|
||||
|
||||
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) {
|
||||
this.options.showDoc?.(event, _id, _class)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.entries(attributes).forEach(([key, value]) => {
|
||||
span.setAttribute(key, value)
|
||||
})
|
||||
|
||||
const query = createQuery(true)
|
||||
const options = this.options
|
||||
|
||||
const renderLabel = (props: ReferenceNodeProps): void => {
|
||||
span.setAttribute('data-label', props.label)
|
||||
span.innerText = options.renderLabel({ options, props: props ?? node.attrs })
|
||||
}
|
||||
|
||||
const id = node.attrs.id
|
||||
const objectclass: Ref<Class<Doc>> = node.attrs.objectclass
|
||||
|
||||
renderLabel({ id, objectclass, label: node.attrs.label })
|
||||
|
||||
if (id !== undefined && objectclass !== undefined) {
|
||||
query.query(objectclass, { _id: id }, async (result) => {
|
||||
const obj = result[0]
|
||||
if (obj === undefined) return
|
||||
|
||||
const label = await getReferenceLabel(objectclass, id, obj)
|
||||
if (label === '') return
|
||||
|
||||
renderLabel({ id, objectclass, label })
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
dom: span,
|
||||
update (node, decorations) {
|
||||
renderLabel({ id, objectclass, label: node.attrs.label })
|
||||
return true
|
||||
},
|
||||
destroy () {
|
||||
query.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
}),
|
||||
// ReferenceClickHandler(this.options),
|
||||
ResolveReferenceUrlsPlugin(this.editor)
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const referenceConfig: Partial<ReferenceExtensionOptions> = {
|
||||
HTMLAttributes: {
|
||||
class: 'reference'
|
||||
},
|
||||
suggestion: {
|
||||
items: async () => {
|
||||
return []
|
||||
},
|
||||
render: () => {
|
||||
let component: any
|
||||
|
||||
return {
|
||||
onStart: (props: SuggestionProps) => {
|
||||
component = new SvelteRenderer(MentionList, {
|
||||
element: document.body,
|
||||
props: {
|
||||
...props,
|
||||
close: () => {
|
||||
component?.destroy()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
onUpdate (props: SuggestionProps) {
|
||||
component?.updateProps(props)
|
||||
},
|
||||
onKeyDown (props: SuggestionKeyDownProps) {
|
||||
if (props.event.key === 'Escape') {
|
||||
props.event.stopPropagation()
|
||||
}
|
||||
return component?.onKeyDown(props)
|
||||
},
|
||||
onExit () {
|
||||
component?.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResolveReferenceUrlsPluginState {
|
||||
references: Map<string, { id: Ref<Doc>, objectclass: Ref<Class<Doc>>, label: string }>
|
||||
queue: Set<string>
|
||||
}
|
||||
|
||||
const resolveReferencePluginKey = new PluginKey<ResolveReferenceUrlsPluginState>('linkToReference')
|
||||
|
||||
export function ResolveReferenceUrlsPlugin (editor: Editor): Plugin<ResolveReferenceUrlsPluginState> {
|
||||
return new Plugin<ResolveReferenceUrlsPluginState>({
|
||||
key: resolveReferencePluginKey,
|
||||
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
if (transactions[0]?.getMeta('linkToReference') === undefined) return undefined
|
||||
if (editor.schema.nodes.reference === undefined) return
|
||||
|
||||
const references = resolveReferencePluginKey.getState(newState)?.references ?? new Map()
|
||||
|
||||
const { tr } = newState
|
||||
tr.doc.descendants((node, pos) => {
|
||||
if (!node.isText || !node.marks.some((m) => m.type.name === 'link')) return
|
||||
|
||||
const url = node.textContent
|
||||
const mapping = references.get(url)
|
||||
if (mapping === undefined) return
|
||||
|
||||
const replacementNode = editor.schema.nodes.reference.create(mapping)
|
||||
const mpos = tr.mapping.map(pos)
|
||||
tr.replaceWith(mpos, mpos + node.nodeSize, replacementNode)
|
||||
})
|
||||
|
||||
if (tr.steps.length > 0) return tr
|
||||
},
|
||||
|
||||
state: {
|
||||
init () {
|
||||
return {
|
||||
references: new Map(),
|
||||
queue: new Set()
|
||||
}
|
||||
},
|
||||
apply (tr, prev, oldState, newState) {
|
||||
if (tr.getMeta('linkToReference') !== undefined) {
|
||||
const references = tr.getMeta('linkToReference').references as ResolveReferenceUrlsPluginState['references']
|
||||
const urls = new Set(references.keys())
|
||||
return {
|
||||
queue: new Set(Array.from(prev.queue).filter((url) => !urls.has(url))),
|
||||
references: new Map([...prev.references, ...references])
|
||||
}
|
||||
}
|
||||
|
||||
if (!tr.docChanged || oldState.doc.eq(newState.doc)) return prev
|
||||
|
||||
const urls: string[] = []
|
||||
tr.doc.descendants((node) => {
|
||||
if (!node.isText || !node.marks.some((m) => m.type.name === 'link')) return
|
||||
const url = node.textContent
|
||||
|
||||
const hasNoMapping = prev.references.has(url) && prev.references.get(url) === undefined
|
||||
if (prev.queue.has(url) || hasNoMapping) return
|
||||
|
||||
urls.push(url)
|
||||
})
|
||||
|
||||
const promises = urls.map(async (url) => {
|
||||
try {
|
||||
return [url, await getReferenceFromUrl(url)] as const
|
||||
} catch {
|
||||
return [url, undefined] as const
|
||||
}
|
||||
})
|
||||
|
||||
if (promises.length > 0) {
|
||||
void Promise.all(promises).then((references) => {
|
||||
editor.view.dispatch(editor.state.tr.setMeta('linkToReference', { references: new Map(references) }))
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
references: prev.references,
|
||||
queue: new Set([...prev.queue, ...urls])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function getReferenceLabel<T extends Doc> (
|
||||
objectclass: Ref<Class<T>>,
|
||||
id: Ref<T>,
|
||||
doc?: T
|
||||
): Promise<string> {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const labelProvider = hierarchy.classHierarchyMixin(objectclass as Ref<Class<Doc>>, view.mixin.ObjectIdentifier)
|
||||
const labelProviderFn = labelProvider !== undefined ? await getResource(labelProvider.provider) : undefined
|
||||
|
||||
const titleMixin = hierarchy.classHierarchyMixin(objectclass as Ref<Class<Doc>>, view.mixin.ObjectTitle)
|
||||
const titleProviderFn = titleMixin !== undefined ? await getResource(titleMixin.titleProvider) : undefined
|
||||
|
||||
const identifier = (await labelProviderFn?.(client, id, doc)) ?? ''
|
||||
const title = (await titleProviderFn?.(client, id, doc)) ?? ''
|
||||
|
||||
const label = identifier !== '' && title !== '' && identifier !== title ? `${identifier} ${title}` : title ?? ''
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
export async function getReferenceFromUrl (text: string): Promise<ReferenceNodeProps | undefined> {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const url = new URL(text)
|
||||
|
||||
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
|
||||
if (url.origin !== frontUrl) return
|
||||
|
||||
const location = parseLocation(url)
|
||||
|
||||
const appAlias = (location.path[2] ?? '').trim()
|
||||
if (!(appAlias.length > 0)) return
|
||||
|
||||
const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
|
||||
const apps: Application[] = client
|
||||
.getModel()
|
||||
.findAllSync<Application>(workbench.class.Application, { hidden: false, _id: { $nin: excludedApps } })
|
||||
|
||||
const app = apps.find((p) => p.alias === appAlias)
|
||||
|
||||
if (app?.locationResolver === undefined) return
|
||||
const locationResolverFn = await getResource(app.locationResolver)
|
||||
const resolvedLocation = await locationResolverFn(location)
|
||||
|
||||
const locationParts = decodeURIComponent(resolvedLocation?.loc?.fragment ?? '').split('|')
|
||||
const id = locationParts[1] as Ref<Doc>
|
||||
const objectclass = locationParts[2] as Ref<Class<Doc>>
|
||||
if (id === undefined || objectclass === undefined) return
|
||||
|
||||
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
|
||||
const linkProvider = linkProviders.find(({ _id }) => hierarchy.isDerived(objectclass, _id))
|
||||
const _id: Ref<Doc> | undefined =
|
||||
linkProvider !== undefined ? (await (await getResource(linkProvider.decode))(id)) ?? id : id
|
||||
|
||||
const label = await getReferenceLabel(objectclass, id)
|
||||
if (label === '') return
|
||||
|
||||
return {
|
||||
id: _id,
|
||||
objectclass,
|
||||
label
|
||||
}
|
||||
}
|
@ -13,16 +13,15 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import view from '@hcengineering/view'
|
||||
import { type Editor, type Range } from '@tiptap/core'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
import { IconScribble } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { type Editor, type Range } from '@tiptap/core'
|
||||
|
||||
import { type CompletionOptions } from '../Completion'
|
||||
import MentionList from './MentionList.svelte'
|
||||
import { SvelteRenderer } from './node-view'
|
||||
import { type InlineCommandsOptions } from './extension/inlineCommands'
|
||||
import type { SuggestionKeyDownProps, SuggestionProps } from './extension/suggestion'
|
||||
import InlineCommandsList from './InlineCommandsList.svelte'
|
||||
import { SvelteRenderer } from './node-view'
|
||||
|
||||
export const mInsertTable = [
|
||||
{
|
||||
@ -87,49 +86,6 @@ export const mInsertTable = [
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const completionConfig: Partial<CompletionOptions> = {
|
||||
HTMLAttributes: {
|
||||
class: 'reference'
|
||||
},
|
||||
suggestion: {
|
||||
items: async () => {
|
||||
return []
|
||||
},
|
||||
render: () => {
|
||||
let component: any
|
||||
|
||||
return {
|
||||
onStart: (props: SuggestionProps) => {
|
||||
component = new SvelteRenderer(MentionList, {
|
||||
element: document.body,
|
||||
props: {
|
||||
...props,
|
||||
close: () => {
|
||||
component?.destroy()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
onUpdate (props: SuggestionProps) {
|
||||
component?.updateProps(props)
|
||||
},
|
||||
onKeyDown (props: SuggestionKeyDownProps) {
|
||||
if (props.event.key === 'Escape') {
|
||||
props.event.stopPropagation()
|
||||
}
|
||||
return component?.onKeyDown(props)
|
||||
},
|
||||
onExit () {
|
||||
component?.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const inlineCommandsIds = [
|
||||
'image',
|
||||
'table',
|
||||
@ -147,7 +103,7 @@ export type InlineCommandId = (typeof inlineCommandsIds)[number]
|
||||
export function inlineCommandsConfig (
|
||||
handleSelect: (id: string, pos: number, targetItem?: MouseEvent | HTMLElement) => Promise<void>,
|
||||
excludedCommands: InlineCommandId[] = []
|
||||
): Partial<CompletionOptions> {
|
||||
): Partial<InlineCommandsOptions> {
|
||||
return {
|
||||
suggestion: {
|
||||
items: () => {
|
||||
|
@ -19,7 +19,7 @@
|
||||
import { AnyComponent, LabelAndProps, themeStore, tooltip } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
import { getDocIdentifier } from '../utils'
|
||||
import { getReferenceLabel } from '@hcengineering/text-editor-resources/src/components/extension/reference'
|
||||
import DocNavLink from './DocNavLink.svelte'
|
||||
|
||||
export let _id: Ref<Doc> | undefined = undefined
|
||||
@ -91,7 +91,7 @@
|
||||
}
|
||||
|
||||
async function updateDocTitle (doc: Doc | undefined): Promise<void> {
|
||||
docTitle = doc ? await getDocIdentifier(client, doc._id, doc._class, doc) : undefined
|
||||
docTitle = doc ? await getReferenceLabel(doc._class, doc._id, doc) : undefined
|
||||
}
|
||||
|
||||
async function updateDocTooltip (doc?: Doc): Promise<void> {
|
||||
|
@ -194,7 +194,7 @@ test.describe('Documents tests', () => {
|
||||
await documentContentPage.checkLinkInTheText(contentLink, 'http://test/link/123456')
|
||||
})
|
||||
|
||||
test('Check Link to Reference conversion', async () => {
|
||||
test('Check Link to Reference conversion and sync', async () => {
|
||||
const sourceDocument: NewDocument = {
|
||||
title: `Reference Document Title-${generateId()}`,
|
||||
space: 'Default'
|
||||
@ -204,23 +204,43 @@ test.describe('Documents tests', () => {
|
||||
space: 'Default'
|
||||
}
|
||||
|
||||
await leftSideMenuPage.clickDocuments()
|
||||
await test.step('Create two documents and reference one from the other', async () => {
|
||||
await leftSideMenuPage.clickDocuments()
|
||||
|
||||
await documentsPage.clickOnButtonCreateDocument()
|
||||
await documentsPage.createDocument(targetDocument)
|
||||
await documentsPage.openDocument(targetDocument.title)
|
||||
await documentContentPage.checkDocumentTitle(targetDocument.title)
|
||||
const targetDocumentUrl = documentsPage.page.url()
|
||||
await documentsPage.clickOnButtonCreateDocument()
|
||||
await documentsPage.createDocument(targetDocument)
|
||||
await documentsPage.openDocument(targetDocument.title)
|
||||
await documentContentPage.checkDocumentTitle(targetDocument.title)
|
||||
const targetDocumentUrl = documentsPage.page.url()
|
||||
|
||||
await documentsPage.clickOnButtonCreateDocument()
|
||||
await documentsPage.createDocument(sourceDocument)
|
||||
await documentsPage.openDocument(sourceDocument.title)
|
||||
await documentContentPage.checkDocumentTitle(sourceDocument.title)
|
||||
await documentsPage.clickOnButtonCreateDocument()
|
||||
await documentsPage.createDocument(sourceDocument)
|
||||
await documentsPage.openDocument(sourceDocument.title)
|
||||
await documentContentPage.checkDocumentTitle(sourceDocument.title)
|
||||
|
||||
await documentContentPage.addRandomLines(5)
|
||||
await documentContentPage.addContentToTheNewLine(targetDocumentUrl)
|
||||
await documentContentPage.addRandomLines(5)
|
||||
await documentContentPage.checkReferenceInTheText(targetDocument.title)
|
||||
await documentContentPage.addRandomLines(5)
|
||||
await documentContentPage.addContentToTheNewLine(targetDocumentUrl)
|
||||
await documentContentPage.addRandomLines(5)
|
||||
await documentContentPage.checkReferenceInTheText(targetDocument.title)
|
||||
|
||||
await leftSideMenuPage.clickDocuments()
|
||||
})
|
||||
|
||||
await test.step('Rename the document and check sync', async () => {
|
||||
await leftSideMenuPage.clickDocuments()
|
||||
|
||||
const targetDocumentSecondTitle = `Updated Reference Document Title-${generateId()}`
|
||||
|
||||
await documentsPage.openDocument(targetDocument.title)
|
||||
await documentContentPage.checkDocumentTitle(targetDocument.title)
|
||||
await documentContentPage.updateDocumentTitle(targetDocumentSecondTitle)
|
||||
await documentContentPage.checkDocumentTitle(targetDocumentSecondTitle)
|
||||
|
||||
await documentsPage.openDocument(sourceDocument.title)
|
||||
await documentContentPage.checkDocumentTitle(sourceDocument.title)
|
||||
|
||||
await documentContentPage.checkReferenceInTheText(targetDocumentSecondTitle)
|
||||
})
|
||||
})
|
||||
|
||||
test('Locked document and checking URL', async ({ page, context }) => {
|
||||
|
Loading…
Reference in New Issue
Block a user