Merge branch 'staging-new'

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov
2025-10-02 08:44:03 +07:00
538 changed files with 13698 additions and 6584 deletions
+5 -21
View File
@@ -104,9 +104,12 @@
"UPLOAD_URL": "/files",
"AI_BOT_URL": "http://localhost:4010",
"STATS_URL": "http://huly.local:4900",
"QUEUE_CONFIG": "localhost:19092",
"QUEUE_CONFIG": "huly.local:19092",
"REGION": "cockroach",
"QUEUE_REGION": "cockroach",
"FILES_URL": "http://huly.local:4030/blob/:workspace/:blobId/:filename",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://huly.local:4318/v1/traces"
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://huly.local:4318/v1/traces",
"BRANDING_PATH": "${workspaceRoot}/dev/branding.json"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
@@ -972,25 +975,6 @@
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"cwd": "${workspaceRoot}/services/export/pod-export"
},
{
"name": "Msg2File",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"ACCOUNTS_URL": "http://localhost:3000",
"DB_URL": "postgresql://root@localhost:26257/defaultdb?sslmode=disable",
"PORT": "9087",
"SECRET": "secret",
"SERVICE_ID": "msg2file-service",
"STORAGE_CONFIG": "datalake|http://huly.local:4030"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/services/msg2file"
}
]
}
+106 -210
View File
@@ -36,13 +36,13 @@ importers:
version: 8.4.1
'@hocuspocus/provider':
specifier: ^2.15.2
version: 2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)
version: 2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
'@hocuspocus/server':
specifier: ^2.15.2
version: 2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)
version: 2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
'@hocuspocus/transformer':
specifier: ^2.15.2
version: 2.15.2(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))(yjs@13.6.23)
version: 2.15.2(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27)
'@koa/cors':
specifier: ^5.0.0
version: 5.0.0
@@ -267,7 +267,7 @@ importers:
version: file:projects/collaboration.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
'@rush-temp/collaborator':
specifier: file:./projects/collaborator.tgz
version: file:projects/collaborator.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(@tiptap/pm@2.11.7)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))(y-protocols@1.0.6(yjs@13.6.23))
version: file:projects/collaborator.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(@tiptap/pm@2.11.7)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(y-protocols@1.0.6(yjs@13.6.27))
'@rush-temp/collaborator-client':
specifier: file:./projects/collaborator-client.tgz
version: file:projects/collaborator-client.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
@@ -304,9 +304,6 @@ importers:
'@rush-temp/communication-types':
specifier: file:./projects/communication-types.tgz
version: file:projects/communication-types.tgz(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
'@rush-temp/communication-yaml':
specifier: file:./projects/communication-yaml.tgz
version: file:projects/communication-yaml.tgz(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
'@rush-temp/contact':
specifier: file:./projects/contact.tgz
version: file:projects/contact.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
@@ -912,7 +909,7 @@ importers:
version: file:projects/pod-fulltext.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))
'@rush-temp/pod-github':
specifier: file:./projects/pod-github.tgz
version: file:projects/pod-github.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))
version: file:projects/pod-github.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))
'@rush-temp/pod-gmail':
specifier: file:./projects/pod-gmail.tgz
version: file:projects/pod-gmail.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
@@ -928,9 +925,6 @@ importers:
'@rush-temp/pod-media':
specifier: file:./projects/pod-media.tgz
version: file:projects/pod-media.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))
'@rush-temp/pod-msg2file':
specifier: file:./projects/pod-msg2file.tgz
version: file:projects/pod-msg2file.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))
'@rush-temp/pod-notification':
specifier: file:./projects/pod-notification.tgz
version: file:projects/pod-notification.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))
@@ -1413,7 +1407,7 @@ importers:
version: file:projects/text-markdown.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
'@rush-temp/text-ydoc':
specifier: file:./projects/text-ydoc.tgz
version: file:projects/text-ydoc.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.23))
version: file:projects/text-ydoc.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.27))
'@rush-temp/theme':
specifier: file:./projects/theme.tgz
version: file:projects/theme.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
@@ -1539,10 +1533,10 @@ importers:
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/extension-code-block@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(highlight.js@11.11.1)(lowlight@3.3.0)
'@tiptap/extension-collaboration':
specifier: ^2.11.7
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))
'@tiptap/extension-collaboration-cursor':
specifier: ^2.11.7
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))
'@tiptap/extension-document':
specifier: ^2.11.7
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
@@ -1732,9 +1726,6 @@ importers:
'@types/node':
specifier: ^22.15.29
version: 22.15.29
'@types/node-cron':
specifier: ^3.0.11
version: 3.0.11
'@types/nodemailer':
specifier: ^6.4.17
version: 6.4.17
@@ -2032,6 +2023,9 @@ importers:
graphql-binding:
specifier: ^2.5.2
version: 2.5.2
hash-it:
specifier: ^6.0.0
version: 6.0.0
heic-decode:
specifier: ^2.1.0
version: 2.1.0
@@ -2102,8 +2096,8 @@ importers:
specifier: ~1.0.4
version: 1.0.5
lib0:
specifier: ^0.2.88
version: 0.2.89
specifier: ^0.2.109
version: 0.2.114
libphonenumber-js:
specifier: ^1.9.46
version: 1.10.56
@@ -2155,9 +2149,6 @@ importers:
msgpackr-extract:
specifier: ^3.0.3
version: 3.0.3
node-cron:
specifier: ^3.0.3
version: 3.0.3
node-forge:
specifier: ^1.3.1
version: 1.3.1
@@ -2391,19 +2382,19 @@ importers:
version: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
y-indexeddb:
specifier: ^9.0.12
version: 9.0.12(yjs@13.6.23)
version: 9.0.12(yjs@13.6.27)
y-prosemirror:
specifier: ^1.3.6
version: 1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)
specifier: ^1.3.7
version: 1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
y-protocols:
specifier: ^1.0.6
version: 1.0.6(yjs@13.6.23)
version: 1.0.6(yjs@13.6.27)
y-websocket:
specifier: ^2.1.0
version: 2.1.0(yjs@13.6.23)
version: 2.1.0(yjs@13.6.27)
yjs:
specifier: ^13.6.23
version: 13.6.23
specifier: ^13.6.27
version: 13.6.27
zod:
specifier: ^3.22.4
version: 3.24.2
@@ -4564,7 +4555,7 @@ packages:
version: 0.0.0
'@rush-temp/collaboration@file:projects/collaboration.tgz':
resolution: {integrity: sha512-aDcBIrPlESppS8EWtyzKhLJSC7IEHzPdLxtA05aZtp63v6+LRARuzfhT1x53KJ1u03/2ttjGu/ts7dVEnct2kw==, tarball: file:projects/collaboration.tgz}
resolution: {integrity: sha512-Ij/ksdlgcTdsHBU00nzKWoYkQNM0RMJdaPR1ZSmOMlKe9lndge3UT23w3G0KHXelXlkzBxKfKWPEfnAy1biedw==, tarball: file:projects/collaboration.tgz}
version: 0.0.0
'@rush-temp/collaborator-client@file:projects/collaborator-client.tgz':
@@ -4572,7 +4563,7 @@ packages:
version: 0.0.0
'@rush-temp/collaborator@file:projects/collaborator.tgz':
resolution: {integrity: sha512-cGZGxft0YIkGzuUQIhYORUQ00fi/rhyhJnA0TMYMniRDfLLewBPvRtyXIHSHVZG1AJ1TWyyp+Xe+MrKfGYDzFw==, tarball: file:projects/collaborator.tgz}
resolution: {integrity: sha512-+WZ7aCQzJBE8eF+5MM4ruyqa73msf/U6AMvX598LOHy6WwTL/id/IXTSyult37VBTYpCa64I6BhD77gTV8D4Rw==, tarball: file:projects/collaborator.tgz}
version: 0.0.0
'@rush-temp/communication-assets@file:projects/communication-assets.tgz':
@@ -4580,7 +4571,7 @@ packages:
version: 0.0.0
'@rush-temp/communication-client-query@file:projects/communication-client-query.tgz':
resolution: {integrity: sha512-6OA/sAql1ZLhhDX9J2yZOAEgfXqSxYh6yxpH/YGrC3PfYyhVAoe7piCjpB+JpKR5wL1kzpl1eP7Bj0IEq2jNJQ==, tarball: file:projects/communication-client-query.tgz}
resolution: {integrity: sha512-+68bXbpIU6XTujL5Dj3pFCdFelV7mE+Ln6zZEvf2NPHhAcJqy3+KjHQyIbytnWhCFsGyk+HE3S08ps6Kl6TKtw==, tarball: file:projects/communication-client-query.tgz}
version: 0.0.0
'@rush-temp/communication-cockroach@file:projects/communication-cockroach.tgz':
@@ -4588,7 +4579,7 @@ packages:
version: 0.0.0
'@rush-temp/communication-query@file:projects/communication-query.tgz':
resolution: {integrity: sha512-sXcvSDoXmgZH233jFRePio23qQosvQZoACKqKgJ2Obr+oY4ZzJ2uX7DM0lyIKbhFUuLNFl56u2RfM5ra8SxRDg==, tarball: file:projects/communication-query.tgz}
resolution: {integrity: sha512-KMDSFpLNW1ekFoAf+BbRxPeRYnMUjYu+l80F+fgtvkKsX/6oKtb6xRhQyVh4EQiRlQBaMV//1b2d7cBwuF5HDw==, tarball: file:projects/communication-query.tgz}
version: 0.0.0
'@rush-temp/communication-resources@file:projects/communication-resources.tgz':
@@ -4604,21 +4595,17 @@ packages:
version: 0.0.0
'@rush-temp/communication-server@file:projects/communication-server.tgz':
resolution: {integrity: sha512-KVc53phC1b9CNptvJmMb706t74gsCiEQGWvCpvTlRWMd3q+pj7i2994gJ0/KzuCojcHxPYVn2QxNojpNSV66Eg==, tarball: file:projects/communication-server.tgz}
resolution: {integrity: sha512-HfKzdbndaS3opK1inqcTOrVnKvU7PS2asLmKuR4JHooiTTvFFCHcXXYO++t1QmdGmankZRn3Qsq59CIt/UPgsA==, tarball: file:projects/communication-server.tgz}
version: 0.0.0
'@rush-temp/communication-shared@file:projects/communication-shared.tgz':
resolution: {integrity: sha512-SustnNpr/eopCXmm0ZahvUZXZPcD7psSWM+CkWVwiPlLVPGWMVUURFOcnC94F3OhY+7qu9N0Rgr3gDvr4VKXWA==, tarball: file:projects/communication-shared.tgz}
resolution: {integrity: sha512-/ZUh5sviGQkQGJnOe6ImJHpKCcn7xNyyMeOcl4Tr3TqXjsH+uOQ0MAFI0Ul4f1LI+dGIrp1QsZWSVWvEVprRQQ==, tarball: file:projects/communication-shared.tgz}
version: 0.0.0
'@rush-temp/communication-types@file:projects/communication-types.tgz':
resolution: {integrity: sha512-uzZ82V1R+h+TmxBCrmKaAfUgoiX3Z8ciPGrkc42M++M5d5mlfIjTvPh0O+y50pHoFuQ5NQ3pNIjeDkrJE8Dp0w==, tarball: file:projects/communication-types.tgz}
version: 0.0.0
'@rush-temp/communication-yaml@file:projects/communication-yaml.tgz':
resolution: {integrity: sha512-D+fijwXQM2oMeT6QOmkRVLik5uyVCNsml07rz5RvKcPEpbqRh3YBtIvcUABoCStLvNTIzYuXnqbCHzSl/ibidw==, tarball: file:projects/communication-yaml.tgz}
version: 0.0.0
'@rush-temp/communication@file:projects/communication.tgz':
resolution: {integrity: sha512-kO/x+NBE4Nmtz5KhHO0kxty/0N1A2PH4dNVTdOviEV4mVAhFm7DkgWHu0SwWJvQrCzsB0xEga3/CGuGjcPyMjA==, tarball: file:projects/communication.tgz}
version: 0.0.0
@@ -4684,7 +4671,7 @@ packages:
version: 0.0.0
'@rush-temp/desktop@file:projects/desktop.tgz':
resolution: {integrity: sha512-kMVLNqalurSBKidY00TqK2GIBSaEvgAKScF/32N4vsmx+pzMBkE6U63HiW+RxsqVC/ig9QikeohoDXx7Ps9kHg==, tarball: file:projects/desktop.tgz}
resolution: {integrity: sha512-EIuP2KmLDWhA4J63srKU8MI9wWCUTpNtCPGaS9P4guXJdH55cV8cqbWYbEYuWqsa28YPPoS1hel73H92Mf7nKw==, tarball: file:projects/desktop.tgz}
version: 0.0.0
'@rush-temp/devmodel-resources@file:projects/devmodel-resources.tgz':
@@ -4912,7 +4899,7 @@ packages:
version: 0.0.0
'@rush-temp/love-resources@file:projects/love-resources.tgz':
resolution: {integrity: sha512-uB1RrbistrQwA5PIld+dgw+QnpTtS5i6wdiSAemCBe2ay5hTKExnmINPrgM/p7FEkya2sq3IVZXzric/ZYwnng==, tarball: file:projects/love-resources.tgz}
resolution: {integrity: sha512-d/rTH5piCl8x3YfKoZ2yIWklH/MU9h8WrccgwV9VS3a1lYQ2i6l+JVpO/hdMGTN6llF3REFNnMYndfcu8Siy4w==, tarball: file:projects/love-resources.tgz}
version: 0.0.0
'@rush-temp/love@file:projects/love.tgz':
@@ -4976,7 +4963,7 @@ packages:
version: 0.0.0
'@rush-temp/model-all@file:projects/model-all.tgz':
resolution: {integrity: sha512-u5b+jP4WxAVIk6DlawxySE+qDG7iz7YD3dTT3LCgR3Ml5RkMOlej4YtbO0YQmX4wp4l4gMJKdRCgBCpVfytksg==, tarball: file:projects/model-all.tgz}
resolution: {integrity: sha512-1IC5c/j2fgqgamtZGZLUBHUuzMaqM6eguG6WTbmdTrSW8YjfUYeutKSRDRykbF6SilzaGS8eKxhNpZTIRT20fA==, tarball: file:projects/model-all.tgz}
version: 0.0.0
'@rush-temp/model-analytics-collector@file:projects/model-analytics-collector.tgz':
@@ -5424,7 +5411,7 @@ packages:
version: 0.0.0
'@rush-temp/pod-fulltext@file:projects/pod-fulltext.tgz':
resolution: {integrity: sha512-0G3FKVxHWnyPuHWVBiR3Xtns3rLxGTC6ferQCX8NSC3dGOGOvk4TZgB8leohmJavwwxdZQKk1QFJqmXvHux10A==, tarball: file:projects/pod-fulltext.tgz}
resolution: {integrity: sha512-kPi4ermTxQtUKZ5xzbJgT07rLj2gG6m4YQgArWiONz47ooSoizi/Sg8Pu2FFyVV0klzQwqUOseuoVqgdySnFNg==, tarball: file:projects/pod-fulltext.tgz}
version: 0.0.0
'@rush-temp/pod-github@file:projects/pod-github.tgz':
@@ -5451,10 +5438,6 @@ packages:
resolution: {integrity: sha512-5LRHcx/zPNVCJbIBfjigt+of64jfGrH5fjoDyvhASTEJbtG4RGRj8kKZoUI+1JHibDZzcVn2NLc+3nFMhksreA==, tarball: file:projects/pod-media.tgz}
version: 0.0.0
'@rush-temp/pod-msg2file@file:projects/pod-msg2file.tgz':
resolution: {integrity: sha512-7b2GrlsmevpahNIADhqNUBpshocrdjlc2L0kf2BAFVSEFpjzUtRju2XqwwoZ2l7xA0Z/b+Un9PHt+XYMWcNzlg==, tarball: file:projects/pod-msg2file.tgz}
version: 0.0.0
'@rush-temp/pod-notification@file:projects/pod-notification.tgz':
resolution: {integrity: sha512-1EeImD9ZNRxWq0jDp6o50hOpuoB0sNI54V/V1pm5VONdRF0S2lVzPCr35YehJ5BJcvbhy22au/7rk9XadoYI9w==, tarball: file:projects/pod-notification.tgz}
version: 0.0.0
@@ -5524,7 +5507,7 @@ packages:
version: 0.0.0
'@rush-temp/presentation@file:projects/presentation.tgz':
resolution: {integrity: sha512-nMn7FUSF2F6YuJAstYbvV8Yi9upkKqeSccsU3EOrKjJm1c57sa7x8i4K3Z6l4RoSQk86/F5CrzZVgqIMKCjF0w==, tarball: file:projects/presentation.tgz}
resolution: {integrity: sha512-r1AG89qg5hvx+usQjP8KLtkNL/j5z/8OHDcURyAb6yOhWAWNkY5Frs05L9qcPsJoN6raUaZ164jzlk0P2SiPqQ==, tarball: file:projects/presentation.tgz}
version: 0.0.0
'@rush-temp/print-assets@file:projects/print-assets.tgz':
@@ -5552,7 +5535,7 @@ packages:
version: 0.0.0
'@rush-temp/prod@file:projects/prod.tgz':
resolution: {integrity: sha512-OvmMqiOyuDeKVGHc7g8UxMlvr9WAJsqtqICsv6kcx9vSvB7bB0iXOLY9CevyOOBDk5Y6OvRKWJKeY9pif+pjMg==, tarball: file:projects/prod.tgz}
resolution: {integrity: sha512-LkECK91CfEPLwDVaCQWN4XSeBlM1jXqboKuXtu7ftZkgG66wtt1/AlVDqwRY6o1iuvVNvuTJBSp03DMfDzBIKw==, tarball: file:projects/prod.tgz}
version: 0.0.0
'@rush-temp/products-assets@file:projects/products-assets.tgz':
@@ -5804,7 +5787,7 @@ packages:
version: 0.0.0
'@rush-temp/server-indexer@file:projects/server-indexer.tgz':
resolution: {integrity: sha512-OYx2FWCD6jtTnsKmYnhhS3BsvhtCN9pIYkvIoSe0mzZi3xEAhnwu2T59+JpZgRO1fvD1yVxBy7nw0hyFtz03Hg==, tarball: file:projects/server-indexer.tgz}
resolution: {integrity: sha512-InjSApXF2jmClorioQIIV+JxbKtuYjl3a9i4eetEWuG2z7R+Ya7L6f98cqhEhYkdA4hM5TTZYbS+BcRLWlSqQg==, tarball: file:projects/server-indexer.tgz}
version: 0.0.0
'@rush-temp/server-inventory-resources@file:projects/server-inventory-resources.tgz':
@@ -6068,7 +6051,7 @@ packages:
version: 0.0.0
'@rush-temp/text-core@file:projects/text-core.tgz':
resolution: {integrity: sha512-WezIZjq0OzkKSAy6wMqDFrvEsZ0HlLXzmctJ8w/io4+ywOUO2Cb9ZDXn80wwr5t3TFdb2rvfg1lMM5xDuC+RfA==, tarball: file:projects/text-core.tgz}
resolution: {integrity: sha512-wSRif2622acwkDHy2FtsEDhnDTsl8hydf/MFYSgAri0l5sQ6qOPHLuh8FZ2yf81EWTLfK/iuez70cAlYwkypMw==, tarball: file:projects/text-core.tgz}
version: 0.0.0
'@rush-temp/text-editor-assets@file:projects/text-editor-assets.tgz':
@@ -6076,7 +6059,7 @@ packages:
version: 0.0.0
'@rush-temp/text-editor-resources@file:projects/text-editor-resources.tgz':
resolution: {integrity: sha512-vb8UXE1DL6LyA3M51Pj4uKcuxdoaPEYFcNa/P7i0zR8XsebrOJZWNAhvtIsH1NKmNycLBNhvmTxKKEM1XEaiLA==, tarball: file:projects/text-editor-resources.tgz}
resolution: {integrity: sha512-2aRRXC4H573M3+OR/vRe5pGoA4tAB3jEbufqAe4/EgKkUHvRlGVZBBUyDAfYtsIHQd7IrAEEVK1NQoC6iZE1Lg==, tarball: file:projects/text-editor-resources.tgz}
version: 0.0.0
'@rush-temp/text-editor@file:projects/text-editor.tgz':
@@ -6092,7 +6075,7 @@ packages:
version: 0.0.0
'@rush-temp/text-ydoc@file:projects/text-ydoc.tgz':
resolution: {integrity: sha512-vKLzX1+/6aZmMzsR213dQDXADDE4cDw3M2Ofkn3r7QKV6GidHGHipTlBO4jjsCbYK4G0L1QIf8ZHi7QhpE8icQ==, tarball: file:projects/text-ydoc.tgz}
resolution: {integrity: sha512-EdNN9bQUsIb2qPGpotFbm5xIQDH1GJilcCiV66EVTlrsRkQSAjqVDPZyhQALE8200gR8K7OCSayKoQPnb/nPEA==, tarball: file:projects/text-ydoc.tgz}
version: 0.0.0
'@rush-temp/text@file:projects/text.tgz':
@@ -6116,7 +6099,7 @@ packages:
version: 0.0.0
'@rush-temp/tool@file:projects/tool.tgz':
resolution: {integrity: sha512-bsqWzFX5qr02kfG1zQZOgHsGWzR/Z66VC16ZoQnXs6mkdHFSmQ2p2l8eNL/aYPu2qfrRkMNAnI8cu6aULfKuDg==, tarball: file:projects/tool.tgz}
resolution: {integrity: sha512-FwR6SJSyoZvzT/0uIoItaG+S6N+ofaqtNJ4ZXRJ8f454jzOGrzjgjL+BupOX+xJCY4w9zNfpMOIke7e9LPX5pA==, tarball: file:projects/tool.tgz}
version: 0.0.0
'@rush-temp/tracker-assets@file:projects/tracker-assets.tgz':
@@ -7176,9 +7159,6 @@ packages:
'@types/mysql@2.15.27':
resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==}
'@types/node-cron@3.0.11':
resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==}
'@types/node-fetch@2.6.12':
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
@@ -10177,6 +10157,9 @@ packages:
has-unicode@2.0.1:
resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
hash-it@6.0.0:
resolution: {integrity: sha512-KHzmSFx1KwyMPw0kXeeUD752q/Kfbzhy6dAZrjXV9kAIXGqzGvv8vhkUqj+2MGZldTo0IBpw6v7iWE7uxsvH0w==}
hasown@2.0.1:
resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==}
engines: {node: '>= 0.4'}
@@ -11170,18 +11153,8 @@ packages:
lexorank@1.0.5:
resolution: {integrity: sha512-K1B/Yr/gIU0wm68hk/yB0p/mv6xM3ShD5aci42vOwcjof8slG8Kpo3Q7+1WTv7DaRHKWRgLPqrFDt+4GtuFAtA==}
lib0@0.2.109:
resolution: {integrity: sha512-jP0gbnyW0kwlx1Atc4dcHkBbrVAkdHjuyHxtClUPYla7qCmwIif1qZ6vQeJdR5FrOVdn26HvQT0ko01rgW7/Xw==}
engines: {node: '>=16'}
hasBin: true
lib0@0.2.89:
resolution: {integrity: sha512-5j19vcCjsQhvLG6mcDD+nprtJUCbmqLz5Hzt5xgi9SV6RIW/Dty7ZkVZHGBuPOADMKjQuKDvuQTH495wsmw8DQ==}
engines: {node: '>=16'}
hasBin: true
lib0@0.2.99:
resolution: {integrity: sha512-vwztYuUf1uf/1zQxfzRfO5yzfNKhTtgOByCruuiQQxWQXnPb8Itaube5ylofcV0oM0aKal9Mv+S1s1Ky0UYP1w==}
lib0@0.2.114:
resolution: {integrity: sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==}
engines: {node: '>=16'}
hasBin: true
@@ -11808,10 +11781,6 @@ packages:
node-api-version@0.2.0:
resolution: {integrity: sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==}
node-cron@3.0.3:
resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==}
engines: {node: '>=6.0.0'}
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
@@ -14469,8 +14438,8 @@ packages:
peerDependencies:
yjs: ^13.0.0
y-prosemirror@1.3.6:
resolution: {integrity: sha512-vtS2rv8+ll/TBQRqwUiqflgSuN/DhfvUQX0r5O3o5i0pO6K4pSNgFtVkOKtNWPBVkS6l9BDQjbtnDNftZnxq7Q==}
y-prosemirror@1.3.7:
resolution: {integrity: sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
peerDependencies:
prosemirror-model: ^1.7.1
@@ -14538,8 +14507,8 @@ packages:
yauzl@2.10.0:
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
yjs@13.6.23:
resolution: {integrity: sha512-ExtnT5WIOVpkL56bhLeisG/N5c4fmzKn4k0ROVfJa5TY2QHbH7F0Wu2T5ZhR7ErsFWQEFafyrnSI8TPKVF9Few==}
yjs@13.6.27:
resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
ylru@1.3.2:
@@ -15620,41 +15589,41 @@ snapshots:
'@hocuspocus/common@2.15.2':
dependencies:
lib0: 0.2.99
lib0: 0.2.114
'@hocuspocus/provider@2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)':
'@hocuspocus/provider@2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)':
dependencies:
'@hocuspocus/common': 2.15.2
'@lifeomic/attempt': 3.0.3
lib0: 0.2.99
lib0: 0.2.114
ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
y-protocols: 1.0.6(yjs@13.6.23)
yjs: 13.6.23
y-protocols: 1.0.6(yjs@13.6.27)
yjs: 13.6.27
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@hocuspocus/server@2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)':
'@hocuspocus/server@2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)':
dependencies:
'@hocuspocus/common': 2.15.2
async-lock: 1.4.1
kleur: 4.1.5
lib0: 0.2.99
lib0: 0.2.114
uuid: 11.1.0
ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
y-protocols: 1.0.6(yjs@13.6.23)
yjs: 13.6.23
y-protocols: 1.0.6(yjs@13.6.27)
yjs: 13.6.27
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@hocuspocus/transformer@2.15.2(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))(yjs@13.6.23)':
'@hocuspocus/transformer@2.15.2(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27)':
dependencies:
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
'@tiptap/pm': 2.11.7
'@tiptap/starter-kit': 2.11.7
y-prosemirror: 1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)
yjs: 13.6.23
y-prosemirror: 1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
yjs: 13.6.27
'@humanwhocodes/config-array@0.11.14':
dependencies:
@@ -18947,7 +18916,7 @@ snapshots:
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3)
typescript: 5.8.3
yjs: 13.6.23
yjs: 13.6.27
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
@@ -18984,10 +18953,10 @@ snapshots:
- supports-color
- ts-node
'@rush-temp/collaborator@file:projects/collaborator.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(@tiptap/pm@2.11.7)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))(y-protocols@1.0.6(yjs@13.6.23))':
'@rush-temp/collaborator@file:projects/collaborator.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(@tiptap/pm@2.11.7)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(y-protocols@1.0.6(yjs@13.6.27))':
dependencies:
'@hocuspocus/server': 2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)
'@hocuspocus/transformer': 2.15.2(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))(yjs@13.6.23)
'@hocuspocus/server': 2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
'@hocuspocus/transformer': 2.15.2(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27)
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
'@tiptap/html': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
'@types/body-parser': 1.19.5
@@ -19016,7 +18985,7 @@ snapshots:
ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)
typescript: 5.8.3
ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
yjs: 13.6.23
yjs: 13.6.27
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@babel/core'
@@ -19286,29 +19255,6 @@ snapshots:
- supports-color
- ts-node
'@rush-temp/communication-yaml@file:projects/communication-yaml.tgz(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))':
dependencies:
'@types/js-yaml': 4.0.9
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3)
esbuild: 0.25.9
esbuild-plugin-copy: 2.1.1(esbuild@0.25.9)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
js-yaml: 4.1.0
prettier: 3.2.5
typescript: 5.8.3
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- node-notifier
- supports-color
- ts-node
'@rush-temp/communication@file:projects/communication.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))':
dependencies:
'@types/jest': 29.5.12
@@ -25144,7 +25090,7 @@ snapshots:
- node-notifier
- supports-color
'@rush-temp/pod-github@file:projects/pod-github.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))':
'@rush-temp/pod-github@file:projects/pod-github.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))':
dependencies:
'@octokit/types': 12.6.0
'@octokit/webhooks': 12.2.0
@@ -25153,8 +25099,8 @@ snapshots:
'@tiptap/extension-bubble-menu': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
'@tiptap/extension-code': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
'@tiptap/extension-code-block': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
'@tiptap/extension-collaboration': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))
'@tiptap/extension-collaboration-cursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))
'@tiptap/extension-collaboration': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))
'@tiptap/extension-collaboration-cursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))
'@tiptap/extension-gapcursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
'@tiptap/extension-heading': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
'@tiptap/extension-highlight': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
@@ -25435,47 +25381,6 @@ snapshots:
- node-notifier
- supports-color
'@rush-temp/pod-msg2file@file:projects/pod-msg2file.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))':
dependencies:
'@tsconfig/node16': 1.0.4
'@types/cors': 2.8.17
'@types/express': 4.17.21
'@types/jest': 29.5.12
'@types/js-yaml': 4.0.9
'@types/node': 22.15.29
'@types/node-cron': 3.0.11
'@types/uuid': 8.3.4
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3)
cors: 2.8.5
dotenv: 16.0.3
esbuild: 0.25.9
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-node: 11.1.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
express: 4.21.2
jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
js-yaml: 4.1.0
node-cron: 3.0.3
postgres: 3.4.7
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3)
ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)
typescript: 5.8.3
uuid: 8.3.2
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@swc/core'
- '@swc/wasm'
- babel-jest
- babel-plugin-macros
- node-notifier
- supports-color
'@rush-temp/pod-notification@file:projects/pod-notification.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))':
dependencies:
'@tsconfig/node16': 1.0.4
@@ -26133,7 +26038,7 @@ snapshots:
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3)
typescript: 5.8.3
uuid: 8.3.2
yjs: 13.6.23
yjs: 13.6.27
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
@@ -29891,6 +29796,7 @@ snapshots:
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
fast-equals: 5.2.2
hash-it: 6.0.0
jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
jest-environment-jsdom: 29.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
prettier: 3.2.5
@@ -29937,15 +29843,15 @@ snapshots:
'@rush-temp/text-editor-resources@file:projects/text-editor-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(highlight.js@11.11.1)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4)':
dependencies:
'@hocuspocus/provider': 2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)
'@hocuspocus/provider': 2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
'@tiptap/extension-bubble-menu': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
'@tiptap/extension-bullet-list': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
'@tiptap/extension-code': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
'@tiptap/extension-code-block': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
'@tiptap/extension-code-block-lowlight': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/extension-code-block@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(highlight.js@11.11.1)(lowlight@3.3.0)
'@tiptap/extension-collaboration': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))
'@tiptap/extension-collaboration-cursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))
'@tiptap/extension-collaboration': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))
'@tiptap/extension-collaboration-cursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))
'@tiptap/extension-hard-break': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
'@tiptap/extension-heading': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
'@tiptap/extension-highlight': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
@@ -29978,7 +29884,7 @@ snapshots:
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
fast-equals: 5.2.2
jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
lib0: 0.2.89
lib0: 0.2.114
lowlight: 3.3.0
mermaid: 11.4.1
prettier: 3.2.5
@@ -29995,11 +29901,11 @@ snapshots:
tippy.js: 6.3.7
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3)
typescript: 5.8.3
y-indexeddb: 9.0.12(yjs@13.6.23)
y-prosemirror: 1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)
y-protocols: 1.0.6(yjs@13.6.23)
y-websocket: 2.1.0(yjs@13.6.23)
yjs: 13.6.23
y-indexeddb: 9.0.12(yjs@13.6.27)
y-prosemirror: 1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
y-protocols: 1.0.6(yjs@13.6.27)
y-websocket: 2.1.0(yjs@13.6.27)
yjs: 13.6.27
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
@@ -30108,7 +30014,7 @@ snapshots:
- supports-color
- ts-node
'@rush-temp/text-ydoc@file:projects/text-ydoc.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.23))':
'@rush-temp/text-ydoc@file:projects/text-ydoc.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.27))':
dependencies:
'@types/jest': 29.5.12
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3)
@@ -30121,11 +30027,12 @@ snapshots:
fast-equals: 5.2.2
jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
jest-environment-jsdom: 29.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
lib0: 0.2.114
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3)
typescript: 5.8.3
y-prosemirror: 1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)
yjs: 13.6.23
y-prosemirror: 1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
yjs: 13.6.27
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
@@ -30348,6 +30255,7 @@ snapshots:
'@elastic/elasticsearch': 7.17.14
'@faker-js/faker': 8.4.1
'@types/jest': 29.5.12
'@types/js-yaml': 4.0.9
'@types/mime-types': 2.1.4
'@types/minio': 7.0.18
'@types/node': 22.15.29
@@ -31519,16 +31427,16 @@ snapshots:
dependencies:
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
'@tiptap/extension-collaboration-cursor@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))':
'@tiptap/extension-collaboration-cursor@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))':
dependencies:
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
y-prosemirror: 1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)
y-prosemirror: 1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
'@tiptap/extension-collaboration@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))':
'@tiptap/extension-collaboration@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))':
dependencies:
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
'@tiptap/pm': 2.11.7
y-prosemirror: 1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)
y-prosemirror: 1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
'@tiptap/extension-document@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
dependencies:
@@ -32146,8 +32054,6 @@ snapshots:
dependencies:
'@types/node': 22.15.29
'@types/node-cron@3.0.11': {}
'@types/node-fetch@2.6.12':
dependencies:
'@types/node': 22.15.29
@@ -35811,6 +35717,8 @@ snapshots:
has-unicode@2.0.1: {}
hash-it@6.0.0: {}
hasown@2.0.1:
dependencies:
function-bind: 1.1.2
@@ -37085,15 +36993,7 @@ snapshots:
lexorank@1.0.5: {}
lib0@0.2.109:
dependencies:
isomorphic.js: 0.2.5
lib0@0.2.89:
dependencies:
isomorphic.js: 0.2.5
lib0@0.2.99:
lib0@0.2.114:
dependencies:
isomorphic.js: 0.2.5
@@ -37734,10 +37634,6 @@ snapshots:
dependencies:
semver: 7.7.2
node-cron@3.0.3:
dependencies:
uuid: 8.3.2
node-domexception@1.0.0: {}
node-fetch@2.7.0(encoding@0.1.13):
@@ -40752,41 +40648,41 @@ snapshots:
xtend@4.0.2: {}
y-indexeddb@9.0.12(yjs@13.6.23):
y-indexeddb@9.0.12(yjs@13.6.27):
dependencies:
lib0: 0.2.89
yjs: 13.6.23
yjs: 13.6.27
y-leveldb@0.1.2(yjs@13.6.23):
y-leveldb@0.1.2(yjs@13.6.27):
dependencies:
level: 6.0.1
lib0: 0.2.99
yjs: 13.6.23
lib0: 0.2.109
yjs: 13.6.27
optional: true
y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23):
y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27):
dependencies:
lib0: 0.2.109
prosemirror-model: 1.24.1
prosemirror-state: 1.4.3
prosemirror-view: 1.37.2
y-protocols: 1.0.6(yjs@13.6.23)
yjs: 13.6.23
y-protocols: 1.0.6(yjs@13.6.27)
yjs: 13.6.27
y-protocols@1.0.6(yjs@13.6.23):
y-protocols@1.0.6(yjs@13.6.27):
dependencies:
lib0: 0.2.89
yjs: 13.6.23
yjs: 13.6.27
y-websocket@2.1.0(yjs@13.6.23):
y-websocket@2.1.0(yjs@13.6.27):
dependencies:
lib0: 0.2.89
lib0: 0.2.114
lodash.debounce: 4.0.8
y-protocols: 1.0.6(yjs@13.6.23)
yjs: 13.6.23
y-protocols: 1.0.6(yjs@13.6.27)
yjs: 13.6.27
optionalDependencies:
ws: 6.2.2
y-leveldb: 0.1.2(yjs@13.6.23)
y-leveldb: 0.1.2(yjs@13.6.27)
y18n@4.0.3: {}
@@ -40841,9 +40737,9 @@ snapshots:
buffer-crc32: 0.2.13
fd-slicer: 1.1.0
yjs@13.6.23:
yjs@13.6.27:
dependencies:
lib0: 0.2.99
lib0: 0.2.109
ylru@1.3.2: {}
-1
View File
@@ -19,7 +19,6 @@ rush docker:build -p 20 \
--to @hcengineering/pod-datalake \
--to @hcengineering/pod-mail-worker \
--to @hcengineering/pod-export \
--to @hcengineering/pod-msg2file \
--to @hcengineering/pod-media \
--to @hcengineering/pod-preview \
--to @hcengineering/pod-external \
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

@@ -14,9 +14,8 @@
//
describe('node environment self checking', () => {
test('check Jest\'s node environment', () => {
const isNodeAvailable = typeof process !== 'undefined';
expect(isNodeAvailable).toBe(true);
})
test('check Jest\'s node environment', () => {
const isNodeAvailable = typeof process !== 'undefined'
expect(isNodeAvailable).toBe(true)
})
})
+198
View File
@@ -0,0 +1,198 @@
//
// Copyright © 2025 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 { Settings } from '../../main/settings'
import { PackedConfig } from '../../main/config'
const createMockStore = (): { get: jest.Mock, set: jest.Mock } => {
const store: Record<string, any> = {}
return {
get: jest.fn((key: string) => store[key]),
set: jest.fn((key: string, value: any) => { store[key] = value })
}
}
jest.mock('electron-store', () => {
return jest.fn()
})
describe('Settings', () => {
let originalEnvironment: NodeJS.ProcessEnv
let stubStoreInstance: ReturnType<typeof createMockStore>
beforeEach(() => {
originalEnvironment = process.env
process.env = { ...originalEnvironment }
stubStoreInstance = createMockStore()
jest.clearAllMocks()
})
afterEach(() => {
process.env = originalEnvironment
jest.clearAllMocks()
})
describe('readServerUrl', () => {
test('isDev is true', () => {
const systemUnderTest = new Settings(stubStoreInstance as any, true)
const actualUrl = systemUnderTest.readServerUrl()
expect(actualUrl).toBe('http://huly.local:8087')
})
test('isDev is true and FRONT_URL is set', () => {
process.env.FRONT_URL = 'http://custom-dev.local:3000'
const systemUnderTest = new Settings(stubStoreInstance as any, true)
const actualUrl = systemUnderTest.readServerUrl()
expect(actualUrl).toBe('http://custom-dev.local:3000')
})
test('isDev is false and server is in store', () => {
const expectedUrl = 'https://stored.server.com'
stubStoreInstance.get.mockReturnValue(expectedUrl)
const systemUnderTest = new Settings(stubStoreInstance as any, false)
const actualUrl = systemUnderTest.readServerUrl()
expect(actualUrl).toBe(expectedUrl)
})
test('store is empty and packed config exists', () => {
stubStoreInstance.get.mockReturnValue(undefined)
const expectedUrl = 'https://packed.server.com'
const packedConfig: PackedConfig = { server: expectedUrl }
const systemUnderTest = new Settings(stubStoreInstance as any, false, packedConfig)
const actualUrl = systemUnderTest.readServerUrl()
expect(actualUrl).toBe(expectedUrl)
})
test('store and packed config are empty', () => {
stubStoreInstance.get.mockReturnValue(undefined)
const expectedUrl = 'https://env.server.com'
process.env.FRONT_URL = expectedUrl
const systemUnderTest = new Settings(stubStoreInstance as any, false)
const actualUrl = systemUnderTest.readServerUrl()
expect(actualUrl).toBe(expectedUrl)
})
test('all options are unavailable', () => {
stubStoreInstance.get.mockReturnValue(undefined)
delete process.env.FRONT_URL
const systemUnderTest = new Settings(stubStoreInstance as any, false)
const result = systemUnderTest.readServerUrl()
expect(result).toBe('https://huly.app')
})
test('all (store, packed config, environment) options are available', () => {
const expectedUrl = 'https://store.priority.com'
stubStoreInstance.get.mockReturnValue(expectedUrl)
process.env.FRONT_URL = 'https://env.server.com'
const packedConfig: PackedConfig = { server: 'https://packed.server.com' }
const systemUnderTest = new Settings(stubStoreInstance as any, false, packedConfig)
const actualUrl = systemUnderTest.readServerUrl()
expect(actualUrl).toBe(expectedUrl)
})
})
describe('read/write', () => {
let systemUnderTest: Settings
beforeEach(() => {
systemUnderTest = new Settings(stubStoreInstance as any, false)
})
describe('isMinimizeToTrayEnabled', () => {
test.each([
{ storedValue: true, expected: true, description: 'stored value is true' },
{ storedValue: false, expected: false, description: 'stored value is false' }
])('$description', ({ storedValue, expected }) => {
stubStoreInstance.get.mockReturnValue(storedValue)
const actualValue = systemUnderTest.isMinimizeToTrayEnabled()
expect(actualValue).toBe(expected)
})
test.each([
{ storedValue: undefined, description: 'no value is stored (undefined)' },
{ storedValue: null, description: 'stored value is null' }
])('$description', ({ storedValue }) => {
stubStoreInstance.get.mockReturnValue(storedValue)
const actualValue = systemUnderTest.isMinimizeToTrayEnabled()
expect(actualValue).toBe(false)
})
})
describe('setMinimizeToTrayEnabled', () => {
test.each([
{ value: true, description: 'stored value is true' },
{ value: false, description: 'stored value is false' }
])('$description', ({ value }) => {
systemUnderTest.setMinimizeToTrayEnabled(value)
expect(systemUnderTest.isMinimizeToTrayEnabled()).toBe(value)
})
})
describe('setServerUrl', () => {
test('write than read', () => {
const expectedUrl = 'https://new.server.com'
systemUnderTest.setServerUrl(expectedUrl)
expect(systemUnderTest.readServerUrl()).toBe(expectedUrl)
})
})
describe('getWindowBounds', () => {
test('read', () => {
const expectedValue = { x: 100, y: 300, width: 500, height: 700 }
stubStoreInstance.get.mockReturnValue(expectedValue)
const actulValue = systemUnderTest.getWindowBounds()
expect(actulValue).toEqual(expectedValue)
})
test('no bounds are stored', () => {
stubStoreInstance.get.mockReturnValue(undefined)
expect(systemUnderTest.getWindowBounds()).toBeUndefined()
})
})
describe('setWindowBounds', () => {
test('write than read', () => {
const expectedValue = { x: 100, y: 200, width: 800, height: 600 }
systemUnderTest.setWindowBounds(expectedValue)
expect(systemUnderTest.getWindowBounds()).toBe(expectedValue)
})
})
})
})
@@ -0,0 +1,67 @@
//
// Copyright © 2025 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 { getBadgeIconInfo } from '../../main/trayUtils'
describe('getBadgeIconInfo', () => {
const baseTitle = 'Huly'
it('count of 0', () => {
const result = getBadgeIconInfo(0, baseTitle)
expect(result).toEqual({
fileName: '',
tooltip: baseTitle
})
})
it('negative count', () => {
const result = getBadgeIconInfo(-5, baseTitle)
expect(result).toEqual({
fileName: '',
tooltip: baseTitle
})
})
it.each([1, 2, 3, 4, 5, 6, 7, 8, 9])('count of %i', (count: number) => {
const result = getBadgeIconInfo(count, baseTitle)
expect(result).toEqual({
fileName: 'TrayIconWithBadge.ico',
tooltip: `${baseTitle}: ${count} unread`
})
})
describe('badge count 10-98', () => {
const testCases: number[] = []
for (let i = 10; i <= 98; i++) {
testCases.push(i)
}
it.each(testCases)('count of %i', (count: number) => {
const result = getBadgeIconInfo(count, baseTitle)
expect(result).toEqual({
fileName: 'TrayIconWithBadge.ico',
tooltip: `${baseTitle}: ${count} unread`
})
})
})
it.each([99, 100, 200, 301])('count of %i', (count: number) => {
const result = getBadgeIconInfo(count, baseTitle)
expect(result).toEqual({
fileName: 'TrayIconWithBadge.ico',
tooltip: `${baseTitle}: ${count} unread`
})
})
})
+2 -2
View File
@@ -15,12 +15,12 @@
import * as path from 'path'
import * as fs from 'fs'
interface PackedConfig {
export interface PackedConfig {
server?: string
updatesChannelKey?: string
}
const configPath = path.join(process.resourcesPath, 'config/config.json')
const configPath = path.join(process.resourcesPath, 'config', 'config.json')
export function readPackedConfig (): PackedConfig | undefined {
if (fs.existsSync(configPath)) {
+11 -3
View File
@@ -13,10 +13,11 @@
// limitations under the License.
//
import { app, BrowserWindow } from 'electron'
import { BrowserWindow } from 'electron'
import { MenuBarAction, CommandLogout, CommandSelectWorkspace, CommandOpenSettings } from '../ui/types'
import { TrayController } from './tray'
export function dispatchMenuBarAction (mainWindow: BrowserWindow | undefined, action: MenuBarAction): void {
export function dispatchMenuBarAction (mainWindow: BrowserWindow | undefined, action: MenuBarAction, tray: TrayController | undefined): void {
if (mainWindow == null) {
return
}
@@ -42,7 +43,7 @@ export function dispatchMenuBarAction (mainWindow: BrowserWindow | undefined, ac
mainWindow.webContents.send(CommandLogout)
break
case 'exit':
app.quit()
mainWindow.close()
break
case 'undo':
mainWindow.webContents.undo()
@@ -86,6 +87,13 @@ export function dispatchMenuBarAction (mainWindow: BrowserWindow | undefined, ac
case 'toggle-fullscreen':
mainWindow.setFullScreen(!mainWindow.isFullScreen())
break
case 'toggle-minimize-to-tray': {
if (tray != null) {
const newSetting = tray.toggleMinimizeToTray()
mainWindow.webContents.send('minimize-to-tray-setting-changed', newSetting)
}
break
}
default:
console.log('unknown menu action:', action)
}
@@ -13,9 +13,9 @@
// limitations under the License.
//
import { config } from 'dotenv'
import { app } from 'electron'
import path from 'path'
import { main } from './main'
config()
void main()
export function getFileInPublicBundledFolder (fileName: string): string {
return path.join(app.getAppPath(), 'dist', 'ui', 'public', fileName)
}
+65
View File
@@ -0,0 +1,65 @@
//
// 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 Store from 'electron-store'
import { PackedConfig } from './config'
export class Settings {
private static readonly SETTINGS_KEY_SERVER = 'server'
private static readonly SETTINGS_KEY_MINIMIZE_TO_TRAY = 'minimize-to-tray'
private static readonly SETTINGS_KEY_WINDOW_BOUNDS = 'windowBounds'
private readonly store: Store
private readonly isDev: boolean
private readonly packedConfig?: PackedConfig
constructor (store: Store, isDev: boolean, packedConfig?: PackedConfig) {
this.store = store
this.isDev = isDev
this.packedConfig = packedConfig
}
readServerUrl (): string {
if (this.isDev) {
return process.env.FRONT_URL ?? 'http://huly.local:8087'
}
return (
(this.store as any).get(Settings.SETTINGS_KEY_SERVER) as string ??
this.packedConfig?.server ??
process.env.FRONT_URL ??
'https://huly.app'
)
}
isMinimizeToTrayEnabled (): boolean {
return (this.store as any).get(Settings.SETTINGS_KEY_MINIMIZE_TO_TRAY) as boolean ?? false
}
setMinimizeToTrayEnabled (enabled: boolean): void {
(this.store as any).set(Settings.SETTINGS_KEY_MINIMIZE_TO_TRAY, enabled)
}
setServerUrl (serverUrl: string): void {
(this.store as any).set(Settings.SETTINGS_KEY_SERVER, serverUrl)
}
getWindowBounds (): any {
return (this.store as any).get(Settings.SETTINGS_KEY_WINDOW_BOUNDS)
}
setWindowBounds (bounds: any): void {
(this.store as any).set(Settings.SETTINGS_KEY_WINDOW_BOUNDS, bounds)
}
}
+559 -508
View File
File diff suppressed because it is too large Load Diff
+133
View File
@@ -0,0 +1,133 @@
//
// Copyright © 2025 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 { Menu, NativeImage, nativeImage, Tray } from 'electron'
import { WindowAction } from '../ui/types'
import { Settings } from './settings'
import { getFileInPublicBundledFolder } from './path'
import { getBadgeIconInfo } from './trayUtils'
export class TrayController {
private tray: Tray | undefined = undefined
private readonly defaultIcon: NativeImage
private readonly notificationIcon: NativeImage
private badgeCount: number = 0
constructor (
private readonly settings: Settings,
private readonly activateWindow: WindowAction,
private readonly quitApplication: WindowAction) {
this.defaultIcon = nativeImage.createFromPath(getFileInPublicBundledFolder('AppIcon.ico'))
this.defaultIcon.setTemplateImage(true)
this.notificationIcon = nativeImage.createFromPath(getFileInPublicBundledFolder('AppIconNotification.png'))
this.notificationIcon.setTemplateImage(true)
if (this.settings.isMinimizeToTrayEnabled()) {
this.instantiateTray()
}
}
public updateTrayBadge (count: number): void {
const countChanged = this.badgeCount !== count
this.badgeCount = count
if (!countChanged) {
return
}
this.evaluateIcon(count)
}
public setMinimizeToTrayEnabled (enabled: boolean): void {
this.settings.setMinimizeToTrayEnabled(enabled)
if (enabled) {
if (this.tray == null) {
this.instantiateTray()
}
} else {
if (this.tray != null) {
this.tray.destroy()
this.tray = undefined
}
}
}
public toggleMinimizeToTray (): boolean {
const currentSetting = this.settings.isMinimizeToTrayEnabled()
const newSetting = !currentSetting
this.setMinimizeToTrayEnabled(newSetting)
return newSetting
}
private instantiateTray (): void {
this.tray = createTray(
() => { this.activateWindow() },
() => { this.quitApplication() },
this.defaultIcon
)
this.evaluateIcon(this.badgeCount)
}
private evaluateIcon (badgeCount: number): void {
if (this.tray == null) {
return
}
const iconInfo = getBadgeIconInfo(badgeCount, BASE_TITLE)
if (badgeCount > 0) {
const iconFilePath = getFileInPublicBundledFolder(iconInfo.fileName)
const icon = nativeImage.createFromPath(iconFilePath)
this.tray.setImage(icon)
} else {
this.tray.setImage(this.defaultIcon)
}
this.tray.setToolTip(iconInfo.tooltip)
}
}
const BASE_TITLE = 'Huly'
function createTray (activateWindow: WindowAction, quitApplication: WindowAction, icon: NativeImage): Tray {
const tray = new Tray(icon)
const contextMenu = Menu.buildFromTemplate([
{
label: 'Show',
click: () => {
activateWindow()
}
},
{
label: 'Exit',
click: () => {
quitApplication()
}
}
])
tray.setContextMenu(contextMenu)
tray.setToolTip(BASE_TITLE)
tray.on('double-click', () => {
activateWindow()
})
tray.on('click', (): void => {
activateWindow()
})
return tray
}
@@ -13,28 +13,21 @@
// limitations under the License.
//
import { Readable } from 'stream'
import { ParsedFile } from '@hcengineering/communication-types'
import { parseYaml } from '@hcengineering/communication-yaml'
export async function parseFileStream (stream: Readable): Promise<ParsedFile> {
return await new Promise((resolve, reject) => {
let yamlData = ''
stream.on('data', (chunk) => {
yamlData += chunk.toString()
})
stream.on('end', () => {
try {
resolve(parseYaml(yamlData))
} catch (error) {
reject(error)
}
})
stream.on('error', (error) => {
reject(error)
})
})
export interface BadgeIconInfo {
fileName: string
tooltip: string
}
export function getBadgeIconInfo (badgeCount: number, baseTitle: string): BadgeIconInfo {
if (badgeCount <= 0) {
return {
fileName: '',
tooltip: baseTitle
}
}
return {
fileName: 'TrayIconWithBadge.ico',
tooltip: `${baseTitle}: ${badgeCount} unread`
}
}
+79 -82
View File
@@ -14,106 +14,103 @@
//
import { app, JumpListItem, JumpListCategory } from 'electron'
import { Command, CommandOpenSettings, CommandOpenInbox, CommandOpenApplication, JumpListSpares } from '../ui/types'
import { Command, CommandOpenSettings, CommandOpenInbox, CommandOpenApplication, JumpListSpares, WindowAction, SendCommandDelegate } from '../ui/types'
import * as path from 'path'
const JUMP_COMMANDS = {
INBOX: '--jump-to-inbox',
SETTINGS: '--jump-to-settings',
SETTINGS: '--jump-to-settings'
} as const
type JumpCommand = typeof JUMP_COMMANDS[keyof typeof JUMP_COMMANDS]
const JUMP_TO_APP_COMMAND_PREFIX = '--jump-to-app='
function getIconPath(iconName: string): string {
return path.join(process.resourcesPath, 'icons', iconName)
function getIconPath (iconName: string): string {
return path.join(process.resourcesPath, 'icons', iconName)
}
function addFixedFunctionJumpListItems(spares: JumpListSpares, tasks: JumpListItem[]): void {
function addFixedFunctionJumpListItems (spares: JumpListSpares, tasks: JumpListItem[]): void {
tasks.push({
type: 'task',
program: process.execPath,
args: JUMP_COMMANDS.SETTINGS,
iconPath: getIconPath('SettingsIcon.ico'),
title: spares.settingsLabel,
iconIndex: 0
})
tasks.push({
type: 'task',
program: process.execPath,
args: JUMP_COMMANDS.INBOX,
iconPath: getIconPath('InboxIcon.ico'),
title: spares.inboxLabel,
iconIndex: 0
})
}
export function rebuildJumpList (spares: JumpListSpares): void {
const tasks: JumpListItem[] = []
addFixedFunctionJumpListItems(spares, tasks)
if (spares.applications.length > 0) {
tasks.push({
type: 'task',
program: process.execPath,
args: JUMP_COMMANDS.SETTINGS,
iconPath: getIconPath('SettingsIcon.ico'),
title: spares.settingsLabel,
iconIndex: 0,
type: 'separator'
})
}
for (const application of spares.applications) {
const iconFileName = application.id.replaceAll(':', '_') + '.ico'
const cliArguments = `${JUMP_TO_APP_COMMAND_PREFIX}${application.alias}`
tasks.push({
type: 'task',
program: process.execPath,
args: JUMP_COMMANDS.INBOX,
iconPath: getIconPath('InboxIcon.ico'),
title: spares.inboxLabel,
iconIndex: 0,
type: 'task',
program: process.execPath,
args: cliArguments,
iconPath: getIconPath(iconFileName),
title: application.title,
iconIndex: 0
})
}
const category: JumpListCategory = {
type: 'tasks',
items: tasks
}
app.setJumpList([category])
}
export function rebuildJumpList(spares: JumpListSpares): void {
const tasks: JumpListItem[] = [];
export function setupWindowsSpecific (activateWindow: WindowAction, sendCommand: SendCommandDelegate): void {
app.setAppUserModelId(app.getName())
addFixedFunctionJumpListItems(spares, tasks)
if (spares.applications.length > 0) {
tasks.push({
type: 'separator'
})
}
for (const application of spares.applications) {
const iconFileName = application.id.replaceAll(":", "_") + '.ico'
const cliArguments = `${JUMP_TO_APP_COMMAND_PREFIX}${application.alias}`
tasks.push({
type: 'task',
program: process.execPath,
args: cliArguments,
iconPath: getIconPath(iconFileName),
title: application.title,
iconIndex: 0,
})
}
const category: JumpListCategory = {
type: 'tasks',
items: tasks,
}
app.setJumpList([category])
}
export function setupWindowsSpecific(activateWindow: () => void, sendCommand: (cmd: Command, ...args: any[]) => void): void {
app.setAppUserModelId(app.getName())
app.on('second-instance', (_event: any, commandLine: any, _workingDirectory: any) => {
const commandArgument = commandLine[1] as string
if (typeof commandArgument === 'string' && commandArgument.startsWith(JUMP_TO_APP_COMMAND_PREFIX)) {
const applicationId = commandArgument.replace(JUMP_TO_APP_COMMAND_PREFIX, '')
sendCommand(CommandOpenApplication, [applicationId])
} else {
const jumpCommandCode = commandArgument as JumpCommand
let command: Command | undefined
switch (jumpCommandCode) {
case JUMP_COMMANDS.INBOX:
command = CommandOpenInbox
break;
case JUMP_COMMANDS.SETTINGS:
command = CommandOpenSettings
break;
default: {
// compile-time check: if jumpCommand is not a known value, this line errors
const _exhaustive: never = jumpCommandCode
return; // or handle error
}
}
if (command) {
sendCommand(command)
}
app.on('second-instance', (_event: any, commandLine: any, _workingDirectory: any) => {
const commandArgument = commandLine[1] as string
if (typeof commandArgument === 'string' && commandArgument.startsWith(JUMP_TO_APP_COMMAND_PREFIX)) {
const applicationId = commandArgument.replace(JUMP_TO_APP_COMMAND_PREFIX, '')
sendCommand(CommandOpenApplication, [applicationId])
} else {
const jumpCommandCode = commandArgument as JumpCommand
let command: Command | undefined
switch (jumpCommandCode) {
case JUMP_COMMANDS.INBOX:
command = CommandOpenInbox
break
case JUMP_COMMANDS.SETTINGS:
command = CommandOpenSettings
break
default: {
// compile-time check: if jumpCommand is not a known value, this line errors
const _exhaustive: never = jumpCommandCode
break
}
}
activateWindow()
})
}
if (command !== undefined) {
sendCommand(command)
}
}
activateWindow()
})
}
+32 -15
View File
@@ -2,7 +2,7 @@ import { loginId } from '@hcengineering/login'
import { loveId } from '@hcengineering/love'
import { timeId } from '@hcengineering/time'
import { getEmbeddedLabel, getMetadata, translate } from '@hcengineering/platform'
import { getEmbeddedLabel, getMetadata, getResource, translate } from '@hcengineering/platform'
import presentation, { MessageBox, setDownloadProgress, getClient } from '@hcengineering/presentation'
import setting, { settingId } from '@hcengineering/setting'
import {
@@ -20,14 +20,14 @@ import { handleDownloadItem } from '@hcengineering/desktop-downloads'
import notification, { notificationId } from '@hcengineering/notification'
import { chatId } from '@hcengineering/chat'
import workbench, { workbenchId, logOut } from '@hcengineering/workbench'
import { encodeObjectURI } from '@hcengineering/view'
import view, { Action, encodeObjectURI } from '@hcengineering/view'
import { resolveLocation } from '@hcengineering/notification-resources'
import { isOwnerOrMaintainer, getCurrentAccount } from '@hcengineering/core'
import { isOwnerOrMaintainer, getCurrentAccount, Ref } from '@hcengineering/core'
import { configurePlatform } from './platform'
import { setupTitleBarMenu } from './titleBarMenu'
import { defineScreenShare, defineGetDisplayMedia } from './screenShare'
import { CommandLogout, CommandSelectWorkspace, CommandOpenSettings, CommandOpenInbox, CommandOpenPlanner, CommandOpenOffice, CommandOpenApplication, LaunchApplication, NotificationParams } from './types'
import { CommandLogout, CommandSelectWorkspace, CommandOpenSettings, CommandOpenInbox, CommandOpenPlanner, CommandOpenOffice, CommandOpenApplication, LaunchApplication, NotificationParams, CommandCloseTab } from './types'
import { ipcMainExposed } from './typesUtils'
import { themeStore, ThemeVariant } from '@hcengineering/theme'
import type { Application } from '@hcengineering/workbench'
@@ -37,6 +37,19 @@ function currentOsIsWindows (): boolean {
return (window as any).windowsPlatform === true
}
async function executePlatformAction (action: Ref<Action>): Promise<void> {
try {
const client = getClient()
const actions = client.getModel().findAllSync<Action>(view.class.Action, { _id: action })
if (actions.length > 0) {
const actionBody = await getResource(actions[0].action)
await actionBody(undefined)
}
} catch (error) {
console.error('failed to execute platform action', error)
}
}
defineScreenShare()
defineGetDisplayMedia()
@@ -46,18 +59,18 @@ window.addEventListener('DOMContentLoaded', () => {
if (currentOsIsWindows()) {
const titleBarRoot = document.getElementById('desktop-app-titlebar-root')
if (titleBarRoot != null) {
const menuBar = setupTitleBarMenu(ipcMain, titleBarRoot)
void setupTitleBarMenu(ipcMain, titleBarRoot).then((menuBar) => {
themeStore.subscribe((themeOptions) => {
if (themeOptions != null) {
menuBar.setTheme(themeOptions.variant)
}
})
themeStore.subscribe((themeOptions) => {
if (themeOptions != null) {
menuBar.setTheme(themeOptions.variant)
}
})
void ipcMain.isOsUsingDarkTheme().then((isDarkTheme) => {
menuBar.setTheme(isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light)
}).catch(() => {
menuBar.setTheme(ThemeVariant.Light) // fallback
void ipcMain.isOsUsingDarkTheme().then((isDarkTheme) => {
menuBar.setTheme(isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light)
}).catch(() => {
menuBar.setTheme(ThemeVariant.Light) // fallback
})
})
}
}
@@ -124,6 +137,10 @@ window.addEventListener('DOMContentLoaded', () => {
openScreen(loveId)
})
ipcMain.on(CommandCloseTab, () => {
void executePlatformAction(workbench.action.CloseCurrentTab)
})
ipcMain.on(CommandOpenPlanner, () => {
openScreen(timeId)
})
+2 -1
View File
@@ -298,6 +298,7 @@ export async function configurePlatform (onWorkbenchConnect?: () => Promise<void
setMetadata(presentation.metadata.MailUrl, config.MAIL_URL)
setMetadata(recorder.metadata.StreamUrl, config.STREAM_URL ?? '')
setMetadata(presentation.metadata.StatsUrl, config.STATS_URL)
setMetadata(presentation.metadata.HulylakeUrl, config.HULYLAKE_URL ?? '')
setMetadata(presentation.metadata.PulseUrl, config.PULSE_URL ?? '')
setMetadata(textEditor.metadata.Collaborator, config.COLLABORATOR ?? '')
@@ -338,7 +339,7 @@ export async function configurePlatform (onWorkbenchConnect?: () => Promise<void
setMetadata(billingPlugin.metadata.BillingURL, config.BILLING_URL ?? '')
const languages = myBranding.languages !== undefined && myBranding.languages !== '' ? myBranding.languages.split(',').map((l) => l.trim()) : ['en', 'ru', 'es', 'pt', 'zh', 'fr', 'cs', 'it', 'de', 'ja']
const languages = myBranding.languages !== undefined && myBranding.languages !== '' ? myBranding.languages.split(',').map((l) => l.trim()) : ['en', 'ru', 'es', 'pt', 'zh', 'fr', 'cs', 'it', 'de', 'ja', 'tr']
setMetadata(uiPlugin.metadata.Languages, languages)
+17 -14
View File
@@ -1,7 +1,7 @@
// preload.js
import { contextBridge, ipcRenderer } from 'electron'
import { BrandingMap, Config, IPCMainExposed, MenuBarAction, NotificationParams, JumpListSpares } from './types'
import { BrandingMap, Config, IPCMainExposed, JumpListSpares, MenuBarAction, NotificationParams } from './types'
/**
* @public
@@ -18,27 +18,23 @@ export function concatLink (host: string, path: string): string {
}
async function loadServerConfig (url: string): Promise<any> {
let retries = 5
let retries = 1
let res: Response | undefined
do {
while (true) {
try {
res = await fetch(url, {
keepalive: true
})
if (res === undefined) {
// In theory should never get here
throw new Error('Failed to load server config')
}
break
} catch (e) {
retries--
if (retries === 0) {
throw new Error(`Failed to load server config: ${e}`)
}
await new Promise((resolve) => setTimeout(resolve, 1000 * (5 - retries)))
retries++
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.min(5, retries)))
}
} while (retries > 0)
if (res === undefined) {
// In theory should never get here
throw new Error('Failed to load server config')
}
return await res.json()
@@ -181,6 +177,13 @@ const expose: IPCMainExposed = {
cancelBackup: () => { ipcRenderer.send('cancel-backup') },
startBackup: (token, endpoint, wsIds) => { ipcRenderer.send('start-backup', token, endpoint, wsIds) },
rebuildJumpList: (spares: JumpListSpares) => { ipcRenderer.send('rebuild-user-jump-list', spares) }
rebuildJumpList: (spares: JumpListSpares) => { ipcRenderer.send('rebuild-user-jump-list', spares) },
isMinimizeToTrayEnabled: async () => {
return await ipcRenderer.invoke('get-minimize-to-tray-enabled')
},
onMinimizeToTraySettingChanged: (callback: (enabled: boolean) => void) => {
ipcRenderer.on('minimize-to-tray-setting-changed', (_event, enabled: boolean) => { callback(enabled) })
}
}
contextBridge.exposeInMainWorld('electron', expose)
+23 -6
View File
@@ -18,9 +18,14 @@ import { isMenuBarAction } from './typesUtils'
import { TitleBarMenuState } from './titleBarMenuState'
import { ThemeVariant, type ThemeVariantType } from '@hcengineering/theme'
export function setupTitleBarMenu (ipcMain: IPCMainExposed, root: HTMLElement): MenuBar {
const ToggleMinimizeToTrayAction: MenuBarAction = 'toggle-minimize-to-tray'
const LabelMinimizeToTrayEnabled = '☑ Minimize to tray'
const LabelMinimizeToTrayDisabled = '☐ Minimize to tray'
export async function setupTitleBarMenu (ipcMain: IPCMainExposed, root: HTMLElement): Promise<MenuBar> {
const themeManager = new ThemeManager(ThemeVariant.Light)
const menuManager = new MenuBarManager(root)
const menuManager = new MenuBarManager(root, await ipcMain.isMinimizeToTrayEnabled())
const menuBar = menuManager.getView()
@@ -35,6 +40,13 @@ export function setupTitleBarMenu (ipcMain: IPCMainExposed, root: HTMLElement):
menuManager.attachEventListeners(ipcMain)
ipcMain.onMinimizeToTraySettingChanged((enabled) => {
const toggle = root.querySelector(`[data-action="${ToggleMinimizeToTrayAction}"]`)
if (toggle != null) {
toggle.textContent = enabled ? LabelMinimizeToTrayEnabled : LabelMinimizeToTrayDisabled
}
})
ipcMain.onWindowStateChange((_event, state) => {
const maximizeButton = root.querySelector('#maximize-button')
if (maximizeButton != null) {
@@ -58,7 +70,7 @@ export class MenuBar {
}
}
export function buildHulyApplicationMenu (): HTMLElement {
export function buildHulyApplicationMenu (minimizeToTrayEnabled: boolean): HTMLElement {
const menuBuilder = new MenuBuilder()
const MenuFileIndex = 0
@@ -93,6 +105,11 @@ export function buildHulyApplicationMenu (): HTMLElement {
.addSeparator(MenuViewIndex)
.addMenuItem(MenuViewIndex, 'Toggle Fullscreen', 'toggle-fullscreen', 'F11', 'l')
const MenuWindowIndex = 3
const ToggleMinimizeToTrayLabel = minimizeToTrayEnabled ? LabelMinimizeToTrayEnabled : LabelMinimizeToTrayDisabled
menuBuilder.addTopLevelMenu('Window', 'w')
.addMenuItem(MenuWindowIndex, ToggleMinimizeToTrayLabel, 'toggle-minimize-to-tray', undefined, 'm')
return menuBuilder.build()
}
@@ -140,7 +157,7 @@ export class MenuBuilder {
label: string,
action: MenuBarAction,
shortcut?: string,
acceleratorChar: string | null = null
acceleratorChar: string | undefined = undefined
): this {
if (topLevelMenuIndex >= 0 && topLevelMenuIndex < this.menus.length) {
const item: MenuItem = {
@@ -281,7 +298,7 @@ class MenuBarManager {
private readonly StateStyleKeyboardSelected = 'desktop-app-keyboard-selected'
private readonly StateStyleAltModeActive = 'desktop-app-alt-active'
constructor (private readonly root: HTMLElement) {
constructor (private readonly root: HTMLElement, minimizeToTrayEnabled: boolean) {
this.state = new TitleBarMenuState(
() => this.topLevelMenus().length,
(topLevelMenuIndex: number) => {
@@ -290,7 +307,7 @@ class MenuBarManager {
}
)
this.view = buildHulyApplicationMenu()
this.view = buildHulyApplicationMenu(minimizeToTrayEnabled)
}
public getView (): HTMLElement {
+94 -94
View File
@@ -14,113 +14,113 @@
//
export class TitleBarMenuState {
private altModeActive: boolean = false
private topLevelMenuExpanded: boolean = false
private focusedTopLevelMenuIndex: number | null = null
private focusedChildMenuIndex: number | null = null
private altModeActive: boolean = false
private topLevelMenuExpanded: boolean = false
private focusedTopLevelMenuIndex: number | null = null
private focusedChildMenuIndex: number | null = null
private readonly topLevelMenuCount: () => number;
private readonly menuItemsCount: (topLevelMenuIndex: number) => number;
private readonly topLevelMenuCount: () => number
private readonly menuItemsCount: (topLevelMenuIndex: number) => number
public constructor(
topLevelMenuCount: () => number,
menuItemsCount: (topLevelMenuIndex: number) => number
) {
this.topLevelMenuCount = topLevelMenuCount
this.menuItemsCount = menuItemsCount
public constructor (
topLevelMenuCount: () => number,
menuItemsCount: (topLevelMenuIndex: number) => number
) {
this.topLevelMenuCount = topLevelMenuCount
this.menuItemsCount = menuItemsCount
}
public focusChildMenu (): void {
if (this.focusedTopLevelMenuIndex != null && this.topLevelMenuExpanded) {
this.focusedChildMenuIndex = 0
}
}
public expandTopLevelMenu (topLevelMenuIndex: number): void {
if (topLevelMenuIndex < 0 || topLevelMenuIndex >= this.topLevelMenuCount()) {
return
}
public focusChildMenu() {
if (null != this.focusedTopLevelMenuIndex && this.topLevelMenuExpanded) {
this.focusedChildMenuIndex = 0
}
if (this.focusedTopLevelMenuIndex === topLevelMenuIndex && this.topLevelMenuExpanded) {
this.closeAll()
} else {
this.focusedTopLevelMenuIndex = topLevelMenuIndex
this.topLevelMenuExpanded = true
}
}
public exitAltMode (): void {
this.altModeActive = false
}
public enterAltMode (topLevelMenuIndex: number | null): void {
this.altModeActive = true
if (topLevelMenuIndex == null || topLevelMenuIndex < 0 || topLevelMenuIndex >= this.topLevelMenuCount()) {
return
}
public expandTopLevelMenu(topLevelMenuIndex: number): void {
if (0 > topLevelMenuIndex || topLevelMenuIndex >= this.topLevelMenuCount()) {
return
}
if (this.focusedTopLevelMenuIndex === topLevelMenuIndex && this.topLevelMenuExpanded) {
this.closeAll()
} else {
this.focusedTopLevelMenuIndex = topLevelMenuIndex
this.topLevelMenuExpanded = true
}
this.focusedTopLevelMenuIndex = topLevelMenuIndex
}
public closeAll (): void {
this.altModeActive = false
this.topLevelMenuExpanded = false
this.focusedTopLevelMenuIndex = null
this.focusedChildMenuIndex = null
}
public defocus (): void {
if (this.topLevelMenuExpanded) {
this.topLevelMenuExpanded = false
this.focusedChildMenuIndex = null
} else {
this.closeAll()
}
}
public moveFocusHorizontal (increment: number): void {
if (Math.abs(increment) !== 1 || this.focusedTopLevelMenuIndex == null) {
return
}
public exitAltMode(): void {
this.altModeActive = false
const topLevelMenuCount = this.topLevelMenuCount()
this.focusedTopLevelMenuIndex = (this.focusedTopLevelMenuIndex + topLevelMenuCount + increment) % topLevelMenuCount
if (this.topLevelMenuExpanded) {
this.focusedChildMenuIndex = 0
}
}
public moveFocusVertical (increment: number): void {
if (Math.abs(increment) !== 1 || this.focusedTopLevelMenuIndex == null) {
return
}
public enterAltMode(topLevelMenuIndex: number | null): void {
this.altModeActive = true
if (null == topLevelMenuIndex || 0 > topLevelMenuIndex || topLevelMenuIndex >= this.topLevelMenuCount()) {
return
}
this.focusedTopLevelMenuIndex = topLevelMenuIndex
const menuItemsCount = this.menuItemsCount(this.focusedTopLevelMenuIndex)
this.topLevelMenuExpanded = true
if (menuItemsCount === 0) {
return
}
public closeAll(): void {
this.altModeActive = false
this.topLevelMenuExpanded = false
this.focusedTopLevelMenuIndex = null
this.focusedChildMenuIndex = null
}
this.focusedChildMenuIndex = ((this.focusedChildMenuIndex ?? -1) + menuItemsCount + increment) % menuItemsCount
}
public defocus(): void {
if (this.topLevelMenuExpanded) {
this.topLevelMenuExpanded = false
this.focusedChildMenuIndex = null
} else {
this.closeAll();
}
}
public get isAltModeActive (): boolean {
return this.altModeActive
}
public moveFocusHorizontal(increment: number): void {
if (Math.abs(increment) != 1 || null == this.focusedTopLevelMenuIndex) {
return;
}
const topLevelMenuCount = this.topLevelMenuCount()
this.focusedTopLevelMenuIndex = (this.focusedTopLevelMenuIndex + topLevelMenuCount + increment) % topLevelMenuCount
if (this.topLevelMenuExpanded) {
this.focusedChildMenuIndex = 0
}
}
public get isTopLevelMenuExpanded (): boolean {
return this.topLevelMenuExpanded
}
public moveFocusVertical(increment: number): void {
if (Math.abs(increment) != 1 || null == this.focusedTopLevelMenuIndex) {
return;
}
public get FocusedTopLevelMenuIndex (): number | null {
return this.focusedTopLevelMenuIndex
}
const menuItemsCount = this.menuItemsCount(this.focusedTopLevelMenuIndex)
this.topLevelMenuExpanded = true
if (menuItemsCount === 0) {
return;
}
this.focusedChildMenuIndex = ((this.focusedChildMenuIndex ?? -1) + menuItemsCount + increment) % menuItemsCount
}
public get isAltModeActive(): boolean {
return this.altModeActive
}
public get isTopLevelMenuExpanded(): boolean {
return this.topLevelMenuExpanded
}
public get FocusedTopLevelMenuIndex(): number | null {
return this.focusedTopLevelMenuIndex
}
public get FocusedChildMenuIndex(): number | null {
return this.focusedChildMenuIndex
}
}
public get FocusedChildMenuIndex (): number | null {
return this.focusedChildMenuIndex
}
}
+30 -3
View File
@@ -56,6 +56,7 @@ export interface Config {
PULSE_URL?: string
PASSWORD_STRICTNESS?: 'very_strict' | 'strict' | 'normal' | 'none'
EXCLUDED_APPLICATIONS_FOR_ANONYMOUS?: string
HULYLAKE_URL?: string
}
export interface Branding {
@@ -84,6 +85,7 @@ export const CommandOpenPlanner = 'open-planner' as const
export const CommandSelectWorkspace = 'select-workspace' as const
export const CommandLogout = 'logout' as const
export const CommandOpenApplication = 'open-application' as const
export const CommandCloseTab = 'close-tab' as const
export type Command =
typeof CommandOpenSettings |
@@ -92,7 +94,8 @@ export type Command =
typeof CommandOpenPlanner |
typeof CommandSelectWorkspace |
typeof CommandLogout |
typeof CommandOpenApplication
typeof CommandOpenApplication |
typeof CommandCloseTab
export interface NotificationParams {
title: string
@@ -104,8 +107,26 @@ export interface NotificationParams {
objectClass?: Ref<Class<Doc>>
}
export const MenuBarActions = ['settings', 'select-workspace', 'logout', 'exit', 'undo', 'redo', 'cut', 'copy', 'paste', 'delete', 'select-all', 'reload', 'force-reload', 'toggle-devtools',
'zoom-in', 'zoom-out', 'restore-size', 'toggle-fullscreen'] as const
export const MenuBarActions = [
'settings',
'select-workspace',
'logout',
'exit',
'undo',
'redo',
'cut',
'copy',
'paste',
'delete',
'select-all',
'reload',
'force-reload',
'toggle-devtools',
'zoom-in',
'zoom-out',
'restore-size',
'toggle-fullscreen',
'toggle-minimize-to-tray'] as const
export type MenuBarAction = typeof MenuBarActions[number]
@@ -151,4 +172,10 @@ export interface IPCMainExposed {
executeMenuBarAction: (action: MenuBarAction) => void
rebuildJumpList: (spares: JumpListSpares) => void
isMinimizeToTrayEnabled: () => Promise<boolean>
onMinimizeToTraySettingChanged: (callback: (enabled: boolean) => void) => void
}
export type SendCommandDelegate = (cmd: Command, ...args: any[]) => void
export type WindowAction = () => void
+9 -11
View File
@@ -1,11 +1,9 @@
$MODEL_VERSION = node ../common/scripts/show_version.js | Out-String
$MODEL_VERSION = $MODEL_VERSION.Trim()
$VERSION = node ../common/scripts/show_tag.js | Out-String
$VERSION = $VERSION.Trim()
# Set environment variable
$env:NODE_ENV = "development"
# Run Electron
electron --no-sandbox ./
$MODEL_VERSION = node ../common/scripts/show_version.js | Out-String
$MODEL_VERSION = $MODEL_VERSION.Trim()
$VERSION = node ../common/scripts/show_tag.js | Out-String
$VERSION = $VERSION.Trim()
$env:NODE_ENV = "development"
electron --no-sandbox ./
+16 -1
View File
@@ -236,7 +236,6 @@ services:
- LAST_NAME_FIRST=true
- BRANDING_PATH=/var/cfg/branding.json
- AI_BOT_URL=http://huly.local:4010
- MSG2FILE_URL=http://huly.local:9087
- COMMUNICATION_TIME_LOGGING_ENABLED=true
restart: unless-stopped
fulltext_cockroach:
@@ -285,6 +284,22 @@ services:
- DB_URL=${DB_CR_URL}
- BUCKETS=blobs,eu|http://minio:9000?accessKey=minioadmin&secretKey=minioadmin
restart: unless-stopped
redis:
image: redis:8.0.2-alpine3.21
ports:
- 6379:6379
restart: unless-stopped
hulypulse:
image: hardcoreeng/hulypulse
depends_on:
redis:
condition: service_started
ports:
- 8099:8099
environment:
- HULY_REDIS_URLS=redis://redis:6379
- HULY_BIND_PORT=8099
restart: unless-stopped
volumes:
db:
dbpg:
+3 -16
View File
@@ -286,6 +286,7 @@ services:
# - DISABLE_SIGNUP=true
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
- PULSE_URL=ws://huly.local:8099/ws
- HULYLAKE_URL=http://huly.local:8096
restart: unless-stopped
transactor_cockroach:
image: hardcoreeng/transactor
@@ -321,13 +322,13 @@ services:
- LAST_NAME_FIRST=true
- BRANDING_PATH=/var/cfg/branding.json
- AI_BOT_URL=http://huly.local:4010
- MSG2FILE_URL=http://huly.local:9087
- COMMUNICATION_TIME_LOGGING_ENABLED=true
- RATE_LIMIT_MAX=250 # 250 requests per 30 seconds
- RATE_LIMIT_WINDOW=30000
- FILES_URL=http://huly.local:4030/blob/:workspace/:blobId/:filename
- COMMUNICATION_API_ENABLED=true
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
- HYLYLAKE_URL=http://huly.local:8096
restart: unless-stopped
rekoni:
image: hardcoreeng/rekoni-service
@@ -366,6 +367,7 @@ services:
- REKONI_URL=http://huly.local:4004
- ACCOUNTS_URL=http://huly.local:3000
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
- HULYLAKE_URL=http://huly.local:8096
print:
image: hardcoreeng/print
extra_hosts:
@@ -440,21 +442,6 @@ services:
# - POSTHOG_HOST=${POSTHOG_HOST}
# - POSTHOG_API_KEY=${POSTHOG_API_KEY}
# - MAX_PAYLOAD_SIZE=10mb
msg2file:
image: hardcoreeng/msg2file
ports:
- 9087:9087
extra_hosts:
- 'huly.local:host-gateway'
restart: unless-stopped
environment:
- ACCOUNTS_URL=http://huly.local:3000
- DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable
- PORT=9087
- SECRET=secret
- SERVICE_ID=msg2file-service
- STORAGE_CONFIG=${STORAGE_CONFIG}
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
export:
image: hardcoreeng/export
extra_hosts:
+1
View File
@@ -28,5 +28,6 @@
"COMMUNICATION_API_ENABLED": "true",
"BACKUP_URL": "http://huly.local:4039/api/backup",
"PULSE_URL": "ws://huly.local:8099/ws",
"HULYLAKE_URL": "http://huly.local:8096",
"EXCLUDED_APPLICATIONS_FOR_ANONYMOUS": "[\"chunter\", \"notification\"]"
}
+4 -2
View File
@@ -201,7 +201,8 @@ export interface Config {
COMMUNICATION_API_ENABLED?: string
BILLING_URL?: string,
EXCLUDED_APPLICATIONS_FOR_ANONYMOUS?: string,
PULSE_URL?: string
PULSE_URL?: string,
HULYLAKE_URL?: string
}
export interface Branding {
@@ -491,10 +492,11 @@ export async function configurePlatform() {
setMetadata(billingPlugin.metadata.BillingURL, config.BILLING_URL ?? '')
setMetadata(presentation.metadata.PulseUrl, config.PULSE_URL)
setMetadata(presentation.metadata.HulylakeUrl, config.HULYLAKE_URL ?? '')
const languages = myBranding.languages
? (myBranding.languages as string).split(',').map((l) => l.trim())
: ['en', 'ru', 'es', 'pt', 'zh', 'fr', 'cs', 'it', 'de', 'ja']
: ['en', 'ru', 'es', 'pt', 'zh', 'fr', 'cs', 'it', 'de', 'ja', 'tr']
setMetadata(uiPlugin.metadata.Languages, languages)
+6 -3
View File
@@ -21,7 +21,7 @@
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/tool staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/tool",
"run-local-mongo": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4700 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 ./bundle/bundle.js",
"run-local": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4702 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 STORAGE_CONFIG='datalake|http://huly.local:4030' ACCOUNT_DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 REGION_INFO='cockroach|CockroachDB' MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 $TOOL_OPT ./bundle/bundle.js",
"run-local": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4702 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 STORAGE_CONFIG='datalake|http://huly.local:4030' HULYLAKE_URL=http://huly.local:8096 ACCOUNT_DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 REGION_INFO='cockroach|CockroachDB' MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 $TOOL_OPT ./bundle/bundle.js",
"run-local-brk": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --inspect-brk --enable-source-maps --max-old-space-size=18000 ./bundle/bundle.js",
"run": "rush bundle --to @hcengineering/tool >/dev/null && cross-env node --max-old-space-size=8000 ./bundle/bundle.js",
"upgrade-mongo": "rushx run-local-mongo upgrade-workspace -- $1",
@@ -55,7 +55,8 @@
"@types/request": "~2.48.8",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5"
"@types/jest": "^29.5.5",
"@types/js-yaml": "^4.0.9"
},
"dependencies": {
"@elastic/elasticsearch": "^7.17.14",
@@ -180,6 +181,8 @@
"msgpackr-extract": "^3.0.3",
"@hcengineering/kafka": "^0.6.0",
"@hcengineering/api-client": "^0.6.0",
"@faker-js/faker": "^8.4.1"
"@faker-js/faker": "^8.4.1",
"@hcengineering/hulylake-client": "^0.6.0",
"@hcengineering/communication-types": "^0.1.0"
}
}
+895
View File
@@ -0,0 +1,895 @@
// Copyright © 2025 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 Workspace } from '@hcengineering/account'
import { type HulylakeClient, type JsonPatch } from '@hcengineering/hulylake-client'
import type postgres from 'postgres'
import {
generateUuid,
groupByArray,
type MeasureContext,
notEmpty,
type PersonId,
type PersonUuid,
type WorkspaceUuid
} from '@hcengineering/core'
import {
type Attachment,
type AttachmentData,
type AttachmentDoc,
type AttachmentID,
type AttachmentUpdateData,
type BlobID,
type CardID,
type CardType,
type Emoji,
type Markdown,
type MessageDoc,
type MessageExtra,
type MessageID,
type MessagesDoc,
type MessagesGroup,
type MessagesGroupDoc,
type MessagesGroupsDoc,
MessageType,
type SocialID,
type ThreadDoc
} from '@hcengineering/communication-types'
import { type AccountClient } from '@hcengineering/account-client'
const MAX_MESSAGES_SIZE = 95 * 1024
const MESSAGES_TABLE = 'communication.message'
const PATCH_TABLE = 'communication.patch'
const ATTACHMENT_TABLE = 'communication.attachment'
const REACTION_TABLE = 'communication.reaction'
const THREAD_INDEX_TABLE = 'communication.thread_index'
export async function migrateWorkspaceMessages (
ctx: MeasureContext,
ws: Workspace,
card: CardID | undefined,
db: postgres.Sql,
hulylake: HulylakeClient,
accountClient: AccountClient,
personUuidBySocialId: Map<PersonId, PersonUuid>
): Promise<void> {
await migrateMessages(ctx, ws, card, db, hulylake, accountClient, personUuidBySocialId)
}
async function migrateMessages (
ctx: MeasureContext,
ws: Workspace,
card: CardID | undefined,
db: postgres.Sql,
hulylake: HulylakeClient,
accountClient: AccountClient,
personUuidBySocialId: Map<PersonId, PersonUuid>
): Promise<void> {
const force = card != null && card !== ''
const cards = card != null && card !== '' ? [card] : await getMessagesCards(ws.uuid, db)
ctx.info(`Cards for migration: ${cards.length}, force: ${force}`)
let i = 0
for (const card of cards) {
try {
i++
const groups = await getGroups(ctx, hulylake, card)
const lastGroup = groups[groups.length - 1]
ctx.info(`Migrating messages for card ${card} index ${i} from ${lastGroup?.toDate?.toISOString() ?? 'null'}`)
const cursor = getMessagesCursor(ws.uuid, card, db, force ? undefined : lastGroup?.toDate)
for await (const messages of cursor) {
ctx.info(`Messages count ${messages.length}`)
if (messages.length === 0) continue
const oldMessages = messages.map(deserializeOldMessage).filter(notEmpty)
await migrateMessagesBatch(ctx, card, hulylake, accountClient, personUuidBySocialId, oldMessages)
}
} catch (e) {
ctx.error('Failed to migrate messages for card', card)
ctx.error('Error', { error: e })
}
}
}
async function migrateMessagesBatch (
ctx: MeasureContext,
cardId: CardID,
hulylake: HulylakeClient,
accountClient: AccountClient,
personUuidBySocialId: Map<PersonId, PersonUuid>,
messages: OldMessage[]
): Promise<void> {
const newMessages: MessageDoc[] = []
for (const oldMessage of messages) {
const newMessageDoc = await oldMessageToNewMessageDoc(oldMessage, accountClient, personUuidBySocialId)
newMessages.push(newMessageDoc)
}
const chunks = chunkMessagesBySize(ctx, newMessages)
for (const chunk of chunks) {
const fromDate = chunk.from
const toDate = chunk.to
const blobId = generateUuid() as BlobID
const newGroupDoc: MessagesGroupDoc = {
cardId,
blobId,
fromDate: fromDate.toISOString(),
toDate: toDate.toISOString(),
count: chunk.count
}
const newMessagesDoc: MessagesDoc = {
cardId,
fromDate: fromDate.toISOString(),
toDate: toDate.toISOString(),
messages: chunk.chunk,
language: 'original'
}
const jsonPatches: JsonPatch[] = [
{
hop: 'add',
path: `/${blobId}`,
value: newGroupDoc,
safe: true
} as const
]
await hulylake.patchJson(`${cardId}/messages/groups`, jsonPatches, undefined, {
maxRetries: 3,
isRetryable: () => true,
delayStrategy: {
getDelay: () => 1000
}
})
await hulylake.putJson(`${cardId}/messages/${blobId}`, newMessagesDoc, undefined, {
maxRetries: 3,
isRetryable: () => true,
delayStrategy: {
getDelay: () => 1000
}
})
}
}
async function getGroups (ctx: MeasureContext, hulylake: HulylakeClient, cardId: CardID): Promise<MessagesGroup[]> {
const res = await hulylake.getJson<MessagesGroupsDoc>(`${cardId}/messages/groups`, {
maxRetries: 3,
isRetryable: () => true,
delayStrategy: {
getDelay: () => 500
}
})
if (res.body != null) {
return deserializeMessagesGroups(res.body)
}
await hulylake.putJson(`${cardId}/messages/groups`, {}, undefined, {
maxRetries: 3,
isRetryable: () => true,
delayStrategy: {
getDelay: () => 1000
}
})
return []
}
function chunkMessagesBySize (
ctx: MeasureContext,
messages: MessageDoc[]
): Array<{ chunk: Record<MessageID, MessageDoc>, from: Date, to: Date, count: number }> {
const chunks: Array<{ chunk: Record<MessageID, MessageDoc>, from: Date, to: Date, count: number }> = []
let current: { chunk: Record<MessageID, MessageDoc>, from?: Date, to?: Date, count: number } = {
chunk: {},
count: 0
}
for (const msg of messages) {
current.chunk[msg.id] = msg
if (sizeOfJson(current.chunk) <= MAX_MESSAGES_SIZE) {
const d = new Date(msg.created)
current.count += 1
current.from = current.from != null ? (d < current.from ? d : current.from) : d
current.to = current.to != null ? (d > current.to ? d : current.to) : d
continue
}
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete current.chunk[msg.id]
if (sizeOfJson(msg) > MAX_MESSAGES_SIZE) {
ctx.warn('Message size is too big, skipping')
continue
}
if (Object.keys(current.chunk).length === 0) {
ctx.warn('Message size is too big, skipping')
continue
}
if (current.from != null && current.to != null) {
chunks.push({
chunk: current.chunk,
from: current.from,
to: current.to,
count: current.count
})
}
current = {
chunk: {},
count: 0
}
current.chunk[msg.id] = msg
if (sizeOfJson(current.chunk) <= MAX_MESSAGES_SIZE) {
const d = new Date(msg.created)
current.from = current.from != null ? (d < current.from ? d : current.from) : d
current.to = current.to != null ? (d > current.to ? d : current.to) : d
current.count = 1
} else {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete current.chunk[msg.id]
ctx.warn(`Message ${msg.id} still exceeds limit, skipping`)
}
}
if (current.from != null && current.to != null) {
chunks.push({
chunk: current.chunk,
from: current.from,
to: current.to,
count: current.count
})
}
return chunks
}
async function oldMessageToNewMessageDoc (
oldMessage: OldMessage,
accountClient: AccountClient,
personUuidBySocialId: Map<PersonId, PersonUuid>
): Promise<MessageDoc> {
const reactions: Record<Emoji, Record<PersonUuid, { count: number, date: string }>> = {}
const attachments: Record<AttachmentID, AttachmentDoc> = {}
const threads: Record<CardID, ThreadDoc> = {}
const reactionsByEmoji = groupByArray(oldMessage.reactions, (it) => it.reaction)
for (const [_emoji, oldReactions] of reactionsByEmoji.entries()) {
const emoji = _emoji as Emoji
if (reactions[emoji] == null) {
reactions[emoji] = {}
}
for (const oldReaction of oldReactions) {
const personUuid = await getPersonUuidBySocialId(accountClient, personUuidBySocialId, oldReaction.creator)
if (personUuid == null) continue
if (reactions[emoji][personUuid] == null) {
reactions[emoji][personUuid] = {
count: 1,
date: oldReaction.created.toISOString()
}
}
}
}
for (const oldAttachment of oldMessage.attachments) {
attachments[oldAttachment.id] = {
id: oldAttachment.id,
mimeType:
oldAttachment.mimeType ??
(oldAttachment as any)?.params?.mimeType ??
(oldAttachment as any).type ??
(oldAttachment as any)?.params?.type ??
'',
params: oldAttachment.params,
creator: oldAttachment.creator,
created: oldAttachment.created.toISOString(),
modified: oldAttachment.modified?.toISOString() ?? null
}
}
if (oldMessage.thread != null) {
threads[oldMessage.thread.threadId] = {
threadId: oldMessage.thread.threadId,
threadType: oldMessage.thread.threadType,
repliesCount: oldMessage.thread.repliesCount,
lastReplyDate: oldMessage.thread.lastReply?.toISOString() ?? null,
repliedPersons: {}
}
}
return {
id: oldMessage.id,
cardId: oldMessage.cardId,
type: oldMessage.type === 'activity' ? MessageType.Activity : MessageType.Text,
content: oldMessage.content,
extra: oldMessage.extra ?? {},
creator: oldMessage.creator,
created: oldMessage.created.toISOString(),
modified: oldMessage.edited?.toISOString() ?? null,
reactions,
attachments,
threads
}
}
function sizeOfJson (obj: unknown): number {
return Buffer.byteLength(JSON.stringify(obj), 'utf8')
}
async function getPersonUuidBySocialId (
ccountClient: AccountClient,
personUuidBySocialId: Map<PersonId, PersonUuid>,
socialId: PersonId
): Promise<PersonUuid | undefined> {
if (personUuidBySocialId.has(socialId)) {
return personUuidBySocialId.get(socialId)
}
const personUuid = await ccountClient.findPersonBySocialId(socialId)
if (personUuid != null) {
personUuidBySocialId.set(socialId, personUuid)
}
return personUuid
}
async function getMessagesCards (ws: WorkspaceUuid, db: postgres.Sql): Promise<CardID[]> {
const table = db(MESSAGES_TABLE)
const res = await db`
SELECT DISTINCT card_id
FROM ${table}
WHERE workspace_id = ${ws}
`
return res.map((r) => r.card_id)
}
function getMessagesCursor (
ws: WorkspaceUuid,
cardId: CardID,
db: postgres.Sql,
from?: Date
): AsyncIterable<postgres.Row[]> {
const messagesTable = db(MESSAGES_TABLE)
const threadIndexTable = db(THREAD_INDEX_TABLE)
const attachmentTable = db(ATTACHMENT_TABLE)
const reactionTable = db(REACTION_TABLE)
const patchTable = db(PATCH_TABLE)
return db`WITH
limited_messages AS (
SELECT *
FROM ${messagesTable} m
WHERE m.workspace_id = ${ws}
AND m.card_id = ${cardId}
${from != null ? db`AND m.created > ${from}` : db``}
ORDER BY m.created ASC
),
agg_attachments AS (
SELECT
a.workspace_id,
a.card_id,
a.message_id,
jsonb_agg(jsonb_build_object(
'id', a.id,
'type', a.type,
'params', a.params,
'creator', a.creator,
'created', a.created,
'modified', a.modified
)) AS attachments
FROM ${attachmentTable} a
INNER JOIN limited_messages m
ON m.workspace_id = a.workspace_id
AND m.card_id = a.card_id
AND m.id = a.message_id
GROUP BY a.workspace_id, a.card_id, a.message_id
),
agg_reactions AS (
SELECT
r.workspace_id,
r.card_id,
r.message_id,
jsonb_agg(jsonb_build_object(
'reaction', r.reaction,
'creator', r.creator,
'created', r.created
)) AS reactions
FROM ${reactionTable} r
INNER JOIN limited_messages m
ON m.workspace_id = r.workspace_id
AND m.card_id = r.card_id
AND m.id = r.message_id
GROUP BY r.workspace_id, r.card_id, r.message_id
),
agg_patches AS (
SELECT
p.workspace_id,
p.card_id,
p.message_id,
jsonb_agg(
jsonb_build_object(
'type', p.type,
'data', p.data,
'creator', p.creator,
'created', p.created
) ORDER BY p.created ASC
) AS patches
FROM ${patchTable} p
INNER JOIN limited_messages m
ON m.workspace_id = p.workspace_id
AND m.card_id = p.card_id
AND m.id = p.message_id
GROUP BY p.workspace_id, p.card_id, p.message_id
)
SELECT m.id::text,
m.card_id,
m.type,
m.content,
m.creator,
m.created,
m.data,
t.thread_id AS thread_id,
t.thread_type AS thread_type,
t.replies_count::int AS replies_count,
t.last_reply AS last_reply,
COALESCE(a.attachments, '[]'::jsonb) AS attachments,
COALESCE(r.reactions, '[]'::jsonb) AS reactions,
COALESCE(p.patches, '[]'::jsonb) AS patches
FROM limited_messages m
LEFT JOIN ${threadIndexTable} t
ON t.workspace_id = m.workspace_id
AND t.card_id = m.card_id
AND t.message_id = m.id
LEFT JOIN agg_attachments a
ON a.workspace_id = m.workspace_id
AND a.card_id = m.card_id
AND a.message_id = m.id
LEFT JOIN agg_reactions r
ON r.workspace_id = m.workspace_id
AND r.card_id = m.card_id
AND r.message_id = m.id
LEFT JOIN agg_patches p
ON p.workspace_id = m.workspace_id
AND p.card_id = m.card_id
AND p.message_id = m.id
ORDER BY m.created ASC;`.cursor(200)
}
function deserializeMessagesGroups (groups: MessagesGroupsDoc): MessagesGroup[] {
return Object.values(groups)
.map((group) => {
const g: MessagesGroup = {
cardId: group.cardId,
blobId: group.blobId,
fromDate: new Date(group.fromDate),
toDate: new Date(group.toDate),
count: Number(group.count)
}
return g
})
.sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime())
}
function deserializePatch (raw: any): Patch {
return {
type: raw.type,
messageId: String(raw.message_id) as MessageID,
data: raw.data,
creator: raw.creator,
created: new Date(raw.created)
}
}
function deserializeOldMessage (raw: any): OldMessage | undefined {
const patches: Patch[] = (raw.patches ?? []).map((it: any) => deserializePatch(it))
const rawMessage: OldMessage = {
id: String(raw.id) as MessageID,
type: raw.type,
cardId: raw.card_id,
content: raw.content,
creator: raw.creator,
created: new Date(raw.created),
removed: false,
extra: raw.data,
thread:
raw.thread_id != null && raw.thread_type != null
? {
cardId: raw.card_id,
messageId: String(raw.id) as MessageID,
threadId: raw.thread_id,
threadType: raw.thread_type,
repliesCount: raw.replies_count != null ? Number(raw.replies_count) : 0,
lastReply: raw.last_reply ?? new Date()
}
: undefined,
reactions: (raw.reactions ?? []).map(deserializeReaction),
attachments: (raw.attachments ?? []).map(deserializeAttachment)
}
if (patches.length === 0) {
return rawMessage
}
return applyPatches(
rawMessage,
patches.filter((it) => it.type === PatchType.update || it.type === PatchType.remove)
)
}
function deserializeReaction (raw: any): OldReaction {
return {
reaction: raw.reaction,
creator: raw.creator,
created: new Date(raw.created)
}
}
function deserializeAttachment (raw: any): Attachment {
return {
id: String(raw.id) as AttachmentID,
type: raw.type,
params: raw.params,
creator: raw.creator,
created: new Date(raw.created),
modified: raw.modified != null ? new Date(raw.modified) : undefined
} as any as Attachment
}
export function applyPatches (message: OldMessage, patches: Patch[]): OldMessage | undefined {
if (patches.length === 0) return message
let result: OldMessage | undefined = message
for (const p of patches) {
result = applyPatch(message, p)
}
return result
}
function applyPatch (message: OldMessage, patch: Patch): OldMessage | undefined {
if (message.removed) {
return undefined
}
switch (patch.type) {
case PatchType.update: {
if (patch.created.getTime() < (message.edited?.getTime() ?? 0)) {
return message
}
return {
...message,
edited: patch.created,
content: patch.data.content ?? message.content,
extra: patch.data.extra ?? message.extra
}
}
case PatchType.remove: {
return undefined
}
case PatchType.reaction:
return patchReactions(message, patch)
case PatchType.attachment:
return patchAttachments(message, patch)
case PatchType.thread:
return patchThread(message, patch)
}
}
function patchAttachments (message: OldMessage, patch: AttachmentPatch): OldMessage {
if (patch.data.operation === 'add') {
return addAttachments(message, patch.data.attachments, patch.created, patch.creator)
} else if (patch.data.operation === 'remove') {
return removeAttachments(message, patch.data.ids)
} else if (patch.data.operation === 'set') {
return setAttachments(message, patch.data.attachments, patch.created, patch.creator)
} else if (patch.data.operation === 'update') {
return updateAttachments(message, patch.data.attachments, patch.created)
}
return message
}
function patchReactions (message: OldMessage, patch: ReactionPatch): OldMessage {
if (patch.data.operation === 'add') {
return setReaction(message, patch.data.reaction, patch.creator, patch.created)
} else if (patch.data.operation === 'remove') {
return removeReaction(message, patch.data.reaction, patch.creator)
}
return message
}
function setReaction (message: OldMessage, reaction: string, creator: SocialID, created: Date): OldMessage {
const isExist = message.reactions.some((it) => it.reaction === reaction && it.creator === creator)
if (isExist) return message
message.reactions.push({
reaction,
creator,
created
})
return message
}
function removeReaction (message: OldMessage, reaction: string, creator: SocialID): OldMessage {
const reactions = message.reactions.filter((it) => it.reaction !== reaction || it.creator !== creator)
if (reactions.length === message.reactions.length) return message
return {
...message,
reactions
}
}
function addAttachments (message: OldMessage, data: AttachmentData[], created: Date, creator: SocialID): OldMessage {
const newAttachments: Attachment[] = []
for (const attach of data) {
const isExists = message.attachments.some((it) => it.id === attach.id)
if (isExists === undefined) continue
const attachment: Attachment = {
...attach,
created,
creator
} as any
newAttachments.push(attachment)
}
if (newAttachments.length === 0) return message
return {
...message,
attachments: [...message.attachments, ...newAttachments]
}
}
function updateAttachments (message: OldMessage, updates: AttachmentUpdateData[], date: Date): OldMessage {
if (updates.length === 0) return message
const updatedAttachments: Attachment[] = []
for (const attachment of message.attachments) {
const update = updates.find((it) => it.id === attachment.id)
if (update === undefined) {
updatedAttachments.push(attachment)
} else {
updatedAttachments.push({
...attachment,
params: {
...attachment.params,
...update.params
},
modified: date.getTime() > (attachment.modified?.getTime() ?? 0) ? date : attachment.modified
} as any)
}
}
return {
...message,
attachments: updatedAttachments
}
}
function removeAttachments (message: OldMessage, ids: AttachmentID[]): OldMessage {
const attachments = message.attachments.filter((it) => !ids.includes(it.id))
if (attachments.length === message.attachments.length) return message
return {
...message,
attachments
}
}
function setAttachments (message: OldMessage, data: AttachmentData[], created: Date, creator: SocialID): OldMessage {
if (data.length === 0) return message
return {
...message,
attachments: data.map(
(it) =>
({
...it,
created,
creator
}) as any
)
}
}
function patchThread (message: OldMessage, patch: ThreadPatch): OldMessage {
if (patch.data.operation === 'attach') {
return attachThread(message, patch.data.threadId, patch.data.threadType)
} else if (patch.data.operation === 'update') {
return updateThread(
message,
patch.data.threadId,
patch.data.threadType,
patch.data.repliesCountOp,
patch.data.lastReply
)
}
return message
}
function attachThread (message: OldMessage, threadId: CardID, threadType: CardType): OldMessage {
if (message.thread !== undefined) return message
return {
...message,
thread: {
cardId: message.cardId,
messageId: message.id,
threadId,
threadType,
repliesCount: 0,
lastReply: new Date()
}
}
}
function updateThread (
message: OldMessage,
threadId: CardID,
threadType?: CardType,
repliesCountOp?: 'increment' | 'decrement',
lastReply?: Date
): OldMessage {
if (repliesCountOp === undefined && lastReply === undefined) return message
if (message.thread === undefined) return message
if (message.thread.threadId !== threadId) return message
let count = message.thread.repliesCount
if (repliesCountOp === 'increment') {
count = count + 1
}
if (repliesCountOp === 'decrement') {
count = Math.max(count - 1, 0)
}
return {
...message,
thread: {
...message.thread,
repliesCount: count,
threadType: threadType ?? message.thread.threadType,
lastReply: lastReply ?? message.thread.lastReply
}
}
}
interface OldMessage {
id: MessageID
cardId: CardID
type: 'message' | 'activity'
content: Markdown
extra?: MessageExtra
creator: SocialID
created: Date
removed: boolean
edited?: Date
reactions: OldReaction[]
attachments: Attachment[]
thread?: OldThread
}
interface OldReaction {
reaction: string
creator: SocialID
created: Date
}
interface OldThread {
cardId: CardID
messageId: MessageID
threadId: CardID
threadType: CardType
repliesCount: number
lastReply: Date
}
type Patch = UpdatePatch | RemovePatch | ReactionPatch | ThreadPatch | AttachmentPatch
enum PatchType {
update = 'update',
remove = 'remove',
reaction = 'reaction',
attachment = 'attachment',
thread = 'thread'
}
interface BasePatch {
messageId: MessageID
type: PatchType
creator: SocialID
created: Date
data: Record<string, any>
}
interface UpdatePatch extends BasePatch {
type: PatchType.update
data: UpdatePatchData
}
interface UpdatePatchData {
content?: Markdown
extra?: MessageExtra
}
interface RemovePatch extends BasePatch {
type: PatchType.remove
data: RemovePatchData
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface RemovePatchData {}
interface ReactionPatch extends BasePatch {
type: PatchType.reaction
data: AddReactionPatchData | RemoveReactionPatchData
}
interface AddReactionPatchData {
operation: 'add'
reaction: string
}
interface RemoveReactionPatchData {
operation: 'remove'
reaction: string
}
interface AttachmentPatch extends BasePatch {
type: PatchType.attachment
data: AddAttachmentsPatchData | RemoveAttachmentsPatchData | SetAttachmentsPatchData | UpdateAttachmentsPatchData
}
interface AddAttachmentsPatchData {
operation: 'add'
attachments: AttachmentData[]
}
interface RemoveAttachmentsPatchData {
operation: 'remove'
ids: AttachmentID[]
}
interface SetAttachmentsPatchData {
operation: 'set'
attachments: AttachmentData[]
}
interface UpdateAttachmentsPatchData {
operation: 'update'
attachments: AttachmentUpdateData[]
}
interface ThreadPatch extends BasePatch {
type: PatchType.thread
data: AttachThreadPatchData | UpdateThreadPatchData
}
interface AttachThreadPatchData {
operation: 'attach'
threadId: CardID
threadType: CardType
}
interface UpdateThreadPatchData {
operation: 'update'
threadId: CardID
threadType?: CardType
repliesCountOp?: 'increment' | 'decrement'
lastReply?: Date
}
+1 -1
View File
@@ -1911,7 +1911,7 @@ export async function restoreTrustedV6Workspace (
url
}
const queue = getPlatformQueue('tool', workspace.region)
const queue = getPlatformQueue('tool', region)
const wsProducer = queue.getProducer<QueueWorkspaceMessage>(ctx, QueueTopic.Workspace)
await wsProducer.send(ctx, uuid, [workspaceEvents.restoring()])
+61 -7
View File
@@ -56,6 +56,14 @@ import {
type Account as OldAccount,
type Workspace as OldWorkspace
} from '@hcengineering/account-service'
import { getClient as getHulylakeClient } from '@hcengineering/hulylake-client'
import {
getDBClient,
createPostgreeDestroyAdapter,
createPostgresAdapter,
createPostgresTxAdapter,
shutdownPostgres
} from '@hcengineering/postgres'
import { faker } from '@faker-js/faker'
import { getPlatformQueue } from '@hcengineering/kafka'
@@ -69,6 +77,7 @@ import {
isDeletingMode,
MeasureMetricsContext,
metricsToString,
SocialId,
SocialIdType,
systemAccountEmail,
systemAccountUuid,
@@ -80,7 +89,8 @@ import {
type Tx,
type Version,
type WorkspaceDataId,
type WorkspaceUuid
type WorkspaceUuid,
type PersonUuid
} from '@hcengineering/core'
import { consoleModelLogger, type MigrateOperation } from '@hcengineering/model'
import {
@@ -92,12 +102,6 @@ import {
} from '@hcengineering/mongo'
import { getModelVersion } from '@hcengineering/model-all'
import {
createPostgreeDestroyAdapter,
createPostgresAdapter,
createPostgresTxAdapter,
shutdownPostgres
} from '@hcengineering/postgres'
import {
QueueTopic,
workspaceEvents,
@@ -131,6 +135,8 @@ import { mkdir, writeFile } from 'fs/promises'
import { dirname } from 'path'
import { restoreMarkupRefs } from './markup'
import { restoreGithubIntegrations } from './restoreGithub'
import { migrateWorkspaceMessages } from './communication'
import { type CardID } from '@hcengineering/communication-types'
const colorConstants = {
colorRed: '\u001b[31m',
@@ -2845,6 +2851,54 @@ export function devTool (
}, dbUrl)
})
program
.command('migrate-communication-to-hulylake <workspace>')
.description('Migrate communication messages to hulylake')
.option('-c, --card <card>', 'Card to migrate')
.action(async (workspace: WorkspaceUuid, cmd: { card?: CardID }) => {
const { dbUrl } = prepareTools()
const hulylakeUrl = process.env.HULYLAKE_URL ?? ''
console.log('Workspace', workspace)
console.log('Card', cmd.card)
if (hulylakeUrl === '') {
throw new Error('HULYLAKE_URL should be specified')
}
const token = generateToken(systemAccountUuid, undefined, {
service: 'tool'
})
const db = getDBClient(dbUrl, undefined, 'tool')
const dbClient = await db.getClient()
const accountClient = getAccountClient(token)
const personUuidBySocialId = new Map<PersonId, PersonUuid>()
await withAccountDatabase(async (accountDb) => {
const ws = (await accountDb.workspace.find({ uuid: workspace }))[0]
if (ws === undefined) {
throw new Error(`Workspace ${workspace} not found`)
}
try {
const hulylake = getHulylakeClient(hulylakeUrl, ws.uuid, token)
console.log('start workspace migration', ws.name)
await migrateWorkspaceMessages(
toolCtx.newChild(ws.name, {}),
ws,
cmd.card,
dbClient,
hulylake,
accountClient,
personUuidBySocialId
)
console.log('done workspace migration', ws.name)
} catch (err: any) {
console.error('failed to migrate workspace', ws.name)
console.error(err)
}
db.close()
}, dbUrl)
})
extendProgram?.(program)
process.on('unhandledRejection', (reason, promise) => {
+1 -1
View File
@@ -474,7 +474,7 @@ export default function buildModel (): Builder {
[
chatModel,
chatId,
{ label: chat.string.Chat, hidden: true, enabled: false, beta: true, classFilter: defaultFilter }
{ label: chat.string.Chat, hidden: false, enabled: true, beta: true, classFilter: defaultFilter }
],
[achievementModel, achievementId],
[emojiModel, emojiId],
+218
View File
@@ -0,0 +1,218 @@
// Copyright © 2025 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 Builder } from '@hcengineering/model'
import presentation from '@hcengineering/model-presentation'
import setting from '@hcengineering/model-setting'
import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import card from './plugin'
export function createActions (builder: Builder): void {
createAction(builder, {
action: card.actionImpl.EditSpace,
label: presentation.string.Edit,
icon: view.icon.Edit,
input: 'focus',
category: view.category.General,
target: card.class.CardSpace,
visibilityTester: view.function.CanEditSpace,
query: {},
context: {
mode: ['context', 'browser'],
group: 'edit'
}
})
createAction(builder, {
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: card.function.GetSpaceAccessPublicLink
},
label: card.string.GetIndividualPublicLink,
icon: view.icon.CopyLink,
input: 'any',
category: view.category.General,
target: card.class.CardSpace,
query: {},
visibilityTester: card.function.CanGetSpaceAccessPublicLink,
context: {
mode: ['context', 'browser']
}
})
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: card.component.SetParentActionPopup,
element: 'top',
fillProps: {
_objects: 'value'
}
},
label: card.string.SetParent,
icon: card.icon.MasterTag,
input: 'none',
category: card.category.Card,
target: card.class.Card,
context: {
mode: ['context'],
application: card.app.Card,
group: 'associate'
}
},
card.action.SetParent
)
createAction(
builder,
{
action: view.actionImpl.UpdateDocument,
actionProps: {
key: 'parent',
value: null
},
query: {
parent: { $ne: null, $exists: true }
},
label: card.string.UnsetParent,
icon: card.icon.MasterTag,
input: 'none',
category: card.category.Card,
target: card.class.Card,
context: {
mode: ['context'],
application: card.app.Card,
group: 'associate'
}
},
card.action.UnsetParent
)
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: card.component.CreateTag,
props: {
_class: card.class.Tag
},
fillProps: {
_object: 'parent'
}
},
label: card.string.CreateTag,
input: 'focus',
icon: view.icon.Add,
category: setting.category.Settings,
target: card.class.MasterTag,
context: {
mode: ['context', 'browser'],
group: 'edit'
}
})
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: card.component.CreateTag,
props: {
_class: card.class.Tag
},
fillProps: {
_object: 'parent'
}
},
label: card.string.CreateTag,
input: 'focus',
icon: view.icon.Add,
category: setting.category.Settings,
target: card.class.Tag,
context: {
mode: ['context', 'browser'],
group: 'edit'
}
})
createAction(
builder,
{
action: card.actionImpl.DeleteMasterTag,
label: workbench.string.Delete,
icon: view.icon.Delete,
input: 'any',
category: view.category.General,
target: card.class.MasterTag,
context: {
mode: ['context', 'browser'],
group: 'remove'
}
},
card.action.DeleteMasterTag
)
createAction(builder, {
action: card.actionImpl.DeleteMasterTag,
label: workbench.string.Delete,
icon: view.icon.Delete,
input: 'any',
category: view.category.General,
target: card.class.Tag,
context: {
mode: ['context', 'browser'],
group: 'remove'
}
})
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: card.component.ChangeType,
fillProps: {
_object: 'value'
}
},
label: card.string.ChangeType,
input: 'focus',
icon: card.icon.MasterTag,
category: setting.category.Settings,
target: card.class.Card,
context: {
mode: ['context', 'browser'],
group: 'edit'
}
})
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: card.component.CreateTag,
props: {
_class: card.class.MasterTag
},
fillProps: {
_object: 'parent'
}
},
label: card.string.CreateMasterTag,
input: 'focus',
icon: card.icon.MasterTag,
category: setting.category.Settings,
target: card.class.MasterTag,
context: {
mode: ['context', 'browser'],
group: 'edit'
}
})
}
+23 -200
View File
@@ -13,6 +13,7 @@
import activity from '@hcengineering/activity'
import {
type CanCreateCardResource,
type Card,
cardId,
type CardNavigation,
@@ -20,7 +21,6 @@ import {
type CardSpace,
type CardViewDefaults,
type CreateCardExtension,
type CanCreateCardResource,
DOMAIN_CARD,
type FavoriteCard,
type MasterTag,
@@ -60,17 +60,18 @@ import {
import attachment from '@hcengineering/model-attachment'
import { TAttachedDoc, TClass, TDoc, TMixin, TSpace } from '@hcengineering/model-core'
import { createPublicLinkAction } from '@hcengineering/model-guest'
import preference, { TPreference } from '@hcengineering/model-preference'
import presentation from '@hcengineering/model-presentation'
import setting from '@hcengineering/model-setting'
import view, { createAction, type Viewlet } from '@hcengineering/model-view'
import view, { type Viewlet } from '@hcengineering/model-view'
import workbench, { WidgetType } from '@hcengineering/model-workbench'
import { type Asset, getEmbeddedLabel, type IntlString, type Resource } from '@hcengineering/platform'
import time, { type ToDo } from '@hcengineering/time'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import { type BuildModelKey } from '@hcengineering/view'
import preference, { TPreference } from '@hcengineering/model-preference'
import card from './plugin'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import { createActions } from './actions'
export { cardId } from '@hcengineering/card'
@@ -399,37 +400,7 @@ export function createModel (builder: Builder): void {
}
})
createAction(builder, {
action: card.actionImpl.EditSpace,
label: presentation.string.Edit,
icon: view.icon.Edit,
input: 'focus',
category: view.category.General,
target: card.class.CardSpace,
visibilityTester: view.function.CanEditSpace,
query: {},
context: {
mode: ['context', 'browser'],
group: 'edit'
}
})
createAction(builder, {
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: card.function.GetSpaceAccessPublicLink
},
label: card.string.GetIndividualPublicLink,
icon: view.icon.CopyLink,
input: 'any',
category: view.category.General,
target: card.class.CardSpace,
query: {},
visibilityTester: card.function.CanGetSpaceAccessPublicLink,
context: {
mode: ['context', 'browser']
}
})
createActions(builder)
builder.mixin(card.class.CardSpace, core.class.Class, workbench.mixin.SpaceView, {
view: {
@@ -450,6 +421,19 @@ export function createModel (builder: Builder): void {
locationDataResolver: card.resolver.LocationData,
navigatorModel: {
specials: [
{
id: 'all',
label: card.string.AllCards,
icon: card.icon.All,
component: workbench.component.SpecialView,
componentProps: {
_class: card.class.Card,
icon: card.icon.All,
label: card.string.AllCards,
defaultViewletDescriptor: card.viewlet.CardFeedDescriptor
},
position: 'top'
},
{
id: 'browser',
label: core.string.Spaces,
@@ -484,31 +468,6 @@ export function createModel (builder: Builder): void {
presenter: card.component.SpacePresenter
})
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: card.component.SetParentActionPopup,
element: 'top',
fillProps: {
_objects: 'value'
}
},
label: card.string.SetParent,
icon: card.icon.MasterTag,
input: 'none',
category: card.category.Card,
target: card.class.Card,
context: {
mode: ['context'],
application: card.app.Card,
group: 'associate'
}
},
card.action.SetParent
)
builder.mixin(card.class.Card, core.class.Class, view.mixin.AttributeEditor, {
inlineEditor: card.component.CardEditor
})
@@ -517,31 +476,6 @@ export function createModel (builder: Builder): void {
inlineEditor: card.component.CardArrayEditor
})
createAction(
builder,
{
action: view.actionImpl.UpdateDocument,
actionProps: {
key: 'parent',
value: null
},
query: {
parent: { $ne: null, $exists: true }
},
label: card.string.UnsetParent,
icon: card.icon.MasterTag,
input: 'none',
category: card.category.Card,
target: card.class.Card,
context: {
mode: ['context'],
application: card.app.Card,
group: 'associate'
}
},
card.action.UnsetParent
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
@@ -680,50 +614,6 @@ export function createModel (builder: Builder): void {
encode: card.function.GetCardLink
})
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: card.component.CreateTag,
props: {
_class: card.class.Tag
},
fillProps: {
_object: 'parent'
}
},
label: card.string.CreateTag,
input: 'focus',
icon: view.icon.Add,
category: setting.category.Settings,
target: card.class.MasterTag,
context: {
mode: ['context', 'browser'],
group: 'edit'
}
})
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: card.component.CreateTag,
props: {
_class: card.class.Tag
},
fillProps: {
_object: 'parent'
}
},
label: card.string.CreateTag,
input: 'focus',
icon: view.icon.Add,
category: setting.category.Settings,
target: card.class.Tag,
context: {
mode: ['context', 'browser'],
group: 'edit'
}
})
builder.mixin(card.class.MasterTag, core.class.Class, view.mixin.IgnoreActions, {
actions: [setting.action.CreateMixin, view.action.OpenInNewTab, view.action.Delete, setting.action.DeleteMixin]
})
@@ -732,77 +622,6 @@ export function createModel (builder: Builder): void {
actions: [setting.action.CreateMixin, view.action.OpenInNewTab, view.action.Delete, setting.action.DeleteMixin]
})
createAction(
builder,
{
action: card.actionImpl.DeleteMasterTag,
label: workbench.string.Delete,
icon: view.icon.Delete,
input: 'any',
category: view.category.General,
target: card.class.MasterTag,
context: {
mode: ['context', 'browser'],
group: 'remove'
}
},
card.action.DeleteMasterTag
)
createAction(builder, {
action: card.actionImpl.DeleteMasterTag,
label: workbench.string.Delete,
icon: view.icon.Delete,
input: 'any',
category: view.category.General,
target: card.class.Tag,
context: {
mode: ['context', 'browser'],
group: 'remove'
}
})
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: card.component.ChangeType,
fillProps: {
_object: 'value'
}
},
label: card.string.ChangeType,
input: 'focus',
icon: card.icon.MasterTag,
category: setting.category.Settings,
target: card.class.Card,
context: {
mode: ['context', 'browser'],
group: 'edit'
}
})
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: card.component.CreateTag,
props: {
_class: card.class.MasterTag
},
fillProps: {
_object: 'parent'
}
},
label: card.string.CreateMasterTag,
input: 'focus',
icon: card.icon.MasterTag,
category: setting.category.Settings,
target: card.class.MasterTag,
context: {
mode: ['context', 'browser'],
group: 'edit'
}
})
builder.createDoc(
setting.class.WorkspaceSettingCategory,
core.space.Model,
@@ -849,6 +668,10 @@ export function createModel (builder: Builder): void {
forceIndex: true
})
builder.mixin(card.class.Card, core.class.Class, view.mixin.ObjectFactory, {
create: card.function.CardFactory
})
builder.createDoc(
presentation.class.ObjectSearchCategory,
core.space.Model,
+1 -1
View File
@@ -36,7 +36,7 @@ export function buildMessageActions (builder: Builder): void {
communication.class.MessageAction,
core.space.Model,
{
label: communication.string.ReplyInThread,
label: communication.string.Reply,
icon: communication.icon.MessageMultiple,
action: communication.messageActionImpl.ReplyInThread,
visibilityTester: communication.function.CanReplyInThread,
+5
View File
@@ -921,6 +921,10 @@ export function createModel (builder: Builder): void {
presenter: contact.component.PersonPresenter
})
builder.mixin(contact.class.SocialIdentity, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.SocialIdentityPresenter
})
builder.mixin(core.class.TypeAccountUuid, core.class.Class, view.mixin.ArrayEditor, {
inlineEditor: contact.component.AccountArrayEditor
})
@@ -1141,6 +1145,7 @@ export function createModel (builder: Builder): void {
label: contact.string.MergePersons,
category: contact.category.Contact,
target: contact.class.Person,
visibilityTester: contact.function.CanMergePersons,
input: 'focus',
context: {
mode: ['context'],
+1
View File
@@ -32,6 +32,7 @@ export default mergeIds(contactId, contact, {
},
component: {
PersonPresenter: '' as AnyComponent,
SocialIdentityPresenter: '' as AnyComponent,
ContactRefPresenter: '' as AnyComponent,
ContactPresenter: '' as AnyComponent,
EditPerson: '' as AnyComponent,
@@ -12,6 +12,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.ReviewDocumentPermission,
scope: 'space',
description: documents.string.ReviewDocumentDescription
},
documents.permission.ReviewDocument
@@ -22,6 +23,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.ApproveDocumentPermission,
scope: 'space',
description: documents.string.ApproveDocumentDescription
},
documents.permission.ApproveDocument
@@ -32,6 +34,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.ArchiveDocumentPermission,
scope: 'space',
description: documents.string.ArchiveDocumentDescription
},
documents.permission.ArchiveDocument
@@ -42,6 +45,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.CoAuthorDocumentPermission,
scope: 'space',
description: documents.string.CoAuthorDocumentDescription
},
documents.permission.CoAuthorDocument
@@ -52,6 +56,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.CreateDocumentPermission,
scope: 'space',
description: documents.string.CreateDocumentDescription
},
documents.permission.CreateDocument
@@ -62,6 +67,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.UpdateDocumentOwnerPermission,
scope: 'space',
description: documents.string.UpdateDocumentOwnerDescription
},
documents.permission.UpdateDocumentOwner
@@ -72,6 +78,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.CreateDocumentCategoryPermission,
scope: 'space',
description: documents.string.CreateDocumentCategoryDescription
},
documents.permission.CreateDocumentCategory
@@ -82,6 +89,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.UpdateDocumentCategoryPermission,
scope: 'space',
description: documents.string.UpdateDocumentCategoryDescription
},
documents.permission.UpdateDocumentCategory
@@ -92,6 +100,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.DeleteDocumentCategoryPermission,
scope: 'space',
description: documents.string.DeleteDocumentCategoryDescription
},
documents.permission.DeleteDocumentCategory
+8 -20
View File
@@ -23,6 +23,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: core.string.CreateObject,
scope: 'space',
description: core.string.CreateObjectDescription
},
core.permission.CreateObject
@@ -33,6 +34,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: core.string.UpdateObject,
scope: 'space',
description: core.string.UpdateObjectDescription
},
core.permission.UpdateObject
@@ -43,6 +45,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: core.string.DeleteObject,
scope: 'space',
description: core.string.DeleteObjectDescription
},
core.permission.DeleteObject
@@ -53,36 +56,20 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: core.string.ForbidDeleteObject,
txClass: core.class.TxRemoveDoc,
forbid: true,
scope: 'space',
description: core.string.ForbidDeleteObjectDescription
},
core.permission.ForbidDeleteObject
)
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: core.string.UpdateObject,
description: core.string.UpdateObjectDescription
},
core.permission.UpdateObject
)
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: core.string.DeleteObject,
description: core.string.DeleteObjectDescription
},
core.permission.DeleteObject
)
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: core.string.UpdateSpace,
scope: 'space',
description: core.string.UpdateSpaceDescription
},
core.permission.UpdateSpace
@@ -93,6 +80,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: core.string.ArchiveSpace,
scope: 'space',
description: core.string.ArchiveSpaceDescription
},
core.permission.ArchiveSpace
+7 -1
View File
@@ -29,7 +29,9 @@ import {
type SpaceTypeDescriptor,
type TypedSpace,
type AccountUuid,
type TxAccessLevel
type TxAccessLevel,
type Tx,
type Doc
} from '@hcengineering/core'
import {
ArrOf,
@@ -155,6 +157,10 @@ export class TRole extends TAttachedDoc implements Role {
@UX(core.string.Permission)
export class TPermission extends TDoc implements Permission {
label!: IntlString
txClass?: Ref<Class<Tx>>
forbid?: boolean
objectClass?: Ref<Class<Doc<Space>>>
scope?: 'space' | 'workspace'
description?: IntlString
icon?: Asset
}
+2
View File
@@ -63,6 +63,7 @@ import { type Asset, getEmbeddedLabel } from '@hcengineering/platform'
import tags from '@hcengineering/tags'
import time, { type ToDo, type Todoable } from '@hcengineering/time'
import document from './plugin'
import { definePermissions } from './permissions'
export { documentId } from '@hcengineering/document'
@@ -550,6 +551,7 @@ export function createModel (builder: Builder): void {
defineDocument(builder)
defineApplication(builder)
definePermissions(builder)
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_DOCUMENT,
+19
View File
@@ -0,0 +1,19 @@
import type { Builder } from '@hcengineering/model'
import core from '@hcengineering/core'
import document from '@hcengineering/document'
export function definePermissions (builder: Builder): void {
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: document.string.ForbidCreateTeamspacePermission,
scope: 'workspace',
txClass: core.class.TxCreateDoc,
objectClass: document.class.Teamspace,
forbid: true,
description: document.string.ForbidCreateTeamspacePermissionDescription
},
document.permission.ForbidCreateTeamspace
)
}
+2
View File
@@ -63,6 +63,7 @@ import workbench from '@hcengineering/model-workbench'
import { getEmbeddedLabel } from '@hcengineering/platform'
import drive from './plugin'
import { definePermissions } from './permissions'
export { driveId } from '@hcengineering/drive'
export { driveOperation } from './migration'
@@ -790,4 +791,5 @@ export function createModel (builder: Builder): void {
defineFile(builder)
defineFileVersion(builder)
defineApplication(builder)
definePermissions(builder)
}
+19
View File
@@ -0,0 +1,19 @@
import type { Builder } from '@hcengineering/model'
import core from '@hcengineering/core'
import drive from '@hcengineering/drive'
export function definePermissions (builder: Builder): void {
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: drive.string.ForbidCreateDrivePermission,
scope: 'workspace',
txClass: core.class.TxCreateDoc,
objectClass: drive.class.Drive,
forbid: true,
description: drive.string.ForbidCreateDrivePermissionDescription
},
drive.permission.ForbidCreateDrive
)
}
+2
View File
@@ -32,6 +32,7 @@ import { type ViewOptionsModel } from '@hcengineering/view'
import lead from './plugin'
import { defineSpaceType } from './spaceType'
import { definePermissions } from './permissions'
import { TCustomer, TFunnel, TLead } from './types'
export { leadId } from '@hcengineering/lead'
@@ -660,4 +661,5 @@ export function createModel (builder: Builder): void {
})
defineSpaceType(builder)
definePermissions(builder)
}
+19
View File
@@ -0,0 +1,19 @@
import type { Builder } from '@hcengineering/model'
import core from '@hcengineering/core'
import lead from '@hcengineering/lead'
export function definePermissions (builder: Builder): void {
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: lead.string.ForbidCreateFunnelPermission,
scope: 'workspace',
txClass: core.class.TxCreateDoc,
objectClass: lead.class.Funnel,
forbid: true,
description: lead.string.ForbidCreateFunnelPermissionDescription
},
lead.permission.ForbidCreateFunnel
)
}
+6
View File
@@ -719,6 +719,12 @@ export function createModel (builder: Builder): void {
love.completion.MeetingMinutesCategory
)
// Extensions
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
extension: contact.extension.EmployeePopupActions,
component: love.component.InviteEmployeeButton
})
createAttributePresenter(
builder,
view.component.DateTimePresenter,
+2 -1
View File
@@ -32,7 +32,8 @@ export default mergeIds(loveId, love, {
WidgetSwitcher: '' as AnyComponent,
RoomLanguageEditor: '' as AnyComponent,
MediaPopupItemExt: '' as AnyComponent,
SharingStateIndicator: '' as AnyComponent
SharingStateIndicator: '' as AnyComponent,
InviteEmployeeButton: '' as AnyComponent
},
app: {
Love: '' as Ref<Doc>
+1
View File
@@ -45,6 +45,7 @@
"@hcengineering/setting": "^0.6.17",
"@hcengineering/model-time": "^0.6.0",
"@hcengineering/rank": "^0.6.4",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/process": "^0.6.0",
"@hcengineering/process-resources": "^0.6.0",
"@hcengineering/model-presentation": "^0.6.0",
+118
View File
@@ -0,0 +1,118 @@
// Copyright © 2025 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 card from '@hcengineering/card'
import core from '@hcengineering/core'
import { type Builder } from '@hcengineering/model'
import process from './plugin'
export function defineMethods (builder: Builder): void {
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.RunProcess,
objectClass: process.class.Process,
editor: process.component.SubProcessEditor,
presenter: process.component.SubProcessPresenter,
createdContext: { _class: process.class.Execution },
requiredParams: ['_id']
},
process.method.RunSubProcess
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.CreateToDo,
editor: process.component.ToDoEditor,
objectClass: process.class.ProcessToDo,
presenter: process.component.ToDoPresenter,
createdContext: { _class: process.class.ProcessToDo, nameField: 'title' },
requiredParams: ['title', 'user'],
defaultParams: {
withRollback: true
}
},
process.method.CreateToDo
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnExecutionStart,
icon: process.icon.Process,
init: true,
requiredParams: []
},
process.trigger.OnExecutionStart
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.UpdateCard,
editor: process.component.UpdateCardEditor,
objectClass: card.class.Card,
createdContext: null,
presenter: process.component.UpdateCardPresenter,
requiredParams: []
},
process.method.UpdateCard
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.CreateCard,
objectClass: card.class.Card,
editor: process.component.CreateCardEditor,
presenter: process.component.CreateCardPresenter,
createdContext: { _class: card.class.Card },
requiredParams: ['title', '_class']
},
process.method.CreateCard
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: core.string.AddRelation,
objectClass: core.class.Relation,
editor: process.component.AddRelationEditor,
presenter: process.component.AddRelationPresenter,
createdContext: { _class: core.class.Relation },
requiredParams: ['association', 'direction', '_id']
},
process.method.AddRelation
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: card.string.AddTag,
objectClass: card.class.Tag,
editor: process.component.AddTagEditor,
presenter: process.component.AddTagPresenter,
createdContext: { _class: card.class.Card },
requiredParams: ['_id']
},
process.method.AddTag
)
}
+520
View File
@@ -0,0 +1,520 @@
// Copyright © 2025 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 contact from '@hcengineering/contact'
import core from '@hcengineering/core'
import { type Builder } from '@hcengineering/model'
import process from './plugin'
export function defineFunctions (builder: Builder): void {
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.UpperCase,
type: 'transform'
},
process.function.UpperCase
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.LowerCase,
type: 'transform'
},
process.function.LowerCase
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Trim,
type: 'transform'
},
process.function.Trim
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Prepend,
allowMany: true,
type: 'transform',
editor: process.transformEditor.AppendEditor
},
process.function.Prepend
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Append,
allowMany: true,
type: 'transform',
editor: process.transformEditor.AppendEditor
},
process.function.Append
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Replace,
allowMany: true,
type: 'transform',
editor: process.transformEditor.ReplaceEditor
},
process.function.Replace
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.ReplaceAll,
allowMany: true,
type: 'transform',
editor: process.transformEditor.ReplaceEditor
},
process.function.ReplaceAll
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Split,
allowMany: true,
type: 'transform',
editor: process.transformEditor.SplitEditor
},
process.function.Split
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Cut,
allowMany: true,
editor: process.transformEditor.CutEditor,
type: 'transform'
},
process.function.Cut
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: undefined,
label: process.string.FirstValue,
type: 'reduce'
},
process.function.FirstValue
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
type: 'reduce',
category: undefined,
label: process.string.LastValue
},
process.function.LastValue
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
type: 'reduce',
category: undefined,
label: process.string.Random
},
process.function.Random
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.Each,
type: 'reduce'
},
process.function.All
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
type: 'transform',
category: 'attribute',
label: process.string.Add,
presenter: process.transformPresenter.NumberPresenter,
allowMany: true,
editor: process.transformEditor.NumberEditor
},
process.function.Add
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Subtract,
presenter: process.transformPresenter.NumberPresenter,
allowMany: true,
editor: process.transformEditor.NumberEditor,
type: 'transform'
},
process.function.Subtract
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Multiply,
presenter: process.transformPresenter.NumberPresenter,
type: 'transform',
allowMany: true,
editor: process.transformEditor.NumberEditor
},
process.function.Multiply
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Divide,
presenter: process.transformPresenter.NumberPresenter,
type: 'transform',
allowMany: true,
editor: process.transformEditor.NumberEditor
},
process.function.Divide
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Modulo,
presenter: process.transformPresenter.NumberPresenter,
type: 'transform',
allowMany: true,
editor: process.transformEditor.NumberEditor
},
process.function.Modulo
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Power,
presenter: process.transformPresenter.NumberPresenter,
type: 'transform',
allowMany: true,
editor: process.transformEditor.NumberEditor
},
process.function.Power
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Sqrt,
presenter: process.transformPresenter.NumberPresenter,
type: 'transform',
allowMany: true
},
process.function.Sqrt
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Round,
type: 'transform',
allowMany: true
},
process.function.Round
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Absolute,
type: 'transform',
allowMany: true
},
process.function.Absolute
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Ceil,
type: 'transform',
allowMany: true
},
process.function.Ceil
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Floor,
type: 'transform',
allowMany: true
},
process.function.Floor
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeDate,
category: 'attribute',
label: process.string.Offset,
editor: process.transformEditor.DateOffsetEditor,
type: 'transform'
},
process.function.Offset
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeDate,
category: 'attribute',
label: process.string.FirstWorkingDayAfter,
type: 'transform'
},
process.function.FirstWorkingDayAfter
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.Insert,
allowMany: true,
editor: process.transformEditor.MultiArrayElementEditor,
type: 'transform'
},
process.function.Insert
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.Remove,
allowMany: true,
editor: process.transformEditor.ArrayElementEditor,
type: 'transform'
},
process.function.Remove
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.RemoveFirst,
allowMany: true,
type: 'transform'
},
process.function.RemoveFirst
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.RemoveLast,
allowMany: true,
type: 'transform'
},
process.function.RemoveLast
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: contact.mixin.Employee,
editor: process.component.RoleEditor,
presenter: process.transformPresenter.RolePresenter,
category: 'array',
label: core.string.Role,
type: 'context'
},
process.function.RoleContext
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.EmptyArray,
type: 'context'
},
process.function.EmptyArray
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: contact.mixin.Employee,
category: 'attribute',
label: process.string.ExecutionInitiator,
type: 'context'
},
process.function.ExecutionEmployeeInitiator
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: contact.class.Person,
category: 'attribute',
label: process.string.ExecutionInitiator,
type: 'context'
},
process.function.ExecutionInitiator
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeDate,
category: 'attribute',
label: process.string.ExecutionStarted,
type: 'context'
},
process.function.ExecutionStarted
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: contact.class.Person,
category: 'attribute',
label: process.string.CurrentUser,
type: 'context'
},
process.function.CurrentUser
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: contact.mixin.Employee,
category: 'attribute',
label: process.string.CurrentUser,
type: 'context'
},
process.function.CurrentEmployee
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeDate,
category: 'attribute',
label: process.string.CurrentDate,
type: 'context'
},
process.function.CurrentDate
)
}
+82 -633
View File
@@ -12,7 +12,6 @@
// limitations under the License.
import card, { type Card, type MasterTag, type Tag } from '@hcengineering/card'
import contact from '@hcengineering/contact'
import core, {
AccountRole,
type Class,
@@ -43,10 +42,12 @@ import presentation from '@hcengineering/model-presentation'
import { TToDo } from '@hcengineering/model-time'
import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification'
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
import {
type CheckFunc,
type ContextId,
type CreatedContext,
type Execution,
type ExecutionContext,
type ExecutionError,
@@ -69,6 +70,9 @@ import {
import time from '@hcengineering/time'
import { type AnyComponent } from '@hcengineering/ui'
import { type AttributeCategory } from '@hcengineering/view'
import { defineMethods } from './actions'
import { defineTriggers } from './triggers'
import { defineFunctions } from './functions'
import process from './plugin'
const DOMAIN_PROCESS = 'process' as Domain
@@ -206,7 +210,7 @@ export class TMethod extends TDoc implements Method<Doc> {
editor!: AnyComponent
contextClass!: Ref<Class<Doc>> | null
createdContext!: CreatedContext
presenter?: AnyComponent
@@ -247,6 +251,8 @@ export class TUpdateCriteriaComponent extends TDoc implements UpdateCriteriaComp
editor!: AnyComponent
of!: Ref<Class<Doc<Space>>>
props!: Record<string, any>
}
export * from './migration'
@@ -265,6 +271,28 @@ export function createModel (builder: Builder): void {
TUpdateCriteriaComponent
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
generated: false,
allowedForAuthor: true,
label: process.string.NewProcessToDo,
group: time.ids.TimeNotificationGroup,
txClasses: [core.class.TxCreateDoc],
objectClass: process.class.ProcessToDo,
onlyOwn: true,
defaultEnabled: true,
templates: {
textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{title}'
}
},
process.ids.ProcessToDoCreated
)
createAction(builder, {
action: view.actionImpl.Delete,
label: view.string.Delete,
@@ -339,444 +367,9 @@ export function createModel (builder: Builder): void {
process.pipeline.ProcessMiddleware
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.UpperCase,
type: 'transform'
},
process.function.UpperCase
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.LowerCase,
type: 'transform'
},
process.function.LowerCase
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Trim,
type: 'transform'
},
process.function.Trim
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Prepend,
allowMany: true,
type: 'transform',
editor: process.transformEditor.AppendEditor
},
process.function.Prepend
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Append,
allowMany: true,
type: 'transform',
editor: process.transformEditor.AppendEditor
},
process.function.Append
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Replace,
allowMany: true,
type: 'transform',
editor: process.transformEditor.ReplaceEditor
},
process.function.Replace
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.ReplaceAll,
allowMany: true,
type: 'transform',
editor: process.transformEditor.ReplaceEditor
},
process.function.ReplaceAll
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Split,
allowMany: true,
type: 'transform',
editor: process.transformEditor.SplitEditor
},
process.function.Split
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Cut,
allowMany: true,
editor: process.transformEditor.CutEditor,
type: 'transform'
},
process.function.Cut
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: undefined,
label: process.string.FirstValue,
type: 'reduce'
},
process.function.FirstValue
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
type: 'reduce',
category: undefined,
label: process.string.LastValue
},
process.function.LastValue
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
type: 'reduce',
category: undefined,
label: process.string.Random
},
process.function.Random
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.Each,
type: 'reduce'
},
process.function.All
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
type: 'transform',
category: 'attribute',
label: process.string.Add,
presenter: process.transformPresenter.NumberPresenter,
allowMany: true,
editor: process.transformEditor.NumberEditor
},
process.function.Add
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Subtract,
presenter: process.transformPresenter.NumberPresenter,
allowMany: true,
editor: process.transformEditor.NumberEditor,
type: 'transform'
},
process.function.Subtract
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Multiply,
presenter: process.transformPresenter.NumberPresenter,
type: 'transform',
allowMany: true,
editor: process.transformEditor.NumberEditor
},
process.function.Multiply
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Divide,
presenter: process.transformPresenter.NumberPresenter,
type: 'transform',
allowMany: true,
editor: process.transformEditor.NumberEditor
},
process.function.Divide
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Modulo,
presenter: process.transformPresenter.NumberPresenter,
type: 'transform',
allowMany: true,
editor: process.transformEditor.NumberEditor
},
process.function.Modulo
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Power,
presenter: process.transformPresenter.NumberPresenter,
type: 'transform',
allowMany: true,
editor: process.transformEditor.NumberEditor
},
process.function.Power
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Round,
type: 'transform',
allowMany: true
},
process.function.Round
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Absolute,
type: 'transform',
allowMany: true
},
process.function.Absolute
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Ceil,
type: 'transform',
allowMany: true
},
process.function.Ceil
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Floor,
type: 'transform',
allowMany: true
},
process.function.Floor
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeDate,
category: 'attribute',
label: process.string.Offset,
editor: process.transformEditor.DateOffsetEditor,
type: 'transform'
},
process.function.Offset
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeDate,
category: 'attribute',
label: process.string.FirstWorkingDayAfter,
type: 'transform'
},
process.function.FirstWorkingDayAfter
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.Insert,
allowMany: true,
editor: process.transformEditor.MultiArrayElementEditor,
type: 'transform'
},
process.function.Insert
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.Remove,
allowMany: true,
editor: process.transformEditor.ArrayElementEditor,
type: 'transform'
},
process.function.Remove
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.RemoveFirst,
allowMany: true,
type: 'transform'
},
process.function.RemoveFirst
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.RemoveLast,
allowMany: true,
type: 'transform'
},
process.function.RemoveLast
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: contact.mixin.Employee,
editor: process.component.RoleEditor,
presenter: process.transformPresenter.RolePresenter,
category: 'array',
label: core.string.Role,
type: 'context'
},
process.function.RoleContext
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: contact.class.Person,
category: 'attribute',
label: process.string.CurrentUser,
type: 'context'
},
process.function.CurrentUser
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: contact.mixin.Employee,
category: 'attribute',
label: process.string.CurrentUser,
type: 'context'
},
process.function.CurrentEmployee
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeDate,
category: 'attribute',
label: process.string.CurrentDate,
type: 'context'
},
process.function.CurrentDate
)
defineFunctions(builder)
defineMethods(builder)
defineTriggers(builder)
builder.mixin(process.class.Process, core.class.Class, view.mixin.AttributePresenter, {
presenter: process.component.ProcessPresenter
@@ -989,183 +582,6 @@ export function createModel (builder: Builder): void {
process.section.CardProcesses
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.RunProcess,
objectClass: process.class.Process,
editor: process.component.SubProcessEditor,
presenter: process.component.SubProcessPresenter,
contextClass: process.class.Execution,
requiredParams: ['_id']
},
process.method.RunSubProcess
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.CreateToDo,
editor: process.component.ToDoEditor,
objectClass: process.class.ProcessToDo,
presenter: process.component.ToDoPresenter,
contextClass: process.class.ProcessToDo,
requiredParams: ['state', 'title', 'user'],
defaultParams: {
withRollback: true
}
},
process.method.CreateToDo
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnExecutionStart,
icon: process.icon.Process,
init: true,
requiredParams: []
},
process.trigger.OnExecutionStart
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.UpdateCard,
editor: process.component.UpdateCardEditor,
objectClass: card.class.Card,
contextClass: null,
presenter: process.component.UpdateCardPresenter,
requiredParams: []
},
process.method.UpdateCard
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.CreateCard,
objectClass: card.class.Card,
editor: process.component.CreateCardEditor,
presenter: process.component.CreateCardPresenter,
contextClass: card.class.Card,
requiredParams: ['title', '_class']
},
process.method.CreateCard
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: core.string.AddRelation,
objectClass: core.class.Relation,
editor: process.component.AddRelationEditor,
presenter: process.component.AddRelationPresenter,
contextClass: core.class.Relation,
requiredParams: ['association', 'direction', '_id']
},
process.method.AddRelation
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: card.string.AddTag,
objectClass: card.class.Tag,
editor: process.component.AddTagEditor,
presenter: process.component.AddTagPresenter,
contextClass: card.class.Card,
requiredParams: ['_id']
},
process.method.AddTag
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnToDoDone,
icon: process.icon.ToDo,
editor: process.component.ToDoCloseEditor,
presenter: process.component.ToDoSettingPresenter,
requiredParams: ['_id'],
checkFunction: process.triggerCheck.ToDo,
init: false,
auto: true
},
process.trigger.OnToDoClose
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnToDoCancelled,
icon: process.icon.ToDoRemove,
editor: process.component.ToDoRemoveEditor,
presenter: process.component.ToDoSettingPresenter,
requiredParams: ['_id'],
checkFunction: process.triggerCheck.ToDo,
init: false,
auto: true
},
process.trigger.OnToDoRemove
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnCardUpdate,
icon: process.icon.OnCardUpdate,
editor: process.component.CardUpdateEditor,
presenter: process.component.CardUpdatePresenter,
requiredParams: [],
checkFunction: process.triggerCheck.UpdateCheck,
init: false,
auto: true
},
process.trigger.OnCardUpdate
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnSubProcessesDone,
icon: process.icon.WaitSubprocesses,
checkFunction: process.triggerCheck.SubProcessesDoneCheck,
requiredParams: [],
init: false,
auto: true
},
process.trigger.OnSubProcessesDone
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.WaitUntil,
icon: process.icon.Time,
requiredParams: [],
init: false,
auto: true,
editor: process.component.TimeEditor,
presenter: process.component.TimePresenter,
checkFunction: process.triggerCheck.Time
},
process.trigger.OnTime
)
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
id: 'processes',
label: process.string.Processes,
@@ -1179,50 +595,83 @@ export function createModel (builder: Builder): void {
builder.createDoc(process.class.UpdateCriteriaComponent, core.space.Model, {
category: 'attribute',
editor: process.criteriaEditor.StringCriteria,
of: core.class.TypeString
editor: process.criteriaEditor.BaseCriteria,
of: core.class.TypeString,
props: {
modes: ['Equal', 'StringContains', 'Exists']
}
})
builder.createDoc(process.class.UpdateCriteriaComponent, core.space.Model, {
category: 'attribute',
editor: process.criteriaEditor.StringCriteria,
of: core.class.TypeHyperlink
editor: process.criteriaEditor.BaseCriteria,
of: core.class.TypeHyperlink,
props: {
modes: ['Equal', 'StringContains', 'Exists']
}
})
builder.createDoc(process.class.UpdateCriteriaComponent, core.space.Model, {
category: 'attribute',
editor: process.criteriaEditor.NumberCriteria,
of: core.class.TypeNumber
editor: process.criteriaEditor.BaseCriteria,
of: core.class.TypeNumber,
props: {
modes: ['Equal', 'GT', 'LT', 'Between', 'Exists']
}
})
builder.createDoc(process.class.UpdateCriteriaComponent, core.space.Model, {
category: 'attribute',
editor: process.criteriaEditor.DateCriteria,
of: core.class.TypeDate
editor: process.criteriaEditor.BaseCriteria,
of: core.class.TypeDate,
props: {
modes: ['Equal', 'GT', 'LT', 'Between', 'Exists']
}
})
builder.createDoc(process.class.UpdateCriteriaComponent, core.space.Model, {
category: 'attribute',
editor: process.criteriaEditor.BooleanCriteria,
of: core.class.TypeBoolean
editor: process.criteriaEditor.BaseCriteria,
of: core.class.TypeBoolean,
props: {
modes: ['Equal', 'NotEqual', 'Exists']
}
})
builder.createDoc(process.class.UpdateCriteriaComponent, core.space.Model, {
category: 'array',
editor: process.criteriaEditor.ArrayCriteria,
of: core.class.ArrOf
editor: process.criteriaEditor.BaseCriteria,
of: core.class.ArrOf,
props: {
modes: [
'ArrayAll',
'ArrayAny',
'ArrayNotIncludes',
'ArraySizeEquals',
'ArraySizeGt',
'ArraySizeGte',
'ArraySizeLt',
'ArraySizeLte'
]
}
})
builder.createDoc(process.class.UpdateCriteriaComponent, core.space.Model, {
category: 'attribute',
editor: process.criteriaEditor.EnumCriteria,
of: core.class.EnumOf
editor: process.criteriaEditor.BaseCriteria,
of: core.class.EnumOf,
props: {
modes: ['Equal', 'NotEqual', 'Exists']
}
})
builder.createDoc(process.class.UpdateCriteriaComponent, core.space.Model, {
category: 'object',
editor: process.criteriaEditor.RefCriteria,
of: core.class.RefTo
editor: process.criteriaEditor.BaseCriteria,
of: core.class.RefTo,
props: {
modes: ['Equal', 'NotEqual', 'Exists']
}
})
}
+3 -1
View File
@@ -30,7 +30,8 @@ export default mergeIds(processId, process, {
ProcessMiddleware: '' as Ref<PresentationMiddlewareFactory>
},
ids: {
ProcessSettings: '' as Ref<Doc>
ProcessSettings: '' as Ref<Doc>,
ProcessToDoCreated: '' as Ref<Doc>
},
actionImpl: {
ContinueExecution: '' as ViewAction
@@ -40,6 +41,7 @@ export default mergeIds(processId, process, {
ContinueExecution: '' as Ref<Action<Doc, any>>
},
string: {
NewProcessToDo: '' as IntlString,
ConfigLabel: '' as IntlString,
ConfigDescription: '' as IntlString,
LogAction: '' as IntlString
+112
View File
@@ -0,0 +1,112 @@
// Copyright © 2025 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 core from '@hcengineering/core'
import { type Builder } from '@hcengineering/model'
import process from './plugin'
export function defineTriggers (builder: Builder): void {
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnToDoDone,
icon: process.icon.ToDo,
editor: process.component.ToDoCloseEditor,
presenter: process.component.ToDoSettingPresenter,
requiredParams: ['_id'],
checkFunction: process.triggerCheck.ToDo,
init: false,
auto: true
},
process.trigger.OnToDoClose
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnToDoCancelled,
icon: process.icon.ToDoRemove,
editor: process.component.ToDoRemoveEditor,
presenter: process.component.ToDoSettingPresenter,
requiredParams: ['_id'],
checkFunction: process.triggerCheck.ToDo,
init: false,
auto: true
},
process.trigger.OnToDoRemove
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.WhenCardMatches,
icon: process.icon.WhenCardMatches,
editor: process.component.CardUpdateEditor,
presenter: process.component.CardUpdatePresenter,
requiredParams: [],
checkFunction: process.triggerCheck.MatchCheck,
init: false,
auto: true
},
process.trigger.OnCardUpdate
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.WhenFieldChanges,
icon: process.icon.WhenFieldChanges,
editor: process.component.FieldChangesEditor,
presenter: process.component.CardUpdatePresenter,
requiredParams: [],
checkFunction: process.triggerCheck.FieldChangedCheck,
init: false,
auto: true
},
process.trigger.WhenFieldChanges
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnSubProcessesDone,
icon: process.icon.WaitSubprocesses,
checkFunction: process.triggerCheck.SubProcessesDoneCheck,
requiredParams: [],
init: false,
auto: true
},
process.trigger.OnSubProcessesDone
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.WaitUntil,
icon: process.icon.Time,
requiredParams: [],
init: false,
auto: true,
editor: process.component.TimeEditor,
presenter: process.component.TimePresenter,
checkFunction: process.triggerCheck.Time
},
process.trigger.OnTime
)
}
+23 -17
View File
@@ -37,6 +37,7 @@ import { type KeyBinding, type ViewOptionModel, type ViewOptionsModel } from '@h
import recruit from './plugin'
import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review'
import { defineSpaceType } from './spaceType'
import { definePermissions } from './permissions'
import { TApplicant, TApplicantMatch, TCandidate, TOpinion, TReview, TVacancy, TVacancyList } from './types'
export { recruitId } from '@hcengineering/recruit'
@@ -1019,24 +1020,28 @@ export function createModel (builder: Builder): void {
},
override: [recruit.action.CreateGlobalApplication]
})
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: recruit.component.CreateCandidate,
element: 'top'
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: recruit.component.CreateCandidate,
element: 'top'
},
label: recruit.string.CreateTalent,
icon: recruit.icon.Create,
keyBinding: ['keyC'],
input: 'none',
category: recruit.category.Recruit,
target: core.class.Doc,
context: {
mode: ['workbench', 'browser'],
application: recruit.app.Recruit,
group: 'create'
}
},
label: recruit.string.CreateTalent,
icon: recruit.icon.Create,
keyBinding: ['keyC'],
input: 'none',
category: recruit.category.Recruit,
target: core.class.Doc,
context: {
mode: ['workbench', 'browser'],
application: recruit.app.Recruit,
group: 'create'
}
})
recruit.action.CreateTalent
)
createAction(builder, {
action: view.actionImpl.ShowPopup,
@@ -1596,4 +1601,5 @@ export function createModel (builder: Builder): void {
)
defineSpaceType(builder)
definePermissions(builder)
}
+19
View File
@@ -0,0 +1,19 @@
import type { Builder } from '@hcengineering/model'
import core from '@hcengineering/core'
import recruit from '@hcengineering/recruit'
export function definePermissions (builder: Builder): void {
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: recruit.string.ForbidCreateVacancyPermission,
scope: 'workspace',
txClass: core.class.TxCreateDoc,
objectClass: recruit.class.Vacancy,
forbid: true,
description: recruit.string.ForbidCreateVacancyPermissionDescription
},
recruit.permission.ForbidCreateVacancy
)
}
+30 -1
View File
@@ -61,7 +61,11 @@ export function createModel (builder: Builder): void {
})
builder.mixin(process.trigger.OnCardUpdate, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
serverCheckFunc: serverProcess.func.OnCardUpdateCheck
serverCheckFunc: serverProcess.func.MatchCardCheck
})
builder.mixin(process.trigger.WhenFieldChanges, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
serverCheckFunc: serverProcess.func.FieldChangedCheck
})
builder.mixin(process.trigger.OnTime, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
@@ -173,6 +177,10 @@ export function createModel (builder: Builder): void {
func: serverProcess.transform.Power
})
builder.mixin(process.function.Sqrt, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.Sqrt
})
builder.mixin(process.function.Round, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.Round
})
@@ -229,6 +237,27 @@ export function createModel (builder: Builder): void {
func: serverProcess.transform.RemoveLast
})
builder.mixin(process.function.EmptyArray, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.EmptyArray
})
builder.mixin(
process.function.ExecutionEmployeeInitiator,
process.class.ProcessFunction,
serverProcess.mixin.FuncImpl,
{
func: serverProcess.transform.ExecutionInitiator
}
)
builder.mixin(process.function.ExecutionInitiator, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.ExecutionInitiator
})
builder.mixin(process.function.ExecutionStarted, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.ExecutionStarted
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverProcess.trigger.OnExecutionContinue,
txMatch: {
+2 -2
View File
@@ -52,7 +52,7 @@ import document from '@hcengineering/model-document'
import tracker from '@hcengineering/model-tracker'
import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification'
import notification, { type NotificationGroup } from '@hcengineering/notification'
import recruit from '@hcengineering/recruit'
import tags from '@hcengineering/tags'
import {
@@ -372,7 +372,7 @@ export function createModel (builder: Builder): void {
generated: false,
allowedForAuthor: true,
label: time.string.NewToDo,
group: time.ids.TimeNotificationGroup,
group: time.ids.TimeNotificationGroup as Ref<NotificationGroup>,
txClasses: [core.class.TxCreateDoc],
objectClass: time.class.ProjectToDo,
onlyOwn: true,
+1 -2
View File
@@ -15,7 +15,7 @@
import { type Client, type Doc, type Ref } from '@hcengineering/core'
import { type Application } from '@hcengineering/model-workbench'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import { type NotificationType } from '@hcengineering/notification'
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
import { timeId } from '@hcengineering/time'
import time from '@hcengineering/time-resources/src/plugin'
@@ -56,7 +56,6 @@ export default mergeIds(timeId, time, {
Team: '' as Ref<Application>
},
ids: {
TimeNotificationGroup: '' as Ref<NotificationGroup>,
ToDoCreated: '' as Ref<NotificationType>
},
function: {
+2
View File
@@ -32,6 +32,7 @@ import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import { createActions as defineActions } from './actions'
import tracker from './plugin'
import { definePresenters } from './presenters'
import { definePermissions } from './permissions'
import {
DOMAIN_TRACKER,
TClassicProjectTypeData,
@@ -709,6 +710,7 @@ export function createModel (builder: Builder): void {
]
})
definePermissions(builder)
defineSpaceType(builder)
}
+19
View File
@@ -0,0 +1,19 @@
import type { Builder } from '@hcengineering/model'
import core from '@hcengineering/core'
import tracker from '@hcengineering/tracker'
export function definePermissions (builder: Builder): void {
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: tracker.string.ForbidCreateProjectPermission,
txClass: core.class.TxCreateDoc,
objectClass: tracker.class.Project,
forbid: true,
scope: 'workspace',
description: tracker.string.ForbidCreateProjectPermissionDescription
},
tracker.permission.ForbidCreateProject
)
}
+8
View File
@@ -354,18 +354,22 @@ function defineTraining (builder: Builder): void {
definePermission(builder, training.permission.ChangeSomeoneElsesTrainingOwner, {
label: training.string.Permission_ChangeSomeoneElsesTrainingOwner,
scope: 'space',
description: training.string.Permission_ChangeSomeoneElsesTrainingOwner_Description
})
definePermission(builder, training.permission.CreateTraining, {
label: training.string.Permission_CreateTraining,
scope: 'space',
description: training.string.Permission_CreateTraining_Description
})
definePermission(builder, training.permission.ViewSomeoneElsesTrainingOverview, {
label: training.string.Permission_ViewSomeoneElsesTrainingOverview,
scope: 'space',
description: training.string.Permission_ViewSomeoneElsesTrainingOverview_Description
})
definePermission(builder, training.permission.ViewSomeoneElsesTrainingQuestions, {
label: training.string.Permission_ViewSomeoneElsesTrainingQuestions,
scope: 'space',
description: training.string.Permission_ViewSomeoneElsesTrainingQuestions_Description
})
}
@@ -491,14 +495,17 @@ function defineTrainingRequest (builder: Builder): void {
definePermission(builder, training.permission.ChangeSomeoneElsesSentRequestOwner, {
label: training.string.Permission_ChangeSomeoneElsesSentRequestOwner,
scope: 'space',
description: training.string.Permission_ChangeSomeoneElsesSentRequestOwner_Description
})
definePermission(builder, training.permission.CreateRequestOnSomeoneElsesTraining, {
label: training.string.Permission_CreateRequestOnSomeoneElsesTraining,
scope: 'space',
description: training.string.Permission_CreateRequestOnSomeoneElsesTraining_Description
})
definePermission(builder, training.permission.ViewSomeoneElsesSentRequest, {
label: training.string.Permission_ViewSomeoneElsesSentRequest,
scope: 'space',
description: training.string.Permission_ViewSomeoneElsesSentRequest_Description
})
})()
@@ -753,6 +760,7 @@ function defineTrainingAttempt (builder: Builder): void {
definePermission(builder, training.permission.ViewSomeoneElsesTraineesResults, {
label: training.string.Permission_ViewSomeoneElsesTraineesResults,
scope: 'space',
description: training.string.Permission_ViewSomeoneElsesTraineesResults_Description
})
}
+20 -1
View File
@@ -244,7 +244,7 @@ export class TObjectValidator extends TClass implements ObjectValidator {
@Mixin(view.mixin.ObjectFactory, core.class.Class)
export class TObjectFactory extends TClass implements ObjectFactory {
component?: AnyComponent
create?: Resource<() => Promise<void>>
create?: Resource<(props?: Record<string, any>) => Promise<Ref<Doc> | undefined>>
}
@Mixin(view.mixin.ObjectTitle, core.class.Class)
@@ -835,6 +835,25 @@ export function createModel (builder: Builder): void {
view.action.Join
)
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: view.component.AddRelationPopup,
fillProps: {
_objects: 'value'
}
},
label: core.string.AddRelation,
input: 'any',
icon: view.icon.CopyLink,
category: view.category.Editor,
target: core.class.Doc,
context: {
mode: ['context', 'browser'],
group: 'associate'
}
})
createAction(
builder,
{
+2 -1
View File
@@ -96,7 +96,8 @@ export default mergeIds(viewId, view, {
BaseDocPresenter: '' as AnyComponent,
MasterDetailView: '' as AnyComponent,
AssociationPresenter: '' as AnyComponent,
TreeView: '' as AnyComponent
TreeView: '' as AnyComponent,
AddRelationPopup: '' as AnyComponent
},
string: {
Table: '' as IntlString,
+16
View File
@@ -192,6 +192,22 @@ export function createModel (builder: Builder): void {
group: 'edit'
}
})
createAction(
builder,
{
action: workbench.actionImpl.CloseCurrentTab,
label: presentation.string.Close,
icon: view.icon.Delete,
keyBinding: ['Meta + keyW', 'Ctrl + keyW'],
input: 'none',
category: workbench.category.Workbench,
allowedForEditableContent: 'always',
target: core.class.Doc,
context: { mode: ['workbench', 'browser', 'panel', 'editor', 'input'] }
},
workbench.action.CloseCurrentTab
)
}
export default workbench
+2 -1
View File
@@ -40,6 +40,7 @@ export default mergeIds(workbenchId, workbench, {
actionImpl: {
PinTab: '' as Resource<(doc?: Doc | Doc[]) => Promise<void>>,
UnpinTab: '' as Resource<(doc?: Doc | Doc[]) => Promise<void>>,
CloseTab: '' as Resource<(doc?: Doc | Doc[]) => Promise<void>>
CloseTab: '' as Resource<(doc?: Doc | Doc[]) => Promise<void>>,
CloseCurrentTab: '' as Resource<(doc?: Doc | Doc[]) => Promise<void>>
}
})
+10
View File
@@ -220,6 +220,7 @@ export interface AccountClient {
getIntegrationSecret: (integrationSecretKey: IntegrationSecretKey) => Promise<IntegrationSecret | null>
listIntegrationsSecrets: (filter: Partial<IntegrationSecretKey>) => Promise<IntegrationSecret[]>
getAccountInfo: (uuid: AccountUuid) => Promise<AccountInfo>
canMergeSpecifiedPersons: (primaryPerson: PersonUuid, secondaryPerson: PersonUuid) => Promise<boolean>
mergeSpecifiedPersons: (primaryPerson: PersonUuid, secondaryPerson: PersonUuid) => Promise<void>
mergeSpecifiedAccounts: (primaryAccount: AccountUuid, secondaryAccount: AccountUuid) => Promise<void>
addEmailSocialId: (email: string) => Promise<OtpInfo>
@@ -1072,6 +1073,15 @@ class AccountClientImpl implements AccountClient {
return await this.rpc(request)
}
async canMergeSpecifiedPersons (primaryPerson: PersonUuid, secondaryPerson: PersonUuid): Promise<boolean> {
const request = {
method: 'canMergeSpecifiedPersons' as const,
params: { primaryPerson, secondaryPerson }
}
return await this.rpc(request)
}
async mergeSpecifiedPersons (primaryPerson: PersonUuid, secondaryPerson: PersonUuid): Promise<void> {
const request = {
method: 'mergeSpecifiedPersons' as const,
+1
View File
@@ -40,6 +40,7 @@ export interface LoginInfoWorkspace {
endpoint: EndpointInfo
role: AccountRole | null
progress?: number
branding?: string
}
export interface LoginInfoWithWorkspaces extends LoginInfo {
+1 -1
View File
@@ -82,7 +82,7 @@ class RestTxClient implements Client {
params: DomainParams,
options?: DomainRequestOptions
): Promise<DomainResult<T>> {
throw new Error('Domain request operation not supported')
return await this.client.domainRequest(domain, params, options)
}
async findOne<T extends Doc>(
+1 -1
View File
@@ -69,4 +69,4 @@
"PersonId": "Osoba",
"AccountId": "Účet"
}
}
}
+1 -1
View File
@@ -69,4 +69,4 @@
"PersonId": "Id de personne",
"AccountId": "Compte"
}
}
}
+72
View File
@@ -0,0 +1,72 @@
{
"string": {
"Id": "Id",
"Space": "Alan",
"Spaces": "Alanlar",
"SpacesDescription": "Tüm alanların alan tipini yönet",
"TypedSpace": "Tipli alan",
"SpaceType": "Alan tipi",
"Modified": "Değiştirildi",
"ModifiedDate": "Değiştirilme tarihi",
"ModifiedBy": "Değiştiren",
"Class": "Sınıf",
"AttachedTo": "Eklenen",
"AttachedToClass": "Eklenen sınıf",
"Name": "İsim",
"Description": "Açıklama",
"ShortDescription": "Kısa açıklama",
"Descriptor": "Tanımlayıcı",
"TargetClass": "Hedef sınıf",
"Role": "Rol",
"Roles": "Roller",
"Private": "Özel",
"Archived": "Arşivlendi",
"ClassLabel": "Tip",
"ClassPropertyLabel": "Etiket",
"String": "Metin",
"Markup": "İşaretleme",
"Number": "Sayı",
"Boolean": "Onay kutusu",
"Timestamp": "Zaman damgası",
"Date": "Tarih",
"IntlString": "IntlString",
"Ref": "Referans",
"Collection": "Koleksiyon",
"Array": "Çoklu Seçim",
"Enum": "Seçim",
"Members": "Üyeler",
"Hyperlink": "URL",
"MarkupBlobRef": "İşbirlikçi",
"Object": "Nesne",
"System": "Sistem",
"CreatedBy": "Oluşturan",
"CreatedDate": "Oluşturulma tarihi",
"Status": "Durum",
"StatusCategory": "Durum kategorisi",
"Account": "Hesap",
"Rank": "Sıra",
"Owners": "Sahipler",
"Permission": "İzin",
"CreateObject": "Nesne oluştur",
"UpdateObject": "Nesneyi güncelle",
"DeleteObject": "Nesneyi sil",
"ForbidDeleteObject": "Nesne silmeyi yasakla",
"UpdateSpace": "Alanı güncelle",
"ArchiveSpace": "Alanı arşivle",
"CreateObjectDescription": "Kullanıcılara alanda nesne oluşturma yetkisi verir",
"UpdateObjectDescription": "Kullanıcılara alandaki nesneleri güncelleme yetkisi verir",
"DeleteObjectDescription": "Kullanıcılara alandaki nesneleri silme yetkisi verir",
"ForbidDeleteObjectDescription": "Kullanıcıların alandaki nesneleri silmesini yasaklar",
"UpdateSpaceDescription": "Kullanıcılara alanı güncelleme yetkisi verir",
"ArchiveSpaceDescription": "Kullanıcılara alanı arşivleme yetkisi verir",
"AutoJoin": "Otomatik katıl",
"AutoJoinDescr": "Yeni çalışanları bu alana otomatik olarak ekle",
"BlobSize": "Boyut",
"BlobContentType": "İçerik türü",
"Relation": "İlişki",
"Relations": "İlişkiler",
"AddRelation": "İlişki ekle",
"PersonId": "Kişi",
"AccountId": "Hesap"
}
}
+14
View File
@@ -17,6 +17,7 @@
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import type { DocumentQuery } from './storage'
import { type WorkspaceDataId, type WorkspaceUuid } from './utils'
import { Tx } from '.'
/**
* @public
@@ -333,6 +334,15 @@ export interface TypeDate extends Type<Date> {
withShift: boolean
}
/**
* @public
*/
export interface TypeNumber extends Type<number> {
min?: number
max?: number
digits?: number // Number of digits after comma
}
/**
* @public
*/
@@ -520,6 +530,10 @@ export type RolesAssignment = Record<Ref<Role>, AccountUuid[] | undefined>
*/
export interface Permission extends Doc {
label: IntlString
txClass?: Ref<Class<Tx>>
forbid?: boolean
objectClass?: Ref<Class<Doc>>
scope?: 'space' | 'workspace'
description?: IntlString
icon?: Asset
}
+1 -1
View File
@@ -33,7 +33,7 @@ class Client implements HulylakeClient {
}
private objectUrl (key: string): string {
return `${this.baseUrl}/api/${this.workspace}/${key}`
return `${this.baseUrl}/api/${this.workspace}/${encodeURIComponent(key)}`
}
private authHeaders (init?: HeadersInit): Headers {
-7
View File
@@ -26,10 +26,3 @@ export class HulylakeError extends Error {
this.name = 'HulylakeError'
}
}
export class NotFoundError extends Error {
constructor (message: string) {
super(message)
this.name = 'NotFoundError'
}
}
+1
View File
@@ -69,6 +69,7 @@ export type JsonPatch =
| { op: 'test', path: string, value: any }
| { hop: 'add', path: string, value: any, safe?: boolean }
| { hop: 'inc', path: string, value: number, safe?: boolean }
| { hop: 'remove', path: string, safe?: boolean }
export interface HulyResponse<Body = ArrayBuffer | string | any> {
ok: boolean
+2 -2
View File
@@ -14,7 +14,7 @@
//
import { RetryOptions, withRetry } from '@hcengineering/retry'
import { HulylakeError, NetworkError, NotFoundError } from './error'
import { HulylakeError, NetworkError } from './error'
async function innerFetchSafe (url: string | URL, init?: RequestInit): Promise<Response> {
let response
@@ -31,7 +31,7 @@ async function innerFetchSafe (url: string | URL, init?: RequestInit): Promise<R
const text = await response.text()
if (response.status === 404) {
throw new NotFoundError(text)
return response
} else {
throw new HulylakeError(text)
}
+3 -2
View File
@@ -50,6 +50,7 @@ import core, {
type ArrOf as TypeArrOf,
type Collection as TypeCollection,
type TypeDate as TypeDateType,
type TypeNumber as TypeNumberType,
generateId,
type AccountUuid
} from '@hcengineering/core'
@@ -413,8 +414,8 @@ export function TypeHyperlink (): Type<Hyperlink> {
/**
* @public
*/
export function TypeNumber (): Type<number> {
return { _class: core.class.TypeNumber, label: core.string.Number, icon: core.icon.TypeNumber }
export function TypeNumber (min?: number, max?: number, digits?: number): TypeNumberType {
return { _class: core.class.TypeNumber, label: core.string.Number, icon: core.icon.TypeNumber, min, max, digits }
}
/**
+29
View File
@@ -0,0 +1,29 @@
{
"status": {
"LoadingPlugin": "{plugin} eklentisi yükleniyor...",
"UnknownError": "Bilinmeyen hata: {message}",
"InvalidId": "Geçersiz Id: {id}",
"BadRequest": "Hatalı istek",
"Forbidden": "Yasak",
"Conflict": "Çakışma",
"ExpiredLink": "Bu davet bağlantısının süresi dolmuş",
"Unauthorized": "Yetkisiz",
"UnknownMethod": "Bilinmeyen metod: {method}",
"InternalServerError": "Dahili sunucu hatası",
"MaintenanceWarning": "Bakım Planlandı:",
"MaintenanceWarningTime": "{time, plural, =1 {bir dakikadan az} other {# dakika}}",
"AccountNotFound": "Hesap bulunamadı veya girilen kimlik bilgileri yanlış",
"AccountNotConfirmed": "Hesap doğrulanmadı",
"WorkspaceNotFound": "Çalışma alanı bulunamadı",
"AccountAlreadyExists": "Hesap zaten mevcut",
"WorkspaceRateLimit": "Sunucu meşgul, Lütfen biraz bekleyin ve tekrar deneyin",
"AccountAlreadyConfirmed": "Hesap zaten doğrulandı",
"WorkspaceAlreadyExists": "Çalışma alanı zaten mevcut",
"InvalidOtp": "Geçersiz kod",
"InviteNotFound": "E-posta ile davet bulunamadı: {email}",
"WorkspaceLimitReached": "Çalışma alanı limitine ulaştınız. Lütfen bizimle iletişime geçin...",
"ReadOnlyAccount": "Anonim Misafir Demo",
"SystemAccount": "Sistem hesabı",
"SocialIdAlreadyExists": "Sosyal ID zaten mevcut"
}
}
+61
View File
@@ -0,0 +1,61 @@
{
"string": {
"Create": "Oluştur",
"Cancel": "İptal",
"Ok": "Tamam",
"Save": "Kaydet",
"Saved": "Kaydedildi...",
"Download": "İndir",
"DownloadOriginal": "Orijinali indir",
"Delete": "Sil",
"Close": "Kapat",
"NotSelected": "Seçilmedi",
"Deselect": "Seçimi kaldır",
"Archived": "(arşivlendi)",
"AddSocialLinks": "Sosyal bağlantılar ekle",
"EditSocialLinks": "Sosyal bağlantıları düzenle",
"Change": "Değiştir",
"Remove": "Kaldır",
"Search": "Ara...",
"Spaces": "Alanlar",
"NumberSpaces": "{count, plural, =0 {İçinde} =1 {1 yerde} other {# yerde}}",
"InThis": "Bu {space} içinde",
"NoMatchesInThis": "Bu {space} içinde eşleşme yok",
"NoMatchesFound": "Eşleşme bulunamadı",
"NotInThis": "Bu {space} içinde değil",
"Match": "Eşleşme",
"Add": "Ekle",
"Edit": "Düzenle",
"DocumentPreview": "Önizleme",
"MakePrivate": "Özel yap",
"MakePrivateDescription": "Sadece üyeler görebilir",
"Created": "Oluşturuldu",
"Selected": "Seçildi",
"NoResults": "Gösterilecek sonuç yok",
"Next": "İleri",
"FailedToPreview": "Önizleme başarısız",
"ContentType": "İçerik türü",
"ContentTypeNotSupported": "Bu içerik türü için önizleme mevcut değil",
"StartDrawing": "Üzerine çiz",
"DrawingHistory": "Çizim geçmişi",
"ColorAdd": "Renk önayarı ekle",
"ColorRemove": "Renk önayarını kaldır",
"ColorReset": "Renk önayarlarını sıfırla",
"Copy": "Kopyala",
"DocumentUrlCopied": "Kopyalandı!",
"CopyLink": "Bağlantıyı kopyala",
"AccessDenied": "Nesne mevcut değil veya erişim izniniz yok.",
"UnableToFollowMention": "Bahsedilen nesne açılamıyor",
"Undo": "Geri al",
"Redo": "Yinele",
"ClearCanvas": "Tuvali temizle",
"PenTool": "Kalem aracı",
"EraserTool": "Silgi aracı",
"PanTool": "Kaydırma aracı",
"TextTool": "Metin aracı",
"PaletteManagementMenu": "Renk önayarlarını yönet"
},
"status": {
"FileTooLarge": "Dosya çok büyük"
}
}
+2 -1
View File
@@ -61,11 +61,12 @@
"@hcengineering/emoji": "^0.6.0",
"@hcengineering/theme": "^0.6.5",
"@hcengineering/retry": "^0.6.0",
"@hcengineering/hulylake-client": "^0.6.0",
"fast-equals": "^5.2.2",
"png-chunks-extract": "^1.0.0",
"svelte": "^4.2.20",
"uuid": "^8.3.2",
"yjs": "^13.6.23"
"yjs": "^13.6.27"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
+57 -34
View File
@@ -33,26 +33,24 @@ import {
type UpdateAttachmentsOperation,
type UpdateNotificationContextEvent,
type UpdateNotificationEvent,
type UpdateNotificationQuery,
type NotificationQuery,
type UpdatePatchEvent
} from '@hcengineering/communication-sdk-types'
import {
type AccountID,
type AccountUuid,
type CardID,
type CardType,
type Collaborator,
type ContextID,
type FindCollaboratorsParams,
type FindLabelsParams,
type FindMessagesGroupsParams,
type FindMessagesParams,
type FindNotificationContextParams,
type FindNotificationsParams,
type FindMessagesMetaParams,
type Label,
type Markdown,
type Message,
type MessageID,
type MessagesGroup,
MessageType,
type Notification,
type NotificationContext,
@@ -61,7 +59,12 @@ import {
type AttachmentData,
type AttachmentParams,
type AttachmentUpdateData,
type WithTotal
type WithTotal,
type NotificationID,
type Emoji,
type MessageMeta,
type FindMessagesGroupParams,
type MessagesGroup
} from '@hcengineering/communication-types'
import core, {
generateId,
@@ -71,17 +74,19 @@ import core, {
SocialIdType,
type Tx,
type TxDomainEvent,
AccountRole
AccountRole,
generateUuid
} from '@hcengineering/core'
import { onDestroy } from 'svelte'
import { addNotification, NotificationSeverity, languageStore } from '@hcengineering/ui'
import { translate } from '@hcengineering/platform'
import { getMetadata, translate } from '@hcengineering/platform'
import view from '@hcengineering/view'
import { v4 as uuid } from 'uuid'
import { getCurrentWorkspaceUuid, getFilesUrl } from './file'
import { addTxListener, removeTxListener, type TxListener } from './utils'
import { get } from 'svelte/store'
import { getClient as getHulylakeClient } from '@hcengineering/hulylake-client'
import { getCurrentWorkspaceUuid } from './file'
import { addTxListener, removeTxListener, type TxListener } from './utils'
import presentation from './plugin'
export {
createCollaboratorsQuery,
@@ -107,7 +112,12 @@ export async function setCommunicationClient (platformClient: PlatformClient): P
client.close()
}
const _client = new Client(platformClient)
initLiveQueries(_client, getCurrentWorkspaceUuid(), getFilesUrl(), onDestroy)
const token = getMetadata(presentation.metadata.Token) ?? ''
const hulylakeUrl = getMetadata(presentation.metadata.HulylakeUrl) ?? ''
const hulylake = getHulylakeClient(hulylakeUrl, getCurrentWorkspaceUuid(), token)
initLiveQueries(_client, hulylake, onDestroy)
client = _client
onClientListeners.forEach((fn) => {
fn()
@@ -161,7 +171,7 @@ class Client {
async createMessage (cardId: CardID, cardType: CardType, content: Markdown): Promise<CreateMessageResult> {
const event: CreateMessageEvent = {
type: MessageEventType.CreateMessage,
messageType: MessageType.Message,
messageType: MessageType.Text,
cardId,
cardType,
content,
@@ -198,28 +208,28 @@ class Client {
await this.sendEvent(event)
}
async addReaction (cardId: CardID, messageId: MessageID, reaction: string): Promise<void> {
async addReaction (cardId: CardID, messageId: MessageID, emoji: Emoji): Promise<void> {
const event: ReactionPatchEvent = {
type: MessageEventType.ReactionPatch,
cardId,
messageId,
operation: {
opcode: 'add',
reaction
reaction: emoji
},
socialId: this.getSocialId()
}
await this.sendEvent(event)
}
async removeReaction (cardId: CardID, messageId: MessageID, reaction: string): Promise<void> {
async removeReaction (cardId: CardID, messageId: MessageID, emoji: Emoji): Promise<void> {
const event: ReactionPatchEvent = {
type: MessageEventType.ReactionPatch,
cardId,
messageId,
operation: {
opcode: 'remove',
reaction
reaction: emoji
},
socialId: this.getSocialId()
}
@@ -245,7 +255,7 @@ class Client {
opcode: 'add',
attachments: ops.add.map((it) => ({
...it,
id: it.id ?? (uuid() as AttachmentID)
id: it.id ?? (generateUuid() as AttachmentID)
}))
})
}
@@ -262,7 +272,7 @@ class Client {
opcode: 'set',
attachments: ops.set.map((it) => ({
...it,
id: it.id ?? (uuid() as AttachmentID)
id: it.id ?? (generateUuid() as AttachmentID)
}))
})
}
@@ -286,7 +296,7 @@ class Client {
await this.sendEvent(event)
}
async addCollaborators (cardId: CardID, cardType: CardType, collaborators: AccountID[]): Promise<void> {
async addCollaborators (cardId: CardID, cardType: CardType, collaborators: AccountUuid[]): Promise<void> {
const event: AddCollaboratorsEvent = {
type: NotificationEventType.AddCollaborators,
cardId,
@@ -297,7 +307,7 @@ class Client {
await this.sendEvent(event)
}
async removeCollaborators (cardId: CardID, cardType: CardType, collaborators: AccountID[]): Promise<void> {
async removeCollaborators (cardId: CardID, cardType: CardType, collaborators: AccountUuid[]): Promise<void> {
const event: RemoveCollaboratorsEvent = {
type: NotificationEventType.RemoveCollaborators,
cardId,
@@ -329,7 +339,11 @@ class Client {
await this.sendEvent(event)
}
async updateNotifications (contextId: ContextID, query: UpdateNotificationQuery, read: boolean): Promise<void> {
async updateNotifications (
contextId: ContextID,
query: Pick<NotificationQuery, 'type' | 'untilDate'> & { id?: NotificationID },
read: boolean
): Promise<void> {
const event: UpdateNotificationEvent = {
type: NotificationEventType.UpdateNotification,
contextId,
@@ -342,15 +356,15 @@ class Client {
await this.sendEvent(event)
}
async findMessages (params: FindMessagesParams, queryId?: number): Promise<Message[]> {
async findMessagesMeta (params: FindMessagesMetaParams): Promise<MessageMeta[]> {
return (
await this.connection.domainRequest<Message[]>(COMMUNICATION, {
findMessages: { params, queryId }
await this.connection.domainRequest<MessageMeta[]>(COMMUNICATION, {
findMessagesMeta: { params }
})
).value
}
async findMessagesGroups (params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
async findMessagesGroups (params: FindMessagesGroupParams): Promise<MessagesGroup[]> {
return (
await this.connection.domainRequest<MessagesGroup[]>(COMMUNICATION, {
findMessagesGroups: { params }
@@ -360,19 +374,22 @@ class Client {
async findNotificationContexts (
params: FindNotificationContextParams,
queryId?: number
subscription?: number | string
): Promise<NotificationContext[]> {
return (
await this.connection.domainRequest<NotificationContext[]>(COMMUNICATION, {
findNotificationContexts: { params, queryId }
findNotificationContexts: { params, subscription }
})
).value
}
async findNotifications (params: FindNotificationsParams, queryId?: number): Promise<WithTotal<Notification>> {
async findNotifications (
params: FindNotificationsParams,
subscription?: number | string
): Promise<WithTotal<Notification>> {
return (
await this.connection.domainRequest<WithTotal<Notification>>(COMMUNICATION, {
findNotifications: { params, queryId }
findNotifications: { params, subscription }
})
).value
}
@@ -393,9 +410,15 @@ class Client {
).value
}
async unsubscribeQuery (id: number): Promise<void> {
async subscribeCard (cardId: CardID, subscription: string | number): Promise<void> {
await this.connection.domainRequest<Message[]>(COMMUNICATION, {
unsubscribeQuery: id
subscribeCard: { cardId, subscription }
})
}
async unsubscribeCard (cardId: CardID, subscription: string | number): Promise<void> {
await this.connection.domainRequest<Message[]>(COMMUNICATION, {
unsubscribeCard: { cardId, subscription }
})
}
@@ -438,7 +461,7 @@ class Client {
return id
}
private getAccount (): AccountID {
private getAccount (): AccountUuid {
return getCurrentAccount().uuid
}
}
@@ -79,14 +79,11 @@
findObjectPresenter(_class)
}
const checkSelected = (item?: Doc): void => {
if (item === undefined) {
return
}
if (selectedElements.has(item._id)) {
selectedElements.delete(item._id)
const checkSelected = (_id: Ref<Doc>): void => {
if (selectedElements.has(_id)) {
selectedElements.delete(_id)
} else {
selectedElements.add(item._id)
selectedElements.add(_id)
}
selectedObjects = Array.from(selectedElements)
@@ -97,12 +94,16 @@
let selection = 0
let list: ListView
async function handleSelection (evt: Event | undefined, objects: Doc[], selection: number): Promise<void> {
function handleSelection (evt: Event | undefined, objects: Doc[], selection: number): void {
const item = objects[selection]
if (item === undefined) {
return
}
select(item)
}
function select (item: Doc): void {
if (!multiSelect) {
if (allowDeselect) {
selected = item._id === selected ? undefined : item._id
@@ -111,7 +112,7 @@
}
dispatch(closeAfterSelect ? 'close' : 'update', selected !== undefined ? item : undefined)
} else {
checkSelected(item)
checkSelected(item._id)
}
}
@@ -129,27 +130,35 @@
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
void handleSelection(key, objects, selection)
handleSelection(key, objects, selection)
}
}
const manager = createFocusManager()
function onCreate (): void {
async function onCreate (): Promise<void> {
if (create === undefined) {
return
}
const c = create
showPopup(c.component, c.props ?? {}, 'top', async (res) => {
const handler = async (res: Ref<Doc> | undefined) => {
if (res != null) {
// We expect reference to new object.
const newPerson = await getClient().findOne(_class, { _id: res })
if (newPerson !== undefined) {
search = c.update?.(newPerson) ?? ''
dispatch('created', newPerson)
dispatch('search', search)
const newObject = await getClient().findOne(_class, { _id: res })
if (newObject !== undefined) {
dispatch('created', newObject)
select(newObject)
}
}
})
}
const c = create
if (c.component !== undefined) {
showPopup(c.component, c.props ?? {}, 'top', handler)
} else if (c.func !== undefined) {
const impl = await getResource(c.func)
if (impl !== undefined) {
const newObjectId = await impl(c.props)
await handler(newObjectId)
}
}
}
function toAny (obj: any): any {
return obj
@@ -248,7 +257,7 @@
class="menu-item withList w-full flex-row-center"
disabled={readonly || isDeselectDisabled || loading}
on:click={() => {
void handleSelection(undefined, objects, item)
handleSelection(undefined, objects, item)
}}
>
{#if type === 'text'}
+2 -1
View File
@@ -180,7 +180,8 @@ export default plugin(presentationId, {
StatsUrl: '' as Metadata<string>,
MailUrl: '' as Metadata<string>,
PreviewUrl: '' as Metadata<string>,
PulseUrl: '' as Metadata<string>
PulseUrl: '' as Metadata<string>,
HulylakeUrl: '' as Metadata<string>
},
status: {
FileTooLarge: '' as StatusCode
+2 -1
View File
@@ -35,10 +35,11 @@ export interface ObjectSearchResult {
* @public
*/
export interface ObjectCreate {
component: AnyComponent
component: AnyComponent | undefined
props?: Record<string, any>
label: IntlString
update?: (doc: Doc) => string
func?: Resource<(props?: Record<string, any>) => Promise<Ref<Doc> | undefined>>
}
/**
+2 -1
View File
@@ -40,7 +40,8 @@
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
"fast-equals": "^5.2.2"
"fast-equals": "^5.2.2",
"hash-it": "^6.0.0"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
@@ -0,0 +1,49 @@
import { hashAttrs, stripHash } from '../utils'
describe('hashAttrs', () => {
it('should return a hash of length 8', () => {
const attrs = { a: 1 }
const hash = hashAttrs(attrs)
expect(hash.length).toEqual(8)
})
it('should return the same hash for the same attrs', () => {
const attrs = { a: 1, b: 2 }
const hash1 = hashAttrs(attrs)
const hash2 = hashAttrs(attrs)
expect(hash1).toEqual(hash2)
})
it('should return different hashes for different attrs', () => {
const attrs1 = { a: 1, b: 2 }
const attrs2 = { a: 1, b: 3 }
const hash1 = hashAttrs(attrs1)
const hash2 = hashAttrs(attrs2)
expect(hash1).not.toEqual(hash2)
})
})
describe('stripHash', () => {
it('should return the name without the hash', () => {
const name = 'bold--c0decafe'
const result = stripHash(name)
expect(result).toEqual('bold')
})
it('should return the original name if no hash is present', () => {
const name = 'bold'
const result = stripHash(name)
expect(result).toEqual(name)
})
it('should return the original name if the hash is not 8 characters long', () => {
const name = 'bold--1234567'
const result = stripHash(name)
expect(result).toEqual(name)
})
it('should return the original name if the hash is not a valid base64 string', () => {
const name = 'bold--invalid!'
const result = stripHash(name)
expect(result).toEqual(name)
})
})
+23 -3
View File
@@ -16,9 +16,11 @@
import { Markup } from '@hcengineering/core'
import { deepEqual } from 'fast-equals'
import hashIt from 'hash-it'
import { nodeDoc, nodeParagraph, nodeText } from './dsl'
import { MarkupMark, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model'
import { traverseNode } from './traverse'
import { MarkupMark, MarkupMarkType, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model'
import { traverseAllMarks, traverseNode } from './traverse'
/** @public */
export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode())
@@ -124,7 +126,11 @@ export function markupToJSON (markup: Markup): MarkupNode {
// But there seem to be some cases when it contains HTML or plain text
// So we need to handle those cases and produce valid MarkupNode
if (markup.startsWith('{')) {
return JSON.parse(markup) as MarkupNode
const json = JSON.parse(markup) as MarkupNode
traverseAllMarks(json, (node, mark) => {
mark.type = stripHash(mark.type) as MarkupMarkType
})
return json
} else {
return nodeDoc(nodeParagraph(nodeText(markup)))
}
@@ -207,3 +213,17 @@ export function markupToText (markup: Markup): string {
return fragments.join('').trim()
}
// see https://github.com/yjs/y-prosemirror/blob/master/src/lib.js#L402
const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/
/** @public */
export function stripHash (attrName: string): string {
return hashedMarkNameRegex.exec(attrName)?.[1] ?? attrName
}
/** @public */
export function hashAttrs (attrs: any): string {
const hash = hashIt(attrs)
return (hash >>> 0).toString(16).padStart(8, '0')
}
+3 -2
View File
@@ -37,12 +37,13 @@
"@types/jest": "^29.5.5",
"jest-environment-jsdom": "^29.7.0",
"fast-equals": "^5.2.2",
"y-prosemirror": "^1.3.6"
"y-prosemirror": "^1.3.7"
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
"@hcengineering/text": "^0.6.5",
"yjs": "^13.6.23"
"@hcengineering/text-core": "^0.6.0",
"yjs": "^13.6.27"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
+170 -48
View File
@@ -25,6 +25,7 @@ declare global {
namespace jest {
interface Matchers<R> {
toEqualMarkup: (expected: string) => R
toEqualYdoc: (expected: YDoc) => R
}
}
}
@@ -35,8 +36,22 @@ expect.extend({
return {
message: () =>
pass
? `Expected markup strings NOT to be equal:\n Received: ${received}\n Expected: ${expected}`
: `Expected markup strings to be equal:\n Received: ${received}\n Expected: ${expected}`,
? `Expected markup strings NOT to be equal:\n Expected: ${expected}\n Received: ${received}`
: `Expected markup strings to be equal:\n Expected: ${expected}\n Received: ${received}`,
pass
}
},
toEqualYdoc (received: YDoc, expected: YDoc) {
const expectedJSON = expected.toJSON()
const receivedJSON = received.toJSON()
const pass = deepEqual(expectedJSON, receivedJSON)
return {
message: () =>
pass
? `Expected yjs documents NOT to be equal:\n Expected: ${JSON.stringify(expectedJSON)}\n Received: ${JSON.stringify(receivedJSON)}`
: `Expected yjs documents to be equal:\n Expected: ${JSON.stringify(expectedJSON)}\n Received: ${JSON.stringify(receivedJSON)}`,
pass
}
}
@@ -54,52 +69,157 @@ function referenceYDocToMarkup (ydoc: YDoc, field: string): Markup {
return jsonToMarkup(json as MarkupNode)
}
const markups: Array<[string, Markup]> = [
['text', '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello world"}]}]}'],
[
'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":{}}]}]}]}'
],
[
'image',
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"image","attrs":{"src":"http://example.com/image.jpg","alt":"image"}}]}]}'
],
[
'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"}]}]}]}]}]}'
const markups: Array<{ name: string, markup: Markup, skipYdocCompare?: boolean }> = [
{
name: 'text',
markup: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello world"}]}]}'
},
{
name: 'text with bold mark',
markup:
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"hello world"}]}]}'
},
{
name: 'separate paragraphs with bold mark',
markup:
'{"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"}]}]}'
},
{
name: 'mixed text and text with bold mark',
markup:
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello "},{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"world"}]}]}'
},
{
name: 'mixed text with italic and text with bold and italic marks',
markup:
'{"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"}]}]}'
},
{
name: 'text with link and italic marks',
markup:
'{"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":{}}]}]}]}'
},
{
name: 'image',
markup:
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"image","attrs":{"src":"http://example.com/image.jpg","alt":"image"}}]}]}'
},
{
name: 'table with formatting inside',
markup:
'{"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"}]}]}]}]}]}'
},
{
name: 'non-overlapping marks',
markup: `{
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "xy",
"marks": [
{
"type": "inline-comment",
"attrs": {
"thread": "1"
}
}
]
},
{
"type": "text",
"text": "z",
"marks": [
{
"type": "inline-comment",
"attrs": {
"thread": "2"
}
}
]
}
]
}
]
}`,
skipYdocCompare: true
},
{
name: 'overlapping marks',
markup: `{
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x",
"marks": [
{
"type": "inline-comment",
"attrs": {
"thread": "1"
}
}
]
},
{
"type": "text",
"text": "y",
"marks": [
{
"type": "inline-comment",
"attrs": {
"thread": "1"
}
},
{
"type": "inline-comment",
"attrs": {
"thread": "2"
}
}
]
},
{
"type": "text",
"text": "z",
"marks": [
{
"type": "inline-comment",
"attrs": {
"thread": "2"
}
}
]
}
]
}
]
}`,
skipYdocCompare: true
}
]
describe('markupToYDoc', () => {
describe.each(markups)('compare with reference', (name, markup) => {
it(name, () => {
const expected = referenceMarkupToYDoc(markup, 'test')
const actual = markupToYDoc(markup, 'test')
expect(actual.toJSON()).toEqual(expected.toJSON())
})
describe.each(markups)('compare with reference', ({ name, markup, skipYdocCompare }) => {
if (skipYdocCompare !== true) {
it(name, () => {
const expected = referenceMarkupToYDoc(markup, 'test')
const actual = markupToYDoc(markup, 'test')
expect(actual).toEqualYdoc(expected)
})
}
})
describe.each(markups)('converts markup to ydoc and back', (name, markup) => {
describe.each(markups)('converts markup to ydoc and back', ({ name, markup }) => {
it(name, () => {
console.log(markup, name)
const ydoc = markupToYDoc(markup, 'test')
const actual = referenceYDocToMarkup(ydoc, 'test')
const actual = yDocToMarkup(ydoc, 'test')
expect(actual).toEqualMarkup(markup)
})
@@ -107,7 +227,7 @@ describe('markupToYDoc', () => {
})
describe('yDocToMarkup', () => {
describe.each(markups)('compare with original', (name, markup) => {
describe.each(markups)('compare with original', ({ name, markup }) => {
it(name, () => {
const ydoc = referenceMarkupToYDoc(markup, 'test')
const actual = yDocToMarkup(ydoc, 'test')
@@ -116,7 +236,7 @@ describe('yDocToMarkup', () => {
})
})
describe.each(markups)('compare with reference', (name, markup) => {
describe.each(markups)('compare with reference', ({ name, markup }) => {
it(name, () => {
const ydoc = referenceMarkupToYDoc(markup, 'test')
const expected = referenceYDocToMarkup(ydoc, 'test')
@@ -126,12 +246,14 @@ describe('yDocToMarkup', () => {
})
})
describe.each(markups)('converts ydoc to markup and back', (name, markup) => {
it(name, () => {
const expected = referenceMarkupToYDoc(markup, 'test')
const actual = referenceMarkupToYDoc(yDocToMarkup(expected, 'test'), 'test')
describe.each(markups)('converts ydoc to markup and back', ({ name, markup, skipYdocCompare }) => {
if (skipYdocCompare !== true) {
it(name, () => {
const expected = referenceMarkupToYDoc(markup, 'test')
const actual = referenceMarkupToYDoc(yDocToMarkup(expected, 'test'), 'test')
expect(actual.toJSON()).toEqual(expected.toJSON())
})
expect(actual).toEqualYdoc(expected)
})
}
})
})
+12 -3
View File
@@ -15,6 +15,7 @@
import { generateId, Markup } from '@hcengineering/core'
import { jsonToMarkup, MarkupMarkType, MarkupNodeType, markupToJSON, type MarkupNode } from '@hcengineering/text'
import { hashAttrs, stripHash } from '@hcengineering/text-core'
import { Doc as YDoc, XmlElement as YXmlElement, XmlFragment as YXmlFragment, XmlText as YXmlText } from 'yjs'
/**
@@ -49,14 +50,22 @@ function nodeToXmlElement (parent: YXmlFragment, node: MarkupNode): YXmlElement
if (node.content !== undefined && node.content.length > 0) {
node.content.map((p) => nodeToXmlElement(elem, p))
}
} else {
} else if (elem instanceof YXmlText) {
// https://github.com/yjs/y-prosemirror/blob/master/src/plugins/sync-plugin.js#L777
const attributes: Record<string, any> = {}
if (node.marks !== undefined) {
const attrCount = new Map<string, number>()
node.marks.forEach((mark) => {
attributes[mark.type] = mark.attrs ?? {}
attrCount.set(mark.type, (attrCount.get(mark.type) ?? 0) + 1)
})
node.marks.forEach((mark) => {
const count = attrCount.get(mark.type) ?? 0
const type = count > 1 ? `${mark.type}--${hashAttrs(mark.attrs)}` : mark.type
attributes[type] = mark.attrs ?? {}
})
}
elem.applyDelta([
{
insert: node.text ?? '',
@@ -119,7 +128,7 @@ function xmlFragmentToNode (fragment: YXmlFragment): MarkupNode[] {
// Convert attributes to marks
if (op.attributes != null) {
textNode.marks = Object.entries(op.attributes).map(([type, attrs]) => ({
type: type as MarkupMarkType,
type: stripHash(type) as MarkupMarkType,
attrs: attrs as Record<string, any>
}))
}
+1
View File
@@ -561,6 +561,7 @@ input.search {
.mx-2 { margin: 0 .5rem; }
.mx-3 { margin: 0 .75rem; }
.mx-4 { margin: 0 1rem; }
.mx-8 { margin: 0 2rem; }
.mx-10 { margin: 0 2.5rem; }
.mx-auto { margin: 0 auto; }
.my-1 { margin: .25rem 0; }
+1
View File
@@ -303,6 +303,7 @@
.text-editor-image-container {
img {
max-width: 100%;
height: auto;
}
}
+1
View File
@@ -57,6 +57,7 @@
"Czech": "Čeština",
"German": "Deutsch",
"Japanese": "日本語",
"Turkish": "Türkçe",
"MinutesBefore": "{minutes, plural, =1 {1 minuta před} other {# minut před}}",
"HoursBefore": "{hours, plural, =1 {1 hodina před} other {# hodin před}}",
"DaysBefore": "{days, plural, =1 {1 den před} other {# dní před}}",
+1
View File
@@ -57,6 +57,7 @@
"Czech": "Tschechisch",
"German": "Deutsch",
"Japanese": "Japanisch",
"Turkish": "Türkçe",
"MinutesBefore": "{minutes, plural, =1 {eine Minute vorher} other {# Minuten vorher}}",
"HoursBefore": "{hours, plural, =1 {eine Stunde vorher} other {# Stunden vorher}}",
"DaysBefore": "{days, plural, =1 {einen Tag vorher} other {# Tage vorher}}",
+1
View File
@@ -57,6 +57,7 @@
"Czech": "Čeština",
"German": "Deutsch",
"Japanese": "日本語",
"Turkish": "Türkçe",
"MinutesBefore": "{minutes, plural, =1 {a minute before} other {# minutes before}}",
"HoursBefore": "{hours, plural, =1 {an hour before} other {# hours before}}",
"DaysBefore": "{days, plural, =1 {a day before} other {# days before}}",
+1
View File
@@ -57,6 +57,7 @@
"Czech": "Čeština",
"German": "Deutsch",
"Japanese": "日本語",
"Turkish": "Türkçe",
"MinutesBefore": "{minutes, plural, =1 {un minuto antes} other {# minutos antes}}",
"HoursBefore": "{hours, plural, =1 {una hora antes} other {# horas antes}}",
"DaysBefore": "{days, plural, =1 {un día antes} other {# días antes}}",
+1
View File
@@ -57,6 +57,7 @@
"Czech": "Čeština",
"German": "Deutsch",
"Japanese": "日本語",
"Turkish": "Türkçe",
"MinutesBefore": "{minutes, plural, =1 {une minute avant} other {# minutes avant}}",
"HoursBefore": "{hours, plural, =1 {une heure avant} other {# heures avant}}",
"DaysBefore": "{days, plural, =1 {un jour avant} other {# jours avant}}",
+1
View File
@@ -57,6 +57,7 @@
"Czech": "Čeština",
"German": "Deutsch",
"Japanese": "日本語",
"Turkish": "Türkçe",
"MinutesBefore": "{minutes, plural, =1 {un minuto prima} other {# minuti prima}}",
"HoursBefore": "{hours, plural, =1 {un'ora prima} other {# ore prima}}",
"DaysBefore": "{days, plural, =1 {un giorno prima} other {# giorni prima}}",
+1
View File
@@ -57,6 +57,7 @@
"Czech": "Čeština",
"German": "Deutsch",
"Japanese": "日本語",
"Turkish": "Türkçe",
"MinutesBefore": "{minutes, plural, =1 {1分前} other {#分前}}",
"HoursBefore": "{hours, plural, =1 {1時間前} other {#時間前}}",
"DaysBefore": "{days, plural, =1 {1日前} other {#日前}}",

Some files were not shown because too many files have changed in this diff Show More