diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index a4fcb384ed..70df5bc717 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -206,6 +206,7 @@ specifiers: '@rush-temp/templates-assets': file:./projects/templates-assets.tgz '@rush-temp/templates-resources': file:./projects/templates-resources.tgz '@rush-temp/tests-sanity': file:./projects/tests-sanity.tgz + '@rush-temp/text': file:./projects/text.tgz '@rush-temp/text-editor': file:./projects/text-editor.tgz '@rush-temp/theme': file:./projects/theme.tgz '@rush-temp/tool': file:./projects/tool.tgz @@ -249,6 +250,7 @@ specifiers: '@tiptap/extension-task-item': ^2.0.3 '@tiptap/extension-task-list': ^2.0.3 '@tiptap/extension-typography': ^2.0.3 + '@tiptap/html': ^2.0.3 '@tiptap/pm': ^2.0.3 '@tiptap/prosemirror-tables': ^1.1.4 '@tiptap/starter-kit': ^2.0.3 @@ -601,6 +603,7 @@ dependencies: '@rush-temp/templates-assets': file:projects/templates-assets.tgz '@rush-temp/templates-resources': file:projects/templates-resources.tgz_a1d864769aaf53d09b76fe134ab55e60 '@rush-temp/tests-sanity': file:projects/tests-sanity.tgz + '@rush-temp/text': file:projects/text.tgz_a7abb371f17d2bc77abec7bc53398db5 '@rush-temp/text-editor': file:projects/text-editor.tgz_1e646fd62a3adc9d0b41f256dafe3d38 '@rush-temp/theme': file:projects/theme.tgz_a1d864769aaf53d09b76fe134ab55e60 '@rush-temp/tool': file:projects/tool.tgz_bufferutil@4.0.7 @@ -644,6 +647,7 @@ dependencies: '@tiptap/extension-task-item': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607 '@tiptap/extension-task-list': 2.0.3_@tiptap+core@2.0.3 '@tiptap/extension-typography': 2.0.3_@tiptap+core@2.0.3 + '@tiptap/html': 2.1.6_fc2f730f0ef238c57ef10dc1abce4607 '@tiptap/pm': 2.0.3_@tiptap+core@2.0.3 '@tiptap/prosemirror-tables': 1.1.4_a7abb371f17d2bc77abec7bc53398db5 '@tiptap/starter-kit': 2.0.3_@tiptap+pm@2.0.3 @@ -6587,6 +6591,17 @@ packages: '@tiptap/core': 2.0.3_@tiptap+pm@2.0.3 dev: false + /@tiptap/html/2.1.6_fc2f730f0ef238c57ef10dc1abce4607: + resolution: {integrity: sha512-/7PcivMMty2Xw8oNEOqRi1W4WlD9afrmA3vlgNYGnO+knmwG5VzyhYV+oDlKAWvYqfrPxtfGnhNEQywzyHKbBg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + dependencies: + '@tiptap/core': 2.0.3_@tiptap+pm@2.0.3 + '@tiptap/pm': 2.0.3_@tiptap+core@2.0.3 + zeed-dom: 0.9.26 + dev: false + /@tiptap/pm/2.0.3_@tiptap+core@2.0.3: resolution: {integrity: sha512-I9dsInD89Agdm1QjFRO9dmJtU1ldVSILNPW0pEhv9wYqYVvl4HUj/JMtYNqu2jWrCHNXQcaX/WkdSdvGJtmg5g==} peerDependencies: @@ -17210,6 +17225,13 @@ packages: commander: 2.20.3 dev: false + /zeed-dom/0.9.26: + resolution: {integrity: sha512-HWjX8rA3Y/RI32zby3KIN1D+mgskce+She4K7kRyyx62OiVxJ5FnYm8vWq0YVAja3Tf2S1M0XAc6O2lRFcMgcQ==} + engines: {node: '>=14.13.1'} + dependencies: + css-what: 6.1.0 + dev: false + file:projects/account.tgz_bufferutil@4.0.7: resolution: {integrity: sha512-4tS8wBCO4BXrFfa11XEyJzCw6AG+7romshdy1O3kWS2by/z8IpsFcblvOk3Q3zaIExY6MSA0Wjdf+9od8vJMDA==, tarball: file:projects/account.tgz} id: file:projects/account.tgz @@ -20363,7 +20385,7 @@ packages: dev: false file:projects/prod.tgz_91152849c259b1b3925f7bb3d415bc80: - resolution: {integrity: sha512-dB1fl4vWrudVeRZJRFyOejr/xRHzIj4ZgkssAuS59WZIVVbyGvMgjPhC/tCGovi/O581wEJBBO1fgVLfOOXXiw==, tarball: file:projects/prod.tgz} + resolution: {integrity: sha512-wZWIAN+CPlPommWoqqH2yf11giIQ+B7Gbx6NQ8hkQ3PeZS2zJWsN3Ki+4vYu+BOZHwthzEJn3Y9LhKsyTL9m2A==, tarball: file:projects/prod.tgz} id: file:projects/prod.tgz name: '@rush-temp/prod' version: 0.0.0 @@ -20743,11 +20765,15 @@ packages: dev: false file:projects/server-chunter-resources.tgz: - resolution: {integrity: sha512-kNwG1PW31TnvKu/srs+aikf6idi3OZVIt5JTPnSBvGQ++A7ZUz/Cal5onvxUP72/zSfl2VIPSWUn6WViYx5xyg==, tarball: file:projects/server-chunter-resources.tgz} + resolution: {integrity: sha512-/t2NvPZEV7xa90ZuDnc/Bi5KK9bXAozhOmghDjB0ztXFX9QRibqnObPNkR6lKP4QCHeihGFeow0gp5lqqb0DbQ==, tarball: file:projects/server-chunter-resources.tgz} name: '@rush-temp/server-chunter-resources' version: 0.0.0 dependencies: '@rushstack/heft': 0.47.11 + '@tiptap/core': 2.0.3_@tiptap+pm@2.0.3 + '@tiptap/html': 2.1.6_fc2f730f0ef238c57ef10dc1abce4607 + '@tiptap/pm': 2.0.3_@tiptap+core@2.0.3 + '@tiptap/starter-kit': 2.0.3_@tiptap+pm@2.0.3 '@types/heft-jest': 1.0.3 '@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464 '@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4 @@ -22052,7 +22078,7 @@ packages: dev: false file:projects/text-editor.tgz_1e646fd62a3adc9d0b41f256dafe3d38: - resolution: {integrity: sha512-veer1goACGzBNOXgkYDbqPx/5obONA8llhpWAv2M+M/I2tumjLZxG4ohodKBQNa0ijAZ+MUgRlcaS9c8MsKF6A==, tarball: file:projects/text-editor.tgz} + resolution: {integrity: sha512-Omwv7uuoSX4eicxv4jQUEegyhpg0QWeFuCr+DxCR3j7yfsFfF5mUCleLcX7LWh+M9EXbdldBdcHE3OriJg5ECQ==, tarball: file:projects/text-editor.tgz} id: file:projects/text-editor.tgz name: '@rush-temp/text-editor' version: 0.0.0 @@ -22128,6 +22154,50 @@ packages: - y-protocols dev: false + file:projects/text.tgz_a7abb371f17d2bc77abec7bc53398db5: + resolution: {integrity: sha512-QfLKyYxcLXLoqBMnbP4k2ND5871b+ycKCz7+NZ7xBtPtEb3Ee6jPdFrf8wbmnIYX/dLV0VThNKXcJ4ZiWxNvbA==, tarball: file:projects/text.tgz} + id: file:projects/text.tgz + name: '@rush-temp/text' + version: 0.0.0 + dependencies: + '@rushstack/heft': 0.47.11 + '@tiptap/core': 2.0.3_@tiptap+pm@2.0.3 + '@tiptap/extension-gapcursor': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607 + '@tiptap/extension-heading': 2.0.3_@tiptap+core@2.0.3 + '@tiptap/extension-highlight': 2.0.3_@tiptap+core@2.0.3 + '@tiptap/extension-link': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607 + '@tiptap/extension-mention': 2.0.3_dc69df04ca9f1acb766e21a98969ce2c + '@tiptap/extension-table': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607 + '@tiptap/extension-table-cell': 2.0.3_@tiptap+core@2.0.3 + '@tiptap/extension-table-header': 2.0.3_@tiptap+core@2.0.3 + '@tiptap/extension-table-row': 2.0.3_@tiptap+core@2.0.3 + '@tiptap/extension-task-item': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607 + '@tiptap/extension-task-list': 2.0.3_@tiptap+core@2.0.3 + '@tiptap/extension-typography': 2.0.3_@tiptap+core@2.0.3 + '@tiptap/html': 2.1.6_fc2f730f0ef238c57ef10dc1abce4607 + '@tiptap/pm': 2.0.3_@tiptap+core@2.0.3 + '@tiptap/prosemirror-tables': 1.1.4_a7abb371f17d2bc77abec7bc53398db5 + '@tiptap/starter-kit': 2.0.3_@tiptap+pm@2.0.3 + '@tiptap/suggestion': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607 + '@types/heft-jest': 1.0.3 + '@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464 + '@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4 + eslint: 8.27.0 + eslint-config-standard-with-typescript: 23.0.0_c9fe9619f50f4e82337a86c3af25e566 + eslint-plugin-import: 2.26.0_eslint@8.27.0 + eslint-plugin-n: 15.5.1_eslint@8.27.0 + eslint-plugin-promise: 6.1.1_eslint@8.27.0 + prettier: 2.8.8 + typescript: 4.8.4 + transitivePeerDependencies: + - prosemirror-keymap + - prosemirror-model + - prosemirror-state + - prosemirror-transform + - prosemirror-view + - supports-color + dev: false + file:projects/theme.tgz_a1d864769aaf53d09b76fe134ab55e60: resolution: {integrity: sha512-wnu6qKDGjkOGG0e/hcmvG2yHZv9oOFqO1UDrxFCa/+/oMuTnaRsSscLUjPiYWE3iHDFXZNaNJzVrZc7tCwXXQw==, tarball: file:projects/theme.tgz} id: file:projects/theme.tgz diff --git a/dev/prod/package.json b/dev/prod/package.json index 904e6a12e5..68620a7bb8 100644 --- a/dev/prod/package.json +++ b/dev/prod/package.json @@ -47,6 +47,7 @@ "@hcengineering/platform": "^0.6.9", "@hcengineering/ui": "^0.6.10", "@hcengineering/theme": "^0.6.3", + "@hcengineering/text": "^0.6.0", "svelte": "3.55.1", "@hcengineering/login": "^0.6.7", "@hcengineering/login-assets": "^0.6.0", diff --git a/models/server-chunter/src/index.ts b/models/server-chunter/src/index.ts index db828585df..7937d578a9 100644 --- a/models/server-chunter/src/index.ts +++ b/models/server-chunter/src/index.ts @@ -1,5 +1,5 @@ // -// Copyright © 2022 Hardcore Engineering Inc. +// Copyright © 2022, 2023 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may @@ -41,6 +41,10 @@ export function createModel (builder: Builder): void { } ) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverChunter.trigger.BacklinkTrigger + }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverChunter.trigger.ChunterTrigger }) diff --git a/packages/text-editor/package.json b/packages/text-editor/package.json index f61943e426..28eb3faba7 100644 --- a/packages/text-editor/package.json +++ b/packages/text-editor/package.json @@ -36,6 +36,7 @@ "@hcengineering/platform": "^0.6.9", "@hcengineering/core": "^0.6.27", "@hcengineering/ui": "^0.6.10", + "@hcengineering/text": "^0.6.0", "svelte": "3.55.1", "@tiptap/core": "^2.0.3", "@tiptap/pm": "^2.0.3", diff --git a/packages/text/.eslintrc.js b/packages/text/.eslintrc.js new file mode 100644 index 0000000000..7f7cebe58b --- /dev/null +++ b/packages/text/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/config/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/packages/text/config/rig.json b/packages/text/config/rig.json new file mode 100644 index 0000000000..48fd47af6b --- /dev/null +++ b/packages/text/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "default" +} diff --git a/packages/text/package.json b/packages/text/package.json new file mode 100644 index 0000000000..a2d2291886 --- /dev/null +++ b/packages/text/package.json @@ -0,0 +1,53 @@ +{ + "name": "@hcengineering/text", + "version": "0.6.0", + "main": "lib/index.js", + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "heft build", + "test": "heft test", + "build:watch": "tsc", + "lint:fix": "eslint --fix src", + "lint": "eslint src", + "format": "prettier --write src && eslint --fix src" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@types/heft-jest": "^1.0.3", + "@typescript-eslint/eslint-plugin": "^5.41.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.26.0", + "@typescript-eslint/parser": "^5.41.0", + "eslint-config-standard-with-typescript": "^23.0.0", + "prettier": "^2.7.1", + "@rushstack/heft": "^0.47.9", + "typescript": "^4.3.5" + }, + "dependencies": { + "@tiptap/core": "^2.0.3", + "@tiptap/html": "^2.0.3", + "@tiptap/pm": "^2.0.3", + "@tiptap/starter-kit": "^2.0.3", + "@tiptap/extension-gapcursor": "^2.0.3", + "@tiptap/extension-heading": "^2.0.3", + "@tiptap/extension-highlight": "^2.0.3", + "@tiptap/extension-link": "^2.0.3", + "@tiptap/extension-mention": "^2.0.3", + "@tiptap/extension-table": "^2.0.3", + "@tiptap/extension-table-cell": "^2.0.3", + "@tiptap/extension-table-header": "^2.0.3", + "@tiptap/extension-table-row": "^2.0.3", + "@tiptap/extension-task-item": "^2.0.3", + "@tiptap/extension-task-list": "^2.0.3", + "@tiptap/extension-typography": "^2.0.3", + "@tiptap/suggestion": "^2.0.3", + "@tiptap/prosemirror-tables": "^1.1.4" + }, + "repository": "https://github.com/hcenginneing/anticrm", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + } +} diff --git a/packages/text/src/extensions.ts b/packages/text/src/extensions.ts new file mode 100644 index 0000000000..6d7fed7faa --- /dev/null +++ b/packages/text/src/extensions.ts @@ -0,0 +1,94 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { AnyExtension } from '@tiptap/core' +import { Level } from '@tiptap/extension-heading' +import Highlight from '@tiptap/extension-highlight' +import Link from '@tiptap/extension-link' +import Table from '@tiptap/extension-table' +import TableCell from '@tiptap/extension-table-cell' +import TableHeader from '@tiptap/extension-table-header' +import TableRow from '@tiptap/extension-table-row' +import TaskItem from '@tiptap/extension-task-item' +import TaskList from '@tiptap/extension-task-list' +import Typography from '@tiptap/extension-typography' +import StarterKit from '@tiptap/starter-kit' + +/** + * @public + */ +export const headingLevels: Level[] = [1, 2, 3, 4, 5, 6] + +/** + * @public + */ +export const tableExtensions = [ + Table.configure({ + resizable: false, + HTMLAttributes: { + class: 'proseTable' + } + }), + TableRow.configure({}), + TableHeader.configure({}), + TableCell.configure({}) +] + +/** + * @public + */ +export const taskListExtensions = [ + TaskList, + TaskItem.configure({ + nested: true, + HTMLAttributes: { + class: 'flex flex-grow gap-1 checkbox_style' + } + }) +] + +/** + * @public + */ +export const defaultExtensions: AnyExtension[] = [ + StarterKit.configure({ + code: { + HTMLAttributes: { + class: 'proseCode' + } + }, + codeBlock: { + languageClassPrefix: 'language-', + exitOnArrowDown: true, + exitOnTripleEnter: true, + HTMLAttributes: { + class: 'proseCodeBlock' + } + }, + heading: { + levels: headingLevels + } + }), + Highlight.configure({ + multicolor: false + }), + Typography.configure({}), + Link.configure({ + openOnClick: true, + HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' } + }), + ...tableExtensions, + ...taskListExtensions +] diff --git a/packages/text/src/html.ts b/packages/text/src/html.ts new file mode 100644 index 0000000000..da963510d3 --- /dev/null +++ b/packages/text/src/html.ts @@ -0,0 +1,35 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Extensions, getSchema } from '@tiptap/core' +import { generateJSON, generateHTML } from '@tiptap/html' +import { Node as ProseMirrorNode } from '@tiptap/pm/model' + +/** + * @public + */ +export function getHTML (node: ProseMirrorNode, extensions: Extensions): string { + return generateHTML(node.toJSON(), extensions) +} + +/** + * @public + */ +export function parseHTML (content: string, extensions: Extensions): ProseMirrorNode { + const schema = getSchema(extensions) + const json = generateJSON(content, extensions) + + return ProseMirrorNode.fromJSON(schema, json) +} diff --git a/packages/text/src/index.ts b/packages/text/src/index.ts new file mode 100644 index 0000000000..e45901964c --- /dev/null +++ b/packages/text/src/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './extensions' +export * from './html' +export * from './nodes' diff --git a/packages/text/src/nodes/index.ts b/packages/text/src/nodes/index.ts new file mode 100644 index 0000000000..c60bac278c --- /dev/null +++ b/packages/text/src/nodes/index.ts @@ -0,0 +1,16 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './reference' diff --git a/packages/text/src/nodes/reference.ts b/packages/text/src/nodes/reference.ts new file mode 100644 index 0000000000..b30a8e3d61 --- /dev/null +++ b/packages/text/src/nodes/reference.ts @@ -0,0 +1,88 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Node, mergeAttributes } from '@tiptap/core' + +/** + * @public + */ +export const ReferenceNode = Node.create({ + name: 'reference', + group: 'inline', + content: 'inline*', + inline: true, + + addAttributes () { + return { + id: { + default: null, + parseHTML: (element) => element.getAttribute('data-id'), + renderHTML: (attributes) => { + if (attributes.id === null) { + return {} + } + + return { + 'data-id': attributes.id + } + } + }, + + objectClass: { + default: null, + parseHTML: (element) => element.getAttribute('data-objectclass'), + renderHTML: (attributes) => { + if (attributes.objectClass === null) { + return {} + } + + return { + 'data-objectclass': attributes.objectClass + } + } + }, + + label: { + default: null, + parseHTML: (element) => element.getAttribute('data-label'), + renderHTML: (attributes) => { + if (attributes.label === null) { + return {} + } + + return { + 'data-label': attributes.label + } + } + }, + + class: { + default: null + } + } + }, + + parseHTML () { + return [ + { + tag: `span[data-type="${this.name}"]` + } + ] + }, + + renderHTML ({ HTMLAttributes }) { + return ['span', mergeAttributes({ 'data-type': this.name }, HTMLAttributes), 0] + } +}) diff --git a/packages/text/tsconfig.json b/packages/text/tsconfig.json new file mode 100644 index 0000000000..9b47eded03 --- /dev/null +++ b/packages/text/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib" + } +} \ No newline at end of file diff --git a/plugins/attachment-resources/src/components/AttachmentStyleBoxEditor.svelte b/plugins/attachment-resources/src/components/AttachmentStyleBoxEditor.svelte index 12b44cad8c..c937a1efae 100644 --- a/plugins/attachment-resources/src/components/AttachmentStyleBoxEditor.svelte +++ b/plugins/attachment-resources/src/components/AttachmentStyleBoxEditor.svelte @@ -13,7 +13,7 @@ // limitations under the License. --> {#if inline} - -
- -
- Message -
-  #{cutId(value._id.toString())} +
+ {#if presenter && doc} + +   +
+ +
+   + + {:else} +
{:else}
{#await getEmployee(value, $personByIdStore, $personAccountByIdStore) then employee} diff --git a/plugins/chunter-resources/src/components/DirectMessageInput.svelte b/plugins/chunter-resources/src/components/DirectMessageInput.svelte index c841956689..95997c8c4e 100644 --- a/plugins/chunter-resources/src/components/DirectMessageInput.svelte +++ b/plugins/chunter-resources/src/components/DirectMessageInput.svelte @@ -1,6 +1,6 @@ diff --git a/plugins/chunter-resources/src/components/MessagePresenter.svelte b/plugins/chunter-resources/src/components/MessagePresenter.svelte index c824a4118d..c8d25c64a2 100644 --- a/plugins/chunter-resources/src/components/MessagePresenter.svelte +++ b/plugins/chunter-resources/src/components/MessagePresenter.svelte @@ -13,10 +13,44 @@ // limitations under the License. --> -
+{#if inline} + {#if presenter && doc} +
+
+ +
+ + +   + +
+ {/if} +{:else} +
+{/if} diff --git a/plugins/chunter-resources/src/components/Thread.svelte b/plugins/chunter-resources/src/components/Thread.svelte index d705fe5a35..ac7bba6ff9 100644 --- a/plugins/chunter-resources/src/components/Thread.svelte +++ b/plugins/chunter-resources/src/components/Thread.svelte @@ -15,14 +15,13 @@ diff --git a/plugins/tracker-resources/src/components/templates/EditIssueTemplate.svelte b/plugins/tracker-resources/src/components/templates/EditIssueTemplate.svelte index 6bbb07c218..eb0d2328bb 100644 --- a/plugins/tracker-resources/src/components/templates/EditIssueTemplate.svelte +++ b/plugins/tracker-resources/src/components/templates/EditIssueTemplate.svelte @@ -37,7 +37,6 @@ import { createEventDispatcher, onDestroy, onMount } from 'svelte' import tracker from '../../plugin' - import { updateBacklinks } from '@hcengineering/chunter-resources' import SubIssueTemplates from './IssueTemplateChilds.svelte' import TemplateControlPanel from './TemplateControlPanel.svelte' @@ -185,9 +184,6 @@ on:saved={(evt) => { saved = evt.detail }} - updateBacklinks={(doc, description) => { - updateBacklinks(client, doc._id, doc._class, doc._id, description) - }} />
diff --git a/rush.json b/rush.json index 2c7cde3c93..51577d4f5b 100644 --- a/rush.json +++ b/rush.json @@ -481,6 +481,11 @@ "projectFolder": "packages/theme", "shouldPublish": true }, + { + "packageName": "@hcengineering/text", + "projectFolder": "packages/text", + "shouldPublish": true + }, { "packageName": "@hcengineering/ui", "projectFolder": "packages/ui", diff --git a/server-plugins/chunter-resources/package.json b/server-plugins/chunter-resources/package.json index 3727d56963..8efd299581 100644 --- a/server-plugins/chunter-resources/package.json +++ b/server-plugins/chunter-resources/package.json @@ -6,6 +6,7 @@ "license": "EPL-2.0", "scripts": { "build": "heft build", + "test": "heft test", "build:watch": "tsc", "lint:fix": "eslint --fix src", "lint": "eslint src", @@ -36,6 +37,8 @@ "@hcengineering/workbench": "^0.6.8", "@hcengineering/notification": "^0.6.14", "@hcengineering/server-notification": "^0.6.1", - "@hcengineering/server-notification-resources": "^0.6.0" + "@hcengineering/server-notification-resources": "^0.6.0", + "@hcengineering/server": "^0.6.4", + "@hcengineering/text": "^0.6.0" } } diff --git a/server-plugins/chunter-resources/src/__tests__/backlinks.test.ts b/server-plugins/chunter-resources/src/__tests__/backlinks.test.ts new file mode 100644 index 0000000000..eb26fee080 --- /dev/null +++ b/server-plugins/chunter-resources/src/__tests__/backlinks.test.ts @@ -0,0 +1,72 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Class, Doc, Ref } from '@hcengineering/core' +import { getBacklinks } from '../backlinks' + +describe('extractBacklinks', () => { + it('should return no backlinks for empty document', () => { + const content = '

' + + const backlinks = getBacklinks( + 'backlinkId' as Ref, + 'backlinkClass' as Ref>, + 'attachedDocId' as Ref, + content + ) + + expect(backlinks).toEqual([]) + }) + + it('should parse single backlink', () => { + const content = + '

hello @Appleseed John

' + + const backlinks = getBacklinks( + 'backlinkId' as Ref, + 'backlinkClass' as Ref>, + 'attachedDocId' as Ref, + content + ) + + expect(backlinks.length).toBe(1) + expect(backlinks).toEqual([ + { + attachedTo: 'id', + attachedToClass: 'contact:class:Person', + collection: 'backlinks', + backlinkId: 'backlinkId', + backlinkClass: 'backlinkClass', + message: + 'hello @Appleseed John', + attachedDocId: 'attachedDocId' + } + ]) + }) + + it('should parse single backlink for multiple references', () => { + const content = + '

@Appleseed John @Appleseed John

' + + const backlinks = getBacklinks( + 'backlinkId' as Ref, + 'backlinkClass' as Ref>, + 'attachedDocId' as Ref, + content + ) + + expect(backlinks.length).toBe(1) + }) +}) diff --git a/server-plugins/chunter-resources/src/backlinks.ts b/server-plugins/chunter-resources/src/backlinks.ts new file mode 100644 index 0000000000..8118027db6 --- /dev/null +++ b/server-plugins/chunter-resources/src/backlinks.ts @@ -0,0 +1,54 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Backlink } from '@hcengineering/chunter' +import { Class, Data, Doc, Ref } from '@hcengineering/core' +import { defaultExtensions, getHTML, parseHTML, ReferenceNode } from '@hcengineering/text' + +const extensions = [...defaultExtensions, ReferenceNode] + +export function getBacklinks ( + backlinkId: Ref, + backlinkClass: Ref>, + attachedDocId: Ref | undefined, + content: string +): Array> { + const doc = parseHTML(content, extensions) + + const result: Array> = [] + + doc.descendants((node, _pos, parent): boolean => { + if (node.type.name === ReferenceNode.name) { + const ato = node.attrs.id as Ref + const atoClass = node.attrs.objectClass as Ref> + const e = result.find((e) => e.attachedTo === ato && e.attachedToClass === atoClass) + if (e === undefined && ato !== attachedDocId && ato !== backlinkId) { + result.push({ + attachedTo: ato, + attachedToClass: atoClass, + collection: 'backlinks', + backlinkId, + backlinkClass, + message: parent !== null ? getHTML(parent, extensions) : '', + attachedDocId + }) + } + } + + return true + }) + + return result +} diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index 274d89e0dc..5e83731919 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -1,5 +1,5 @@ // -// Copyright © 2022 Hardcore Engineering Inc. +// Copyright © 2022, 2023 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may @@ -29,6 +29,7 @@ import core, { TxCollectionCUD, TxCreateDoc, TxCUD, + TxFactory, TxProcessor, TxRemoveDoc, TxUpdateDoc @@ -38,6 +39,130 @@ import { getMetadata } from '@hcengineering/platform' import serverCore, { TriggerControl } from '@hcengineering/server-core' import { getDocCollaborators, getMixinTx, pushNotification } from '@hcengineering/server-notification-resources' import { workbenchId } from '@hcengineering/workbench' +import { getBacklinks } from './backlinks' + +function getCreateBacklinksTxes (control: TriggerControl, txFactory: TxFactory, doc: Doc): Tx[] { + const txes: Tx[] = [] + + const backlinkId = doc._id + const backlinkClass = doc._class + const attachedDocId = doc._id + + const attributes = control.hierarchy.getAllAttributes(doc._class) + for (const attr of attributes.values()) { + if (attr.type._class === core.class.TypeMarkup) { + const content = (doc as any)[attr.name]?.toString() ?? '' + const backlinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content) + + for (const backlink of backlinks) { + const innerTx = txFactory.createTxCreateDoc(chunter.class.Backlink, chunter.space.Backlinks, backlink) + + const collectionTx = txFactory.createTxCollectionCUD( + backlink.attachedToClass, + backlink.attachedTo, + chunter.space.Backlinks, + backlink.collection, + innerTx + ) + + txes.push(collectionTx) + } + } + } + + return txes +} + +async function getUpdateBacklinksTxes (control: TriggerControl, txFactory: TxFactory, doc: Doc): Promise { + const txes: Tx[] = [] + + const backlinkId = doc._id + const backlinkClass = doc._class + const attachedDocId = doc._id + + const attributes = control.hierarchy.getAllAttributes(doc._class) + for (const attr of attributes.values()) { + if (attr.type._class === core.class.TypeMarkup) { + const content = (doc as any)[attr.name]?.toString() ?? '' + const backlinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content) + + const current = await control.findAll(chunter.class.Backlink, { + backlinkId, + backlinkClass, + attachedDocId, + collection: 'backlinks' + }) + + for (const c of current) { + // Find existing and check if we need to update message + const pos = backlinks.findIndex( + (b) => b.backlinkId === c.backlinkId && b.backlinkClass === c.backlinkClass && b.attachedTo === c.attachedTo + ) + if (pos !== -1) { + // Update existing backlinks when message changed + const data = backlinks[pos] + if (c.message !== data.message) { + const innerTx = txFactory.createTxUpdateDoc(c._class, c.space, c._id, { + message: data.message + }) + txes.push( + txFactory.createTxCollectionCUD( + c.attachedToClass, + c.attachedTo, + chunter.space.Backlinks, + c.collection, + innerTx + ) + ) + } + backlinks.splice(pos, 1) + } else { + // Remove not found backlinks + const innerTx = txFactory.createTxRemoveDoc(c._class, c.space, c._id) + txes.push( + txFactory.createTxCollectionCUD( + c.attachedToClass, + c.attachedTo, + chunter.space.Backlinks, + c.collection, + innerTx + ) + ) + } + } + + // Add missing backlinks + for (const backlink of backlinks) { + const backlinkTx = txFactory.createTxCreateDoc(chunter.class.Backlink, chunter.space.Backlinks, backlink) + txes.push( + txFactory.createTxCollectionCUD( + backlink.attachedToClass, + backlink.attachedTo, + chunter.space.Backlinks, + backlink.collection, + backlinkTx + ) + ) + } + } + } + + return txes +} + +async function getRemoveBacklinksTxes (control: TriggerControl, txFactory: TxFactory, doc: Ref): Promise { + const txes: Tx[] = [] + + const backlinks = await control.findAll(chunter.class.Backlink, { attachedDocId: doc, collection: 'backlinks' }) + for (const b of backlinks) { + const innerTx = txFactory.createTxRemoveDoc(b._class, b.space, b._id) + txes.push( + txFactory.createTxCollectionCUD(b.attachedToClass, b.attachedTo, chunter.space.Backlinks, b.collection, innerTx) + ) + } + + return txes +} /** * @public @@ -193,6 +318,62 @@ async function ThreadMessageDelete (tx: Tx, control: TriggerControl): Promise { + const hierarchy = control.hierarchy + const ctx = TxProcessor.extractTx(tx) as TxCreateDoc + if (ctx._class !== core.class.TxCreateDoc) return [] + if (hierarchy.isDerived(ctx.objectClass, chunter.class.Backlink)) return [] + + const txFactory = new TxFactory(control.txFactory.account) + const doc = TxProcessor.createDoc2Doc(ctx) + + const txes: Tx[] = getCreateBacklinksTxes(control, txFactory, doc) + if (txes.length !== 0) { + await control.apply(txes, true) + } + + return [] +} + +async function BacklinksUpdate (tx: Tx, control: TriggerControl): Promise { + const hierarchy = control.hierarchy + const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc + if (ctx._class !== core.class.TxUpdateDoc) return [] + if (hierarchy.isDerived(ctx.objectClass, chunter.class.Backlink)) return [] + + const rawDoc = (await control.findAll(ctx.objectClass, { _id: ctx.objectId }))[0] + if (rawDoc === undefined) return [] + + const txFactory = new TxFactory(control.txFactory.account) + + if (rawDoc !== undefined) { + const doc = TxProcessor.updateDoc2Doc(rawDoc, ctx) + const txes: Tx[] = await getUpdateBacklinksTxes(control, txFactory, doc) + + if (txes.length !== 0) { + await control.apply(txes, true) + } + } + + return [] +} + +async function BacklinksRemove (tx: Tx, control: TriggerControl): Promise { + const hierarchy = control.hierarchy + const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc + if (ctx._class !== core.class.TxRemoveDoc) return [] + if (hierarchy.isDerived(ctx.objectClass, chunter.class.Backlink)) return [] + + const txFactory = new TxFactory(control.txFactory.account) + + const txes: Tx[] = await getRemoveBacklinksTxes(control, txFactory, ctx.objectId) + if (txes.length !== 0) { + await control.apply(txes, true) + } + + return [] +} + /** * @public */ @@ -255,6 +436,15 @@ export async function OnMessageSent (tx: Tx, control: TriggerControl): Promise { + const promises = [BacklinksCreate(tx, control), BacklinksUpdate(tx, control), BacklinksRemove(tx, control)] + const res = await Promise.all(promises) + return res.flat() +} + /** * @public */ @@ -321,6 +511,7 @@ export async function IsChannelMessage ( // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { + BacklinkTrigger, ChunterTrigger, OnMessageSent }, diff --git a/server-plugins/chunter/src/index.ts b/server-plugins/chunter/src/index.ts index 9518be2548..ebcca7bdd1 100644 --- a/server-plugins/chunter/src/index.ts +++ b/server-plugins/chunter/src/index.ts @@ -29,6 +29,7 @@ export const serverChunterId = 'server-chunter' as Plugin */ export default plugin(serverChunterId, { trigger: { + BacklinkTrigger: '' as Resource, ChunterTrigger: '' as Resource, OnMessageSent: '' as Resource },