From 4ff8a4559f11deea48ee4ec37ba96e77a6e83229 Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Thu, 29 Aug 2024 15:10:37 +0700 Subject: [PATCH] Cloud collaborator refactoring (#6424) --- common/config/rush/pnpm-lock.yaml | 209 ++++++++++++++++++ desktop/src/ui/platform.ts | 2 +- desktop/src/ui/types.ts | 1 + dev/doc-import-tool/src/config.ts | 1 + dev/doc-import-tool/src/index.ts | 3 + dev/prod/src/platform.ts | 3 +- .../src/__tests__/utils.test.ts | 14 +- packages/collaborator-client/src/client.ts | 169 ++------------ packages/collaborator-client/src/utils.ts | 23 +- .../core/src/__tests__/collaboration.test.ts | 30 ++- packages/core/src/collaboration.ts | 6 +- packages/presentation/src/collaborator.ts | 32 +-- packages/text/src/__tests__/ydoc.test.ts | 63 ++++++ packages/text/src/ydoc.ts | 128 ++++++----- .../src/components/EditDocPanel.svelte | 30 ++- .../document/DocumentDiffViewer.svelte | 20 +- .../src/docutils.ts | 39 ++-- .../src/utils.ts | 8 +- .../src/components/CreateSnapshot.svelte | 28 +-- plugins/text-editor-resources/package.json | 1 + .../src/components/Collaboration.svelte | 52 ++--- .../src/components/CollaborationUsers.svelte | 25 ++- .../components/CollaborativeTextEditor.svelte | 48 ++-- plugins/text-editor-resources/src/index.ts | 8 +- .../src/provider/cloud.ts | 44 ++++ .../src/provider/{tiptap.ts => hocuspocus.ts} | 37 +--- .../src/provider/indexeddb.ts | 23 +- .../src/provider/types.ts | 23 ++ .../src/provider/utils.ts | 81 ++++++- plugins/text-editor/src/plugin.ts | 4 +- plugins/text-editor/src/types.ts | 16 +- .../utils/__tests__/collaborative-doc.test.ts | 4 +- .../src/utils/collaborative-doc.ts | 67 +----- server/collaboration/src/utils/storage.ts | 10 +- server/collaborator/src/extensions/storage.ts | 14 +- .../src/rpc/methods/branchDocument.ts | 57 ----- .../src/rpc/methods/copyContent.ts | 64 ------ .../src/rpc/methods/getContent.ts | 2 +- server/collaborator/src/rpc/methods/index.ts | 10 +- .../src/rpc/methods/removeDocument.ts | 53 ----- .../src/rpc/methods/takeSnapshot.ts | 65 ------ .../src/rpc/methods/updateContent.ts | 32 +-- server/collaborator/src/rpc/rpc.ts | 2 + server/collaborator/src/server.ts | 61 +++-- server/collaborator/src/storage/platform.ts | 48 +--- server/collaborator/src/transformers/html.ts | 41 ---- .../collaborator/src/transformers/markup.ts | 19 +- server/collaborator/src/types.ts | 6 +- server/front/src/index.ts | 2 + server/front/src/starter.ts | 3 + server/tool/src/initializer.ts | 7 +- .../github/pod-github/src/sync/issueBase.ts | 22 +- 52 files changed, 762 insertions(+), 998 deletions(-) create mode 100644 packages/text/src/__tests__/ydoc.test.ts create mode 100644 plugins/text-editor-resources/src/provider/cloud.ts rename plugins/text-editor-resources/src/provider/{tiptap.ts => hocuspocus.ts} (64%) create mode 100644 plugins/text-editor-resources/src/provider/types.ts delete mode 100644 server/collaborator/src/rpc/methods/branchDocument.ts delete mode 100644 server/collaborator/src/rpc/methods/copyContent.ts delete mode 100644 server/collaborator/src/rpc/methods/removeDocument.ts delete mode 100644 server/collaborator/src/rpc/methods/takeSnapshot.ts delete mode 100644 server/collaborator/src/transformers/html.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index a5b120e96f..ba45f4b3f5 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1865,6 +1865,9 @@ dependencies: y-protocols: specifier: ^1.0.6 version: 1.0.6(yjs@13.6.12) + y-websocket: + specifier: ^2.0.4 + version: 2.0.4(yjs@13.6.12) yjs: specifier: ^13.5.52 version: 13.6.12 @@ -10213,6 +10216,32 @@ packages: event-target-shim: 5.0.1 dev: false + /abstract-leveldown@6.2.3: + resolution: {integrity: sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + buffer: 5.7.1 + immediate: 3.3.0 + level-concat-iterator: 2.0.1 + level-supports: 1.0.1 + xtend: 4.0.2 + dev: false + optional: true + + /abstract-leveldown@6.3.0: + resolution: {integrity: sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + buffer: 5.7.1 + immediate: 3.3.0 + level-concat-iterator: 2.0.1 + level-supports: 1.0.1 + xtend: 4.0.2 + dev: false + optional: true + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -12519,6 +12548,16 @@ packages: engines: {node: '>=10'} dev: false + /deferred-leveldown@5.3.0: + resolution: {integrity: sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + abstract-leveldown: 6.2.3 + inherits: 2.0.4 + dev: false + optional: true + /define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -13201,6 +13240,18 @@ packages: engines: {node: '>= 0.8'} dev: false + /encoding-down@6.3.0: + resolution: {integrity: sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + abstract-leveldown: 6.3.0 + inherits: 2.0.4 + level-codec: 9.0.2 + level-errors: 2.0.1 + dev: false + optional: true + /end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: @@ -13251,6 +13302,15 @@ packages: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} dev: false + /errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + requiresBuild: true + dependencies: + prr: 1.0.1 + dev: false + optional: true + /error-callsites@2.0.4: resolution: {integrity: sha512-V877Ch4FC4FN178fDK1fsrHN4I1YQIBdtjKrHh3BUHMnh3SMvwUVrqkaOgDpUuevgSNna0RBq6Ox9SGlxYrigA==} engines: {node: '>=6.x'} @@ -15879,6 +15939,12 @@ packages: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} dev: false + /immediate@3.3.0: + resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==} + requiresBuild: true + dev: false + optional: true + /immutable@4.3.5: resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==} dev: false @@ -17417,6 +17483,107 @@ packages: webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) dev: false + /level-codec@9.0.2: + resolution: {integrity: sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + buffer: 5.7.1 + dev: false + optional: true + + /level-concat-iterator@2.0.1: + resolution: {integrity: sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==} + engines: {node: '>=6'} + requiresBuild: true + dev: false + optional: true + + /level-errors@2.0.1: + resolution: {integrity: sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + errno: 0.1.8 + dev: false + optional: true + + /level-iterator-stream@4.0.2: + resolution: {integrity: sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + xtend: 4.0.2 + dev: false + optional: true + + /level-js@5.0.2: + resolution: {integrity: sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==} + requiresBuild: true + dependencies: + abstract-leveldown: 6.2.3 + buffer: 5.7.1 + inherits: 2.0.4 + ltgt: 2.2.1 + dev: false + optional: true + + /level-packager@5.1.1: + resolution: {integrity: sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + encoding-down: 6.3.0 + levelup: 4.4.0 + dev: false + optional: true + + /level-supports@1.0.1: + resolution: {integrity: sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + xtend: 4.0.2 + dev: false + optional: true + + /level@6.0.1: + resolution: {integrity: sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==} + engines: {node: '>=8.6.0'} + requiresBuild: true + dependencies: + level-js: 5.0.2 + level-packager: 5.1.1 + leveldown: 5.6.0 + dev: false + optional: true + + /leveldown@5.6.0: + resolution: {integrity: sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==} + engines: {node: '>=8.6.0'} + requiresBuild: true + dependencies: + abstract-leveldown: 6.2.3 + napi-macros: 2.0.0 + node-gyp-build: 4.1.1 + dev: false + optional: true + + /levelup@4.4.0: + resolution: {integrity: sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + deferred-leveldown: 5.3.0 + level-errors: 2.0.1 + level-iterator-stream: 4.0.2 + level-supports: 1.0.1 + xtend: 4.0.2 + dev: false + optional: true + /leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -17778,6 +17945,12 @@ packages: engines: {node: '>=12'} dev: false + /ltgt@2.2.1: + resolution: {integrity: sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==} + requiresBuild: true + dev: false + optional: true + /lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -18347,6 +18520,12 @@ packages: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} dev: false + /napi-macros@2.0.0: + resolution: {integrity: sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==} + requiresBuild: true + dev: false + optional: true + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: false @@ -18449,6 +18628,13 @@ packages: dev: false optional: true + /node-gyp-build@4.1.1: + resolution: {integrity: sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==} + hasBin: true + requiresBuild: true + dev: false + optional: true + /node-gyp-build@4.8.0: resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==} hasBin: true @@ -19889,6 +20075,12 @@ packages: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + requiresBuild: true + dev: false + optional: true + /pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: false @@ -23791,6 +23983,22 @@ packages: yjs: 13.6.12 dev: false + /y-websocket@2.0.4(yjs@13.6.12): + resolution: {integrity: sha512-UbrkOU4GPNFFTDlJYAxAmzZhia8EPxHkngZ6qjrxgIYCN3gI2l+zzLzA9p4LQJ0IswzpioeIgmzekWe7HoBBjg==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + hasBin: true + peerDependencies: + yjs: ^13.5.6 + dependencies: + lib0: 0.2.89 + lodash.debounce: 4.0.8 + y-protocols: 1.0.6(yjs@13.6.12) + yjs: 13.6.12 + optionalDependencies: + ws: 6.2.2 + y-leveldb: 0.1.2(yjs@13.6.12) + dev: false + /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} dev: false @@ -34026,6 +34234,7 @@ packages: y-indexeddb: 9.0.12(yjs@13.6.12) y-prosemirror: 1.2.2(prosemirror-model@1.19.4)(y-protocols@1.0.6)(yjs@13.6.12) y-protocols: 1.0.6(yjs@13.6.12) + y-websocket: 2.0.4(yjs@13.6.12) yjs: 13.6.12 transitivePeerDependencies: - '@babel/core' diff --git a/desktop/src/ui/platform.ts b/desktop/src/ui/platform.ts index b9fc55bd53..14128d4d75 100644 --- a/desktop/src/ui/platform.ts +++ b/desktop/src/ui/platform.ts @@ -206,7 +206,7 @@ export async function configurePlatform (): Promise { setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG)) setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL) - setMetadata(textEditor.metadata.CollaboratorUrl, config.COLLABORATOR_URL) + setMetadata(textEditor.metadata.Collaborator, config.COLLABORATOR ?? '') setMetadata(github.metadata.GithubApplication, config.GITHUB_APP ?? '') setMetadata(github.metadata.GithubClientID, config.GITHUB_CLIENTID ?? '') diff --git a/desktop/src/ui/types.ts b/desktop/src/ui/types.ts index e6e896ffc4..9dd49655dc 100644 --- a/desktop/src/ui/types.ts +++ b/desktop/src/ui/types.ts @@ -5,6 +5,7 @@ import { ScreenSource } from '@hcengineering/love' */ export interface Config { ACCOUNTS_URL: string + COLLABORATOR?: string COLLABORATOR_URL: string FRONT_URL: string FILES_URL: string diff --git a/dev/doc-import-tool/src/config.ts b/dev/doc-import-tool/src/config.ts index 1503c73eb5..69ea6decbb 100644 --- a/dev/doc-import-tool/src/config.ts +++ b/dev/doc-import-tool/src/config.ts @@ -8,6 +8,7 @@ import { HtmlConversionBackend } from './convert/convert' export interface Config { doc: string token: string + collaborator?: string collaboratorURL: string uploadURL: string workspaceId: WorkspaceId diff --git a/dev/doc-import-tool/src/index.ts b/dev/doc-import-tool/src/index.ts index e90c941049..de88634a56 100644 --- a/dev/doc-import-tool/src/index.ts +++ b/dev/doc-import-tool/src/index.ts @@ -50,6 +50,8 @@ export function docImportTool (): void { process.exit(1) } + const collaborator = process.env.COLLABORATOR + const uploadUrl = process.env.UPLOAD_URL ?? '/files' const mongodbUri = process.env.MONGO_URL @@ -105,6 +107,7 @@ export function docImportTool (): void { uploadURL: uploadUrl, storageAdapter, collaboratorURL: collaboratorUrl, + collaborator, token: generateToken(systemAccountEmail, workspaceId) } diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index 0ddfee20db..74f9e4b9cb 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -124,6 +124,7 @@ export interface Config { MODEL_VERSION: string VERSION: string COLLABORATOR_URL: string + COLLABORATOR?: string REKONI_URL: string TELEGRAM_URL: string GMAIL_URL: string @@ -290,7 +291,7 @@ export async function configurePlatform() { setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL) setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG)) - setMetadata(textEditor.metadata.CollaboratorUrl, config.COLLABORATOR_URL ?? 'ws://localhost:3078') + setMetadata(textEditor.metadata.Collaborator, config.COLLABORATOR) if (config.MODEL_VERSION != null) { console.log('Minimal Model version requirement', config.MODEL_VERSION) diff --git a/packages/collaborator-client/src/__tests__/utils.test.ts b/packages/collaborator-client/src/__tests__/utils.test.ts index f8fc415b72..bca6da39c9 100644 --- a/packages/collaborator-client/src/__tests__/utils.test.ts +++ b/packages/collaborator-client/src/__tests__/utils.test.ts @@ -19,22 +19,18 @@ import { formatDocumentId, parseDocumentId } from '../utils' describe('utils', () => { it('formatDocumentId', () => { - expect(formatDocumentId('minio', 'ws1', 'doc1:HEAD:v1' as CollaborativeDoc)).toEqual( - 'minio://ws1/doc1:HEAD' as DocumentId - ) - expect(formatDocumentId('minio', 'ws1', 'doc1:HEAD:v1#doc2:v2:v2' as CollaborativeDoc)).toEqual( - 'minio://ws1/doc1:HEAD/doc2:v2' as DocumentId + expect(formatDocumentId('ws1', 'doc1:HEAD:v1' as CollaborativeDoc)).toEqual('ws1://doc1:HEAD' as DocumentId) + expect(formatDocumentId('ws1', 'doc1:HEAD:v1#doc2:v2:v2' as CollaborativeDoc)).toEqual( + 'ws1://doc1:HEAD/doc2:v2' as DocumentId ) }) describe('parseDocumentId', () => { - expect(parseDocumentId('minio://ws1/doc1:HEAD' as DocumentId)).toEqual({ - storage: 'minio', + expect(parseDocumentId('ws1://doc1:HEAD' as DocumentId)).toEqual({ workspaceUrl: 'ws1', collaborativeDoc: 'doc1:HEAD:HEAD' as CollaborativeDoc }) - expect(parseDocumentId('minio://ws1/doc1:HEAD/doc2:v2' as DocumentId)).toEqual({ - storage: 'minio', + expect(parseDocumentId('ws1://doc1:HEAD/doc2:v2' as DocumentId)).toEqual({ workspaceUrl: 'ws1', collaborativeDoc: 'doc1:HEAD:HEAD#doc2:v2:v2' as CollaborativeDoc }) diff --git a/packages/collaborator-client/src/client.ts b/packages/collaborator-client/src/client.ts index 2ae2f30307..bca4a8c0b1 100644 --- a/packages/collaborator-client/src/client.ts +++ b/packages/collaborator-client/src/client.ts @@ -13,31 +13,12 @@ // limitations under the License. // -import { - Account, - CollaborativeDoc, - Markup, - Ref, - Timestamp, - WorkspaceId, - collaborativeDocWithLastVersion, - collaborativeDocWithVersion, - concatLink -} from '@hcengineering/core' -import { DocumentId } from './types' -import { formatMinioDocumentId } from './utils' +import { CollaborativeDoc, Markup, WorkspaceId, concatLink } from '@hcengineering/core' +import { formatDocumentId } from './utils' /** @public */ -export interface DocumentSnapshotParams { - createdBy: Ref - versionId: string - versionName?: string -} - -/** @public */ -export interface GetContentRequest { - documentId: DocumentId -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface GetContentRequest {} /** @public */ export interface GetContentResponse { @@ -46,81 +27,18 @@ export interface GetContentResponse { /** @public */ export interface UpdateContentRequest { - documentId: DocumentId content: Record - snapshot?: DocumentSnapshotParams } /** @public */ // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface UpdateContentResponse {} -/** @public */ -export interface CopyContentRequest { - documentId: DocumentId - sourceField: string - targetField: string - snapshot?: DocumentSnapshotParams -} - -/** @public */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CopyContentResponse {} - -/** @public */ -export interface BranchDocumentRequest { - sourceDocumentId: DocumentId - targetDocumentId: DocumentId -} - -/** @public */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface BranchDocumentResponse {} - -/** @public */ -export interface RemoveDocumentRequest { - documentId: DocumentId -} - -/** @public */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface RemoveDocumentResponse {} - -/** @public */ -export interface TakeSnapshotRequest { - documentId: DocumentId - snapshot: DocumentSnapshotParams -} - -/** @public */ -export interface TakeSnapshotResponse { - versionId: string - name: string - - createdBy: Ref - createdOn: Timestamp -} - /** @public */ export interface CollaboratorClient { - // field operations - getContent: (collaborativeDoc: CollaborativeDoc) => Promise> - updateContent: ( - document: CollaborativeDoc, - content: Record, - snapshot?: DocumentSnapshotParams - ) => Promise - copyContent: ( - document: CollaborativeDoc, - sourceField: string, - targetField: string, - snapshot?: DocumentSnapshotParams - ) => Promise - - // document operations - branch: (source: CollaborativeDoc, target: CollaborativeDoc) => Promise - remove: (collaborativeDoc: CollaborativeDoc) => Promise - snapshot: (collaborativeDoc: CollaborativeDoc, params: DocumentSnapshotParams) => Promise + getContent: (document: CollaborativeDoc) => Promise> + updateContent: (document: CollaborativeDoc, content: Record) => Promise + copyContent: (source: CollaborativeDoc, target: CollaborativeDoc) => Promise } /** @public */ @@ -136,7 +54,10 @@ class CollaboratorClientImpl implements CollaboratorClient { private readonly collaboratorUrl: string ) {} - private async rpc (method: string, payload: any): Promise { + private async rpc (document: CollaborativeDoc, method: string, payload: any): Promise { + const workspace = this.workspace.name + const documentId = formatDocumentId(workspace, document) + const url = concatLink(this.collaboratorUrl, '/rpc') const res = await fetch(url, { @@ -145,7 +66,7 @@ class CollaboratorClientImpl implements CollaboratorClient { Authorization: 'Bearer ' + this.token, 'Content-Type': 'application/json' }, - body: JSON.stringify({ method, payload }) + body: JSON.stringify({ method, documentId, payload }) }) const result = await res.json() @@ -158,70 +79,16 @@ class CollaboratorClientImpl implements CollaboratorClient { } async getContent (document: CollaborativeDoc): Promise> { - const workspace = this.workspace.name - - const documentId = formatMinioDocumentId(workspace, document) - const payload: GetContentRequest = { documentId } - const res = (await this.rpc('getContent', payload)) as GetContentResponse - + const res = (await this.rpc(document, 'getContent', {})) as GetContentResponse return res.content ?? {} } - async updateContent ( - document: CollaborativeDoc, - content: Record, - snapshot?: DocumentSnapshotParams - ): Promise { - const workspace = this.workspace.name - - const documentId = formatMinioDocumentId(workspace, document) - const payload: UpdateContentRequest = { documentId, content, snapshot } - await this.rpc('updateContent', payload) - - return snapshot !== undefined ? collaborativeDocWithLastVersion(document, snapshot.versionId) : document + async updateContent (document: CollaborativeDoc, content: Record): Promise { + await this.rpc(document, 'updateContent', { content }) } - async copyContent ( - document: CollaborativeDoc, - sourceField: string, - targetField: string, - snapshot?: DocumentSnapshotParams - ): Promise { - const workspace = this.workspace.name - - const documentId = formatMinioDocumentId(workspace, document) - const payload: CopyContentRequest = { documentId, sourceField, targetField, snapshot } - await this.rpc('copyContent', payload) - - return snapshot !== undefined ? collaborativeDocWithLastVersion(document, snapshot.versionId) : document - } - - async branch (source: CollaborativeDoc, target: CollaborativeDoc): Promise { - const workspace = this.workspace.name - - const sourceDocumentId = formatMinioDocumentId(workspace, source) - const targetDocumentId = formatMinioDocumentId(workspace, target) - - const payload: BranchDocumentRequest = { sourceDocumentId, targetDocumentId } - await this.rpc('branchDocument', payload) - } - - async remove (document: CollaborativeDoc): Promise { - const workspace = this.workspace.name - - const documentId = formatMinioDocumentId(workspace, document) - - const payload: RemoveDocumentRequest = { documentId } - await this.rpc('removeDocument', payload) - } - - async snapshot (document: CollaborativeDoc, snapshot: DocumentSnapshotParams): Promise { - const workspace = this.workspace.name - - const documentId = formatMinioDocumentId(workspace, document) - const payload: TakeSnapshotRequest = { documentId, snapshot } - const res = (await this.rpc('takeSnapshot', payload)) as TakeSnapshotResponse - - return collaborativeDocWithVersion(document, res.versionId) + async copyContent (source: CollaborativeDoc, target: CollaborativeDoc): Promise { + const content = await this.getContent(source) + await this.updateContent(target, content) } } diff --git a/packages/collaborator-client/src/utils.ts b/packages/collaborator-client/src/utils.ts index a8b4b27649..8aa91fd283 100644 --- a/packages/collaborator-client/src/utils.ts +++ b/packages/collaborator-client/src/utils.ts @@ -25,11 +25,6 @@ import { } from '@hcengineering/core' import { DocumentId, PlatformDocumentId } from './types' -/** @public */ -export function formatMinioDocumentId (workspaceUrl: string, collaborativeDoc: CollaborativeDoc): DocumentId { - return formatDocumentId('minio', workspaceUrl, collaborativeDoc) -} - /** * Formats collaborative document as Hocuspocus document name. * @@ -37,15 +32,11 @@ export function formatMinioDocumentId (workspaceUrl: string, collaborativeDoc: C * when document is updated. Hence, we remove lastVersionId component from CollaborativeDoc. * * Example: - * minio://workspace1/doc1:HEAD/doc2:v1 + * workspace1://doc1:HEAD/doc2:v1 * * @public */ -export function formatDocumentId ( - storage: string, - workspaceUrl: string, - collaborativeDoc: CollaborativeDoc -): DocumentId { +export function formatDocumentId (workspaceUrl: string, collaborativeDoc: CollaborativeDoc): DocumentId { const path = collaborativeDocUnchain(collaborativeDoc) .map((p) => { const { documentId, versionId } = collaborativeDocParse(p) @@ -53,25 +44,23 @@ export function formatDocumentId ( }) .join('/') - return `${storage}://${workspaceUrl}/${path}` as DocumentId + return `${workspaceUrl}://${path}` as DocumentId } /** @public */ export function parseDocumentId (documentId: DocumentId): { - storage: string workspaceUrl: string collaborativeDoc: CollaborativeDoc } { - const [storage, path] = documentId.split('://') - const [workspaceUrl, ...rest] = path.split('/') + const [workspaceUrl, path] = documentId.split('://') + const segments = path.split('/') - const collaborativeDocs = rest.map((p) => { + const collaborativeDocs = segments.map((p) => { const [documentId, versionId] = p.split(':') return collaborativeDocFormat({ documentId, versionId, lastVersionId: versionId }) }) return { - storage, workspaceUrl, collaborativeDoc: collaborativeDocChain(...collaborativeDocs) } diff --git a/packages/core/src/__tests__/collaboration.test.ts b/packages/core/src/__tests__/collaboration.test.ts index e68cf747a0..ffc376a30a 100644 --- a/packages/core/src/__tests__/collaboration.test.ts +++ b/packages/core/src/__tests__/collaboration.test.ts @@ -57,37 +57,35 @@ describe('collaborative-doc', () => { describe('collaborativeDocParse', () => { it('parses collaborative doc id', async () => { - expect(collaborativeDocParse('minioDocumentId' as CollaborativeDoc)).toEqual({ - documentId: 'minioDocumentId', + expect(collaborativeDocParse('documentId' as CollaborativeDoc)).toEqual({ + documentId: 'documentId', versionId: 'HEAD', lastVersionId: 'HEAD', source: [] }) }) it('parses collaborative doc id with versionId', async () => { - expect(collaborativeDocParse('minioDocumentId:main' as CollaborativeDoc)).toEqual({ - documentId: 'minioDocumentId', + expect(collaborativeDocParse('documentId:main' as CollaborativeDoc)).toEqual({ + documentId: 'documentId', versionId: 'main', lastVersionId: 'main', source: [] }) }) it('parses collaborative doc id with versionId and lastVersionId', async () => { - expect(collaborativeDocParse('minioDocumentId:HEAD:0' as CollaborativeDoc)).toEqual({ - documentId: 'minioDocumentId', + expect(collaborativeDocParse('documentId:HEAD:0' as CollaborativeDoc)).toEqual({ + documentId: 'documentId', versionId: 'HEAD', lastVersionId: '0', source: [] }) }) it('parses collaborative doc id with versionId, lastVersionId, and source', async () => { - expect( - collaborativeDocParse('minioDocumentId:HEAD:0#minioDocumentId1:main#minioDocumentId2:HEAD' as CollaborativeDoc) - ).toEqual({ - documentId: 'minioDocumentId', + expect(collaborativeDocParse('documentId:HEAD:0#documentId1:main#documentId2:HEAD' as CollaborativeDoc)).toEqual({ + documentId: 'documentId', versionId: 'HEAD', lastVersionId: '0', - source: ['minioDocumentId1:main' as CollaborativeDoc, 'minioDocumentId2:HEAD' as CollaborativeDoc] + source: ['documentId1:main' as CollaborativeDoc, 'documentId2:HEAD' as CollaborativeDoc] }) }) }) @@ -96,21 +94,21 @@ describe('collaborative-doc', () => { it('formats collaborative doc id', async () => { expect( collaborativeDocFormat({ - documentId: 'minioDocumentId', + documentId: 'documentId', versionId: 'HEAD', lastVersionId: '0' }) - ).toEqual('minioDocumentId:HEAD:0') + ).toEqual('documentId:HEAD:0') }) it('formats collaborative doc id with sources', async () => { expect( collaborativeDocFormat({ - documentId: 'minioDocumentId', + documentId: 'documentId', versionId: 'HEAD', lastVersionId: '0', - source: ['minioDocumentId1:main' as CollaborativeDoc, 'minioDocumentId2:HEAD' as CollaborativeDoc] + source: ['documentId1:main' as CollaborativeDoc, 'documentId2:HEAD' as CollaborativeDoc] }) - ).toEqual('minioDocumentId:HEAD:0#minioDocumentId1:main#minioDocumentId2:HEAD') + ).toEqual('documentId:HEAD:0#documentId1:main#documentId2:HEAD') }) it('formats collaborative doc id with invalid characters', async () => { expect( diff --git a/packages/core/src/collaboration.ts b/packages/core/src/collaboration.ts index cda6bd2751..77177ba966 100644 --- a/packages/core/src/collaboration.ts +++ b/packages/core/src/collaboration.ts @@ -19,11 +19,11 @@ import { Doc, Ref } from './classes' * Identifier of the collaborative document holding collaborative content. * * Format: - * {minioDocumentId}:{versionId}:{lastVersionId} - * {minioDocumentId}:{versionId} + * {documentId}:{versionId}:{lastVersionId} + * {documentId}:{versionId} * * Where: - * - minioDocumentId is an identifier of the document in Minio + * - documentId is an identifier of the document in storage * - versionId is an identifier of the document version, HEAD for latest editable version * - lastVersionId is an identifier of the latest available version * diff --git a/packages/presentation/src/collaborator.ts b/packages/presentation/src/collaborator.ts index bde927c5a0..0da961198c 100644 --- a/packages/presentation/src/collaborator.ts +++ b/packages/presentation/src/collaborator.ts @@ -13,20 +13,15 @@ // limitations under the License. // -import { - type CollaboratorClient, - getClient as getCollaborator, - type DocumentSnapshotParams -} from '@hcengineering/collaborator-client' -import { type CollaborativeDoc, type Markup, getCurrentAccount, getWorkspaceId } from '@hcengineering/core' +import { type CollaboratorClient, getClient as getCollaborator } from '@hcengineering/collaborator-client' +import { type CollaborativeDoc, type Markup, getWorkspaceId } from '@hcengineering/core' import { getMetadata } from '@hcengineering/platform' -import { getCurrentLocation } from '@hcengineering/ui' import presentation from './plugin' /** @public */ export function getCollaboratorClient (): CollaboratorClient { - const workspaceId = getWorkspaceId(getCurrentLocation().path[1] ?? '') + const workspaceId = getWorkspaceId(getMetadata(presentation.metadata.Workspace) ?? '') const token = getMetadata(presentation.metadata.Token) ?? '' const collaboratorURL = getMetadata(presentation.metadata.CollaboratorUrl) ?? '' @@ -45,27 +40,8 @@ export async function updateMarkup (collaborativeDoc: CollaborativeDoc, content: await client.updateContent(collaborativeDoc, content) } -/** @public */ -export async function copyDocumentContent ( - collaborativeDoc: CollaborativeDoc, - sourceField: string, - targetField: string -): Promise { - const client = getCollaboratorClient() - await client.copyContent(collaborativeDoc, sourceField, targetField) -} - /** @public */ export async function copyDocument (source: CollaborativeDoc, target: CollaborativeDoc): Promise { const client = getCollaboratorClient() - await client.branch(source, target) -} - -/** @public */ -export async function takeSnapshot (collaborativeDoc: CollaborativeDoc, versionName: string): Promise { - const client = getCollaboratorClient() - const createdBy = getCurrentAccount()._id - - const snapshot: DocumentSnapshotParams = { createdBy, versionId: `${Date.now()}`, versionName } - return await client.snapshot(collaborativeDoc, snapshot) + await client.copyContent(source, target) } diff --git a/packages/text/src/__tests__/ydoc.test.ts b/packages/text/src/__tests__/ydoc.test.ts new file mode 100644 index 0000000000..6894635f07 --- /dev/null +++ b/packages/text/src/__tests__/ydoc.test.ts @@ -0,0 +1,63 @@ +// +// Copyright © 2024 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 { Markup } from '@hcengineering/core' +import { markupToYDoc, markupToYDocNoSchema, yDocToMarkup } from '../ydoc' + +describe('ydoc', () => { + const markups: Markup[] = [ + // just text + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello world"}]}]}', + // just text with bold mark + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"hello world"}]}]}', + // separate paragraphs with bold mark + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"hello"}]},{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"world"}]}]}', + // mixed text and text with bold mark + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello "},{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"world"}]}]}', + // mixed text with italic and text with bold and italic marks + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"hello "},{"type":"text","marks":[{"type":"bold","attrs":{}},{"type":"italic","attrs":{}}],"text":"world"}]}]}', + // text with link and italic marks + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello "},{"type":"text","text":"hello world","marks":[{"type":"link","attrs":{"href":"http://example.com","target":"_blank","rel":"noopener noreferrer","class":"cursor-pointer"}},{"type":"italic","attrs":{}}]}]}]}', + // an image + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"image","attrs":{"src":"http://example.com/image.jpg","alt":"image"}}]}]}', + // a table with formatting inside + '{"type":"doc","content":[{"type":"table","content":[{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"1"}]}]},{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"2"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"codeBlock","content":[{"type":"text","text":"3"}]}]},{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","text":"4"}]}]}]}]}]}' + ] + + describe.each(markups)('markupToYDoc', (markup) => { + it('converts markup to ydoc and back', () => { + const ydoc = markupToYDoc(markup, 'test') + const back = yDocToMarkup(ydoc, 'test') + + expect(JSON.parse(back)).toEqual(JSON.parse(markup)) + }) + }) + + describe.each(markups)('markupToYDocNoSchema', (markup) => { + it('converts markup to ydoc', () => { + const ydoc1 = markupToYDoc(markup, 'test') + const ydoc2 = markupToYDocNoSchema(markup, 'test') + + expect(ydoc2.getXmlFragment('test').toJSON()).toEqual(ydoc1.getXmlFragment('test').toJSON()) + }) + + it('converts markup to ydoc and back', () => { + const ydoc = markupToYDocNoSchema(markup, 'test') + const back = yDocToMarkup(ydoc, 'test') + + expect(JSON.parse(back)).toEqual(JSON.parse(markup)) + }) + }) +}) diff --git a/packages/text/src/ydoc.ts b/packages/text/src/ydoc.ts index f00e6c0b53..5497267706 100644 --- a/packages/text/src/ydoc.ts +++ b/packages/text/src/ydoc.ts @@ -17,19 +17,83 @@ import { Markup } from '@hcengineering/core' import { Extensions, getSchema } from '@tiptap/core' import { Node, Schema } from 'prosemirror-model' import { prosemirrorJSONToYDoc, prosemirrorToYDoc, yDocToProsemirrorJSON } from 'y-prosemirror' -import { Doc as YDoc, applyUpdate, encodeStateAsUpdate } from 'yjs' +import { Doc as YDoc, applyUpdate, encodeStateAsUpdate, XmlElement as YXmlElement, XmlText as YXmlText } from 'yjs' import { defaultExtensions } from './extensions' import { MarkupNode } from './markup/model' -import { jsonToMarkup, markupToPmNode } from './markup/utils' +import { jsonToMarkup, markupToJSON, markupToPmNode } from './markup/utils' + +const defaultSchema = getSchema(defaultExtensions) /** * @public */ -export function markupToYDoc (markup: Markup, field: string): YDoc { - const node = markupToPmNode(markup) +export function markupToYDoc (markup: Markup, field: string, schema?: Schema, extensions?: Extensions): YDoc { + const node = markupToPmNode(markup, schema, extensions) return prosemirrorToYDoc(node, field) } +/** + * Convert markup to Y.Doc without using ProseMirror schema + * + * @public + */ +export function markupToYDocNoSchema (markup: Markup, field: string): YDoc { + return jsonToYDocNoSchema(markupToJSON(markup), field) +} + +/** + * Convert ProseMirror JSON to Y.Doc without using ProseMirror schema + * + * @public + */ +export function jsonToYDocNoSchema (json: MarkupNode, field: string): YDoc { + const nodes = json.type === 'doc' ? json.content ?? [] : [json] + const content = nodes.map(nodeToYXmlElement) + + const ydoc = new YDoc() + + const fragment = ydoc.getXmlFragment(field) + fragment.push(content) + + return ydoc +} + +/** + * Convert ProseMirror JSON Node representation to YXmlElement + * */ +function nodeToYXmlElement (node: MarkupNode): YXmlElement | YXmlText { + const elem = node.type === 'text' ? new YXmlText() : new YXmlElement(node.type) + + if (elem instanceof YXmlElement) { + if (node.content !== undefined && node.content.length > 0) { + const content = node.content.map(nodeToYXmlElement) + elem.push(content) + } + } else { + // https://github.com/yjs/y-prosemirror/blob/master/src/plugins/sync-plugin.js#L777 + const attributes: Record = {} + if (node.marks !== undefined) { + node.marks.forEach((mark) => { + attributes[mark.type] = mark.attrs ?? {} + }) + } + elem.applyDelta([ + { + insert: node.text ?? '', + attributes + } + ]) + } + + if (node.attrs !== undefined) { + Object.entries(node.attrs).forEach(([key, value]) => { + elem.setAttribute(key, value) + }) + } + + return elem +} + /** * @public */ @@ -38,43 +102,6 @@ export function yDocToMarkup (ydoc: YDoc, field: string): Markup { return jsonToMarkup(json as MarkupNode) } -/** - * Get ProseMirror node from Y.Doc content - * - * @public - */ -export function yDocContentToNode ( - content: ArrayBuffer, - field?: string, - schema?: Schema, - extensions?: Extensions -): Node { - const ydoc = new YDoc() - const uint8arr = new Uint8Array(content) - applyUpdate(ydoc, uint8arr) - - return yDocToNode(ydoc, field, schema, extensions) -} - -const defaultSchema = getSchema(defaultExtensions) - -/** - * Get ProseMirror node from Y.Doc - * - * @public - */ -export function yDocToNode (ydoc: YDoc, field?: string, schema?: Schema, extensions?: Extensions): Node { - schema ??= getSchema(extensions ?? defaultExtensions) - - try { - const body = yDocToProsemirrorJSON(ydoc, field) - return schema.nodeFromJSON(body) - } catch (err: any) { - console.error(err) - return schema.node(schema.topNodeType) - } -} - /** * Get ProseMirror nodes from Y.Doc content * @@ -117,7 +144,7 @@ export function updateYDocContent ( schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions) try { - const ydoc = new YDoc() + const ydoc = new YDoc({ gc: false }) const res = new YDoc({ gc: false }) const uint8arr = new Uint8Array(content) applyUpdate(ydoc, uint8arr) @@ -135,20 +162,3 @@ export function updateYDocContent ( console.error(err) } } - -/** - * Create Y.Doc - * - * @public - */ -export function YDocFromContent (content: MarkupNode, field: string, schema?: Schema, extensions?: Extensions): YDoc { - schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions) - - const res = new YDoc({ gc: false }) - - const yDoc = prosemirrorJSONToYDoc(schema, content, field) - const update = encodeStateAsUpdate(yDoc) - applyUpdate(res, update) - - return res -} diff --git a/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte b/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte index 8d4edeb947..8661ce0c7a 100644 --- a/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte +++ b/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte @@ -16,7 +16,7 @@ import { Class, Doc, Ref } from '@hcengineering/core' import notification from '@hcengineering/notification' import { Panel } from '@hcengineering/panel' - import { getResource } from '@hcengineering/platform' + import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform' import { getClient } from '@hcengineering/presentation' import { Collaboration } from '@hcengineering/text-editor-resources' import { @@ -218,15 +218,19 @@ const project = await getLatestProjectId($controlledDocument.space) if (project !== undefined) { - const id = await createNewDraftForControlledDoc( - client, - $controlledDocument, - $controlledDocument.space, - version, - project - ) - const loc = getProjectDocumentLink(id, project) - navigate(loc) + try { + const id = await createNewDraftForControlledDoc( + client, + $controlledDocument, + $controlledDocument.space, + version, + project + ) + const loc = getProjectDocumentLink(id, project) + navigate(loc) + } catch (err) { + await setPlatformStatus(unknownError(err)) + } } else { console.warn('No document project found for space', $controlledDocument.space) } @@ -237,7 +241,11 @@ async function onEditDocument (): Promise { if ($controlledDocument != null && $canCreateNewSnapshot && $isProjectEditable) { - await createDocumentSnapshotAndEdit(client, $controlledDocument) + try { + await createDocumentSnapshotAndEdit(client, $controlledDocument) + } catch (err) { + await setPlatformStatus(unknownError(err)) + } } else { console.warn('Unexpected document state', $documentState) } diff --git a/plugins/controlled-documents-resources/src/components/document/DocumentDiffViewer.svelte b/plugins/controlled-documents-resources/src/components/document/DocumentDiffViewer.svelte index 13f7bd6ed3..802da8264e 100644 --- a/plugins/controlled-documents-resources/src/components/document/DocumentDiffViewer.svelte +++ b/plugins/controlled-documents-resources/src/components/document/DocumentDiffViewer.svelte @@ -5,10 +5,9 @@ import { CollaborationIds, type Ydoc } from '@hcengineering/text-editor' import { CollaborationDiffViewer, + Provider, StringDiffViewer, - TiptapCollabProvider, - createTiptapCollaborationData, - formatCollaborativeDocumentId + createTiptapCollaborationData } from '@hcengineering/text-editor-resources' import { Dropdown, Label, ListItem, Loading, Scroller, themeStore } from '@hcengineering/ui' import documents, { @@ -25,7 +24,7 @@ $documentComparisonVersions as documentComparisonVersions, comparisonRequested } from '../../stores/editors/document' - import { COLLABORATOR_URL, TOKEN, getTranslatedControlledDocStates, getTranslatedDocumentStates } from '../../utils' + import { getTranslatedControlledDocStates, getTranslatedDocumentStates } from '../../utils' import DocumentTitle from './DocumentTitle.svelte' const client = getClient() @@ -33,7 +32,7 @@ const ydoc = getContext(CollaborationIds.Doc) let comparedYdoc: Ydoc | undefined = undefined - let comparedProvider: TiptapCollabProvider | undefined = undefined + let comparedProvider: Provider | undefined = undefined let loading = true const handleSelect = (event: CustomEvent) => { @@ -86,23 +85,20 @@ })) $: if ($compareTo) { if (comparedProvider) { - comparedProvider.disconnect() + comparedProvider.destroy() } loading = true - const collaborativeDoc = $compareTo.content const data = createTiptapCollaborationData({ - collaboratorURL: COLLABORATOR_URL, - token: TOKEN, - documentId: formatCollaborativeDocumentId(collaborativeDoc) + document: $compareTo.content }) comparedYdoc = data.ydoc comparedProvider = data.provider - comparedProvider.loaded.then(() => (loading = false)) + void comparedProvider.loaded.then(() => (loading = false)) } onDestroy(() => { - comparedProvider?.destroy() + void comparedProvider?.destroy() }) diff --git a/plugins/controlled-documents-resources/src/docutils.ts b/plugins/controlled-documents-resources/src/docutils.ts index d0ed0e42c9..0a148dc981 100644 --- a/plugins/controlled-documents-resources/src/docutils.ts +++ b/plugins/controlled-documents-resources/src/docutils.ts @@ -25,7 +25,7 @@ import { type MixinData } from '@hcengineering/core' import { translate } from '@hcengineering/platform' -import { copyDocument, takeSnapshot } from '@hcengineering/presentation' +import { copyDocument } from '@hcengineering/presentation' import { themeStore } from '@hcengineering/ui' import documents, { type ControlledDocument, @@ -55,6 +55,18 @@ export async function createNewDraftForControlledDoc ( newDraftDocId = newDraftDocId ?? generateId() + const collaborativeDoc = getCollaborativeDocForDocument( + `DOC-${document.prefix}`, + document.seqNumber, + document.major, + document.minor, + true + ) + + if (document.content !== undefined) { + await copyDocument(document.content, collaborativeDoc) + } + // Create new change control for new version const newCCId = generateId() const newCCSpec: Data = { @@ -66,14 +78,6 @@ export async function createNewDraftForControlledDoc ( await createChangeControl(client, newCCId, newCCSpec, document.space) - const collaborativeDoc = getCollaborativeDocForDocument( - `DOC-${document.prefix}`, - document.seqNumber, - document.major, - document.minor, - true - ) - // TODO: copy labels? const docSpec: AttachedData = { ...(document.template != null ? { template: document.template } : {}), @@ -133,10 +137,6 @@ export async function createNewDraftForControlledDoc ( }) } - if (document.content !== undefined) { - await copyDocument(document.content, collaborativeDoc) - } - const documentTraining = getDocumentTraining(hierarchy, document) if (documentTraining !== undefined) { const newDraftDoc = await client.findOne(document._class, { _id: newDraftDocId }) @@ -158,10 +158,19 @@ export async function createNewDraftForControlledDoc ( } export async function createDocumentSnapshotAndEdit (client: TxOperations, document: ControlledDocument): Promise { + const collaborativeDoc = getCollaborativeDocForDocument( + `DOC-${document.prefix}`, + document.seqNumber, + document.major, + document.minor, + true + ) + + await copyDocument(document.content, collaborativeDoc) + const language = get(themeStore).language const namePrefix = await translate(documents.string.DraftRevision, {}, language) const name = `${namePrefix} ${(document.snapshots ?? 0) + 1}` - const snapshot = await takeSnapshot(document.content, name) const newSnapshotId = generateId() const op = client.apply(document._id) @@ -176,7 +185,7 @@ export async function createDocumentSnapshotAndEdit (client: TxOperations, docum name, state: document.state, controlledState: document.controlledState, - content: snapshot + content: collaborativeDoc }, newSnapshotId ) diff --git a/plugins/controlled-documents-resources/src/utils.ts b/plugins/controlled-documents-resources/src/utils.ts index 98c9a52027..7cb2685325 100644 --- a/plugins/controlled-documents-resources/src/utils.ts +++ b/plugins/controlled-documents-resources/src/utils.ts @@ -27,11 +27,10 @@ import core, { getCurrentAccount, checkPermission } from '@hcengineering/core' -import { type IntlString, getMetadata, translate } from '@hcengineering/platform' -import presentation, { getClient } from '@hcengineering/presentation' +import { type IntlString, translate } from '@hcengineering/platform' +import { getClient } from '@hcengineering/presentation' import { type Person, type Employee, type PersonAccount } from '@hcengineering/contact' import request, { RequestStatus } from '@hcengineering/request' -import textEditor from '@hcengineering/text-editor' import { isEmptyMarkup } from '@hcengineering/text' import { showPopup, getUserTimezone, type Location } from '@hcengineering/ui' import { type KeyFilter } from '@hcengineering/view' @@ -62,9 +61,6 @@ import { wizardOpened } from './stores/wizards/create-document' export type TranslatedDocumentStates = Readonly> -export const TOKEN = getMetadata(presentation.metadata.Token) ?? '' -export const COLLABORATOR_URL = getMetadata(textEditor.metadata.CollaboratorUrl) ?? '' - export const isDocumentCommentAttachedTo = ( value: DocumentComment | null | undefined, location: { nodeId?: string | null } diff --git a/plugins/document-resources/src/components/CreateSnapshot.svelte b/plugins/document-resources/src/components/CreateSnapshot.svelte index 0d8e5cb288..c6dca74eba 100644 --- a/plugins/document-resources/src/components/CreateSnapshot.svelte +++ b/plugins/document-resources/src/components/CreateSnapshot.svelte @@ -16,7 +16,7 @@ --> diff --git a/plugins/text-editor-resources/package.json b/plugins/text-editor-resources/package.json index b176d93471..41e0a33fa8 100644 --- a/plugins/text-editor-resources/package.json +++ b/plugins/text-editor-resources/package.json @@ -72,6 +72,7 @@ "prosemirror-codemark": "^0.4.2", "y-protocols": "^1.0.6", "y-prosemirror": "^1.2.1", + "y-websocket": "^2.0.4", "yjs": "^13.5.52", "fast-equals": "^5.0.1", "rfc6902": "^5.0.1", diff --git a/plugins/text-editor-resources/src/components/Collaboration.svelte b/plugins/text-editor-resources/src/components/Collaboration.svelte index 70cd6bf023..ac9e2b4647 100644 --- a/plugins/text-editor-resources/src/components/Collaboration.svelte +++ b/plugins/text-editor-resources/src/components/Collaboration.svelte @@ -16,14 +16,12 @@ --> diff --git a/plugins/text-editor-resources/src/components/CollaborationUsers.svelte b/plugins/text-editor-resources/src/components/CollaborationUsers.svelte index b3beced048..717e38c8e3 100644 --- a/plugins/text-editor-resources/src/components/CollaborationUsers.svelte +++ b/plugins/text-editor-resources/src/components/CollaborationUsers.svelte @@ -16,29 +16,32 @@ --> diff --git a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte index c534421264..60494cc050 100644 --- a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte +++ b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte @@ -15,10 +15,9 @@ // -->