Collaboration editor diff (#2356)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-11-08 20:45:19 +07:00 committed by GitHub
parent bfd120b887
commit b756b4a035
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1546 additions and 226 deletions

View File

@ -197,8 +197,11 @@ specifiers:
'@rushstack/heft': ^0.47.9 '@rushstack/heft': ^0.47.9
'@rushstack/heft-jest-plugin': ^0.3.16 '@rushstack/heft-jest-plugin': ^0.3.16
'@tiptap/core': ~2.0.0-beta.199 '@tiptap/core': ~2.0.0-beta.199
'@tiptap/extension-code-block': ~2.0.0-beta.200
'@tiptap/extension-collaboration': ~2.0.0-beta.199 '@tiptap/extension-collaboration': ~2.0.0-beta.199
'@tiptap/extension-collaboration-cursor': ~2.0.0-beta.199 '@tiptap/extension-collaboration-cursor': ~2.0.0-beta.199
'@tiptap/extension-gapcursor': ~2.0.0-beta.200
'@tiptap/extension-heading': ~2.0.0-beta.200
'@tiptap/extension-highlight': ~2.0.0-beta.199 '@tiptap/extension-highlight': ~2.0.0-beta.199
'@tiptap/extension-link': ~2.0.0-beta.199 '@tiptap/extension-link': ~2.0.0-beta.199
'@tiptap/extension-mention': ~2.0.0-beta.199 '@tiptap/extension-mention': ~2.0.0-beta.199
@ -213,6 +216,7 @@ specifiers:
'@types/cors': ^2.8.12 '@types/cors': ^2.8.12
'@types/crypto-js': ^4.1.1 '@types/crypto-js': ^4.1.1
'@types/deep-equal': ^1.0.1 '@types/deep-equal': ^1.0.1
'@types/diff': ~5.0.2
'@types/express': ^4.17.13 '@types/express': ^4.17.13
'@types/express-fileupload': ^1.1.7 '@types/express-fileupload': ^1.1.7
'@types/faker': ~5.5.9 '@types/faker': ~5.5.9
@ -248,6 +252,7 @@ specifiers:
css-loader: ^5.2.1 css-loader: ^5.2.1
csv-parse: ~5.1.0 csv-parse: ~5.1.0
deep-equal: ^2.0.5 deep-equal: ^2.0.5
diff: ~5.1.0
dotenv: ~16.0.0 dotenv: ~16.0.0
dotenv-webpack: ^7.0.2 dotenv-webpack: ^7.0.2
elastic-apm-node: ~3.26.0 elastic-apm-node: ~3.26.0
@ -288,9 +293,13 @@ specifiers:
postcss-loader: ^6.1.0 postcss-loader: ^6.1.0
prettier: ^2.7.1 prettier: ^2.7.1
prettier-plugin-svelte: ^2.8.0 prettier-plugin-svelte: ^2.8.0
prosemirror-changeset: ~2.2.0
prosemirror-collab: ~1.3.0 prosemirror-collab: ~1.3.0
prosemirror-model: ~1.18.1
prosemirror-state: ~1.4.1 prosemirror-state: ~1.4.1
prosemirror-transform: ~1.7.0 prosemirror-transform: ~1.7.0
prosemirror-view: ~1.29.0
rfc6902: ~5.0.1
sass: ^1.53.0 sass: ^1.53.0
sass-loader: ^12.1.0 sass-loader: ^12.1.0
sharp: ~0.30.7 sharp: ~0.30.7
@ -515,13 +524,16 @@ dependencies:
'@rushstack/heft': 0.47.9 '@rushstack/heft': 0.47.9
'@rushstack/heft-jest-plugin': 0.3.16_e810491d602256cb9138da3f42d797a2 '@rushstack/heft-jest-plugin': 0.3.16_e810491d602256cb9138da3f42d797a2
'@tiptap/core': 2.0.0-beta.199 '@tiptap/core': 2.0.0-beta.199
'@tiptap/extension-collaboration': 2.0.0-beta.199_8e74c563ac0499ca22e9a6d9d9b7c0ed '@tiptap/extension-code-block': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-collaboration-cursor': 2.0.0-beta.199_04364fb270bfa949258a810b562e2e26 '@tiptap/extension-collaboration': 2.0.0-beta.199_a496b31b65360f0ca30cf7e3de4ce96e
'@tiptap/extension-collaboration-cursor': 2.0.0-beta.199_17c6db99c9b1488ff14e755dd0559594
'@tiptap/extension-gapcursor': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-heading': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-highlight': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-highlight': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-link': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-link': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-mention': 2.0.0-beta.199_c8f353cb3abc70247a8f6c56ebb87d62 '@tiptap/extension-mention': 2.0.0-beta.199_c8f353cb3abc70247a8f6c56ebb87d62
'@tiptap/extension-placeholder': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-placeholder': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-task-item': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-task-item': 2.0.0-beta.199_ec97b388f910dbe754a14ff2f0072b88
'@tiptap/extension-task-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-task-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-typography': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-typography': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/starter-kit': 2.0.0-beta.199 '@tiptap/starter-kit': 2.0.0-beta.199
@ -531,6 +543,7 @@ dependencies:
'@types/cors': 2.8.12 '@types/cors': 2.8.12
'@types/crypto-js': 4.1.1 '@types/crypto-js': 4.1.1
'@types/deep-equal': 1.0.1 '@types/deep-equal': 1.0.1
'@types/diff': 5.0.2
'@types/express': 4.17.13 '@types/express': 4.17.13
'@types/express-fileupload': 1.2.2 '@types/express-fileupload': 1.2.2
'@types/faker': 5.5.9 '@types/faker': 5.5.9
@ -566,6 +579,7 @@ dependencies:
css-loader: 5.2.7_webpack@5.73.0 css-loader: 5.2.7_webpack@5.73.0
csv-parse: 5.1.0 csv-parse: 5.1.0
deep-equal: 2.0.5 deep-equal: 2.0.5
diff: 5.1.0
dotenv: 16.0.1 dotenv: 16.0.1
dotenv-webpack: 7.1.1_webpack@5.73.0 dotenv-webpack: 7.1.1_webpack@5.73.0
elastic-apm-node: 3.26.0 elastic-apm-node: 3.26.0
@ -606,9 +620,13 @@ dependencies:
postcss-loader: 6.2.1_postcss@8.4.14+webpack@5.73.0 postcss-loader: 6.2.1_postcss@8.4.14+webpack@5.73.0
prettier: 2.7.1 prettier: 2.7.1
prettier-plugin-svelte: 2.8.0_prettier@2.7.1+svelte@3.48.0 prettier-plugin-svelte: 2.8.0_prettier@2.7.1+svelte@3.48.0
prosemirror-changeset: 2.2.0
prosemirror-collab: 1.3.0 prosemirror-collab: 1.3.0
prosemirror-model: 1.18.1
prosemirror-state: 1.4.1 prosemirror-state: 1.4.1
prosemirror-transform: 1.7.0 prosemirror-transform: 1.7.0
prosemirror-view: 1.29.0
rfc6902: 5.0.1
sass: 1.53.0 sass: 1.53.0
sass-loader: 12.6.0_sass@1.53.0+webpack@5.73.0 sass-loader: 12.6.0_sass@1.53.0+webpack@5.73.0
sharp: 0.30.7 sharp: 0.30.7
@ -1612,7 +1630,7 @@ packages:
prosemirror-schema-list: 1.2.2 prosemirror-schema-list: 1.2.2
prosemirror-state: 1.4.1 prosemirror-state: 1.4.1
prosemirror-transform: 1.7.0 prosemirror-transform: 1.7.0
prosemirror-view: 1.28.3 prosemirror-view: 1.29.0
dev: false dev: false
/@tiptap/extension-blockquote/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199: /@tiptap/extension-blockquote/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
@ -1639,8 +1657,8 @@ packages:
'@tiptap/core': 2.0.0-beta.199 '@tiptap/core': 2.0.0-beta.199
dev: false dev: false
/@tiptap/extension-code-block/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199: /@tiptap/extension-code-block/2.0.0-beta.202_@tiptap+core@2.0.0-beta.199:
resolution: {integrity: sha512-ZfftYE1kHA2pD46hXDkeYd1vuxp3bJLS854B2yHfw1cp3JVDjMXzm4Mzg7zLfr+YV1dT/N/fUfdCg38fqEUCyA==} resolution: {integrity: sha512-tfK9khIroGjsXQvk2K/9z1/UyQrB4+zghkjyK1xikzRmhgfOeqQzA0TDrFrz7ywFXmSFQ7GnnYAp+RW6r6wyUg==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.0.0-beta.193 '@tiptap/core': ^2.0.0-beta.193
dependencies: dependencies:
@ -1656,13 +1674,13 @@ packages:
'@tiptap/core': 2.0.0-beta.199 '@tiptap/core': 2.0.0-beta.199
dev: false dev: false
/@tiptap/extension-collaboration-cursor/2.0.0-beta.199_04364fb270bfa949258a810b562e2e26: /@tiptap/extension-collaboration-cursor/2.0.0-beta.199_17c6db99c9b1488ff14e755dd0559594:
resolution: {integrity: sha512-3LOfuSFYZz5CQOm+3rCKSm/hKXfP+KgW6aDYDfDTI3IKtMkC9b8FYa2ZoW5Qj/H1H+2m0+8aT6p9RIybtVi9jQ==} resolution: {integrity: sha512-3LOfuSFYZz5CQOm+3rCKSm/hKXfP+KgW6aDYDfDTI3IKtMkC9b8FYa2ZoW5Qj/H1H+2m0+8aT6p9RIybtVi9jQ==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.0.0-beta.193 '@tiptap/core': ^2.0.0-beta.193
dependencies: dependencies:
'@tiptap/core': 2.0.0-beta.199 '@tiptap/core': 2.0.0-beta.199
y-prosemirror: 1.0.20_aeff9fb8f06ec077e7a357cd76186471 y-prosemirror: 1.0.20_0101a562c8137253afa9fb877870d27d
transitivePeerDependencies: transitivePeerDependencies:
- prosemirror-model - prosemirror-model
- prosemirror-state - prosemirror-state
@ -1671,21 +1689,6 @@ packages:
- yjs - yjs
dev: false dev: false
/@tiptap/extension-collaboration/2.0.0-beta.199_8e74c563ac0499ca22e9a6d9d9b7c0ed:
resolution: {integrity: sha512-ub3doQvy7o7YLwLDz8B/LK7zZ6aEy19C36FJbSXfr93Ws8FhVy2PYvgwBnRYPIifXrS8FVkNL5ULjsH+QlEd5Q==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.199
prosemirror-state: 1.4.1
y-prosemirror: 1.0.20_aeff9fb8f06ec077e7a357cd76186471
transitivePeerDependencies:
- prosemirror-model
- prosemirror-view
- y-protocols
- yjs
dev: false
/@tiptap/extension-collaboration/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199: /@tiptap/extension-collaboration/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
resolution: {integrity: sha512-ub3doQvy7o7YLwLDz8B/LK7zZ6aEy19C36FJbSXfr93Ws8FhVy2PYvgwBnRYPIifXrS8FVkNL5ULjsH+QlEd5Q==} resolution: {integrity: sha512-ub3doQvy7o7YLwLDz8B/LK7zZ6aEy19C36FJbSXfr93Ws8FhVy2PYvgwBnRYPIifXrS8FVkNL5ULjsH+QlEd5Q==}
peerDependencies: peerDependencies:
@ -1701,6 +1704,21 @@ packages:
- yjs - yjs
dev: false dev: false
/@tiptap/extension-collaboration/2.0.0-beta.199_a496b31b65360f0ca30cf7e3de4ce96e:
resolution: {integrity: sha512-ub3doQvy7o7YLwLDz8B/LK7zZ6aEy19C36FJbSXfr93Ws8FhVy2PYvgwBnRYPIifXrS8FVkNL5ULjsH+QlEd5Q==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.199
prosemirror-state: 1.4.1
y-prosemirror: 1.0.20_0101a562c8137253afa9fb877870d27d
transitivePeerDependencies:
- prosemirror-model
- prosemirror-view
- y-protocols
- yjs
dev: false
/@tiptap/extension-document/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199: /@tiptap/extension-document/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
resolution: {integrity: sha512-l/3k9N2O4wIMQoN/SM3aIBwOhZ2KRxQoqGJfsbAUUwBURBDiT4N2VZaNiJC/w3xCVQXIxHSIlqtm9ZBcZeiH/Q==} resolution: {integrity: sha512-l/3k9N2O4wIMQoN/SM3aIBwOhZ2KRxQoqGJfsbAUUwBURBDiT4N2VZaNiJC/w3xCVQXIxHSIlqtm9ZBcZeiH/Q==}
peerDependencies: peerDependencies:
@ -1718,8 +1736,8 @@ packages:
prosemirror-dropcursor: 1.5.0 prosemirror-dropcursor: 1.5.0
dev: false dev: false
/@tiptap/extension-gapcursor/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199: /@tiptap/extension-gapcursor/2.0.0-beta.202_@tiptap+core@2.0.0-beta.199:
resolution: {integrity: sha512-0TDpDfDyay+IbD+wJMsBJ2c0Cq0NtllUOxbi0NPjjWW94Jrvs1yqUSzX4Qp9m5MW8qP24IV6krgZBM1JyQc6ng==} resolution: {integrity: sha512-jOPMPPnTfVuc5YpFTcQM42/cg1J3+OeHitYb1/vBMpaNinVijuafsK14xDoVP8+sydKVgtBzYkfP/faN82I9iA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.0.0-beta.193 '@tiptap/core': ^2.0.0-beta.193
dependencies: dependencies:
@ -1735,8 +1753,8 @@ packages:
'@tiptap/core': 2.0.0-beta.199 '@tiptap/core': 2.0.0-beta.199
dev: false dev: false
/@tiptap/extension-heading/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199: /@tiptap/extension-heading/2.0.0-beta.202_@tiptap+core@2.0.0-beta.199:
resolution: {integrity: sha512-WGQ7ET2TBpldrD8JX37OXHXq05LU3OWItIVBs9nKGh4otZTUwPtwfOyMlFfA+IMfQif+ilwLGvUC6EHOw/LwxQ==} resolution: {integrity: sha512-sF271jSWHgtoJLDNFLS7eyUcUStl7mBDQNJIENWVI+lFu2Ax8GmO7AoB74Q6L5Zaw4h73L6TAvaafHIXurz7tA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.0.0-beta.193 '@tiptap/core': ^2.0.0-beta.193
dependencies: dependencies:
@ -1832,7 +1850,7 @@ packages:
'@tiptap/core': 2.0.0-beta.199 '@tiptap/core': 2.0.0-beta.199
prosemirror-model: 1.18.1 prosemirror-model: 1.18.1
prosemirror-state: 1.4.1 prosemirror-state: 1.4.1
prosemirror-view: 1.28.3 prosemirror-view: 1.29.0
dev: false dev: false
/@tiptap/extension-strike/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199: /@tiptap/extension-strike/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
@ -1852,6 +1870,16 @@ packages:
'@tiptap/core': 2.0.0-beta.199 '@tiptap/core': 2.0.0-beta.199
dev: false dev: false
/@tiptap/extension-task-item/2.0.0-beta.199_ec97b388f910dbe754a14ff2f0072b88:
resolution: {integrity: sha512-dvMgXr4B/V8dYvksLtbby3R2wM9zk3xdkOBuohTLQuRq73dK12Bh/h5xrl4cey8i/2tQBWgUfFiGVPsEUJjQCQ==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
prosemirror-model: ^1.18.1
dependencies:
'@tiptap/core': 2.0.0-beta.199
prosemirror-model: 1.18.1
dev: false
/@tiptap/extension-task-list/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199: /@tiptap/extension-task-list/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
resolution: {integrity: sha512-//1bw2Wd4IYKxYLw3iaxBcd0/iFw1Jwc/Q1j41oBc5QTZDuRxhEO/5Gjy1UmEZsWhsH39bS2za4uMBX4DbHBUQ==} resolution: {integrity: sha512-//1bw2Wd4IYKxYLw3iaxBcd0/iFw1Jwc/Q1j41oBc5QTZDuRxhEO/5Gjy1UmEZsWhsH39bS2za4uMBX4DbHBUQ==}
peerDependencies: peerDependencies:
@ -1884,12 +1912,12 @@ packages:
'@tiptap/extension-bold': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-bold': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-bullet-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-bullet-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-code': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-code': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-code-block': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-code-block': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-document': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-document': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-dropcursor': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-dropcursor': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-gapcursor': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-gapcursor': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-hard-break': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-hard-break': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-heading': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-heading': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-history': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-history': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-horizontal-rule': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-horizontal-rule': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-italic': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-italic': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
@ -1908,7 +1936,7 @@ packages:
'@tiptap/core': 2.0.0-beta.199 '@tiptap/core': 2.0.0-beta.199
prosemirror-model: 1.18.1 prosemirror-model: 1.18.1
prosemirror-state: 1.4.1 prosemirror-state: 1.4.1
prosemirror-view: 1.28.3 prosemirror-view: 1.29.0
dev: false dev: false
/@tootallnate/once/1.1.2: /@tootallnate/once/1.1.2:
@ -2052,6 +2080,10 @@ packages:
resolution: {integrity: sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==} resolution: {integrity: sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==}
dev: false dev: false
/@types/diff/5.0.2:
resolution: {integrity: sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg==}
dev: false
/@types/email-addresses/3.0.0: /@types/email-addresses/3.0.0:
resolution: {integrity: sha512-jGUOSgpOEWhTH4tMCj56NZenkzER259nJ5NGRvxXld3X7Lai/lxC3QNfDM0rVGMkj+WhANMpvIf195tgwnE7wQ==} resolution: {integrity: sha512-jGUOSgpOEWhTH4tMCj56NZenkzER259nJ5NGRvxXld3X7Lai/lxC3QNfDM0rVGMkj+WhANMpvIf195tgwnE7wQ==}
deprecated: This is a stub types definition for email-addresses (https://github.com/jackbowman/email-addresses). email-addresses provides its own type definitions, so you don't need @types/email-addresses installed! deprecated: This is a stub types definition for email-addresses (https://github.com/jackbowman/email-addresses). email-addresses provides its own type definitions, so you don't need @types/email-addresses installed!
@ -4098,6 +4130,11 @@ packages:
engines: {node: '>=0.3.1'} engines: {node: '>=0.3.1'}
dev: false dev: false
/diff/5.1.0:
resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==}
engines: {node: '>=0.3.1'}
dev: false
/diffie-hellman/5.0.3: /diffie-hellman/5.0.3:
resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
dependencies: dependencies:
@ -7949,6 +7986,12 @@ packages:
resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==}
dev: false dev: false
/prosemirror-changeset/2.2.0:
resolution: {integrity: sha512-QM7ohGtkpVpwVGmFb8wqVhaz9+6IUXcIQBGZ81YNAKYuHiFJ1ShvSzab4pKqTinJhwciZbrtBEk/2WsqSt2PYg==}
dependencies:
prosemirror-transform: 1.7.0
dev: false
/prosemirror-collab/1.3.0: /prosemirror-collab/1.3.0:
resolution: {integrity: sha512-+S/IJ69G2cUu2IM5b3PBekuxs94HO1CxJIWOFrLQXUaUDKL/JfBx+QcH31ldBlBXyDEUl+k3Vltfi1E1MKp2mA==} resolution: {integrity: sha512-+S/IJ69G2cUu2IM5b3PBekuxs94HO1CxJIWOFrLQXUaUDKL/JfBx+QcH31ldBlBXyDEUl+k3Vltfi1E1MKp2mA==}
dependencies: dependencies:
@ -7968,7 +8011,7 @@ packages:
dependencies: dependencies:
prosemirror-state: 1.4.1 prosemirror-state: 1.4.1
prosemirror-transform: 1.7.0 prosemirror-transform: 1.7.0
prosemirror-view: 1.28.3 prosemirror-view: 1.29.0
dev: false dev: false
/prosemirror-gapcursor/1.3.1: /prosemirror-gapcursor/1.3.1:
@ -7977,7 +8020,7 @@ packages:
prosemirror-keymap: 1.2.0 prosemirror-keymap: 1.2.0
prosemirror-model: 1.18.1 prosemirror-model: 1.18.1
prosemirror-state: 1.4.1 prosemirror-state: 1.4.1
prosemirror-view: 1.28.3 prosemirror-view: 1.29.0
dev: false dev: false
/prosemirror-history/1.3.0: /prosemirror-history/1.3.0:
@ -8022,8 +8065,8 @@ packages:
prosemirror-model: 1.18.1 prosemirror-model: 1.18.1
dev: false dev: false
/prosemirror-view/1.28.3: /prosemirror-view/1.29.0:
resolution: {integrity: sha512-YnJxLRzIaCNEt3VKiy+PBxtpwsCbjrfiBKIgHJeqbKhdeP8bU2qL4ngdGmxp9K4+06cZG5bE9vipuhP+KUl+BQ==} resolution: {integrity: sha512-bifVd5aD9uCNtpLL1AyhquG/cVbNZSv+ALBxTEGYv51a6OHDhq+aOuzqq4MermNdeBdT+5uyURXCALgzk0EN5g==}
dependencies: dependencies:
prosemirror-model: 1.18.1 prosemirror-model: 1.18.1
prosemirror-state: 1.4.1 prosemirror-state: 1.4.1
@ -8353,6 +8396,10 @@ packages:
engines: {iojs: '>=1.0.0', node: '>=0.10.0'} engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
dev: false dev: false
/rfc6902/5.0.1:
resolution: {integrity: sha512-tYGfLpKIq9X7lrt4o3IkD9w9bpeAtsejfAqWNR98AoxfTsZqCepKa8eDlRiX8QMiCOD9vMx0/YbKLx0G1nPi5w==}
dev: false
/rfdc/1.3.0: /rfdc/1.3.0:
resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
dev: false dev: false
@ -10000,7 +10047,7 @@ packages:
dev: false dev: false
optional: true optional: true
/y-prosemirror/1.0.20_aeff9fb8f06ec077e7a357cd76186471: /y-prosemirror/1.0.20_0101a562c8137253afa9fb877870d27d:
resolution: {integrity: sha512-LVMtu3qWo0emeYiP+0jgNcvZkqhzE/otOoro+87q0iVKxy/sMKuiJZnokfJdR4cn9qKx0Un5fIxXqbAlR2bFkA==} resolution: {integrity: sha512-LVMtu3qWo0emeYiP+0jgNcvZkqhzE/otOoro+87q0iVKxy/sMKuiJZnokfJdR4cn9qKx0Un5fIxXqbAlR2bFkA==}
peerDependencies: peerDependencies:
prosemirror-model: ^1.7.1 prosemirror-model: ^1.7.1
@ -10010,7 +10057,9 @@ packages:
yjs: ^13.3.2 yjs: ^13.3.2
dependencies: dependencies:
lib0: 0.2.52 lib0: 0.2.52
prosemirror-model: 1.18.1
prosemirror-state: 1.4.1 prosemirror-state: 1.4.1
prosemirror-view: 1.29.0
y-protocols: 1.0.5 y-protocols: 1.0.5
yjs: 13.5.42 yjs: 13.5.42
dev: false dev: false
@ -14491,25 +14540,30 @@ packages:
dev: false dev: false
file:projects/text-editor.tgz_13653a9d42656433759444fbd2afc848: file:projects/text-editor.tgz_13653a9d42656433759444fbd2afc848:
resolution: {integrity: sha512-nR7pmL9ssos3gvV+tv6lfjcG6IldNayfbzZFp5678fazvi5PDxm4FPboUlKFhwBIVFacbmc2GiP+H7PQASn8jg==, tarball: file:projects/text-editor.tgz} resolution: {integrity: sha512-fTQmayD5wtd7FqRklhl13kDS5Y9tyBXfC5FAoJP+xUHbNk61+FzgKnmHFOjbsJVvgFVkag98AJA9xb9nHfjFzw==, tarball: file:projects/text-editor.tgz}
id: file:projects/text-editor.tgz id: file:projects/text-editor.tgz
name: '@rush-temp/text-editor' name: '@rush-temp/text-editor'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@tiptap/core': 2.0.0-beta.199 '@tiptap/core': 2.0.0-beta.199
'@tiptap/extension-collaboration': 2.0.0-beta.199_8e74c563ac0499ca22e9a6d9d9b7c0ed '@tiptap/extension-code-block': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-collaboration-cursor': 2.0.0-beta.199_04364fb270bfa949258a810b562e2e26 '@tiptap/extension-collaboration': 2.0.0-beta.199_a496b31b65360f0ca30cf7e3de4ce96e
'@tiptap/extension-collaboration-cursor': 2.0.0-beta.199_17c6db99c9b1488ff14e755dd0559594
'@tiptap/extension-gapcursor': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-heading': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-highlight': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-highlight': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-link': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-link': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-mention': 2.0.0-beta.199_c8f353cb3abc70247a8f6c56ebb87d62 '@tiptap/extension-mention': 2.0.0-beta.199_c8f353cb3abc70247a8f6c56ebb87d62
'@tiptap/extension-placeholder': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-placeholder': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-task-item': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-task-item': 2.0.0-beta.199_ec97b388f910dbe754a14ff2f0072b88
'@tiptap/extension-task-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-task-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/extension-typography': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/extension-typography': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@tiptap/starter-kit': 2.0.0-beta.199 '@tiptap/starter-kit': 2.0.0-beta.199
'@tiptap/suggestion': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199 '@tiptap/suggestion': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
'@types/diff': 5.0.2
'@typescript-eslint/eslint-plugin': 5.42.0_8b6083565a963e7484743e25607ce89c '@typescript-eslint/eslint-plugin': 5.42.0_8b6083565a963e7484743e25607ce89c
'@typescript-eslint/parser': 5.42.0_eslint@8.26.0+typescript@4.7.4 '@typescript-eslint/parser': 5.42.0_eslint@8.26.0+typescript@4.7.4
diff: 5.1.0
emoji-regex: 10.1.0 emoji-regex: 10.1.0
eslint: 8.26.0 eslint: 8.26.0
eslint-config-standard-with-typescript: 23.0.0_35db0d754f34ccffcc0e5a361183072e eslint-config-standard-with-typescript: 23.0.0_35db0d754f34ccffcc0e5a361183072e
@ -14520,9 +14574,13 @@ packages:
eslint-plugin-svelte3: 4.0.0_eslint@8.26.0+svelte@3.48.0 eslint-plugin-svelte3: 4.0.0_eslint@8.26.0+svelte@3.48.0
prettier: 2.7.1 prettier: 2.7.1
prettier-plugin-svelte: 2.8.0_prettier@2.7.1+svelte@3.48.0 prettier-plugin-svelte: 2.8.0_prettier@2.7.1+svelte@3.48.0
prosemirror-changeset: 2.2.0
prosemirror-collab: 1.3.0 prosemirror-collab: 1.3.0
prosemirror-model: 1.18.1
prosemirror-state: 1.4.1 prosemirror-state: 1.4.1
prosemirror-transform: 1.7.0 prosemirror-transform: 1.7.0
prosemirror-view: 1.29.0
rfc6902: 5.0.1
sass: 1.53.0 sass: 1.53.0
svelte: 3.48.0 svelte: 3.48.0
svelte-check: 2.8.0_818cdd0cbd32f329a17bf389fa6ed6e6 svelte-check: 2.8.0_818cdd0cbd32f329a17bf389fa6ed6e6
@ -14538,8 +14596,6 @@ packages:
- node-sass - node-sass
- postcss - postcss
- postcss-load-config - postcss-load-config
- prosemirror-model
- prosemirror-view
- pug - pug
- stylus - stylus
- sugarss - sugarss
@ -14855,7 +14911,7 @@ packages:
dev: false dev: false
file:projects/workbench-resources.tgz_1e3963ebf0ceeb25b2fa6a1cc87e253c: file:projects/workbench-resources.tgz_1e3963ebf0ceeb25b2fa6a1cc87e253c:
resolution: {integrity: sha512-4m7K7jULDi99Dty4n+D3lmGmbi+r/NKlp39QbY1b+CJyKgm2VMXciabLOpOpZsCDx42W3fSmRXaVpgRmkuwDew==, tarball: file:projects/workbench-resources.tgz} resolution: {integrity: sha512-ff56NyjSj5PgQmkJbToL25SlAvhBjTKIJ1Zu6cTQuAwoji1/c+K5VC5xNj5lgE+ZD0hVkGqKUHZwmZNK5ti8RA==, tarball: file:projects/workbench-resources.tgz}
id: file:projects/workbench-resources.tgz id: file:projects/workbench-resources.tgz
name: '@rush-temp/workbench-resources' name: '@rush-temp/workbench-resources'
version: 0.0.0 version: 0.0.0

View File

@ -34,9 +34,18 @@
export let isSub: boolean = true export let isSub: boolean = true
export let isAside: boolean = true export let isAside: boolean = true
export let isCustomAttr: boolean = true export let isCustomAttr: boolean = true
export let floatAside = false
</script> </script>
<Panel bind:isAside isHeader={$$slots.header || isHeader} bind:panelWidth bind:innerWidth bind:withoutTitle on:close> <Panel
bind:isAside
isHeader={$$slots.header || isHeader}
bind:panelWidth
bind:innerWidth
bind:withoutTitle
on:close
{floatAside}
>
<svelte:fragment slot="navigator"> <svelte:fragment slot="navigator">
{#if $$slots.navigator} {#if $$slots.navigator}
<div class="buttons-group xsmall-gap mx-2"> <div class="buttons-group xsmall-gap mx-2">

View File

@ -22,6 +22,7 @@
"Objects": "Objects", "Objects": "Objects",
"Food": "Food", "Food": "Food",
"FullDescription": "Full description", "FullDescription": "Full description",
"NoFullDescription": "There are no detailed description" "NoFullDescription": "There are no detailed description",
"EnableDiffMode": "Diff mode"
} }
} }

View File

@ -22,6 +22,7 @@
"Objects": "Объекты", "Objects": "Объекты",
"Food": "Еда", "Food": "Еда",
"FullDescription": "Детальное описание", "FullDescription": "Детальное описание",
"NoFullDescription": "Нет детального описания" "NoFullDescription": "Нет детального описания",
"EnableDiffMode": "Режим сравнения"
} }
} }

View File

@ -27,7 +27,8 @@
"eslint": "^8.26.0", "eslint": "^8.26.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"svelte-check": "^2.8.0", "svelte-check": "^2.8.0",
"typescript": "^4.3.5" "typescript": "^4.3.5",
"@types/diff": "~5.0.2"
}, },
"dependencies": { "dependencies": {
"@hcengineering/presentation": "~0.6.2", "@hcengineering/presentation": "~0.6.2",
@ -53,6 +54,14 @@
"prosemirror-state": "~1.4.1", "prosemirror-state": "~1.4.1",
"prosemirror-transform": "~1.7.0", "prosemirror-transform": "~1.7.0",
"yjs": "^13.5.42", "yjs": "^13.5.42",
"y-websocket": "^1.4.5" "y-websocket": "^1.4.5",
"prosemirror-changeset": "~2.2.0",
"prosemirror-model": "~1.18.1",
"prosemirror-view": "~1.29.0",
"rfc6902": "~5.0.1",
"diff": "~5.1.0",
"@tiptap/extension-code-block": "~2.0.0-beta.200",
"@tiptap/extension-gapcursor": "~2.0.0-beta.200",
"@tiptap/extension-heading": "~2.0.0-beta.200"
} }
} }

View File

@ -0,0 +1,168 @@
import { Extension, Mark, mergeAttributes } from '@tiptap/core'
import { ChangeSet } from 'prosemirror-changeset'
import { Plugin } from 'prosemirror-state'
export interface ChangeHighlightOptions {
multicolor: boolean
HTMLAttributes: Record<string, any>
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
changeHighlight: {
/**
* Set a highlight mark
*/
setChangeHighlight: (attributes?: { color: string }) => ReturnType
/**
* Toggle a highlight mark
*/
toggleChangeHighlight: (attributes?: { color: string }) => ReturnType
/**
* Unset a highlight mark
*/
unsetChangeHighlight: () => ReturnType
}
}
}
export const ChangeHighlight = Mark.create<ChangeHighlightOptions>({
name: 'changeHighlight',
addOptions () {
return {
multicolor: true,
HTMLAttributes: {
changeColor: 'yellow'
}
}
},
addAttributes () {
if (!this.options.multicolor) {
return {}
}
return {
color: {
default: null,
parseHTML: (element) => element.getAttribute('color'),
renderHTML: (attributes) => {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!attributes.color) {
return {}
}
const color = attributes.color as string
return {
color,
style: `border-top: 1px solid ${color}; border-bottom: 1px solid ${color}; border-radius: 2px;`
}
}
}
}
},
parseHTML () {
return [
{
tag: 'cmark'
}
]
},
renderHTML ({ HTMLAttributes }) {
return ['cmark', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
},
addCommands () {
return {
setChangeHighlight:
(attributes) =>
({ commands }) => {
return commands.setMark(this.name, attributes)
},
toggleChangeHighlight:
(attributes) =>
({ commands }) => {
return commands.toggleMark(this.name, attributes)
},
unsetChangeHighlight:
() =>
({ commands }) => {
return commands.unsetMark(this.name)
}
}
}
})
export interface ChangesetExtensionOptions {
isSuggestMode: () => boolean
}
export const ChangesetExtension = Extension.create<ChangesetExtensionOptions>({
// addInputRules () {
// return [changeSetRule]
// },
addProseMirrorPlugins () {
return [
new Plugin({
appendTransaction: (_transactions, oldState, newState) => {
// no changes
if (newState.doc === oldState.doc) {
return
}
const tr = newState.tr
if (this.options.isSuggestMode()) {
let changes = ChangeSet.create(oldState.doc)
for (const tr of _transactions) {
changes = changes.addSteps(tr.doc, tr.mapping.maps, undefined)
}
for (const r of changes.changes) {
const from = r.fromB
const to = r.toB
if (r.inserted.length > 0 && from !== to) {
tr.addMark(from, to, newState.schema.marks.changeHighlight.create({ color: 'lightblue' }))
}
if (r.deleted.length > 0) {
const deletedText = oldState.doc.textBetween(r.fromA, r.toA)
tr.insertText(deletedText, from)
tr.addMark(
from,
from + deletedText.length,
newState.schema.marks.changeHighlight.create({ color: 'orange' })
)
}
}
}
return tr
}
})
]
},
onCreate () {
// The editor is ready.
},
onUpdate () {
// The content has changed.
},
// onSelectionUpdate ({ editor }) {
// // The selection has changed.
// },
onTransaction ({ transaction }) {
// The editor state has changed.
},
onFocus ({ event }) {
// The editor is focused.
},
onBlur ({ event }) {
// The editor isnt focused anymore.
},
onDestroy () {
// The editor is being destroyed.
}
})

View File

@ -0,0 +1,278 @@
<!--
//
// Copyright © 2022 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.
//
-->
<script lang="ts">
import { Editor, Extension } from '@tiptap/core'
import Highlight from '@tiptap/extension-highlight'
import Link from '@tiptap/extension-link'
import Heading, { Level } from '@tiptap/extension-heading'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import StarterKit from '@tiptap/starter-kit'
import { Plugin, PluginKey } from 'prosemirror-state'
import { onDestroy, onMount } from 'svelte'
import { Markup } from '@hcengineering/core'
import { IconSize } from '@hcengineering/ui'
import StyleButton from './StyleButton.svelte'
import TipTapCodeBlock from '@tiptap/extension-code-block'
import Gapcursor from '@tiptap/extension-gapcursor'
import { DecorationSet } from 'prosemirror-view'
import textEditorPlugin from '../plugin'
import { calculateDecorations } from './diff/decorations'
import Objects from './icons/Objects.svelte'
export let content: Markup
export let buttonSize: IconSize = 'small'
export let comparedVersion: Markup | undefined = undefined
export let headingLevels: Level[] = [1, 2, 3, 4]
let element: HTMLElement
let editor: Editor
let _decoration = DecorationSet.empty
let oldContent = ''
function updateEditor (editor?: Editor, comparedVersion?: Markup): void {
const r = calculateDecorations(editor, oldContent, comparedVersion)
if (r !== undefined) {
oldContent = r.oldContent
_decoration = r.decorations
}
}
const updateDecorations = () => {
if (editor && editor.schema) {
updateEditor(editor, comparedVersion)
}
}
const DecorationExtension = Extension.create({
addProseMirrorPlugins () {
return [
new Plugin({
key: new PluginKey('diffs'),
props: {
decorations (state) {
updateDecorations()
if (showDiff) {
return _decoration
}
return undefined
}
}
})
]
}
})
$: updateEditor(editor, comparedVersion)
onMount(() => {
editor = new Editor({
element,
content,
editable: true,
extensions: [
StarterKit,
Highlight.configure({
multicolor: false
}),
TipTapCodeBlock.configure({
languageClassPrefix: 'language-',
exitOnArrowDown: true,
exitOnTripleEnter: true,
HTMLAttributes: {
class: 'code-block'
}
}),
Gapcursor,
Heading.configure({
levels: headingLevels
}),
Link.configure({ openOnClick: false }),
TaskList,
TaskItem.configure({
nested: true,
HTMLAttributes: {
class: 'flex flex-grow gap-1 checkbox_style'
}
}),
DecorationExtension
// ...extensions
],
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor
}
})
})
onDestroy(() => {
if (editor) {
editor.destroy()
}
})
let showDiff = true
</script>
<div class="ref-container">
{#if comparedVersion !== undefined}
<div class="flex">
<div class="flex-grow" />
<div class="formatPanel buttons-group xsmall-gap mb-4">
<StyleButton
icon={Objects}
size={buttonSize}
selected={showDiff}
showTooltip={{ label: textEditorPlugin.string.EnableDiffMode }}
on:click={() => {
showDiff = !showDiff
editor.chain().focus()
}}
/>
</div>
</div>
{/if}
<div class="textInput">
<div class="select-text" style="width: 100%;" bind:this={element} />
</div>
</div>
<style lang="scss" global>
.ProseMirror {
flex-grow: 1;
overflow: auto;
max-height: 60vh;
outline: none;
line-height: 150%;
color: var(--accent-color);
p:not(:last-child) {
margin-block-end: 1em;
}
pre {
white-space: pre !important;
}
> * + * {
margin-top: 0.75em;
}
/* Placeholder (at the top) */
p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: var(--dark-color);
pointer-events: none;
height: 0;
}
&::-webkit-scrollbar-thumb {
background-color: var(--theme-bg-accent-hover);
}
&::-webkit-scrollbar-corner {
background-color: var(--theme-bg-accent-hover);
}
&::-webkit-scrollbar-track {
margin: 0;
}
}
/* Placeholder (at the top) */
.ProseMirror p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.lint-icon {
display: inline-block;
position: absolute;
right: 2px;
cursor: pointer;
border-radius: 100px;
// background: #f22;
color: white;
font-family: times, georgia, serif;
font-size: 15px;
font-weight: bold;
width: 0.7em;
height: 0.7em;
text-align: center;
padding-left: 0.5px;
line-height: 1.1em;
&.add {
background: lightblue;
}
&.delete {
background: orange;
}
}
/* Give a remote user a caret */
.collaboration-cursor__caret {
border-left: 1px solid #0d0d0d;
border-right: 1px solid #0d0d0d;
margin-left: -1px;
margin-right: -1px;
pointer-events: none;
position: relative;
word-break: normal;
}
/* Render the username above the caret */
.collaboration-cursor__label {
border-radius: 3px 3px 3px 0;
color: #0d0d0d;
font-size: 12px;
font-style: normal;
font-weight: 600;
left: -1px;
line-height: normal;
padding: 0.1rem 0.3rem;
position: absolute;
top: -1.4em;
user-select: none;
white-space: nowrap;
}
.code-block {
border: 1px solid var(--divider-color);
border-radius: 4px;
padding: 0.5rem;
}
cmark {
border-top: 1px solid lightblue;
border-bottom: 1px solid lightblue;
border-radius: 2px;
}
span.insertion {
border-top: 1px solid lightblue;
border-bottom: 1px solid lightblue;
border-radius: 2px;
}
span.deletion {
text-decoration: line-through;
}
</style>

View File

@ -15,38 +15,45 @@
// //
--> -->
<script lang="ts"> <script lang="ts">
import { IntlString, translate } from '@hcengineering/platform' import { getEmbeddedLabel, IntlString, translate } from '@hcengineering/platform'
import { Editor, HTMLContent } from '@tiptap/core' import { Editor, Extension, HTMLContent } from '@tiptap/core'
import Highlight from '@tiptap/extension-highlight' import Highlight from '@tiptap/extension-highlight'
import Link from '@tiptap/extension-link' import Link from '@tiptap/extension-link'
// import Typography from '@tiptap/extension-typography'
import Placeholder from '@tiptap/extension-placeholder' import Placeholder from '@tiptap/extension-placeholder'
// import Collab from '@tiptap/extension-collaboration'
import Collaboration from '@tiptap/extension-collaboration' import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor' import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import Heading, { Level } from '@tiptap/extension-heading'
import TaskItem from '@tiptap/extension-task-item' import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list' import TaskList from '@tiptap/extension-task-list'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import { Transaction } from 'prosemirror-state' import { Plugin, PluginKey, Transaction } from 'prosemirror-state'
import { createEventDispatcher, onDestroy, onMount } from 'svelte' import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { EmployeeAccount } from '@hcengineering/contact' import { EmployeeAccount } from '@hcengineering/contact'
import { getCurrentAccount } from '@hcengineering/core' import { getCurrentAccount, Markup } from '@hcengineering/core'
import { getPlatformColorForText, IconSize, showPopup } from '@hcengineering/ui' import { getEventPositionElement, getPlatformColorForText, IconSize, SelectPopup, showPopup } from '@hcengineering/ui'
import { WebsocketProvider } from 'y-websocket' import { WebsocketProvider } from 'y-websocket'
import * as Y from 'yjs' import * as Y from 'yjs'
import StyleButton from './StyleButton.svelte' import StyleButton from './StyleButton.svelte'
import TipTapCodeBlock from '@tiptap/extension-code-block'
import Gapcursor from '@tiptap/extension-gapcursor'
import { DecorationSet } from 'prosemirror-view'
import textEditorPlugin from '../plugin' import textEditorPlugin from '../plugin'
import { FormatMode, FORMAT_MODES } from '../types' import { FormatMode, FORMAT_MODES } from '../types'
import Bold from './icons/Bold.svelte' import Bold from './icons/Bold.svelte'
import Code from './icons/Code.svelte' import Code from './icons/Code.svelte'
import CodeBlock from './icons/CodeBlock.svelte' import CodeBlock from './icons/CodeBlock.svelte'
import { calculateDecorations } from './diff/decorations'
import Header from './icons/Header.svelte'
import Italic from './icons/Italic.svelte' import Italic from './icons/Italic.svelte'
import LinkEl from './icons/Link.svelte' import LinkEl from './icons/Link.svelte'
import ListBullet from './icons/ListBullet.svelte' import ListBullet from './icons/ListBullet.svelte'
import ListNumber from './icons/ListNumber.svelte' import ListNumber from './icons/ListNumber.svelte'
import Objects from './icons/Objects.svelte'
import Quote from './icons/Quote.svelte' import Quote from './icons/Quote.svelte'
import Strikethrough from './icons/Strikethrough.svelte' import Strikethrough from './icons/Strikethrough.svelte'
import LinkPopup from './LinkPopup.svelte' import LinkPopup from './LinkPopup.svelte'
@ -61,6 +68,10 @@
export let focusable: boolean = false export let focusable: boolean = false
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
export let initialContentId: string | undefined = undefined export let initialContentId: string | undefined = undefined
export let suggestMode = false
export let comparedVersion: Markup | undefined = undefined
export let headingLevels: Level[] = [1, 2, 3, 4]
const ydoc = new Y.Doc() const ydoc = new Y.Doc()
const wsProvider = new WebsocketProvider(collaboratorURL, documentId, ydoc, { const wsProvider = new WebsocketProvider(collaboratorURL, documentId, ydoc, {
@ -104,6 +115,11 @@
export function toggleStrike () { export function toggleStrike () {
editor.commands.toggleStrike() editor.commands.toggleStrike()
} }
export function getHTML (): string {
return editor.getHTML()
}
export function getLink () { export function getLink () {
return editor.getAttributes('link').href return editor.getAttributes('link').href
} }
@ -150,14 +166,73 @@
editor.setEditable(!readonly) editor.setEditable(!readonly)
} }
// const isSuggestMode = () => suggestMode
let _decoration = DecorationSet.empty
let oldContent = ''
function updateEditor (editor?: Editor, comparedVersion?: Markup): void {
const r = calculateDecorations(editor, oldContent, comparedVersion)
if (r !== undefined) {
oldContent = r.oldContent
_decoration = r.decorations
}
}
const updateDecorations = () => {
if (editor && editor.schema) {
updateEditor(editor, comparedVersion)
}
}
const DecorationExtension = Extension.create({
addProseMirrorPlugins () {
return [
new Plugin({
key: new PluginKey('diffs'),
props: {
decorations (state) {
updateDecorations()
if (showDiff) {
return _decoration
}
return undefined
}
}
})
]
}
})
$: updateEditor(editor, comparedVersion)
onMount(() => { onMount(() => {
ph.then(() => { ph.then(() => {
editor = new Editor({ editor = new Editor({
element, element,
editable: !readonly, // content: 'Hello world<br/> This is simple text<br/>Some more text<br/>Yahoo <br/>Cool <br/><br/> Done',
editable: true,
extensions: [ extensions: [
StarterKit, StarterKit,
Highlight, Highlight.configure({
multicolor: false
}),
TipTapCodeBlock.configure({
languageClassPrefix: 'language-',
exitOnArrowDown: true,
exitOnTripleEnter: true,
HTMLAttributes: {
class: 'code-block'
}
}),
Gapcursor,
Heading.configure({
levels: headingLevels
}),
// ChangeHighlight,
// ChangesetExtension.configure({
// isSuggestMode
// }),
Link.configure({ openOnClick: false }), Link.configure({ openOnClick: false }),
// ...(supportSubmit ? [Handle] : []), // order important // ...(supportSubmit ? [Handle] : []), // order important
// Typography, // we need to disable 1/2 -> ½ rule (https://github.com/hcengineering/anticrm/issues/345) // Typography, // we need to disable 1/2 -> ½ rule (https://github.com/hcengineering/anticrm/issues/345)
@ -169,7 +244,6 @@
class: 'flex flex-grow gap-1 checkbox_style' class: 'flex flex-grow gap-1 checkbox_style'
} }
}), }),
// UniqId,
Collaboration.configure({ Collaboration.configure({
document: ydoc document: ydoc
}), }),
@ -179,7 +253,8 @@
name: currentUser.name, name: currentUser.name,
color: getPlatformColorForText(currentUser.email) color: getPlatformColorForText(currentUser.email)
} }
}) }),
DecorationExtension
// ...extensions // ...extensions
], ],
onTransaction: () => { onTransaction: () => {
@ -192,10 +267,11 @@
}, },
onFocus: () => { onFocus: () => {
focused = true focused = true
dispatch('focus', editor.getHTML())
}, },
onUpdate: (op: { editor: Editor; transaction: Transaction }) => { onUpdate: (op: { editor: Editor; transaction: Transaction }) => {
// _decoration = DecorationSet.empty
dispatch('content', editor.getHTML()) dispatch('content', editor.getHTML())
updateFormattingState()
}, },
onSelectionUpdate: () => { onSelectionUpdate: () => {
dispatch('selection-update') dispatch('selection-update')
@ -219,8 +295,19 @@
return editor.isActive(formatMode) return editor.isActive(formatMode)
} }
let headingLevel = 0
function updateFormattingState () { function updateFormattingState () {
activeModes = new Set(FORMAT_MODES.filter(checkIsActive)) activeModes = new Set(FORMAT_MODES.filter(checkIsActive))
for (const l of headingLevels) {
if (editor.isActive('heading', { level: l })) {
headingLevel = l
activeModes.add('heading')
}
}
if (!activeModes.has('heading')) {
headingLevel = 0
}
isSelectionEmpty = editor.view.state.selection.empty isSelectionEmpty = editor.view.state.selection.empty
} }
@ -232,6 +319,29 @@
} }
} }
function toggleHeader (event: MouseEvent) {
if (activeModes.has('heading')) {
editor.commands.toggleHeading({ level: headingLevel as Level })
needFocus = true
updateFormattingState()
} else {
showPopup(
SelectPopup,
{
value: Array.from(headingLevels).map((it) => ({ id: it.toString(), label: it.toString() }))
},
getEventPositionElement(event),
(val) => {
if (val !== undefined) {
editor.commands.toggleHeading({ level: parseInt(val) as Level })
needFocus = true
updateFormattingState()
}
}
)
}
}
async function formatLink (): Promise<void> { async function formatLink (): Promise<void> {
const link = editor.getAttributes('link').href const link = editor.getAttributes('link').href
@ -243,80 +353,106 @@
} }
}) })
} }
let showDiff = true
</script> </script>
<div class="ref-container"> <div class="ref-container">
{#if isFormatting && !readonly} <div class="flex">
<div class="formatPanel buttons-group xsmall-gap mb-4"> {#if isFormatting && !readonly}
<StyleButton <div class="formatPanel buttons-group xsmall-gap mb-4">
icon={Bold} <StyleButton
size={buttonSize} icon={Header}
selected={activeModes.has('bold')} size={buttonSize}
showTooltip={{ label: textEditorPlugin.string.Bold }} selected={activeModes.has('heading')}
on:click={getToggler(toggleBold)} showTooltip={{ label: getEmbeddedLabel(`H${headingLevel}`) }}
/> on:click={toggleHeader}
<StyleButton />
icon={Italic}
size={buttonSize} <StyleButton
selected={activeModes.has('italic')} icon={Bold}
showTooltip={{ label: textEditorPlugin.string.Italic }} size={buttonSize}
on:click={getToggler(toggleItalic)} selected={activeModes.has('bold')}
/> showTooltip={{ label: textEditorPlugin.string.Bold }}
<StyleButton on:click={getToggler(toggleBold)}
icon={Strikethrough} />
size={buttonSize} <StyleButton
selected={activeModes.has('strike')} icon={Italic}
showTooltip={{ label: textEditorPlugin.string.Strikethrough }} size={buttonSize}
on:click={getToggler(toggleStrike)} selected={activeModes.has('italic')}
/> showTooltip={{ label: textEditorPlugin.string.Italic }}
<StyleButton on:click={getToggler(toggleItalic)}
icon={LinkEl} />
size={buttonSize} <StyleButton
selected={activeModes.has('link')} icon={Strikethrough}
disabled={isSelectionEmpty && !activeModes.has('link')} size={buttonSize}
showTooltip={{ label: textEditorPlugin.string.Link }} selected={activeModes.has('strike')}
on:click={formatLink} showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
/> on:click={getToggler(toggleStrike)}
<div class="buttons-divider" /> />
<StyleButton <StyleButton
icon={ListNumber} icon={LinkEl}
size={buttonSize} size={buttonSize}
selected={activeModes.has('orderedList')} selected={activeModes.has('link')}
showTooltip={{ label: textEditorPlugin.string.OrderedList }} disabled={isSelectionEmpty && !activeModes.has('link')}
on:click={getToggler(toggleOrderedList)} showTooltip={{ label: textEditorPlugin.string.Link }}
/> on:click={formatLink}
<StyleButton />
icon={ListBullet} <div class="buttons-divider" />
size={buttonSize} <StyleButton
selected={activeModes.has('bulletList')} icon={ListNumber}
showTooltip={{ label: textEditorPlugin.string.BulletedList }} size={buttonSize}
on:click={getToggler(toggleBulletList)} selected={activeModes.has('orderedList')}
/> showTooltip={{ label: textEditorPlugin.string.OrderedList }}
<div class="buttons-divider" /> on:click={getToggler(toggleOrderedList)}
<StyleButton />
icon={Quote} <StyleButton
size={buttonSize} icon={ListBullet}
selected={activeModes.has('blockquote')} size={buttonSize}
showTooltip={{ label: textEditorPlugin.string.Blockquote }} selected={activeModes.has('bulletList')}
on:click={getToggler(toggleBlockquote)} showTooltip={{ label: textEditorPlugin.string.BulletedList }}
/> on:click={getToggler(toggleBulletList)}
<div class="buttons-divider" /> />
<StyleButton <div class="buttons-divider" />
icon={Code} <StyleButton
size={buttonSize} icon={Quote}
selected={activeModes.has('code')} size={buttonSize}
showTooltip={{ label: textEditorPlugin.string.Code }} selected={activeModes.has('blockquote')}
on:click={getToggler(toggleCode)} showTooltip={{ label: textEditorPlugin.string.Blockquote }}
/> on:click={getToggler(toggleBlockquote)}
<StyleButton />
icon={CodeBlock} <div class="buttons-divider" />
size={buttonSize} <StyleButton
selected={activeModes.has('codeBlock')} icon={Code}
showTooltip={{ label: textEditorPlugin.string.CodeBlock }} size={buttonSize}
on:click={getToggler(toggleCodeBlock)} selected={activeModes.has('code')}
/> showTooltip={{ label: textEditorPlugin.string.Code }}
</div> on:click={getToggler(toggleCode)}
{/if} />
<StyleButton
icon={CodeBlock}
size={buttonSize}
selected={activeModes.has('codeBlock')}
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
on:click={getToggler(toggleCodeBlock)}
/>
</div>
{/if}
<div class="flex-grow" />
{#if comparedVersion !== undefined}
<div class="formatPanel buttons-group xsmall-gap mb-4">
<StyleButton
icon={Objects}
size={buttonSize}
selected={showDiff}
showTooltip={{ label: textEditorPlugin.string.EnableDiffMode }}
on:click={() => {
showDiff = !showDiff
editor.chain().focus()
}}
/>
</div>
{/if}
</div>
<div class="textInput" class:focusable> <div class="textInput" class:focusable>
<div class="select-text" style="width: 100%;" bind:this={element} /> <div class="select-text" style="width: 100%;" bind:this={element} />
</div> </div>
@ -325,7 +461,7 @@
<style lang="scss" global> <style lang="scss" global>
.ProseMirror { .ProseMirror {
flex-grow: 1; flex-grow: 1;
overflow-y: auto; overflow: auto;
max-height: 60vh; max-height: 60vh;
outline: none; outline: none;
line-height: 150%; line-height: 150%;
@ -335,6 +471,10 @@
margin-block-end: 1em; margin-block-end: 1em;
} }
pre {
white-space: pre !important;
}
> * + * { > * + * {
margin-top: 0.75em; margin-top: 0.75em;
} }
@ -367,6 +507,30 @@
pointer-events: none; pointer-events: none;
} }
.lint-icon {
display: inline-block;
position: absolute;
right: 2px;
cursor: pointer;
border-radius: 100px;
// background: #f22;
color: white;
font-family: times, georgia, serif;
font-size: 15px;
font-weight: bold;
width: 0.7em;
height: 0.7em;
text-align: center;
padding-left: 0.5px;
line-height: 1.1em;
&.add {
background: lightblue;
}
&.delete {
background: orange;
}
}
/* Give a remote user a caret */ /* Give a remote user a caret */
.collaboration-cursor__caret { .collaboration-cursor__caret {
border-left: 1px solid #0d0d0d; border-left: 1px solid #0d0d0d;
@ -393,4 +557,25 @@
user-select: none; user-select: none;
white-space: nowrap; white-space: nowrap;
} }
.code-block {
border: 1px solid var(--divider-color);
border-radius: 4px;
padding: 0.5rem;
}
cmark {
border-top: 1px solid lightblue;
border-bottom: 1px solid lightblue;
border-radius: 2px;
}
span.insertion {
border-top: 1px solid lightblue;
border-bottom: 1px solid lightblue;
border-radius: 2px;
}
span.deletion {
text-decoration: line-through;
}
</style> </style>

View File

@ -15,15 +15,12 @@
<script lang="ts"> <script lang="ts">
import type { Asset } from '@hcengineering/platform' import type { Asset } from '@hcengineering/platform'
import { AnySvelteComponent, Icon, IconSize, LabelAndProps, tooltip } from '@hcengineering/ui' import { AnySvelteComponent, Icon, IconSize, LabelAndProps, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
export let icon: Asset | AnySvelteComponent export let icon: Asset | AnySvelteComponent
export let size: IconSize export let size: IconSize
export let selected: boolean = false export let selected: boolean = false
export let showTooltip: LabelAndProps | undefined = undefined export let showTooltip: LabelAndProps | undefined = undefined
export let disabled: boolean = false export let disabled: boolean = false
const dispatch = createEventDispatcher()
</script> </script>
<button <button
@ -32,11 +29,9 @@
{disabled} {disabled}
use:tooltip={showTooltip} use:tooltip={showTooltip}
tabindex="0" tabindex="0"
on:mousedown|preventDefault|stopPropagation={() => { on:click|preventDefault|stopPropagation
dispatch('click')
}}
> >
<div class="icon {size}"> <div class="icon {size} flex">
<Icon {icon} {size} /> <Icon {icon} {size} />
</div> </div>
</button> </button>

View File

@ -0,0 +1,99 @@
//
// Copyright © 2022 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 { Editor } from '@tiptap/core'
import { ChangeSet } from 'prosemirror-changeset'
import { DOMParser, Node, Schema } from 'prosemirror-model'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { recreateTransform } from './recreate'
/**
* @public
*/
export function createDocument (schema: Schema, content: Markup): Node {
const wrappedValue = `<body>${content}</body>`
const body = new window.DOMParser().parseFromString(wrappedValue, 'text/html').body
return DOMParser.fromSchema(schema).parse(body)
}
/**
* @public
*/
export function calculateDecorations (
editor?: Editor,
oldContent?: string,
comparedVersion?: Markup
):
| {
decorations: DecorationSet
oldContent: string
}
| undefined {
try {
if (editor === undefined || editor.schema === undefined) {
return
}
if (comparedVersion === undefined) {
return
}
const schema = editor.schema
const docOld = createDocument(schema, comparedVersion)
const docNew = editor.state.doc
const c = editor.getHTML()
if (c === oldContent) {
return
}
const tr = recreateTransform(docOld, docNew)
const changeSet = ChangeSet.create(docOld).addSteps(tr.doc, tr.mapping.maps, undefined)
const changes = changeSet.changes
const decorations: Decoration[] = []
function lintIcon (color: string): any {
const icon = document.createElement('div')
icon.className = `lint-icon ${color}`
return icon
}
function deleted (prob: any): any {
const icon = document.createElement('span')
icon.className = 'deletion'
icon.innerText = prob
return icon
}
changes.forEach((change) => {
if (change.inserted.length > 0) {
decorations.push(Decoration.inline(change.fromB, change.toB, { class: 'diff insertion' }, {}))
decorations.push(Decoration.widget(change.fromB, lintIcon('add')))
}
if (change.deleted.length > 0) {
const cont = docOld.textBetween(change.fromA, change.toA)
decorations.push(Decoration.widget(change.fromB, deleted(cont)))
decorations.push(Decoration.widget(change.fromB, lintIcon('delete')))
}
})
if (decorations.length > 0) {
return { decorations: DecorationSet.empty.add(docNew, decorations), oldContent: c }
}
} catch (error: any) {
console.error(error)
}
}

View File

@ -0,0 +1,163 @@
//
// Copyright © 2022 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 { Diff, diffAny, Operation } from 'rfc6902/diff'
import { Pointer } from 'rfc6902/pointer'
interface ArrayOperation {
op: 'add' | 'remove' | 'replace'
index: number
value?: any
original?: number
}
interface MemoValue {
operations: ArrayOperation[]
cost: number
}
/**
* @public
* Modification of {diffArray} from rfc6902/diff, with respect to prosemirror model.
*/
export function diffArraysPM (input: any, output: any, ptr: Pointer, diff: Diff = diffAny): any {
// set up cost matrix (very simple initialization: just a map)
const memo: Record<string, MemoValue> = {
'0,0': { operations: [], cost: 0 }
}
/**
Calculate the cheapest sequence of operations required to get from
input.slice(0, i) to output.slice(0, j).
There may be other valid sequences with the same cost, but none cheaper.
@param i The row in the layout above
@param j The column in the layout above
@returns An object containing a list of operations, along with the total cost
of applying them (+1 for each add/remove/replace operation)
*/
function dist (i: number, j: number): MemoValue {
// memoized
const memoKey = `${i},${j}`
let memoized = memo[memoKey]
if (memoized === undefined) {
// TODO: this !diff(...).length usage could/should be lazy
if (i > 0 && j > 0 && diff(input[i - 1], output[j - 1], ptr.add(String(i - 1))).length === 0) {
// equal (no operations => no cost)
memoized = dist(i - 1, j - 1)
} else {
const alternatives: MemoValue[] = []
if (i > 0) {
// NOT topmost row
const removeBase = dist(i - 1, j)
const removeOperation: ArrayOperation = {
op: 'remove',
index: i - 1
}
alternatives.push(appendArrayOperation(removeBase, removeOperation))
}
if (j > 0) {
// NOT leftmost column
const addBase: MemoValue = dist(i, j - 1)
const addOperation: ArrayOperation = {
op: 'add',
index: i - 1,
value: output[j - 1]
}
alternatives.push(appendArrayOperation(addBase, addOperation))
}
if (i > 0 && j > 0) {
// TABLE MIDDLE
// supposing we replaced it, compute the rest of the costs:
const replaceBase = dist(i - 1, j - 1)
// okay, the general plan is to replace it, but we can be smarter,
// recursing into the structure and replacing only part of it if
// possible, but to do so we'll need the original value
const replaceOperation: ArrayOperation = {
op: 'replace',
index: i - 1,
original: input[i - 1],
value: output[j - 1]
}
// Replace only if simple or object's with type equal
const io = input[i - 1]
const jo = output[j - 1]
if (
(typeof io !== 'object' && typeof jo !== 'object') ||
(typeof io === 'object' &&
typeof jo === 'object' &&
io.type === jo.type &&
Array.isArray(io.content) &&
Array.isArray(jo.content))
) {
alternatives.push(appendArrayOperation(replaceBase, replaceOperation))
}
}
// the only other case, i === 0 && j === 0, has already been memoized
// the meat of the algorithm:
// sort by cost to find the lowest one (might be several ties for lowest)
// [4, 6, 7, 1, 2].sort((a, b) => a - b) -> [ 1, 2, 4, 6, 7 ]
const best = alternatives.sort(function (a, b) {
return a.cost - b.cost
})[0]
memoized = best
}
memo[memoKey] = memoized
}
return memoized
}
// handle weird objects masquerading as Arrays that don't have proper length
// properties by using 0 for everything but positive numbers
const inputLength: number = isNaN(input.length) || input.length <= 0 ? 0 : input.length
const outputLength: number = isNaN(output.length) || output.length <= 0 ? 0 : output.length
const arrayOperations = dist(inputLength, outputLength).operations
const paddedOperations = arrayOperations.reduce(
function (_a: any, arrayOperation: ArrayOperation) {
const operations: Operation[] = _a[0]
const padding: number = _a[1]
if (arrayOperation.op === 'add') {
const paddedIndex = arrayOperation.index + 1 + padding
const indexToken = paddedIndex < inputLength + padding ? String(paddedIndex) : '-'
const operation: Operation = {
op: arrayOperation.op,
path: ptr.add(indexToken).toString(),
value: arrayOperation.value
}
// padding++ // maybe only if array_operation.index > -1 ?
return [operations.concat(operation), padding + 1]
} else if (arrayOperation.op === 'remove') {
const operation: Operation = {
op: arrayOperation.op,
path: ptr.add(String(arrayOperation.index + padding)).toString()
}
// padding--
return [operations.concat(operation), padding - 1]
} else {
// replace
const replacePtr = ptr.add(String(arrayOperation.index + padding))
const replaceOperations = diff(arrayOperation.original, arrayOperation.value, replacePtr)
return [operations.concat.apply(operations, replaceOperations), padding]
}
},
[[], 0]
)[0]
return paddedOperations
}
function appendArrayOperation (base: MemoValue, operation: ArrayOperation): MemoValue {
return {
// the new operation must be pushed on the end
operations: base.operations.concat(operation),
cost: base.cost + 1
}
}

View File

@ -0,0 +1,331 @@
//
// Copyright © 2022 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.
//
// Parts of source code is taken from https://github.com/sueddeutsche/prosemirror-recreate-transform
// based on Apache License 2.0
//
import { Change, diffWordsWithSpace } from 'diff'
import { Node, Schema } from 'prosemirror-model'
import { ReplaceStep, Step, Transform } from 'prosemirror-transform'
import { applyPatch, createPatch, Operation } from 'rfc6902'
import { Pointer } from 'rfc6902/pointer'
import { diffArraysPM } from './diff'
/**
* @public
*/
export function getReplaceStep (fromDoc: Node, toDoc: Node): ReplaceStep | undefined {
const start = toDoc.content.findDiffStart(fromDoc.content)
if (start !== null) {
const pos = toDoc.content.findDiffEnd(fromDoc.content)
if (pos != null) {
return getReplaceStepOverlap(pos, start, fromDoc, toDoc)
}
}
}
function getReplaceStepOverlap (pos: { a: number, b: number }, start: number, fromDoc: Node, toDoc: Node): ReplaceStep {
let { a: endA, b: endB } = pos
const overlap = start - Math.min(endA, endB)
if (overlap > 0) {
if (fromDoc.resolve(start - overlap).depth < toDoc.resolve(endA + overlap).depth) {
start -= overlap
} else {
endA += overlap
endB += overlap
}
}
return new ReplaceStep(start, endB, toDoc.slice(start, endA))
}
export function simplifyTransform (tr: Transform): Transform | undefined {
if (tr.steps.length === 0) {
return undefined
}
const newTr = new Transform(tr.docs[0])
const oldSteps = tr.steps.slice()
while (oldSteps.length > 0) {
let step = oldSteps.shift()
while (oldSteps.length > 0 && step?.merge(oldSteps[0]) != null) {
step = simplifyStep(step, oldSteps.shift(), newTr)
}
if (step === undefined) {
return undefined
}
newTr.step(step)
}
return newTr
}
function simplifyStep (step: Step | undefined, addedStep: Step | undefined, newTr: Transform): Step | undefined {
if (step != null && step instanceof ReplaceStep && addedStep != null && addedStep instanceof ReplaceStep) {
const stepA = step.apply(newTr.doc)
if (stepA.doc != null) {
const stepB = addedStep.apply(stepA.doc)
if (stepB.doc !== null) {
step = getReplaceStep(newTr.doc, stepB.doc)
} else {
step = undefined
}
} else {
step = undefined
}
} else if (addedStep !== undefined) {
step = step?.merge(addedStep) ?? undefined
}
return step
}
function getFromPath (obj: any, path: string): any {
const pathParts = path.split('/')
pathParts.shift() // remove root
while (pathParts.length > 0) {
const property = pathParts.shift()
obj = obj[property ?? '']
}
return obj
}
function removeMarks (doc: Node): Node {
const tr = new Transform(doc)
tr.removeMark(0, doc.nodeSize - 2)
return tr.doc
}
function clone (obj: any): any {
if (typeof obj === 'function') {
return obj
}
const result: any = Array.isArray(obj) ? [] : {}
for (const key in obj) {
// include prototype properties
const value = obj[key]
const type = {}.toString.call(value).slice(8, -1)
if (type === 'Array') {
result[key] = clone(value)
} else if (type === 'Object') {
result[key] = clone(value)
} else if (type === 'Date') {
result[key] = new Date(value.getTime())
} else {
result[key] = value
}
}
return result
}
export class StepTransform {
schema: Schema
tr: Transform
currentDoc: any
finalDoc: any
ops: Operation[] = []
constructor (readonly fromDoc: Node, readonly toDoc: Node) {
this.schema = fromDoc.type.schema
this.tr = new Transform(fromDoc)
}
init (): Transform {
this.currentDoc = removeMarks(this.fromDoc).toJSON()
this.finalDoc = removeMarks(this.toDoc).toJSON()
this.ops = createPatch(this.currentDoc, this.finalDoc, (input: any, output: any, ptr: Pointer) => {
if (Array.isArray(input) && Array.isArray(output)) {
return diffArraysPM(input, output, ptr)
}
})
this.recreateChangeContentSteps()
this.recreateChangeMarkSteps()
this.tr = simplifyTransform(this.tr) ?? this.tr
return this.tr
}
recreateChangeContentSteps (): void {
// First step: find content changing steps.
let ops = []
while (this.ops.length > 0) {
// get next
let op = this.ops.shift() as Operation
ops.push(op)
let toDoc
const afterStepJSON = clone(this.currentDoc) // working document receiving patches
const pathParts = op.path.split('/')
// collect operations until we receive a valid document:
// apply ops-patches until a valid prosemirror document is retrieved,
// then try to create a transformation step or retry with next operation
while (toDoc == null) {
applyPatch(afterStepJSON, [op])
try {
toDoc = this.schema.nodeFromJSON(afterStepJSON)
toDoc.check()
} catch (error: any) {
toDoc = null
if (this.ops.length > 0) {
op = this.ops.shift() as Operation
ops.push(op)
} else {
throw new Error(`No valid diff possible applying ${op.path} ${JSON.stringify(error, undefined, 2)}`)
}
}
}
// apply operation (ignoring afterStepJSON)
if (ops.length === 1 && (pathParts.includes('attrs') || pathParts.includes('type'))) {
// Node markup is changing
this.addSetNodeMarkup() // a lost update is ignored
ops = []
} else if (ops.length === 1 && op.op === 'replace' && pathParts[pathParts.length - 1] === 'text') {
// Text is being replaced, we apply text diffing to find the smallest possible diffs.
this.addReplaceTextSteps(op, afterStepJSON)
ops = []
} else if (this.addReplaceStep(toDoc, afterStepJSON)) {
// operations have been applied
ops = []
}
}
}
addSetNodeMarkup (): boolean {
const fromDoc = this.schema.nodeFromJSON(this.currentDoc)
const toDoc = this.schema.nodeFromJSON(this.finalDoc)
const start = toDoc.content.findDiffStart(fromDoc.content)
// @note start is the same (first) position for current and target document
const fromNode = fromDoc.nodeAt(start ?? 0)
const toNode = toDoc.nodeAt(start ?? 0)
if (start != null) {
// @note this completly updates all attributes in one step, by completely replacing node
const nodeType = fromNode?.type === toNode?.type ? null : toNode?.type
try {
this.tr.setNodeMarkup(start, nodeType, toNode?.attrs, toNode?.marks)
} catch (e: any) {
if (nodeType != null && (e.message as string).includes('Invalid content')) {
// @todo add test-case for this scenario
if (fromNode != null && toNode != null) {
this.tr.replaceWith(start, start + fromNode.nodeSize, toNode)
}
} else {
throw e
}
}
this.currentDoc = removeMarks(this.tr.doc).toJSON()
// setting the node markup may have invalidated the following ops, so we calculate them again.
this.ops = createPatch(this.currentDoc, this.finalDoc)
return true
}
return false
}
recreateChangeMarkSteps (): void {
// Now the documents should be the same, except their marks, so everything should map 1:1.
// Second step: Iterate through the toDoc and make sure all marks are the same in tr.doc
this.toDoc.descendants((tNode, tPos) => {
if (!tNode.isInline) {
return true
}
this.tr.doc.nodesBetween(tPos, tPos + tNode.nodeSize, (fNode, fPos) => {
if (!fNode.isInline) {
return true
}
const from = Math.max(tPos, fPos)
const to = Math.min(tPos + tNode.nodeSize, fPos + fNode.nodeSize)
fNode.marks.forEach((nodeMark) => {
if (!nodeMark.isInSet(tNode.marks)) {
this.tr.removeMark(from, to, nodeMark)
}
})
tNode.marks.forEach((nodeMark) => {
if (!nodeMark.isInSet(fNode.marks)) {
this.tr.addMark(from, to, nodeMark)
}
})
})
})
}
addReplaceStep (toDoc: Node, afterStepJSON: any): boolean {
const fromDoc = this.schema.nodeFromJSON(this.currentDoc)
const step = getReplaceStep(fromDoc, toDoc)
if (step == null) {
return false
} else if (this.tr.maybeStep(step).failed === null) {
this.currentDoc = afterStepJSON
return true // @change previously null
}
throw new Error('No valid step found.')
}
addReplaceTextSteps (op: any, afterStepJSON: any): void {
// We find the position number of the first character in the string
const op1 = { ...op, value: 'xx' }
const op2 = { ...op, value: 'yy' }
const afterOP1JSON = clone(this.currentDoc)
const afterOP2JSON = clone(this.currentDoc)
applyPatch(afterOP1JSON, [op1])
applyPatch(afterOP2JSON, [op2])
const op1Doc = this.schema.nodeFromJSON(afterOP1JSON)
const op2Doc = this.schema.nodeFromJSON(afterOP2JSON)
// get text diffs
const finalText = op.value
const currentText = getFromPath(this.currentDoc, op.path)
const textDiffs = diffWordsWithSpace(currentText, finalText)
let offset = op1Doc.content.findDiffStart(op2Doc.content) as number
const marks = op1Doc.resolve(offset + 1).marks()
while (textDiffs.length > 0) {
const diff = textDiffs.shift() as Change
if (diff.added === true) {
const textNode = this.schema.nodeFromJSON({ type: 'text', text: diff.value }).mark(marks)
if (textDiffs.length > 0 && textDiffs[0].removed === true) {
const nextDiff = textDiffs.shift() as Change
this.tr.replaceWith(offset, offset + nextDiff.value.length, textNode)
} else {
this.tr.insert(offset, textNode)
}
offset += diff.value.length
} else if (diff.removed === true) {
if (textDiffs.length > 0 && textDiffs[0].added === true) {
const nextDiff = textDiffs.shift() as Change
const textNode = this.schema.nodeFromJSON({ type: 'text', text: nextDiff.value }).mark(marks)
this.tr.replaceWith(offset, offset + diff.value.length, textNode)
offset += nextDiff.value.length
} else {
this.tr.delete(offset, offset + diff.value.length)
}
} else {
offset += diff.value.length
}
}
this.currentDoc = afterStepJSON
}
}
export function recreateTransform (fromDoc: Node, toDoc: Node): Transform {
const recreator = new StepTransform(fromDoc, toDoc)
return recreator.init()
}

View File

@ -0,0 +1,8 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 19 24" xmlns="http://www.w3.org/2000/svg">
<path d="M0.818182 24V0.727272H3.63636V11.0909H16.0455V0.727272H18.8636V24H16.0455V13.5909H3.63636V24H0.818182Z" />
</svg>

View File

@ -26,6 +26,7 @@ export { default as TextEditor } from './components/TextEditor.svelte'
export { default as EmojiPopup } from './components/EmojiPopup.svelte' export { default as EmojiPopup } from './components/EmojiPopup.svelte'
export { default as FullDescriptionBox } from './components/FullDescriptionBox.svelte' export { default as FullDescriptionBox } from './components/FullDescriptionBox.svelte'
export { default as CollaboratorEditor } from './components/CollaboratorEditor.svelte' export { default as CollaboratorEditor } from './components/CollaboratorEditor.svelte'
export { default as CollaborationDiffViewer } from './components/CollaborationDiffViewer.svelte'
export { default } from './plugin' export { default } from './plugin'
export * from './types' export * from './types'

View File

@ -54,6 +54,7 @@ export default plugin(textEditorId, {
Food: '' as IntlString, Food: '' as IntlString,
Objects: '' as IntlString, Objects: '' as IntlString,
FullDescription: '' as IntlString, FullDescription: '' as IntlString,
NoFullDescription: '' as IntlString NoFullDescription: '' as IntlString,
EnableDiffMode: '' as IntlString
} }
}) })

View File

@ -34,7 +34,8 @@ export const FORMAT_MODES = [
'bulletList', 'bulletList',
'blockquote', 'blockquote',
'code', 'code',
'codeBlock' 'codeBlock',
'heading'
] as const ] as const
export type FormatMode = typeof FORMAT_MODES[number] export type FormatMode = typeof FORMAT_MODES[number]

View File

@ -1,61 +0,0 @@
//
// Copyright © 2022 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 { generateId } from '@hcengineering/core'
import { Node } from '@tiptap/core'
import { Plugin } from 'prosemirror-state'
export const UniqId = Node.create({
name: 'blockId',
addGlobalAttributes () {
return [
{
types: ['heading', 'paragraph'],
attributes: {
uid: {
default: undefined,
rendered: false,
keepOnSplit: false
}
}
}
]
},
addProseMirrorPlugins () {
return [
new Plugin({
appendTransaction: (_transactions, oldState, newState) => {
// no changes
if (newState.doc === oldState.doc) {
return
}
const tr = newState.tr
newState.doc.descendants((node, pos, parent) => {
if (node.isBlock && parent === newState.doc && node.attrs?.uid === undefined) {
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
uid: generateId()
})
}
})
return tr
}
})
]
}
})

View File

@ -29,6 +29,7 @@
export let isAside: boolean = true export let isAside: boolean = true
export let isFullSize: boolean = false export let isFullSize: boolean = false
export let withoutTitle: boolean = false export let withoutTitle: boolean = false
export let floatAside = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -39,7 +40,9 @@
$: twoRows = $deviceInfo.minWidth $: twoRows = $deviceInfo.minWidth
const checkPanel = (): void => { const checkPanel = (): void => {
if (panelWidth <= 900 && !asideFloat) asideFloat = true if (floatAside) {
asideFloat = true
} else if (panelWidth <= 900 && !asideFloat) asideFloat = true
else if (panelWidth > 900 && asideFloat) { else if (panelWidth > 900 && asideFloat) {
asideFloat = false asideFloat = false
asideShown = false asideShown = false

View File

@ -46,6 +46,8 @@
"Approve": "Approve", "Approve": "Approve",
"Reject": "Reject", "Reject": "Reject",
"Approved": "Approved", "Approved": "Approved",
"Rejected": "Rejected" "Rejected": "Rejected",
"CompareTo": "Compare to..."
} }
} }

View File

@ -46,6 +46,8 @@
"Approve": "Утвердить", "Approve": "Утвердить",
"Reject": "Отказать", "Reject": "Отказать",
"Approved": "Утверждено", "Approved": "Утверждено",
"Rejected": "Отказано" "Rejected": "Отказано",
"CompareTo": "Сравнить с..."
} }
} }

View File

@ -22,20 +22,32 @@
import document from '../plugin' import document from '../plugin'
import { CollaboratorEditor } from '@hcengineering/text-editor' import { CollaboratorEditor } from '@hcengineering/text-editor'
import { Markup } from '@hcengineering/core'
export let object: DocumentVersion export let object: DocumentVersion
export let readonly = false export let readonly = false
export let initialContentId: string | undefined = undefined export let initialContentId: string | undefined = undefined
export let suggestMode = false
export let comparedVersion: Markup | undefined = undefined
const token = getMetadata(login.metadata.LoginToken) ?? '' const token = getMetadata(login.metadata.LoginToken) ?? ''
const collaboratorURL = getMetadata(document.metadata.CollaboratorUrl) ?? '' const collaboratorURL = getMetadata(document.metadata.CollaboratorUrl) ?? ''
let editor: CollaboratorEditor
export function getHTML (): string {
return editor.getHTML()
}
</script> </script>
<CollaboratorEditor {#key comparedVersion}
documentId={object.contentAttachmentId} <CollaboratorEditor
{token} documentId={object.contentAttachmentId}
{collaboratorURL} {token}
{readonly} {suggestMode}
on:content {collaboratorURL}
{initialContentId} {readonly}
/> on:content
{initialContentId}
{comparedVersion}
bind:this={editor}
/>
{/key}

View File

@ -29,8 +29,9 @@
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel' import { Panel } from '@hcengineering/panel'
import { getResource, translate } from '@hcengineering/platform' import { getResource, translate } from '@hcengineering/platform'
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags' import tags from '@hcengineering/tags'
import { CollaborationDiffViewer } from '@hcengineering/text-editor'
import { import {
Button, Button,
@ -161,6 +162,7 @@
{ sort: { version: 1 } } { sort: { version: 1 } }
) )
let version: DocumentVersion | undefined let version: DocumentVersion | undefined
let compareTo: DocumentVersion | undefined
let info: any let info: any
@ -205,6 +207,25 @@
) )
} }
function selectCompareToVersion (event: MouseEvent): void {
showPopup(
SelectPopup,
{
value: [{ id: null, text: '-' }, ...info.slice(0, info.length - 1)],
placeholder: document.string.Version,
searchable: true
},
eventToHTMLElement(event),
(res) => {
if (res != null) {
compareTo = versions.find((it) => it._id === res)
} else if (res === null) {
compareTo = undefined
}
}
)
}
$: readonly = !documentObject?.authors.includes(currentUser.employee) $: readonly = !documentObject?.authors.includes(currentUser.employee)
let autoSelect = true let autoSelect = true
@ -213,8 +234,8 @@
let mode: ModelType = 'view' let mode: ModelType = 'view'
const modeLabels = { const modeLabels = {
view: document.string.ViewMode, view: document.string.ViewMode,
edit: document.string.EditMode edit: document.string.EditMode,
// ,suggest: document.string.SuggestMode suggest: document.string.SuggestMode
} }
function selectMode (event: MouseEvent): void { function selectMode (event: MouseEvent): void {
@ -304,8 +325,6 @@
let processing = false let processing = false
let content = ''
const updateRequests = async (kind: DocumentRequestKind): Promise<void> => { const updateRequests = async (kind: DocumentRequestKind): Promise<void> => {
processing = true processing = true
if (documentObject === undefined) { if (documentObject === undefined) {
@ -335,7 +354,7 @@
if (version) { if (version) {
await client.update(version, { await client.update(version, {
content content: editor.getHTML()
}) })
} }
@ -343,7 +362,7 @@
processing = false processing = false
} }
let editor: DocumentEditor
const updateState = async (state: DocumentVersionState): Promise<void> => { const updateState = async (state: DocumentVersionState): Promise<void> => {
processing = true processing = true
if (documentObject === undefined) { if (documentObject === undefined) {
@ -377,6 +396,12 @@
processing = false processing = false
} }
async function switchToDraft (): Promise<void> {
const requests = await client.findAll(document.class.DocumentRequest, { attachedTo: documentObject?._id })
for (const r of requests) {
client.remove(r)
}
}
</script> </script>
{#if documentObject !== undefined} {#if documentObject !== undefined}
@ -386,6 +411,7 @@
isAside={true} isAside={true}
isSub={false} isSub={false}
bind:innerWidth bind:innerWidth
floatAside={true}
on:close={() => dispatch('close')} on:close={() => dispatch('close')}
> >
<svelte:fragment slot="navigator"> <svelte:fragment slot="navigator">
@ -410,6 +436,21 @@
{/if} {/if}
</svelte:fragment> </svelte:fragment>
</Button> </Button>
<Button
loading={processing}
kind={'link-bordered'}
on:click={selectCompareToVersion}
disabled={info.length < 2}
>
<svelte:fragment slot="content">
{#if compareTo}
{compareTo.version} - {labels[compareTo.state]}
{:else}
<Label label={document.string.CompareTo} />
{/if}
</svelte:fragment>
</Button>
</span> </span>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="tools"> <svelte:fragment slot="tools">
@ -465,6 +506,16 @@
icon={IconClose} icon={IconClose}
size={'medium'} size={'medium'}
/> />
{#if !readonly}
<Button
loading={processing}
kind={'link-bordered'}
label={document.string.Draft}
on:click={() => switchToDraft()}
icon={IconEdit}
size={'medium'}
/>
{/if}
{/if} {/if}
{#if !readonly && version?.state === DocumentVersionState.Draft && approveRequest === undefined} {#if !readonly && version?.state === DocumentVersionState.Draft && approveRequest === undefined}
<Button loading={processing} kind={'link-bordered'} on:click={selectMode} icon={IconEdit} size={'medium'}> <Button loading={processing} kind={'link-bordered'} on:click={selectMode} icon={IconEdit} size={'medium'}>
@ -479,17 +530,19 @@
<div class="description-preview select-text mt-2 emphasized"> <div class="description-preview select-text mt-2 emphasized">
{#if version && version.state === DocumentVersionState.Draft && approveRequest === undefined} {#if version && version.state === DocumentVersionState.Draft && approveRequest === undefined}
{#key version?._id} {#key version?._id}
<!-- suggestMode={mode === 'suggest'} -->
<DocumentEditor <DocumentEditor
object={version} object={version}
initialContentId={version.initialContentId} initialContentId={version.initialContentId}
comparedVersion={compareTo?.content ?? versions[versions.length - 1].content}
readonly={mode === 'view'} readonly={mode === 'view'}
on:content={(evt) => { bind:this={editor}
content = evt.detail
}}
/> />
{/key} {/key}
{:else if version} {:else if version}
<MessageViewer message={version.content} /> {#key [compareTo?.content, version.content]}
<CollaborationDiffViewer content={version.content} comparedVersion={compareTo?.content} />
{/key}
{/if} {/if}
</div> </div>
@ -533,6 +586,7 @@
.description-preview { .description-preview {
color: var(--theme-content-color); color: var(--theme-content-color);
line-height: 150%; line-height: 150%;
overflow: auto;
} }
.tab-content { .tab-content {

View File

@ -76,6 +76,8 @@ export default mergeIds(documentId, document, {
Requests: '' as IntlString, Requests: '' as IntlString,
Approve: '' as IntlString, Approve: '' as IntlString,
Reject: '' as IntlString Reject: '' as IntlString,
CompareTo: '' as IntlString
} }
}) })