mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
Cloud collaborator refactoring (#6424)
This commit is contained in:
parent
42b19c39c7
commit
4ff8a4559f
@ -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'
|
||||
|
@ -206,7 +206,7 @@ export async function configurePlatform (): Promise<void> {
|
||||
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 ?? '')
|
||||
|
@ -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
|
||||
|
@ -8,6 +8,7 @@ import { HtmlConversionBackend } from './convert/convert'
|
||||
export interface Config {
|
||||
doc: string
|
||||
token: string
|
||||
collaborator?: string
|
||||
collaboratorURL: string
|
||||
uploadURL: string
|
||||
workspaceId: WorkspaceId
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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<Account>
|
||||
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<string, Markup>
|
||||
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<Account>
|
||||
createdOn: Timestamp
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface CollaboratorClient {
|
||||
// field operations
|
||||
getContent: (collaborativeDoc: CollaborativeDoc) => Promise<Record<string, Markup>>
|
||||
updateContent: (
|
||||
document: CollaborativeDoc,
|
||||
content: Record<string, Markup>,
|
||||
snapshot?: DocumentSnapshotParams
|
||||
) => Promise<CollaborativeDoc>
|
||||
copyContent: (
|
||||
document: CollaborativeDoc,
|
||||
sourceField: string,
|
||||
targetField: string,
|
||||
snapshot?: DocumentSnapshotParams
|
||||
) => Promise<CollaborativeDoc>
|
||||
|
||||
// document operations
|
||||
branch: (source: CollaborativeDoc, target: CollaborativeDoc) => Promise<void>
|
||||
remove: (collaborativeDoc: CollaborativeDoc) => Promise<void>
|
||||
snapshot: (collaborativeDoc: CollaborativeDoc, params: DocumentSnapshotParams) => Promise<CollaborativeDoc>
|
||||
getContent: (document: CollaborativeDoc) => Promise<Record<string, Markup>>
|
||||
updateContent: (document: CollaborativeDoc, content: Record<string, Markup>) => Promise<void>
|
||||
copyContent: (source: CollaborativeDoc, target: CollaborativeDoc) => Promise<void>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@ -136,7 +54,10 @@ class CollaboratorClientImpl implements CollaboratorClient {
|
||||
private readonly collaboratorUrl: string
|
||||
) {}
|
||||
|
||||
private async rpc (method: string, payload: any): Promise<any> {
|
||||
private async rpc (document: CollaborativeDoc, method: string, payload: any): Promise<any> {
|
||||
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<Record<string, Markup>> {
|
||||
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<string, Markup>,
|
||||
snapshot?: DocumentSnapshotParams
|
||||
): Promise<CollaborativeDoc> {
|
||||
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<string, Markup>): Promise<void> {
|
||||
await this.rpc(document, 'updateContent', { content })
|
||||
}
|
||||
|
||||
async copyContent (
|
||||
document: CollaborativeDoc,
|
||||
sourceField: string,
|
||||
targetField: string,
|
||||
snapshot?: DocumentSnapshotParams
|
||||
): Promise<CollaborativeDoc> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<CollaborativeDoc> {
|
||||
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<void> {
|
||||
const content = await this.getContent(source)
|
||||
await this.updateContent(target, content)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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<void> {
|
||||
const client = getCollaboratorClient()
|
||||
await client.copyContent(collaborativeDoc, sourceField, targetField)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function copyDocument (source: CollaborativeDoc, target: CollaborativeDoc): Promise<void> {
|
||||
const client = getCollaboratorClient()
|
||||
await client.branch(source, target)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function takeSnapshot (collaborativeDoc: CollaborativeDoc, versionName: string): Promise<CollaborativeDoc> {
|
||||
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)
|
||||
}
|
||||
|
63
packages/text/src/__tests__/ydoc.test.ts
Normal file
63
packages/text/src/__tests__/ydoc.test.ts
Normal file
@ -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))
|
||||
})
|
||||
})
|
||||
})
|
@ -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<string, any> = {}
|
||||
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
|
||||
}
|
||||
|
@ -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<void> {
|
||||
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)
|
||||
}
|
||||
|
@ -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<Ydoc>(CollaborationIds.Doc)
|
||||
|
||||
let comparedYdoc: Ydoc | undefined = undefined
|
||||
let comparedProvider: TiptapCollabProvider | undefined = undefined
|
||||
let comparedProvider: Provider | undefined = undefined
|
||||
let loading = true
|
||||
|
||||
const handleSelect = (event: CustomEvent<ListItem>) => {
|
||||
@ -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()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -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<ChangeControl>()
|
||||
const newCCSpec: Data<ChangeControl> = {
|
||||
@ -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<ControlledDocument> = {
|
||||
...(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<void> {
|
||||
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<ControlledDocumentSnapshot>()
|
||||
|
||||
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
|
||||
)
|
||||
|
@ -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<Record<DocumentState, string>>
|
||||
|
||||
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 }
|
||||
|
@ -16,7 +16,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Document } from '@hcengineering/document'
|
||||
import { Card, getClient, takeSnapshot } from '@hcengineering/presentation'
|
||||
import { Card, getClient } from '@hcengineering/presentation'
|
||||
import { EditBox } from '@hcengineering/ui'
|
||||
import document from '../plugin'
|
||||
|
||||
@ -27,19 +27,19 @@
|
||||
let name = ''
|
||||
|
||||
async function create (): Promise<void> {
|
||||
const snapshot = await takeSnapshot(doc.content, name)
|
||||
|
||||
await client.addCollection(
|
||||
document.class.DocumentSnapshot,
|
||||
doc.space,
|
||||
doc._id,
|
||||
document.class.Document,
|
||||
'snapshots',
|
||||
{
|
||||
name,
|
||||
content: snapshot
|
||||
}
|
||||
)
|
||||
// TODO implement me
|
||||
// const snapshot = await takeSnapshot(doc.content, name)
|
||||
// await client.addCollection(
|
||||
// document.class.DocumentSnapshot,
|
||||
// doc.space,
|
||||
// doc._id,
|
||||
// document.class.Document,
|
||||
// 'snapshots',
|
||||
// {
|
||||
// name,
|
||||
// content: snapshot
|
||||
// }
|
||||
// )
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -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",
|
||||
|
@ -16,14 +16,12 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { type DocumentId, type PlatformDocumentId } from '@hcengineering/collaborator-client'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { onDestroy, setContext } from 'svelte'
|
||||
import textEditor, { CollaborationIds } from '@hcengineering/text-editor'
|
||||
import { CollaborationIds } from '@hcengineering/text-editor'
|
||||
|
||||
import { TiptapCollabProvider, createTiptapCollaborationData } from '../provider/tiptap'
|
||||
import { formatCollaborativeDocumentId, formatPlatformDocumentId } from '../provider/utils'
|
||||
import { createTiptapCollaborationData } from '../provider/utils'
|
||||
import { Provider } from '../provider/types'
|
||||
import { formatDocumentId } from '@hcengineering/collaborator-client'
|
||||
|
||||
export let collaborativeDoc: CollaborativeDoc
|
||||
export let initialCollaborativeDoc: CollaborativeDoc | undefined = undefined
|
||||
@ -32,50 +30,34 @@
|
||||
export let objectId: Ref<Doc> | undefined = undefined
|
||||
export let objectAttr: string | undefined = undefined
|
||||
|
||||
const token = getMetadata(presentation.metadata.Token) ?? ''
|
||||
const collaboratorURL = getMetadata(textEditor.metadata.CollaboratorUrl) ?? ''
|
||||
let provider: Provider | undefined
|
||||
|
||||
let initialContentId: DocumentId | undefined
|
||||
let platformDocumentId: PlatformDocumentId | undefined
|
||||
// while editing, collaborative doc may change, hence we need to
|
||||
// build stable key to ensure we don't do unnecessary updates
|
||||
$: documentId = formatDocumentId('', collaborativeDoc)
|
||||
|
||||
$: documentId = formatCollaborativeDocumentId(collaborativeDoc)
|
||||
$: if (initialCollaborativeDoc !== undefined) {
|
||||
initialContentId = formatCollaborativeDocumentId(initialCollaborativeDoc)
|
||||
}
|
||||
$: if (objectClass !== undefined && objectId !== undefined && objectAttr !== undefined) {
|
||||
platformDocumentId = formatPlatformDocumentId(objectClass, objectId, objectAttr)
|
||||
}
|
||||
|
||||
let _documentId: DocumentId | undefined
|
||||
|
||||
let provider: TiptapCollabProvider | undefined
|
||||
let _documentId: string | undefined
|
||||
|
||||
$: if (_documentId !== documentId) {
|
||||
_documentId = documentId
|
||||
|
||||
if (provider !== undefined) {
|
||||
provider.disconnect()
|
||||
void provider.destroy()
|
||||
}
|
||||
const data = createTiptapCollaborationData({
|
||||
documentId,
|
||||
initialContentId,
|
||||
platformDocumentId,
|
||||
collaboratorURL,
|
||||
token
|
||||
document: collaborativeDoc,
|
||||
initialDocument: initialCollaborativeDoc,
|
||||
objectClass,
|
||||
objectId,
|
||||
objectAttr
|
||||
})
|
||||
provider = data.provider
|
||||
setContext(CollaborationIds.Doc, data.ydoc)
|
||||
setContext(CollaborationIds.Provider, provider)
|
||||
|
||||
provider.on('status', (event: any) => {
|
||||
console.log('Collaboration:', documentId, event.status) // logs "connected" or "disconnected"
|
||||
})
|
||||
provider.on('synced', (event: any) => {
|
||||
console.log('Collaboration:', event) // logs "connected" or "disconnected"
|
||||
})
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
provider?.destroy()
|
||||
void provider?.destroy()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -16,29 +16,32 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import type { AwarenessState, AwarenessStateMap } from '@hcengineering/text-editor'
|
||||
import { AnySvelteComponent, Button, DelayedCaller } from '@hcengineering/ui'
|
||||
import { onMount } from 'svelte'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { onMount } from 'svelte'
|
||||
import { createRelativePositionFromJSON } from 'yjs'
|
||||
import { relativePositionToAbsolutePosition, ySyncPluginKey } from 'y-prosemirror'
|
||||
import { Provider } from '../provider/types'
|
||||
|
||||
import { TiptapCollabProvider } from '../provider/tiptap'
|
||||
import { AwarenessChangeEvent, CollaborationUserState } from '@hcengineering/text-editor'
|
||||
|
||||
export let provider: TiptapCollabProvider
|
||||
export let provider: Provider
|
||||
export let editor: Editor
|
||||
export let component: AnySvelteComponent
|
||||
|
||||
let states: CollaborationUserState[] = []
|
||||
let states: AwarenessState[] = []
|
||||
|
||||
const debounce = new DelayedCaller(100)
|
||||
const onAwarenessChange = (event: AwarenessChangeEvent): void => {
|
||||
const onAwarenessChange = (): void => {
|
||||
debounce.call(() => {
|
||||
states = event.states.filter((p) => p.user != null).filter((p) => p.clientId !== provider.awareness?.clientID)
|
||||
const map: AwarenessStateMap = provider.awareness?.states ?? new Map()
|
||||
const entries: Array<[number, AwarenessState]> = Array.from(map.entries())
|
||||
states = entries
|
||||
.filter(([clientId, state]) => clientId !== provider.awareness?.clientID && state.user != null)
|
||||
.map(([_, state]) => state)
|
||||
})
|
||||
}
|
||||
|
||||
function goToCursor (state: CollaborationUserState): void {
|
||||
function goToCursor (state: AwarenessState): void {
|
||||
const cursor = state.cursor
|
||||
if (cursor?.head != null) {
|
||||
try {
|
||||
@ -60,8 +63,8 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
provider.on('awarenessUpdate', onAwarenessChange)
|
||||
return () => provider.off('awarenessUpdate', onAwarenessChange)
|
||||
provider.awareness?.on('update', onAwarenessChange)
|
||||
return () => provider.awareness?.off('update', onAwarenessChange)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -15,10 +15,9 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type DocumentId, type PlatformDocumentId } from '@hcengineering/collaborator-client'
|
||||
import { type Space, type Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { IntlString, getMetadata, translate } from '@hcengineering/platform'
|
||||
import presentation, { getFileUrl, getImageSize } from '@hcengineering/presentation'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { getFileUrl, getImageSize } from '@hcengineering/presentation'
|
||||
import { markupToJSON } from '@hcengineering/text'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
@ -43,9 +42,8 @@
|
||||
import { deleteAttachment } from '../command/deleteAttachment'
|
||||
import { textEditorCommandHandler } from '../commands'
|
||||
import { EditorKitOptions, getEditorKit } from '../../src/kits/editor-kit'
|
||||
import { IndexeddbProvider } from '../provider/indexeddb'
|
||||
import { TiptapCollabProvider } from '../provider/tiptap'
|
||||
import { formatCollaborativeDocumentId, formatPlatformDocumentId } from '../provider/utils'
|
||||
import { Provider } from '../provider/types'
|
||||
import { createLocalProvider, createRemoteProvider } from '../provider/utils'
|
||||
import textEditor, {
|
||||
CollaborationIds,
|
||||
CollaborationUser,
|
||||
@ -103,37 +101,19 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const token = getMetadata(presentation.metadata.Token) ?? ''
|
||||
const collaboratorURL = getMetadata(textEditor.metadata.CollaboratorUrl) ?? ''
|
||||
|
||||
const documentId = formatCollaborativeDocumentId(collaborativeDoc)
|
||||
|
||||
let initialContentId: DocumentId | undefined
|
||||
if (initialCollaborativeDoc !== undefined) {
|
||||
initialContentId = formatCollaborativeDocumentId(collaborativeDoc)
|
||||
}
|
||||
|
||||
let platformDocumentId: PlatformDocumentId | undefined
|
||||
if (objectClass !== undefined && objectId !== undefined && objectAttr !== undefined) {
|
||||
platformDocumentId = formatPlatformDocumentId(objectClass, objectId, objectAttr)
|
||||
}
|
||||
|
||||
const ydoc = getContext<YDoc>(CollaborationIds.Doc) ?? new YDoc()
|
||||
const contextProvider = getContext<TiptapCollabProvider>(CollaborationIds.Provider)
|
||||
const contextProvider = getContext<Provider>(CollaborationIds.Provider)
|
||||
|
||||
const localProvider = new IndexeddbProvider(collaborativeDoc, ydoc)
|
||||
const localProvider = createLocalProvider(ydoc, collaborativeDoc)
|
||||
|
||||
const remoteProvider: TiptapCollabProvider =
|
||||
const remoteProvider =
|
||||
contextProvider ??
|
||||
new TiptapCollabProvider({
|
||||
url: collaboratorURL,
|
||||
name: documentId,
|
||||
document: ydoc,
|
||||
token,
|
||||
parameters: {
|
||||
initialContentId,
|
||||
platformDocumentId
|
||||
}
|
||||
createRemoteProvider(ydoc, {
|
||||
document: collaborativeDoc,
|
||||
initialDocument: initialCollaborativeDoc,
|
||||
objectClass,
|
||||
objectId,
|
||||
objectAttr
|
||||
})
|
||||
|
||||
let localSynced = false
|
||||
@ -478,7 +458,7 @@
|
||||
} catch (err: any) {}
|
||||
}
|
||||
if (contextProvider === undefined) {
|
||||
remoteProvider.destroy()
|
||||
void remoteProvider.destroy()
|
||||
}
|
||||
void localProvider.destroy()
|
||||
})
|
||||
|
@ -73,12 +73,8 @@ export { InlineToolbarExtension, type InlineStyleToolbarOptions } from './compon
|
||||
export { ImageExtension, type ImageOptions } from './components/extension/imageExt'
|
||||
export { ImageUploadExtension, type ImageUploadOptions } from './components/extension/imageUploadExt'
|
||||
export * from './command/deleteAttachment'
|
||||
export {
|
||||
TiptapCollabProvider,
|
||||
type TiptapCollabProviderConfiguration,
|
||||
createTiptapCollaborationData
|
||||
} from './provider/tiptap'
|
||||
export { formatCollaborativeDocumentId, formatPlatformDocumentId } from './provider/utils'
|
||||
export { createTiptapCollaborationData } from './provider/utils'
|
||||
export { type Provider } from './provider/types'
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
function: {
|
||||
|
44
plugins/text-editor-resources/src/provider/cloud.ts
Normal file
44
plugins/text-editor-resources/src/provider/cloud.ts
Normal file
@ -0,0 +1,44 @@
|
||||
//
|
||||
// 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 { type Doc as YDoc } from 'yjs'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
|
||||
import { type Provider } from './types'
|
||||
|
||||
export interface DatalakeCollabProviderParameters {
|
||||
url: string
|
||||
name: string
|
||||
token: string
|
||||
|
||||
document: YDoc
|
||||
}
|
||||
|
||||
export class CloudCollabProvider extends WebsocketProvider implements Provider {
|
||||
readonly loaded: Promise<void>
|
||||
|
||||
constructor (params: DatalakeCollabProviderParameters) {
|
||||
const { document, url, name } = params
|
||||
|
||||
super(url, encodeURIComponent(name), document)
|
||||
|
||||
this.loaded = new Promise((resolve) => {
|
||||
this.on('synced', resolve)
|
||||
})
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
this.disconnect()
|
||||
}
|
||||
}
|
@ -14,23 +14,23 @@
|
||||
//
|
||||
import { type DocumentId, type PlatformDocumentId } from '@hcengineering/collaborator-client'
|
||||
import { HocuspocusProvider, type HocuspocusProviderConfiguration } from '@hocuspocus/provider'
|
||||
import { Doc as Ydoc } from 'yjs'
|
||||
import { type Provider } from './types'
|
||||
|
||||
export type TiptapCollabProviderConfiguration = HocuspocusProviderConfiguration &
|
||||
export type HocuspocusCollabProviderConfiguration = HocuspocusProviderConfiguration &
|
||||
Required<Pick<HocuspocusProviderConfiguration, 'token'>> &
|
||||
Omit<HocuspocusProviderConfiguration, 'parameters'> & {
|
||||
parameters: TiptapCollabProviderURLParameters
|
||||
parameters: HocuspocusCollabProviderURLParameters
|
||||
}
|
||||
|
||||
export interface TiptapCollabProviderURLParameters {
|
||||
export interface HocuspocusCollabProviderURLParameters {
|
||||
initialContentId?: DocumentId
|
||||
platformDocumentId?: PlatformDocumentId
|
||||
}
|
||||
|
||||
export class TiptapCollabProvider extends HocuspocusProvider {
|
||||
loaded: Promise<void>
|
||||
export class HocuspocusCollabProvider extends HocuspocusProvider implements Provider {
|
||||
readonly loaded: Promise<void>
|
||||
|
||||
constructor (configuration: TiptapCollabProviderConfiguration) {
|
||||
constructor (configuration: HocuspocusCollabProviderConfiguration) {
|
||||
const parameters: Record<string, any> = {}
|
||||
|
||||
const initialContentId = configuration.parameters?.initialContentId
|
||||
@ -59,26 +59,3 @@ export class TiptapCollabProvider extends HocuspocusProvider {
|
||||
this.configuration.websocketProvider.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
export const createTiptapCollaborationData = (params: {
|
||||
documentId: string
|
||||
initialContentId?: DocumentId
|
||||
platformDocumentId?: PlatformDocumentId
|
||||
collaboratorURL: string
|
||||
token: string
|
||||
}): { provider: TiptapCollabProvider, ydoc: Ydoc } => {
|
||||
const ydoc: Ydoc = new Ydoc()
|
||||
return {
|
||||
ydoc,
|
||||
provider: new TiptapCollabProvider({
|
||||
url: params.collaboratorURL,
|
||||
name: params.documentId,
|
||||
document: ydoc,
|
||||
token: params.token,
|
||||
parameters: {
|
||||
initialContentId: params.initialContentId,
|
||||
platformDocumentId: params.platformDocumentId
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -12,16 +12,23 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { collaborativeDocParse, type CollaborativeDoc } from '@hcengineering/core'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
|
||||
import { type Doc as YDoc } from 'yjs'
|
||||
import { IndexeddbPersistence } from 'y-indexeddb'
|
||||
import { type Awareness } from 'y-protocols/awareness'
|
||||
|
||||
export class IndexeddbProvider extends IndexeddbPersistence {
|
||||
loaded: Promise<void>
|
||||
import { type Provider } from './types'
|
||||
|
||||
constructor (collaborativeDoc: CollaborativeDoc, doc: YDoc) {
|
||||
const { documentId, versionId } = collaborativeDocParse(collaborativeDoc)
|
||||
const name = `${documentId}/${versionId}`
|
||||
export class IndexeddbProvider extends IndexeddbPersistence implements Provider {
|
||||
readonly loaded: Promise<void>
|
||||
readonly awareness: Awareness | null = null
|
||||
|
||||
constructor (documentId: string, doc: YDoc) {
|
||||
const workspaceId: string = getMetadata(presentation.metadata.Workspace) ?? ''
|
||||
|
||||
const name = `${workspaceId}/${documentId}`
|
||||
|
||||
super(name, doc)
|
||||
|
||||
@ -29,4 +36,8 @@ export class IndexeddbProvider extends IndexeddbPersistence {
|
||||
this.on('synced', resolve)
|
||||
})
|
||||
}
|
||||
|
||||
async destroy (): Promise<void> {
|
||||
await super.destroy()
|
||||
}
|
||||
}
|
||||
|
23
plugins/text-editor-resources/src/provider/types.ts
Normal file
23
plugins/text-editor-resources/src/provider/types.ts
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright © 2023, 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 { type Awareness } from 'y-protocols/awareness'
|
||||
|
||||
export interface Provider {
|
||||
loaded: Promise<void>
|
||||
awareness: Awareness | null
|
||||
|
||||
destroy: () => void | Promise<void>
|
||||
}
|
@ -17,18 +17,22 @@ import { type Ref, type CollaborativeDoc, type Doc, type Class } from '@hcengine
|
||||
import {
|
||||
type DocumentId,
|
||||
type PlatformDocumentId,
|
||||
formatMinioDocumentId,
|
||||
formatDocumentId,
|
||||
formatPlatformDocumentId as origFormatPlatformDocumentId
|
||||
} from '@hcengineering/collaborator-client'
|
||||
import { getCurrentLocation } from '@hcengineering/ui'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
import { Doc as Ydoc } from 'yjs'
|
||||
|
||||
function getWorkspace (): string {
|
||||
return getCurrentLocation().path[1] ?? ''
|
||||
}
|
||||
import { CloudCollabProvider } from './cloud'
|
||||
import { HocuspocusCollabProvider } from './hocuspocus'
|
||||
import { IndexeddbProvider } from './indexeddb'
|
||||
import { type Provider } from './types'
|
||||
|
||||
export function formatCollaborativeDocumentId (collaborativeDoc: CollaborativeDoc): DocumentId {
|
||||
const workspace = getWorkspace()
|
||||
return formatMinioDocumentId(workspace, collaborativeDoc)
|
||||
const workspace = getMetadata(presentation.metadata.Workspace) ?? ''
|
||||
return formatDocumentId(workspace, collaborativeDoc)
|
||||
}
|
||||
|
||||
export function formatPlatformDocumentId (
|
||||
@ -38,3 +42,66 @@ export function formatPlatformDocumentId (
|
||||
): PlatformDocumentId {
|
||||
return origFormatPlatformDocumentId(objectClass, objectId, objectAttr)
|
||||
}
|
||||
|
||||
export function createLocalProvider (ydoc: Ydoc, document: CollaborativeDoc): Provider {
|
||||
const documentId = formatCollaborativeDocumentId(document)
|
||||
return new IndexeddbProvider(documentId, ydoc)
|
||||
}
|
||||
|
||||
export function createRemoteProvider (
|
||||
ydoc: Ydoc,
|
||||
params: {
|
||||
document: CollaborativeDoc
|
||||
initialDocument?: CollaborativeDoc
|
||||
objectClass?: Ref<Class<Doc>>
|
||||
objectId?: Ref<Doc>
|
||||
objectAttr?: string
|
||||
}
|
||||
): Provider {
|
||||
const collaborator = getMetadata(textEditor.metadata.Collaborator)
|
||||
|
||||
const token = getMetadata(presentation.metadata.Token) ?? ''
|
||||
const collaboratorUrl = getMetadata(presentation.metadata.CollaboratorUrl) ?? ''
|
||||
|
||||
const documentId = formatCollaborativeDocumentId(params.document)
|
||||
const initialContentId =
|
||||
params.initialDocument !== undefined ? formatCollaborativeDocumentId(params.initialDocument) : undefined
|
||||
|
||||
const { objectClass, objectId, objectAttr } = params
|
||||
const platformDocumentId =
|
||||
objectClass !== undefined && objectId !== undefined && objectAttr !== undefined
|
||||
? formatPlatformDocumentId(objectClass, objectId, objectAttr)
|
||||
: undefined
|
||||
|
||||
return collaborator === 'cloud'
|
||||
? new CloudCollabProvider({
|
||||
url: collaboratorUrl,
|
||||
name: documentId,
|
||||
document: ydoc,
|
||||
token
|
||||
})
|
||||
: new HocuspocusCollabProvider({
|
||||
url: collaboratorUrl,
|
||||
name: documentId,
|
||||
document: ydoc,
|
||||
token,
|
||||
parameters: {
|
||||
initialContentId,
|
||||
platformDocumentId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const createTiptapCollaborationData = (params: {
|
||||
document: CollaborativeDoc
|
||||
initialDocument?: CollaborativeDoc
|
||||
objectClass?: Ref<Class<Doc>>
|
||||
objectId?: Ref<Doc>
|
||||
objectAttr?: string
|
||||
}): { provider: Provider, ydoc: Ydoc } => {
|
||||
const ydoc: Ydoc = new Ydoc()
|
||||
return {
|
||||
ydoc,
|
||||
provider: createRemoteProvider(ydoc, params)
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { type Class, type Ref } from '@hcengineering/core'
|
||||
import { type Asset, type IntlString, type Metadata, type Plugin, plugin } from '@hcengineering/platform'
|
||||
|
||||
import { type TextEditorExtensionFactory, type RefInputActionItem, TextEditorAction } from './types'
|
||||
import { type TextEditorExtensionFactory, type RefInputActionItem, TextEditorAction, CollaboratorType } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -31,7 +31,7 @@ export default plugin(textEditorId, {
|
||||
TextEditorAction: '' as Ref<Class<TextEditorAction>>
|
||||
},
|
||||
metadata: {
|
||||
CollaboratorUrl: '' as Metadata<string>
|
||||
Collaborator: '' as Metadata<CollaboratorType>
|
||||
},
|
||||
string: {
|
||||
TableOfContents: '' as IntlString,
|
||||
|
@ -6,6 +6,12 @@ import { type AnyExtension, type Content, type Editor, type SingleCommands } fro
|
||||
import { type ParseOptions } from '@tiptap/pm/model'
|
||||
|
||||
export type { AnyExtension, Editor } from '@tiptap/core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type CollaboratorType = 'local' | 'cloud'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -122,8 +128,7 @@ export interface CollaborationUser {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface CollaborationUserState {
|
||||
clientId: number
|
||||
export interface AwarenessState {
|
||||
user: CollaborationUser
|
||||
cursor?: {
|
||||
anchor: RelativePosition
|
||||
@ -133,9 +138,10 @@ export interface CollaborationUserState {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface AwarenessChangeEvent {
|
||||
states: CollaborationUserState[]
|
||||
}
|
||||
export type AwarenessClientId = number
|
||||
|
||||
/** @public */
|
||||
export type AwarenessStateMap = Map<AwarenessClientId, AwarenessState>
|
||||
|
||||
export type TextEditorMode = 'full' | 'compact'
|
||||
export type ExtensionCreator = (mode: TextEditorMode, ctx: any) => AnyExtension
|
||||
|
@ -19,11 +19,11 @@ import { collaborativeHistoryDocId, isEditableDoc, isEditableDocVersion } from '
|
||||
describe('collaborative-doc', () => {
|
||||
describe('collaborativeHistoryDocId', () => {
|
||||
it('returns valid history doc id', async () => {
|
||||
expect(collaborativeHistoryDocId('minioDocumentId')).toEqual('minioDocumentId#history')
|
||||
expect(collaborativeHistoryDocId('documentId')).toEqual('documentId#history')
|
||||
})
|
||||
|
||||
it('returns valid history doc id for history doc id', async () => {
|
||||
expect(collaborativeHistoryDocId('minioDocumentId#history')).toEqual('minioDocumentId#history')
|
||||
expect(collaborativeHistoryDocId('documentId#history')).toEqual('documentId#history')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -25,9 +25,7 @@ import {
|
||||
import { Doc as YDoc } from 'yjs'
|
||||
|
||||
import { StorageAdapter } from '@hcengineering/server-core'
|
||||
import { yDocBranch } from '../history/branch'
|
||||
import { YDocVersion } from '../history/history'
|
||||
import { createYdocSnapshot, restoreYdocSnapshot } from '../history/snapshot'
|
||||
import { restoreYdocSnapshot } from '../history/snapshot'
|
||||
import { yDocFromStorage, yDocToStorage } from './storage'
|
||||
|
||||
/** @public */
|
||||
@ -151,69 +149,6 @@ export async function removeCollaborativeDoc (
|
||||
})
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function copyCollaborativeDoc (
|
||||
storageAdapter: StorageAdapter,
|
||||
workspace: WorkspaceId,
|
||||
source: CollaborativeDoc,
|
||||
target: CollaborativeDoc,
|
||||
ctx: MeasureContext
|
||||
): Promise<YDoc | undefined> {
|
||||
const { documentId: sourceDocumentId } = collaborativeDocParse(source)
|
||||
const { documentId: targetDocumentId, versionId: targetVersionId } = collaborativeDocParse(target)
|
||||
|
||||
if (sourceDocumentId === targetDocumentId) {
|
||||
// no need to copy into itself
|
||||
return
|
||||
}
|
||||
|
||||
await ctx.with('copyCollaborativeDoc', {}, async (ctx) => {
|
||||
const ySource = await ctx.with('loadCollaborativeDocVersion', {}, async (ctx) => {
|
||||
return await loadCollaborativeDoc(storageAdapter, workspace, source, ctx)
|
||||
})
|
||||
|
||||
if (ySource === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const yTarget = await ctx.with('yDocBranch', {}, () => {
|
||||
return yDocBranch(ySource)
|
||||
})
|
||||
|
||||
await ctx.with('saveCollaborativeDocVersion', {}, async (ctx) => {
|
||||
await saveCollaborativeDocVersion(storageAdapter, workspace, targetDocumentId, targetVersionId, yTarget, ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function takeCollaborativeDocSnapshot (
|
||||
storageAdapter: StorageAdapter,
|
||||
workspace: WorkspaceId,
|
||||
collaborativeDoc: CollaborativeDoc,
|
||||
ydoc: YDoc,
|
||||
version: YDocVersion,
|
||||
ctx: MeasureContext
|
||||
): Promise<void> {
|
||||
const { documentId } = collaborativeDocParse(collaborativeDoc)
|
||||
const historyDocumentId = collaborativeHistoryDocId(documentId)
|
||||
|
||||
await ctx.with('takeCollaborativeDocSnapshot', {}, async (ctx) => {
|
||||
const yHistory =
|
||||
(await ctx.with('yDocFromStorage', { type: 'history' }, async (ctx) => {
|
||||
return await yDocFromStorage(ctx, storageAdapter, workspace, historyDocumentId, new YDoc({ gc: false }))
|
||||
})) ?? new YDoc()
|
||||
|
||||
await ctx.with('createYdocSnapshot', {}, async () => {
|
||||
createYdocSnapshot(ydoc, yHistory, version)
|
||||
})
|
||||
|
||||
await ctx.with('yDocToStorage', { type: 'history' }, async (ctx) => {
|
||||
await yDocToStorage(ctx, storageAdapter, workspace, historyDocumentId, yHistory)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function isEditableDoc (id: CollaborativeDoc): boolean {
|
||||
const { versionId } = collaborativeDocParse(id)
|
||||
|
@ -24,11 +24,11 @@ export async function yDocFromStorage (
|
||||
ctx: MeasureContext,
|
||||
storageAdapter: StorageAdapter,
|
||||
workspace: WorkspaceId,
|
||||
minioDocumentId: string,
|
||||
documentId: string,
|
||||
ydoc?: YDoc
|
||||
): Promise<YDoc | undefined> {
|
||||
// stat the object to ensure it exists, because read will throw an error in this case
|
||||
const blob = await storageAdapter.stat(ctx, workspace, minioDocumentId)
|
||||
const blob = await storageAdapter.stat(ctx, workspace, documentId)
|
||||
if (blob === undefined) {
|
||||
return undefined
|
||||
}
|
||||
@ -37,7 +37,7 @@ export async function yDocFromStorage (
|
||||
// it is either already gc-ed, or gc not needed and it is disabled
|
||||
ydoc ??= new YDoc({ gc: false })
|
||||
|
||||
const buffer = await storageAdapter.read(ctx, workspace, minioDocumentId)
|
||||
const buffer = await storageAdapter.read(ctx, workspace, documentId)
|
||||
return yDocFromBuffer(Buffer.concat(buffer), ydoc)
|
||||
}
|
||||
|
||||
@ -46,9 +46,9 @@ export async function yDocToStorage (
|
||||
ctx: MeasureContext,
|
||||
storageAdapter: StorageAdapter,
|
||||
workspace: WorkspaceId,
|
||||
minioDocumentId: string,
|
||||
documentId: string,
|
||||
ydoc: YDoc
|
||||
): Promise<void> {
|
||||
const buffer = yDocToBuffer(ydoc)
|
||||
await storageAdapter.put(ctx, workspace, minioDocumentId, buffer, 'application/ydoc', buffer.length)
|
||||
await storageAdapter.put(ctx, workspace, documentId, buffer, 'application/ydoc', buffer.length)
|
||||
}
|
||||
|
@ -26,15 +26,15 @@ import {
|
||||
onLoadDocumentPayload,
|
||||
onStoreDocumentPayload
|
||||
} from '@hocuspocus/server'
|
||||
import { Transformer } from '@hocuspocus/transformer'
|
||||
import { Doc as YDoc } from 'yjs'
|
||||
import { Context, withContext } from '../context'
|
||||
import { CollabStorageAdapter } from '../storage/adapter'
|
||||
import { TransformerFactory } from '../types'
|
||||
|
||||
export interface StorageConfiguration {
|
||||
ctx: MeasureContext
|
||||
adapter: CollabStorageAdapter
|
||||
transformerFactory: TransformerFactory
|
||||
transformer: Transformer
|
||||
}
|
||||
|
||||
export class StorageExtension implements Extension {
|
||||
@ -60,11 +60,8 @@ export class StorageExtension implements Extension {
|
||||
}
|
||||
|
||||
async afterLoadDocument ({ context, documentName, document }: withContext<afterLoadDocumentPayload>): Promise<any> {
|
||||
const { workspaceId } = context
|
||||
|
||||
// remember the markup for the document
|
||||
const transformer = this.configuration.transformerFactory(workspaceId)
|
||||
this.markups.set(documentName, transformer.fromYdoc(document))
|
||||
this.markups.set(documentName, this.configuration.transformer.fromYdoc(document))
|
||||
}
|
||||
|
||||
async onStoreDocument ({ context, documentName, document }: withContext<onStoreDocumentPayload>): Promise<void> {
|
||||
@ -127,13 +124,10 @@ export class StorageExtension implements Extension {
|
||||
|
||||
private async storeDocument (documentName: string, document: Document, context: Context): Promise<void> {
|
||||
const { ctx, adapter } = this.configuration
|
||||
const { workspaceId } = context
|
||||
|
||||
try {
|
||||
const transformer = this.configuration.transformerFactory(workspaceId)
|
||||
|
||||
const prevMarkup = this.markups.get(documentName) ?? {}
|
||||
const currMarkup = transformer.fromYdoc(document)
|
||||
const currMarkup = this.configuration.transformer.fromYdoc(document)
|
||||
|
||||
await ctx.with('save-document', {}, async (ctx) => {
|
||||
await adapter.saveDocument(ctx, documentName as DocumentId, document, context, {
|
||||
|
@ -1,57 +0,0 @@
|
||||
//
|
||||
// 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 { yDocBranchWithGC } from '@hcengineering/collaboration'
|
||||
import { type BranchDocumentRequest, type BranchDocumentResponse } from '@hcengineering/collaborator-client'
|
||||
import { MeasureContext } from '@hcengineering/core'
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs'
|
||||
import { Context } from '../../context'
|
||||
import { RpcMethodParams } from '../rpc'
|
||||
|
||||
export async function branchDocument (
|
||||
ctx: MeasureContext,
|
||||
context: Context,
|
||||
payload: BranchDocumentRequest,
|
||||
params: RpcMethodParams
|
||||
): Promise<BranchDocumentResponse> {
|
||||
const { sourceDocumentId, targetDocumentId } = payload
|
||||
const { hocuspocus } = params
|
||||
|
||||
const sourceConnection = await ctx.with('connect', { type: 'source' }, async () => {
|
||||
return await hocuspocus.openDirectConnection(sourceDocumentId, context)
|
||||
})
|
||||
|
||||
const targetConnection = await ctx.with('connect', { type: 'target' }, async () => {
|
||||
return await hocuspocus.openDirectConnection(targetDocumentId, context)
|
||||
})
|
||||
|
||||
try {
|
||||
let update = new Uint8Array()
|
||||
|
||||
await sourceConnection.transact((document) => {
|
||||
const copy = yDocBranchWithGC(document)
|
||||
update = encodeStateAsUpdate(copy)
|
||||
})
|
||||
|
||||
await targetConnection.transact((document) => {
|
||||
applyUpdate(document, update)
|
||||
})
|
||||
} finally {
|
||||
await sourceConnection.disconnect()
|
||||
await targetConnection.disconnect()
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
//
|
||||
// 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 { YDocVersion, takeCollaborativeDocSnapshot, yDocCopyXmlField } from '@hcengineering/collaboration'
|
||||
import { parseDocumentId, type CopyContentRequest, type CopyContentResponse } from '@hcengineering/collaborator-client'
|
||||
import { MeasureContext } from '@hcengineering/core'
|
||||
import { Doc as YDoc } from 'yjs'
|
||||
import { Context } from '../../context'
|
||||
import { RpcMethodParams } from '../rpc'
|
||||
|
||||
export async function copyContent (
|
||||
ctx: MeasureContext,
|
||||
context: Context,
|
||||
payload: CopyContentRequest,
|
||||
params: RpcMethodParams
|
||||
): Promise<CopyContentResponse> {
|
||||
const { documentId, sourceField, targetField, snapshot } = payload
|
||||
const { hocuspocus, storageAdapter } = params
|
||||
const { workspaceId } = context
|
||||
|
||||
const connection = await ctx.with('connect', {}, async () => {
|
||||
return await hocuspocus.openDirectConnection(documentId, context)
|
||||
})
|
||||
|
||||
try {
|
||||
await ctx.with('copy', {}, async () => {
|
||||
await connection.transact((document) => {
|
||||
yDocCopyXmlField(document, sourceField, targetField)
|
||||
})
|
||||
})
|
||||
|
||||
if (snapshot !== undefined && snapshot.versionId !== 'HEAD') {
|
||||
const ydoc = connection.document ?? new YDoc()
|
||||
const { collaborativeDoc } = parseDocumentId(documentId)
|
||||
|
||||
const version: YDocVersion = {
|
||||
versionId: snapshot.versionId,
|
||||
name: snapshot.versionName ?? snapshot.versionId,
|
||||
createdBy: snapshot.createdBy,
|
||||
createdOn: Date.now()
|
||||
}
|
||||
|
||||
await ctx.with('snapshot', {}, async () => {
|
||||
await takeCollaborativeDocSnapshot(storageAdapter, workspaceId, collaborativeDoc, ydoc, version, ctx)
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
await connection.disconnect()
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
@ -21,10 +21,10 @@ import { RpcMethodParams } from '../rpc'
|
||||
export async function getContent (
|
||||
ctx: MeasureContext,
|
||||
context: Context,
|
||||
documentId: string,
|
||||
payload: GetContentRequest,
|
||||
params: RpcMethodParams
|
||||
): Promise<GetContentResponse> {
|
||||
const { documentId } = payload
|
||||
const { hocuspocus, transformer } = params
|
||||
|
||||
const connection = await ctx.with('connect', {}, async () => {
|
||||
|
@ -14,18 +14,10 @@
|
||||
//
|
||||
|
||||
import { getContent } from './getContent'
|
||||
import { copyContent } from './copyContent'
|
||||
import { updateContent } from './updateContent'
|
||||
import { branchDocument } from './branchDocument'
|
||||
import { removeDocument } from './removeDocument'
|
||||
import { takeSnapshot } from './takeSnapshot'
|
||||
import { RpcMethod } from '../rpc'
|
||||
|
||||
export const methods: Record<string, RpcMethod> = {
|
||||
getContent,
|
||||
copyContent,
|
||||
updateContent,
|
||||
branchDocument,
|
||||
removeDocument,
|
||||
takeSnapshot
|
||||
updateContent
|
||||
}
|
||||
|
@ -1,53 +0,0 @@
|
||||
//
|
||||
// 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 { collaborativeHistoryDocId } from '@hcengineering/collaboration'
|
||||
import {
|
||||
parseDocumentId,
|
||||
type RemoveDocumentRequest,
|
||||
type RemoveDocumentResponse
|
||||
} from '@hcengineering/collaborator-client'
|
||||
import { MeasureContext, collaborativeDocParse } from '@hcengineering/core'
|
||||
import { Context } from '../../context'
|
||||
import { RpcMethodParams } from '../rpc'
|
||||
|
||||
export async function removeDocument (
|
||||
ctx: MeasureContext,
|
||||
context: Context,
|
||||
payload: RemoveDocumentRequest,
|
||||
params: RpcMethodParams
|
||||
): Promise<RemoveDocumentResponse> {
|
||||
const { documentId } = payload
|
||||
const { hocuspocus, storageAdapter } = params
|
||||
const { workspaceId } = context
|
||||
|
||||
const document = hocuspocus.documents.get(documentId)
|
||||
if (document !== undefined) {
|
||||
hocuspocus.closeConnections(documentId)
|
||||
hocuspocus.unloadDocument(document)
|
||||
}
|
||||
|
||||
const { collaborativeDoc } = parseDocumentId(documentId)
|
||||
const { documentId: contentDocumentId } = collaborativeDocParse(collaborativeDoc)
|
||||
const historyDocumentId = collaborativeHistoryDocId(contentDocumentId)
|
||||
|
||||
try {
|
||||
await storageAdapter.remove(ctx, workspaceId, [contentDocumentId, historyDocumentId])
|
||||
} catch (err) {
|
||||
ctx.error('failed to remove document', { documentId, error: err })
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
//
|
||||
// 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 { YDocVersion, takeCollaborativeDocSnapshot } from '@hcengineering/collaboration'
|
||||
import {
|
||||
parseDocumentId,
|
||||
type TakeSnapshotRequest,
|
||||
type TakeSnapshotResponse
|
||||
} from '@hcengineering/collaborator-client'
|
||||
import { CollaborativeDocVersionHead, MeasureContext, collaborativeDocParse } from '@hcengineering/core'
|
||||
import { Doc as YDoc } from 'yjs'
|
||||
import { Context } from '../../context'
|
||||
import { RpcMethodParams } from '../rpc'
|
||||
|
||||
export async function takeSnapshot (
|
||||
ctx: MeasureContext,
|
||||
context: Context,
|
||||
payload: TakeSnapshotRequest,
|
||||
params: RpcMethodParams
|
||||
): Promise<TakeSnapshotResponse> {
|
||||
const { documentId, snapshot } = payload
|
||||
const { hocuspocus, storageAdapter } = params
|
||||
const { workspaceId } = context
|
||||
|
||||
const version: YDocVersion = {
|
||||
versionId: snapshot.versionId,
|
||||
name: snapshot.versionName ?? snapshot.versionId,
|
||||
createdBy: snapshot.createdBy,
|
||||
createdOn: Date.now()
|
||||
}
|
||||
|
||||
const { collaborativeDoc } = parseDocumentId(documentId)
|
||||
const { versionId } = collaborativeDocParse(collaborativeDoc)
|
||||
if (versionId !== CollaborativeDocVersionHead) {
|
||||
throw new Error('invalid document version')
|
||||
}
|
||||
|
||||
const connection = await ctx.with('connect', {}, async () => {
|
||||
return await hocuspocus.openDirectConnection(documentId, context)
|
||||
})
|
||||
|
||||
try {
|
||||
const ydoc = connection.document ?? new YDoc()
|
||||
|
||||
await ctx.with('snapshot', {}, async () => {
|
||||
await takeCollaborativeDocSnapshot(storageAdapter, workspaceId, collaborativeDoc, ydoc, version, ctx)
|
||||
})
|
||||
|
||||
return { ...version }
|
||||
} finally {
|
||||
await connection.disconnect()
|
||||
}
|
||||
}
|
@ -14,25 +14,20 @@
|
||||
//
|
||||
|
||||
import { MeasureContext } from '@hcengineering/core'
|
||||
import {
|
||||
parseDocumentId,
|
||||
type UpdateContentRequest,
|
||||
type UpdateContentResponse
|
||||
} from '@hcengineering/collaborator-client'
|
||||
import { YDocVersion, takeCollaborativeDocSnapshot } from '@hcengineering/collaboration'
|
||||
import { Doc as YDoc, applyUpdate, encodeStateAsUpdate } from 'yjs'
|
||||
import { type UpdateContentRequest, type UpdateContentResponse } from '@hcengineering/collaborator-client'
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs'
|
||||
import { Context } from '../../context'
|
||||
import { RpcMethodParams } from '../rpc'
|
||||
|
||||
export async function updateContent (
|
||||
ctx: MeasureContext,
|
||||
context: Context,
|
||||
documentId: string,
|
||||
payload: UpdateContentRequest,
|
||||
params: RpcMethodParams
|
||||
): Promise<UpdateContentResponse> {
|
||||
const { documentId, content, snapshot } = payload
|
||||
const { hocuspocus, transformer, storageAdapter } = params
|
||||
const { workspaceId } = context
|
||||
const { content } = payload
|
||||
const { hocuspocus, transformer } = params
|
||||
|
||||
const updates = await ctx.with('transform', {}, () => {
|
||||
const updates: Record<string, Uint8Array> = {}
|
||||
@ -41,6 +36,7 @@ export async function updateContent (
|
||||
const ydoc = transformer.toYdoc(markup, field)
|
||||
updates[field] = encodeStateAsUpdate(ydoc)
|
||||
})
|
||||
|
||||
return updates
|
||||
})
|
||||
|
||||
@ -60,22 +56,6 @@ export async function updateContent (
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (snapshot !== undefined && snapshot.versionId !== 'HEAD') {
|
||||
const ydoc = connection.document ?? new YDoc()
|
||||
const { collaborativeDoc } = parseDocumentId(documentId)
|
||||
|
||||
const version: YDocVersion = {
|
||||
versionId: snapshot.versionId,
|
||||
name: snapshot.versionName ?? snapshot.versionId,
|
||||
createdBy: snapshot.createdBy,
|
||||
createdOn: Date.now()
|
||||
}
|
||||
|
||||
await ctx.with('snapshot', {}, async () => {
|
||||
await takeCollaborativeDocSnapshot(storageAdapter, workspaceId, collaborativeDoc, ydoc, version, ctx)
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
await connection.disconnect()
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import { Context } from '../context'
|
||||
|
||||
export interface RpcRequest {
|
||||
method: string
|
||||
documentId: string
|
||||
payload: object
|
||||
}
|
||||
|
||||
@ -33,6 +34,7 @@ export type RpcResponse = object | RpcErrorResponse
|
||||
export type RpcMethod = (
|
||||
ctx: MeasureContext,
|
||||
context: Context,
|
||||
documentId: string,
|
||||
payload: any,
|
||||
params: RpcMethodParams
|
||||
) => Promise<RpcResponse>
|
||||
|
@ -17,7 +17,6 @@ import { Analytics } from '@hcengineering/analytics'
|
||||
import { MeasureContext, generateId, metricsAggregate } from '@hcengineering/core'
|
||||
import type { StorageAdapter } from '@hcengineering/server-core'
|
||||
import { Token, decodeToken } from '@hcengineering/server-token'
|
||||
import { ServerKit } from '@hcengineering/text'
|
||||
import { Hocuspocus } from '@hocuspocus/server'
|
||||
import bp from 'body-parser'
|
||||
import cors from 'cors'
|
||||
@ -33,7 +32,6 @@ import { simpleClientFactory } from './platform'
|
||||
import { RpcErrorResponse, RpcRequest, RpcResponse, methods } from './rpc'
|
||||
import { PlatformStorageAdapter } from './storage/platform'
|
||||
import { MarkupTransformer } from './transformers/markup'
|
||||
import { TransformerFactory } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -53,20 +51,7 @@ export async function start (ctx: MeasureContext, config: Config, storageAdapter
|
||||
app.use(bp.json())
|
||||
|
||||
const extensionsCtx = ctx.newChild('extensions', {})
|
||||
|
||||
const transformerFactory: TransformerFactory = (workspaceId) => {
|
||||
const extensions = [
|
||||
ServerKit.configure({
|
||||
image: {
|
||||
getBlobRef: async (fileId, name, size) => {
|
||||
const src = await storageAdapter.getUrl(ctx, workspaceId, fileId)
|
||||
return { src, srcset: '' }
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
return new MarkupTransformer(extensions)
|
||||
}
|
||||
const transformer = new MarkupTransformer()
|
||||
|
||||
const hocuspocus = new Hocuspocus({
|
||||
address: '0.0.0.0',
|
||||
@ -110,7 +95,7 @@ export async function start (ctx: MeasureContext, config: Config, storageAdapter
|
||||
new StorageExtension({
|
||||
ctx: extensionsCtx.newChild('storage', {}),
|
||||
adapter: new PlatformStorageAdapter(storageAdapter),
|
||||
transformerFactory
|
||||
transformer
|
||||
})
|
||||
]
|
||||
})
|
||||
@ -159,29 +144,39 @@ export async function start (ctx: MeasureContext, config: Config, storageAdapter
|
||||
}
|
||||
|
||||
const request = req.body as RpcRequest
|
||||
|
||||
const documentId = request.documentId
|
||||
if (documentId === undefined || documentId === '') {
|
||||
const response: RpcErrorResponse = {
|
||||
error: 'Missing documentId'
|
||||
}
|
||||
res.status(400).send(response)
|
||||
return
|
||||
}
|
||||
|
||||
const method = methods[request.method]
|
||||
if (method === undefined) {
|
||||
const response: RpcErrorResponse = {
|
||||
error: 'Unknown method'
|
||||
}
|
||||
res.status(400).send(response)
|
||||
} else {
|
||||
const token = decodeToken(authHeader.split(' ')[1])
|
||||
const context = getContext(token)
|
||||
|
||||
rpcCtx.info('rpc', { method: request.method, connectionId: context.connectionId, mode: token.extra?.mode ?? '' })
|
||||
await rpcCtx.with('/rpc', { method: request.method }, async (ctx) => {
|
||||
try {
|
||||
const transformer = transformerFactory(token.workspace)
|
||||
const response: RpcResponse = await rpcCtx.with(request.method, {}, async (ctx) => {
|
||||
return await method(ctx, context, request.payload, { hocuspocus, storageAdapter, transformer })
|
||||
})
|
||||
res.status(200).send(response)
|
||||
} catch (err: any) {
|
||||
res.status(500).send({ error: err.message })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const token = decodeToken(authHeader.split(' ')[1])
|
||||
const context = getContext(token)
|
||||
|
||||
rpcCtx.info('rpc', { method: request.method, connectionId: context.connectionId, mode: token.extra?.mode ?? '' })
|
||||
await rpcCtx.with('/rpc', { method: request.method }, async (ctx) => {
|
||||
try {
|
||||
const response: RpcResponse = await rpcCtx.with(request.method, {}, async (ctx) => {
|
||||
return await method(ctx, context, documentId, request.payload, { hocuspocus, storageAdapter, transformer })
|
||||
})
|
||||
res.status(200).send(response)
|
||||
} catch (err: any) {
|
||||
res.status(500).send({ error: err.message })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const wss = new WebSocketServer({
|
||||
|
@ -14,12 +14,7 @@
|
||||
//
|
||||
|
||||
import activity, { DocUpdateMessage } from '@hcengineering/activity'
|
||||
import {
|
||||
YDocVersion,
|
||||
loadCollaborativeDoc,
|
||||
saveCollaborativeDoc,
|
||||
takeCollaborativeDocSnapshot
|
||||
} from '@hcengineering/collaboration'
|
||||
import { loadCollaborativeDoc, saveCollaborativeDoc } from '@hcengineering/collaboration'
|
||||
import {
|
||||
DocumentId,
|
||||
PlatformDocumentId,
|
||||
@ -98,14 +93,6 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
|
||||
})
|
||||
|
||||
try {
|
||||
let snapshot: YDocVersion | undefined
|
||||
try {
|
||||
ctx.info('take document snapshot', { documentId })
|
||||
snapshot = await this.takeSnapshot(ctx, client, documentId, document, context)
|
||||
} catch (err) {
|
||||
ctx.error('failed to take document snapshot', { documentId, error: err })
|
||||
}
|
||||
|
||||
try {
|
||||
ctx.info('save document content', { documentId })
|
||||
await this.saveDocumentToStorage(ctx, documentId, document, context)
|
||||
@ -120,7 +107,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
|
||||
if (platformDocumentId !== undefined) {
|
||||
ctx.info('save document content to platform', { documentId, platformDocumentId })
|
||||
await ctx.with('save-to-platform', {}, async (ctx) => {
|
||||
await this.saveDocumentToPlatform(ctx, client, documentId, platformDocumentId, snapshot, markup)
|
||||
await this.saveDocumentToPlatform(ctx, client, documentId, platformDocumentId, markup)
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
@ -157,39 +144,11 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
async takeSnapshot (
|
||||
ctx: MeasureContext,
|
||||
client: Omit<TxOperations, 'close'>,
|
||||
documentId: DocumentId,
|
||||
document: YDoc,
|
||||
context: Context
|
||||
): Promise<YDocVersion | undefined> {
|
||||
const { collaborativeDoc } = parseDocumentId(documentId)
|
||||
|
||||
const { workspaceId } = context
|
||||
|
||||
const timestamp = Date.now()
|
||||
|
||||
const yDocVersion: YDocVersion = {
|
||||
versionId: `${timestamp}`,
|
||||
name: 'Automatic snapshot',
|
||||
createdBy: client.user,
|
||||
createdOn: timestamp
|
||||
}
|
||||
|
||||
await ctx.with('take-snapshot', {}, async (ctx) => {
|
||||
await takeCollaborativeDocSnapshot(this.storage, workspaceId, collaborativeDoc, document, yDocVersion, ctx)
|
||||
})
|
||||
|
||||
return yDocVersion
|
||||
}
|
||||
|
||||
async saveDocumentToPlatform (
|
||||
ctx: MeasureContext,
|
||||
client: Omit<TxOperations, 'close'>,
|
||||
documentName: string,
|
||||
platformDocumentId: PlatformDocumentId,
|
||||
snapshot: YDocVersion | undefined,
|
||||
markup: {
|
||||
prev: Record<string, string>
|
||||
curr: Record<string, string>
|
||||
@ -219,8 +178,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
|
||||
}
|
||||
|
||||
const collaborativeDoc = (current as any)[objectAttr] as CollaborativeDoc
|
||||
const newCollaborativeDoc =
|
||||
snapshot !== undefined ? collaborativeDocWithLastVersion(collaborativeDoc, snapshot.versionId) : collaborativeDoc
|
||||
const newCollaborativeDoc = collaborativeDocWithLastVersion(collaborativeDoc, `${Date.now()}`)
|
||||
|
||||
await ctx.with('update', {}, async () => {
|
||||
await client.diffUpdate(current, { [objectAttr]: newCollaborativeDoc })
|
||||
|
@ -1,41 +0,0 @@
|
||||
//
|
||||
// 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 { TiptapTransformer, Transformer } from '@hocuspocus/transformer'
|
||||
import { Extensions } from '@tiptap/core'
|
||||
import { generateHTML, generateJSON } from '@tiptap/html'
|
||||
import { Doc } from 'yjs'
|
||||
|
||||
export class HtmlTransformer implements Transformer {
|
||||
transformer: Transformer
|
||||
|
||||
constructor (private readonly extensions: Extensions) {
|
||||
this.transformer = TiptapTransformer.extensions(extensions)
|
||||
}
|
||||
|
||||
fromYdoc (document: Doc, fieldName?: string | string[] | undefined): any {
|
||||
const json = this.transformer.fromYdoc(document, fieldName)
|
||||
return generateHTML(json, this.extensions)
|
||||
}
|
||||
|
||||
toYdoc (document: any, fieldName: string): Doc {
|
||||
if (typeof document === 'string' && document !== '') {
|
||||
const json = generateJSON(document, this.extensions)
|
||||
return this.transformer.toYdoc(json, fieldName)
|
||||
}
|
||||
|
||||
return new Doc()
|
||||
}
|
||||
}
|
@ -13,22 +13,14 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { jsonToMarkup, markupToJSON } from '@hcengineering/text'
|
||||
import { TiptapTransformer, Transformer } from '@hocuspocus/transformer'
|
||||
import { Extensions } from '@tiptap/core'
|
||||
import { markupToYDocNoSchema, yDocToMarkup } from '@hcengineering/text'
|
||||
import { Transformer } from '@hocuspocus/transformer'
|
||||
import { Doc } from 'yjs'
|
||||
|
||||
export class MarkupTransformer implements Transformer {
|
||||
transformer: Transformer
|
||||
|
||||
constructor (private readonly extensions: Extensions) {
|
||||
this.transformer = TiptapTransformer.extensions(extensions)
|
||||
}
|
||||
|
||||
fromYdoc (document: Doc, fieldName?: string | string[] | undefined): any {
|
||||
const json = this.transformer.fromYdoc(document, fieldName)
|
||||
if (typeof fieldName === 'string') {
|
||||
return jsonToMarkup(json)
|
||||
return yDocToMarkup(document, fieldName)
|
||||
}
|
||||
|
||||
if (fieldName === undefined || fieldName.length === 0) {
|
||||
@ -37,7 +29,7 @@ export class MarkupTransformer implements Transformer {
|
||||
|
||||
const data: Record<string, string> = {}
|
||||
fieldName?.forEach((field) => {
|
||||
data[field] = jsonToMarkup(json[field])
|
||||
data[field] = yDocToMarkup(document, field)
|
||||
})
|
||||
|
||||
return data
|
||||
@ -45,8 +37,7 @@ export class MarkupTransformer implements Transformer {
|
||||
|
||||
toYdoc (document: any, fieldName: string): Doc {
|
||||
if (typeof document === 'string' && document !== '') {
|
||||
const json = markupToJSON(document)
|
||||
return this.transformer.toYdoc(json, fieldName)
|
||||
return markupToYDocNoSchema(document, fieldName)
|
||||
}
|
||||
|
||||
return new Doc()
|
||||
|
@ -13,8 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Class, Doc, Domain, Ref, WorkspaceId } from '@hcengineering/core'
|
||||
import { Transformer } from '@hocuspocus/transformer'
|
||||
import type { Class, Doc, Domain, Ref } from '@hcengineering/core'
|
||||
|
||||
/** @public */
|
||||
export interface DocumentId {
|
||||
@ -30,6 +29,3 @@ export interface PlatformDocumentId {
|
||||
objectId: Ref<Doc>
|
||||
objectAttr: string
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type TransformerFactory = (workspaceId: WorkspaceId) => Transformer
|
||||
|
@ -247,6 +247,7 @@ export function start (
|
||||
telegramUrl: string
|
||||
gmailUrl: string
|
||||
calendarUrl: string
|
||||
collaborator?: string
|
||||
collaboratorUrl: string
|
||||
brandingUrl?: string
|
||||
previewConfig: string
|
||||
@ -297,6 +298,7 @@ export function start (
|
||||
TELEGRAM_URL: config.telegramUrl,
|
||||
GMAIL_URL: config.gmailUrl,
|
||||
CALENDAR_URL: config.calendarUrl,
|
||||
COLLABORATOR: config.collaborator,
|
||||
COLLABORATOR_URL: config.collaboratorUrl,
|
||||
BRANDING_URL: config.brandingUrl,
|
||||
PREVIEW_CONFIG: config.previewConfig,
|
||||
|
@ -81,6 +81,8 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const collaborator = process.env.COLLABORATOR
|
||||
|
||||
const modelVersion = process.env.MODEL_VERSION
|
||||
if (modelVersion === undefined) {
|
||||
console.error('please provide model version requirement')
|
||||
@ -129,6 +131,7 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
|
||||
rekoniUrl,
|
||||
calendarUrl,
|
||||
collaboratorUrl,
|
||||
collaborator,
|
||||
brandingUrl,
|
||||
previewConfig,
|
||||
pushPublicKey
|
||||
|
@ -16,7 +16,7 @@ import core, {
|
||||
import { ModelLogger } from '@hcengineering/model'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import { AggregatorStorageAdapter } from '@hcengineering/server-core'
|
||||
import { parseMessageMarkdown, YDocFromContent } from '@hcengineering/text'
|
||||
import { jsonToYDocNoSchema, parseMessageMarkdown } from '@hcengineering/text'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
const fieldRegexp = /\${\S+?}/
|
||||
@ -266,10 +266,11 @@ export class WorkspaceInitializer {
|
||||
}
|
||||
|
||||
private async createCollab (data: string, field: string, _id: Ref<Doc>): Promise<string> {
|
||||
const json = parseMessageMarkdown(data ?? '', this.imageUrl)
|
||||
const id = `${_id}%${field}`
|
||||
const collabId = `${id}:HEAD:0` as CollaborativeDoc
|
||||
const yDoc = YDocFromContent(json, field)
|
||||
|
||||
const json = parseMessageMarkdown(data ?? '', this.imageUrl)
|
||||
const yDoc = jsonToYDocNoSchema(json, field)
|
||||
|
||||
await saveCollaborativeDoc(this.storageAdapter, this.wsUrl, collabId, yDoc, this.ctx)
|
||||
return collabId
|
||||
|
@ -372,16 +372,7 @@ export abstract class IssueSyncManagerBase {
|
||||
!areEqualMarkups(update.description, syncData.current?.description ?? '')
|
||||
) {
|
||||
try {
|
||||
const versionId = `${Date.now()}`
|
||||
issueData.description = await this.collaborator.updateContent(
|
||||
doc.description,
|
||||
{ description: update.description },
|
||||
{
|
||||
versionId,
|
||||
versionName: versionId,
|
||||
createdBy: account
|
||||
}
|
||||
)
|
||||
await this.collaborator.updateContent(doc.description, { description: update.description })
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
this.ctx.error(err)
|
||||
@ -926,17 +917,8 @@ export abstract class IssueSyncManagerBase {
|
||||
workspace: this.provider.getWorkspaceId().name
|
||||
})
|
||||
try {
|
||||
const versionId = `${Date.now()}`
|
||||
issueData.description = update.description
|
||||
update.description = await this.collaborator.updateContent(
|
||||
existingIssue.description,
|
||||
{ description: update.description },
|
||||
{
|
||||
versionId,
|
||||
versionName: versionId,
|
||||
createdBy: account
|
||||
}
|
||||
)
|
||||
await this.collaborator.updateContent(existingIssue.description, { description: update.description })
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
this.ctx.error('error during description update', err)
|
||||
|
Loading…
Reference in New Issue
Block a user