UBERF-7690: Trigger improvements (#6340)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-08-19 12:41:31 +07:00 committed by GitHub
parent d26a03f814
commit c01a274cef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 868 additions and 699 deletions

2
.vscode/launch.json vendored
View File

@ -77,7 +77,7 @@
"ACCOUNT_PORT": "3000", "ACCOUNT_PORT": "3000",
"FRONT_URL": "http://localhost:8080", "FRONT_URL": "http://localhost:8080",
"outputCapture": "std", "outputCapture": "std",
"SES_URL": "http://localhost:8091", "SES_URL": "",
"MINIO_ACCESS_KEY": "minioadmin", "MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin", "MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost" "MINIO_ENDPOINT": "localhost"

View File

@ -1377,8 +1377,8 @@ dependencies:
specifier: ^1.20.2 specifier: ^1.20.2
version: 1.20.2 version: 1.20.2
browserslist: browserslist:
specifier: 4.21.5 specifier: ^4.23.3
version: 4.21.5 version: 4.23.3
bson: bson:
specifier: ^6.8.0 specifier: ^6.8.0
version: 6.8.0 version: 6.8.0
@ -1815,8 +1815,8 @@ dependencies:
specifier: ^5.3.3 specifier: ^5.3.3
version: 5.3.3 version: 5.3.3
update-browserslist-db: update-browserslist-db:
specifier: ~1.0.11 specifier: ^1.1.0
version: 1.0.13(browserslist@4.21.5) version: 1.1.0(browserslist@4.23.3)
url-loader: url-loader:
specifier: ~4.1.1 specifier: ~4.1.1
version: 4.1.1(file-loader@6.2.0)(webpack@5.90.3) version: 4.1.1(file-loader@6.2.0)(webpack@5.90.3)
@ -2657,7 +2657,7 @@ packages:
dependencies: dependencies:
'@babel/compat-data': 7.23.5 '@babel/compat-data': 7.23.5
'@babel/helper-validator-option': 7.23.5 '@babel/helper-validator-option': 7.23.5
browserslist: 4.23.0 browserslist: 4.23.3
lru-cache: 5.1.1 lru-cache: 5.1.1
semver: 6.3.1 semver: 6.3.1
dev: false dev: false
@ -10805,7 +10805,7 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.1.0 postcss: ^8.1.0
dependencies: dependencies:
browserslist: 4.23.0 browserslist: 4.23.3
caniuse-lite: 1.0.30001589 caniuse-lite: 1.0.30001589
fraction.js: 4.3.7 fraction.js: 4.3.7
normalize-range: 0.1.2 normalize-range: 0.1.2
@ -11196,26 +11196,15 @@ packages:
pako: 0.2.9 pako: 0.2.9
dev: false dev: false
/browserslist@4.21.5: /browserslist@4.23.3:
resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
dependencies: dependencies:
caniuse-lite: 1.0.30001589 caniuse-lite: 1.0.30001651
electron-to-chromium: 1.4.679 electron-to-chromium: 1.5.11
node-releases: 2.0.14 node-releases: 2.0.18
update-browserslist-db: 1.0.13(browserslist@4.21.5) update-browserslist-db: 1.1.0(browserslist@4.23.3)
dev: false
/browserslist@4.23.0:
resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001589
electron-to-chromium: 1.4.679
node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.23.0)
dev: false dev: false
/bs-logger@0.2.6: /bs-logger@0.2.6:
@ -11468,6 +11457,10 @@ packages:
resolution: {integrity: sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==} resolution: {integrity: sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==}
dev: false dev: false
/caniuse-lite@1.0.30001651:
resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==}
dev: false
/case-anything@2.1.13: /case-anything@2.1.13:
resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==}
engines: {node: '>=12.13'} engines: {node: '>=12.13'}
@ -12019,7 +12012,7 @@ packages:
/core-js-compat@3.36.0: /core-js-compat@3.36.0:
resolution: {integrity: sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==} resolution: {integrity: sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==}
dependencies: dependencies:
browserslist: 4.23.0 browserslist: 4.23.3
dev: false dev: false
/core-js@3.36.0: /core-js@3.36.0:
@ -13121,8 +13114,8 @@ packages:
type-fest: 2.19.0 type-fest: 2.19.0
dev: false dev: false
/electron-to-chromium@1.4.679: /electron-to-chromium@1.5.11:
resolution: {integrity: sha512-NhQMsz5k0d6m9z3qAxnsOR/ebal4NAGsrNVRwcDo4Kc/zQ7KdsTKZUxZoygHcVRb0QDW3waEDIcE3isZ79RP6g==} resolution: {integrity: sha512-R1CccCDYqndR25CaXFd6hp/u9RaaMcftMkphmvuepXr5b1vfLkRml6aWVeBhXJ7rbevHkKEMJtz8XqPf7ffmew==}
dev: false dev: false
/electron-updater@6.2.1: /electron-updater@6.2.1:
@ -18443,8 +18436,8 @@ packages:
write-file-atomic: 1.3.4 write-file-atomic: 1.3.4
dev: false dev: false
/node-releases@2.0.14: /node-releases@2.0.18:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
dev: false dev: false
/nopt@1.0.10: /nopt@1.0.10:
@ -19229,6 +19222,10 @@ packages:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
dev: false dev: false
/picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
dev: false
/picomatch@2.3.1: /picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
@ -22785,26 +22782,15 @@ packages:
path-exists: 5.0.0 path-exists: 5.0.0
dev: false dev: false
/update-browserslist-db@1.0.13(browserslist@4.21.5): /update-browserslist-db@1.1.0(browserslist@4.23.3):
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
browserslist: '>= 4.21.0' browserslist: '>= 4.21.0'
dependencies: dependencies:
browserslist: 4.21.5 browserslist: 4.23.3
escalade: 3.1.2 escalade: 3.1.2
picocolors: 1.0.0 picocolors: 1.0.1
dev: false
/update-browserslist-db@1.0.13(browserslist@4.23.0):
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
dependencies:
browserslist: 4.23.0
escalade: 3.1.2
picocolors: 1.0.0
dev: false dev: false
/uri-js@4.4.1: /uri-js@4.4.1:
@ -23282,7 +23268,7 @@ packages:
'@webassemblyjs/wasm-parser': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6
acorn: 8.11.3 acorn: 8.11.3
acorn-import-assertions: 1.9.0(acorn@8.11.3) acorn-import-assertions: 1.9.0(acorn@8.11.3)
browserslist: 4.23.0 browserslist: 4.23.3
chrome-trace-event: 1.0.3 chrome-trace-event: 1.0.3
enhanced-resolve: 5.15.0 enhanced-resolve: 5.15.0
es-module-lexer: 1.4.1 es-module-lexer: 1.4.1
@ -23323,7 +23309,7 @@ packages:
'@webassemblyjs/wasm-parser': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6
acorn: 8.11.3 acorn: 8.11.3
acorn-import-assertions: 1.9.0(acorn@8.11.3) acorn-import-assertions: 1.9.0(acorn@8.11.3)
browserslist: 4.23.0 browserslist: 4.23.3
chrome-trace-event: 1.0.3 chrome-trace-event: 1.0.3
enhanced-resolve: 5.15.0 enhanced-resolve: 5.15.0
es-module-lexer: 1.4.1 es-module-lexer: 1.4.1
@ -24298,7 +24284,7 @@ packages:
dev: false dev: false
file:projects/attachment-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): file:projects/attachment-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-ZEkxtUt4tXWqiEQOGDSFHr65Zw9PtVeCQJ5zS8zdDdpF+vh1BAaLW7OydhYafKYw5RheG50HOK/R6wPDcNd3Lw==, tarball: file:projects/attachment-resources.tgz} resolution: {integrity: sha512-yEPWonMOvXhIk5+IaNQVsyw6wnfqdcB/4UsEh8BLCQwhpepBeVrNSXgS+mLspQaL9g15E81PHwyrWyF1OcqQOQ==, tarball: file:projects/attachment-resources.tgz}
id: file:projects/attachment-resources.tgz id: file:projects/attachment-resources.tgz
name: '@rush-temp/attachment-resources' name: '@rush-temp/attachment-resources'
version: 0.0.0 version: 0.0.0
@ -25127,7 +25113,7 @@ packages:
dev: false dev: false
file:projects/contact-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): file:projects/contact-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-iVkm89qU3Sx3zlhJRYkBsMy03G536YvuMT1tc6WTLMzklcpqYyCOtklXRWUGGiF/+a56Dog4hu7ai10QUIajrA==, tarball: file:projects/contact-resources.tgz} resolution: {integrity: sha512-Vlw7yfM/X0ViZ1mzmcJFVPAZk/2hp4l768lxErLF/sA2HKsRUCAvkjbzfHmImsK5e2WycFWOWwnUJu9p1irQWQ==, tarball: file:projects/contact-resources.tgz}
id: file:projects/contact-resources.tgz id: file:projects/contact-resources.tgz
name: '@rush-temp/contact-resources' name: '@rush-temp/contact-resources'
version: 0.0.0 version: 0.0.0
@ -25320,7 +25306,7 @@ packages:
dev: false dev: false
file:projects/core.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): file:projects/core.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-BgJt/g8bufJyz3tFDrdM6Y3GHHTzKs7DUVMylyP2kyPCyrq3TrBDOGajz6QJ36kObLH/KWWau/LRmB3ZWIFU7Q==, tarball: file:projects/core.tgz} resolution: {integrity: sha512-qorGEpUYJYJMlHr0WkKpS8sUfQt3GQtF7xCdUSU5K9B/aRgAL6z0LGRJoUmn/kYHHV+r6DITWhOiP6zIHcKmdg==, tarball: file:projects/core.tgz}
id: file:projects/core.tgz id: file:projects/core.tgz
name: '@rush-temp/core' name: '@rush-temp/core'
version: 0.0.0 version: 0.0.0
@ -25515,7 +25501,7 @@ packages:
dev: false dev: false
file:projects/desktop.tgz(bufferutil@4.0.8)(sass@1.71.1)(utf-8-validate@6.0.4): file:projects/desktop.tgz(bufferutil@4.0.8)(sass@1.71.1)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-j8onQxy51K29b/9q8g+lOG8e8dOFO2WpQQTdtnhP2vy2EcvIuSgckenQpg++0r1AtJ1G7FC7hr5QYNsrTPWkBA==, tarball: file:projects/desktop.tgz} resolution: {integrity: sha512-+dbbvdgHJD76pLjOUSctj1tb9kC3hWa4CisFJxvSmlObAUII4NI+4EmE4u1x8OWaGEDqRCPCmrGFk1aSiPF5fQ==, tarball: file:projects/desktop.tgz}
id: file:projects/desktop.tgz id: file:projects/desktop.tgz
name: '@rush-temp/desktop' name: '@rush-temp/desktop'
version: 0.0.0 version: 0.0.0
@ -25525,7 +25511,7 @@ packages:
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
'@vercel/webpack-asset-relocator-loader': 1.7.4 '@vercel/webpack-asset-relocator-loader': 1.7.4
autoprefixer: 10.4.17(postcss@8.4.35) autoprefixer: 10.4.17(postcss@8.4.35)
browserslist: 4.21.5 browserslist: 4.23.3
commander: 8.3.0 commander: 8.3.0
compression-webpack-plugin: 10.0.0(webpack@5.90.3) compression-webpack-plugin: 10.0.0(webpack@5.90.3)
copy-webpack-plugin: 11.0.0(webpack@5.90.3) copy-webpack-plugin: 11.0.0(webpack@5.90.3)
@ -25567,7 +25553,7 @@ packages:
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3)
ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3) ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3)
typescript: 5.3.3 typescript: 5.3.3
update-browserslist-db: 1.0.13(browserslist@4.21.5) update-browserslist-db: 1.1.0(browserslist@4.23.3)
webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4)
webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.90.3) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.90.3)
@ -25812,7 +25798,7 @@ packages:
dev: false dev: false
file:projects/document-resources.tgz(@tiptap/pm@2.2.4)(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): file:projects/document-resources.tgz(@tiptap/pm@2.2.4)(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-paT/sxyRFUxsWTwzgk98+gGZZkT1a5EyFzSKE4UPyW9sqjM4IiyyIIrr57YZEpILDApfg51PF3qhjEtWV9kf1Q==, tarball: file:projects/document-resources.tgz} resolution: {integrity: sha512-YK9gh+kYmMkJHyLvlo94mEr3QznTo4V7TMypOcnHxg4ZKTfRl1mrDNI/dSAW566wmbmm7Eln0WRboX30yRxPTA==, tarball: file:projects/document-resources.tgz}
id: file:projects/document-resources.tgz id: file:projects/document-resources.tgz
name: '@rush-temp/document-resources' name: '@rush-temp/document-resources'
version: 0.0.0 version: 0.0.0
@ -26438,7 +26424,7 @@ packages:
dev: false dev: false
file:projects/hr-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): file:projects/hr-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-85fy55EMpfKKMiNAp0tW70KLnbppfEp9cDx93jYlteFoPpsi05GRZ2ZNYM9rLWaZ1wMYF1vsX42M0opDIX6KOQ==, tarball: file:projects/hr-resources.tgz} resolution: {integrity: sha512-O0jhwaTcNjWwviO2wUzfwpH7AroMYBCAbgsRsLwBsDdW08fnp1LqKswgViDL/ZOsQ5ZvboCwRAOaR8VYVfojZg==, tarball: file:projects/hr-resources.tgz}
id: file:projects/hr-resources.tgz id: file:projects/hr-resources.tgz
name: '@rush-temp/hr-resources' name: '@rush-temp/hr-resources'
version: 0.0.0 version: 0.0.0
@ -26776,7 +26762,7 @@ packages:
dev: false dev: false
file:projects/lead-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): file:projects/lead-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-DIL0rCz2r3nKYgE2fOrEdVSwLGq1Zz2MjsU1HvDxnIs4IE1Q4s8oNMBtvtunERIQ2fNnJHcE0XXijawtYilrjQ==, tarball: file:projects/lead-resources.tgz} resolution: {integrity: sha512-TJVh5S1o+GvRWeeNWwXveGOpMtsQTR0n5RMGjK3kXsuAUYCQRPzgMh3noJxe2n4vvijK8IUNjAR9+AjeSPo5kw==, tarball: file:projects/lead-resources.tgz}
id: file:projects/lead-resources.tgz id: file:projects/lead-resources.tgz
name: '@rush-temp/lead-resources' name: '@rush-temp/lead-resources'
version: 0.0.0 version: 0.0.0
@ -27182,7 +27168,7 @@ packages:
dev: false dev: false
file:projects/model-analytics-collector.tgz: file:projects/model-analytics-collector.tgz:
resolution: {integrity: sha512-OcczFEesVXSXUuJ/HpZ880slAZeV8Y/MhZArlxBlO90Tm3jO42cyQLAi0kO36ZAkO/gGVpAHUpuZT0QB70ovvw==, tarball: file:projects/model-analytics-collector.tgz} resolution: {integrity: sha512-ht/R4aa2RvdT8odpbznVfFTed3dJGToS1q5tKHqJIF8BQhjNcVh9ZUmAJNnNZNTeD8J6yuKIPq7n/Io2CuTYdg==, tarball: file:projects/model-analytics-collector.tgz}
name: '@rush-temp/model-analytics-collector' name: '@rush-temp/model-analytics-collector'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
@ -27391,7 +27377,7 @@ packages:
dev: false dev: false
file:projects/model-drive.tgz: file:projects/model-drive.tgz:
resolution: {integrity: sha512-V1Zaxs9HItgN9Zp8M2rdPjTUS1ZSkNaDUCeRcW5xuOdD0YY8xijRwZ+d2HF5G6xYgRwTpfqIw0O1cUBmqJAY8A==, tarball: file:projects/model-drive.tgz} resolution: {integrity: sha512-JG62skvnh8n7bBwsriHH6TE71W/gVm287OVnmy4VRJVf/tDC0/sCKlg+75qvcSu3CmAjaYqnnJz5q1Lz9EH3xw==, tarball: file:projects/model-drive.tgz}
name: '@rush-temp/model-drive' name: '@rush-temp/model-drive'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
@ -28389,7 +28375,7 @@ packages:
dev: false dev: false
file:projects/model-telegram.tgz: file:projects/model-telegram.tgz:
resolution: {integrity: sha512-WjCLxLQvcXBwLNQQnCbClBIum033r+fwS3nwB7RmCTgERzxT7lqedPlCwsd1TUs7PF6C0Y6cp5O/Lrnn+7Z46g==, tarball: file:projects/model-telegram.tgz} resolution: {integrity: sha512-uXXH0LQVtX+rJ0+XLPft4NLphBmtXs9ZY+g16hC9x7j5JToKwsYsBHR/rktANxYU3CyAHeoqA7S7CiYbGLrDVw==, tarball: file:projects/model-telegram.tgz}
name: '@rush-temp/model-telegram' name: '@rush-temp/model-telegram'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
@ -28674,7 +28660,7 @@ packages:
dev: false dev: false
file:projects/notification-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): file:projects/notification-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-D5FLFfyRUTqKy52JGk4IMGhROrTRq+YZ+34+y2awY2U7JBUbpjcxhMkFBMgr/9TrZhSTvZrw7VvL5FCruoEN4Q==, tarball: file:projects/notification-resources.tgz} resolution: {integrity: sha512-xpvJbpFBrKB3RPG8SgT9+cJj2IkysBNcl4jQAV69ACUvnTQMh4RS9P31zpaO7CObYXrm9/ImRF0fXbH+AkiJsg==, tarball: file:projects/notification-resources.tgz}
id: file:projects/notification-resources.tgz id: file:projects/notification-resources.tgz
name: '@rush-temp/notification-resources' name: '@rush-temp/notification-resources'
version: 0.0.0 version: 0.0.0
@ -29099,7 +29085,7 @@ packages:
dev: false dev: false
file:projects/pod-analytics-collector.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4): file:projects/pod-analytics-collector.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-Sx9mTOCsgDTcoC8h9VXeemVHerKLMDzdW4aJ08fdsF9UA35tLWmbDXw+fn3gPUNj2l+QT5ExEF9rV1Iez1FIbw==, tarball: file:projects/pod-analytics-collector.tgz} resolution: {integrity: sha512-15hQZSiqnsgeZDg3n3AE2D2mx6xBToMB+2z+nDKLAKbaa+XAjMyj7J/Z9nLwfjs2EX6Z8x10JoPvs4au/SXxlA==, tarball: file:projects/pod-analytics-collector.tgz}
id: file:projects/pod-analytics-collector.tgz id: file:projects/pod-analytics-collector.tgz
name: '@rush-temp/pod-analytics-collector' name: '@rush-temp/pod-analytics-collector'
version: 0.0.0 version: 0.0.0
@ -30043,7 +30029,7 @@ packages:
dev: false dev: false
file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.71.1)(ts-node@10.9.2)(utf-8-validate@6.0.4): file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.71.1)(ts-node@10.9.2)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-U0i5q0YgmqM+29OoNKE5F7SYHLenVyjGDEus4MgN7CqMIC1n6n2gtztzRm2O6/j8hba5E6asgHgGF+ljESlbGw==, tarball: file:projects/prod.tgz} resolution: {integrity: sha512-lYEzH4pmYid/iB0WNGOZXt3y9nyDbs70r3jYZJ6qIQEyvQVqWXnAd8Rw+nqcEbI/QtTou81FrvAK2Fnp5Imeww==, tarball: file:projects/prod.tgz}
id: file:projects/prod.tgz id: file:projects/prod.tgz
name: '@rush-temp/prod' name: '@rush-temp/prod'
version: 0.0.0 version: 0.0.0
@ -30051,7 +30037,7 @@ packages:
'@sentry/svelte': 7.101.1(svelte@4.2.12) '@sentry/svelte': 7.101.1(svelte@4.2.12)
'@types/node': 20.11.19 '@types/node': 20.11.19
autoprefixer: 10.4.17(postcss@8.4.35) autoprefixer: 10.4.17(postcss@8.4.35)
browserslist: 4.21.5 browserslist: 4.23.3
compression-webpack-plugin: 10.0.0(webpack@5.90.3) compression-webpack-plugin: 10.0.0(webpack@5.90.3)
cross-env: 7.0.3 cross-env: 7.0.3
css-loader: 5.2.7(webpack@5.90.3) css-loader: 5.2.7(webpack@5.90.3)
@ -30074,7 +30060,7 @@ packages:
svgo-loader: 3.0.3 svgo-loader: 3.0.3
ts-loader: 9.5.1(typescript@5.3.3)(webpack@5.90.3) ts-loader: 9.5.1(typescript@5.3.3)(webpack@5.90.3)
typescript: 5.3.3 typescript: 5.3.3
update-browserslist-db: 1.0.13(browserslist@4.21.5) update-browserslist-db: 1.1.0(browserslist@4.23.3)
webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4)
webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.90.3) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.90.3)
@ -31700,7 +31686,7 @@ packages:
dev: false dev: false
file:projects/server-gmail-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): file:projects/server-gmail-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-jc5tLhz7YJs4Nw0dwPgOpZnZA+wMstl6Vt3BuL5BOE8iMTeUTZP1fKJXf6pGVeZAGEaYzayHM9Jmw+S45/l9MQ==, tarball: file:projects/server-gmail-resources.tgz} resolution: {integrity: sha512-V9M3+rquV4MPbPtmwWPdekguzA5WKJRDvU7xtpYKQSJ7J7hkLPKNah4GtSjin0opg6lTgtoAJUQaYLKheeCCbA==, tarball: file:projects/server-gmail-resources.tgz}
id: file:projects/server-gmail-resources.tgz id: file:projects/server-gmail-resources.tgz
name: '@rush-temp/server-gmail-resources' name: '@rush-temp/server-gmail-resources'
version: 0.0.0 version: 0.0.0
@ -33412,7 +33398,7 @@ packages:
dev: false dev: false
file:projects/tags-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): file:projects/tags-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-LuFrjyWlBTgRhkuiQvvLLtUh4kw9sRrUH7kZaJYjXeHh6J4PDfbrLl1cNLCTKXZiCP4X3GIeMz0eK0eUqzdeVg==, tarball: file:projects/tags-resources.tgz} resolution: {integrity: sha512-5ldBcfo7wMt0BzAf8fA0FMc2ICyqweNug+4KPSisBcFJRvqUysw+x97qpqBosCl2BI2ro/MTnUEwYpN/3m5oVQ==, tarball: file:projects/tags-resources.tgz}
id: file:projects/tags-resources.tgz id: file:projects/tags-resources.tgz
name: '@rush-temp/tags-resources' name: '@rush-temp/tags-resources'
version: 0.0.0 version: 0.0.0
@ -33520,7 +33506,7 @@ packages:
dev: false dev: false
file:projects/task-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): file:projects/task-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-BXlz1rd2Pp3cTgW0USdUS2Ha+hDUYnwAvT9uzlZY1Nn0zOxt6rSC/Qb4e6N1n0gNxpI63ZZl+zyhTkR100yL+g==, tarball: file:projects/task-resources.tgz} resolution: {integrity: sha512-GxlxQIm65Qvb6rIh4tqIgtNxEhom+Q8bxQeclK4J+elxv3/YoQdQrS6pKiX+9ueY/TSz+ulPbgpNfk6y1cgSUA==, tarball: file:projects/task-resources.tgz}
id: file:projects/task-resources.tgz id: file:projects/task-resources.tgz
name: '@rush-temp/task-resources' name: '@rush-temp/task-resources'
version: 0.0.0 version: 0.0.0
@ -34127,7 +34113,7 @@ packages:
dev: false dev: false
file:projects/time-resources.tgz(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): file:projects/time-resources.tgz(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-YHDZu9vfplV3yHTWsp3czuSbb59RRHGIYpYvMZKVG7mnk52y121rzcxiu1W6j8vh69xFXgxMowO6TQEwTEFfKQ==, tarball: file:projects/time-resources.tgz} resolution: {integrity: sha512-S0T2DEhL7CMoryh+gjH7KorQwrUvwu96LH2VUaSZTADN69oSN9XQZb05ab1zhn5MbwNoRr2aLnMDqRmhuxT1Ag==, tarball: file:projects/time-resources.tgz}
id: file:projects/time-resources.tgz id: file:projects/time-resources.tgz
name: '@rush-temp/time-resources' name: '@rush-temp/time-resources'
version: 0.0.0 version: 0.0.0

View File

@ -42,8 +42,8 @@
"compression-webpack-plugin": "^10.0.0", "compression-webpack-plugin": "^10.0.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"fork-ts-checker-webpack-plugin": "~7.3.0", "fork-ts-checker-webpack-plugin": "~7.3.0",
"update-browserslist-db": "~1.0.11", "update-browserslist-db": "^1.1.0",
"browserslist": "4.21.5", "browserslist": "^4.23.3",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"ts-node": "^10.8.0", "ts-node": "^10.8.0",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",

View File

@ -62,8 +62,8 @@ services:
- LAST_NAME_FIRST=true - LAST_NAME_FIRST=true
- ACCOUNTS_URL=http://localhost:3000 - ACCOUNTS_URL=http://localhost:3000
- BRANDING_PATH=/var/cfg/branding.json - BRANDING_PATH=/var/cfg/branding.json
- INIT_SCRIPT_URL=https://raw.githubusercontent.com/hcengineering/init/main/script.yaml # - INIT_SCRIPT_URL=https://raw.githubusercontent.com/hcengineering/init/main/script.yaml
- INIT_WORKSPACE=onboarding # - INIT_WORKSPACE=onboarding
restart: unless-stopped restart: unless-stopped
collaborator: collaborator:
image: hardcoreeng/collaborator image: hardcoreeng/collaborator

View File

@ -43,8 +43,8 @@
"compression-webpack-plugin": "^10.0.0", "compression-webpack-plugin": "^10.0.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"fork-ts-checker-webpack-plugin": "~7.3.0", "fork-ts-checker-webpack-plugin": "~7.3.0",
"update-browserslist-db": "~1.0.11", "update-browserslist-db": "^1.1.0",
"browserslist": "4.21.5", "browserslist": "^4.23.3",
"esbuild": "^0.20.0", "esbuild": "^0.20.0",
"esbuild-loader": "^4.0.3", "esbuild-loader": "^4.0.3",
"typescript": "^5.3.3", "typescript": "^5.3.3",

View File

@ -37,8 +37,8 @@ async function createEmployeeEmail (client: TxOperations): Promise<void> {
).filter((it) => it.provider === contact.channelProvider.Email) ).filter((it) => it.provider === contact.channelProvider.Email)
const channelsMap = new Map(channels.map((p) => [p.attachedTo, p])) const channelsMap = new Map(channels.map((p) => [p.attachedTo, p]))
for (const employee of employees) { for (const employee of employees) {
const acc = await client.findOne(contact.class.PersonAccount, { person: employee._id }) const acc = client.getModel().getAccountByPersonId(employee._id)
if (acc === undefined) continue if (acc.length === 0) continue
const current = channelsMap.get(employee._id) const current = channelsMap.get(employee._id)
if (current === undefined) { if (current === undefined) {
await client.addCollection( await client.addCollection(
@ -49,13 +49,13 @@ async function createEmployeeEmail (client: TxOperations): Promise<void> {
'channels', 'channels',
{ {
provider: contact.channelProvider.Email, provider: contact.channelProvider.Email,
value: acc.email.trim() value: acc[0].email.trim()
}, },
undefined, undefined,
employee.modifiedOn employee.modifiedOn
) )
} else if (current.value !== acc.email.trim()) { } else if (current.value !== acc[0].email.trim()) {
await client.update(current, { value: acc.email.trim() }, false, current.modifiedOn) await client.update(current, { value: acc[0].email.trim() }, false, current.modifiedOn)
} }
} }
} }

View File

@ -19,6 +19,7 @@ import core from '@hcengineering/core/src/component'
import serverActivity from '@hcengineering/server-activity' import serverActivity from '@hcengineering/server-activity'
import serverNotification from '@hcengineering/server-notification' import serverNotification from '@hcengineering/server-notification'
import activity from '@hcengineering/activity' import activity from '@hcengineering/activity'
import notification from '@hcengineering/notification'
export { activityServerOperation } from './migration' export { activityServerOperation } from './migration'
export { serverActivityId } from '@hcengineering/server-activity' export { serverActivityId } from '@hcengineering/server-activity'
@ -43,6 +44,9 @@ export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverActivity.trigger.ActivityMessagesHandler, trigger: serverActivity.trigger.ActivityMessagesHandler,
txMatch: {
'tx.objectClass': { $nin: [activity.class.ActivityMessage, notification.class.DocNotifyContext] }
},
isAsync: true isAsync: true
}) })
@ -52,6 +56,10 @@ export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverActivity.trigger.ReferenceTrigger, trigger: serverActivity.trigger.ReferenceTrigger,
txMatch: {
'tx.objectClass': { $ne: activity.class.ActivityMessage },
objectClass: { $nin: [notification.class.InboxNotification, notification.class.DocNotifyContext] }
},
isAsync: true isAsync: true
}) })
} }

View File

@ -49,6 +49,6 @@ export function createModel (builder: Builder): void {
}) })
builder.mixin(gmail.ids.EmailNotification, notification.class.NotificationType, serverNotification.mixin.TypeMatch, { builder.mixin(gmail.ids.EmailNotification, notification.class.NotificationType, serverNotification.mixin.TypeMatch, {
func: serverGmail.function.IsIncomingMessage func: serverGmail.function.IsIncomingMessageTypeMatch
}) })
} }

View File

@ -39,7 +39,7 @@ export function createModel (builder: Builder): void {
notification.class.NotificationType, notification.class.NotificationType,
serverNotification.mixin.TypeMatch, serverNotification.mixin.TypeMatch,
{ {
func: serverNotification.function.IsUserEmployeeInFieldValue func: serverNotification.function.IsUserEmployeeInFieldValueTypeMatch
} }
) )

View File

@ -16,21 +16,22 @@
import { type Builder, Mixin, Model } from '@hcengineering/model' import { type Builder, Mixin, Model } from '@hcengineering/model'
import core, { type Account, type Doc, type Ref, type Tx } from '@hcengineering/core' import core, { type Ref } from '@hcengineering/core'
import { TClass, TDoc } from '@hcengineering/model-core' import { TClass, TDoc } from '@hcengineering/model-core'
import { TNotificationType } from '@hcengineering/model-notification' import { TNotificationType } from '@hcengineering/model-notification'
import notification, { type NotificationProvider, type NotificationType } from '@hcengineering/notification' import notification, { type NotificationProvider } from '@hcengineering/notification'
import { type Resource } from '@hcengineering/platform' import { type Resource } from '@hcengineering/platform'
import serverCore, { type TriggerControl } from '@hcengineering/server-core' import serverCore from '@hcengineering/server-core'
import serverNotification, { import serverNotification, {
type HTMLPresenter, type HTMLPresenter,
type NotificationContentProvider,
type NotificationPresenter, type NotificationPresenter,
type NotificationProviderFunc,
type NotificationProviderResources,
type Presenter, type Presenter,
type TextPresenter, type TextPresenter,
type TypeMatch, type TypeMatch,
type NotificationContentProvider, type TypeMatchFunc
type NotificationProviderResources,
type NotificationProviderFunc
} from '@hcengineering/server-notification' } from '@hcengineering/server-notification'
export { serverNotificationId } from '@hcengineering/server-notification' export { serverNotificationId } from '@hcengineering/server-notification'
@ -52,9 +53,7 @@ export class TNotificationPresenter extends TClass implements NotificationPresen
@Mixin(serverNotification.mixin.TypeMatch, notification.class.NotificationType) @Mixin(serverNotification.mixin.TypeMatch, notification.class.NotificationType)
export class TTypeMatch extends TNotificationType implements TypeMatch { export class TTypeMatch extends TNotificationType implements TypeMatch {
func!: Resource< func!: TypeMatchFunc
(tx: Tx, doc: Doc, user: Ref<Account>, type: NotificationType, control: TriggerControl) => Promise<boolean>
>
} }
@Model(serverNotification.class.NotificationProviderResources, core.class.Doc) @Model(serverNotification.class.NotificationProviderResources, core.class.Doc)

View File

@ -93,7 +93,7 @@ export function createModel (builder: Builder): void {
notification.class.NotificationType, notification.class.NotificationType,
serverNotification.mixin.TypeMatch, serverNotification.mixin.TypeMatch,
{ {
func: serverNotification.function.IsUserEmployeeInFieldValue func: serverNotification.function.IsUserEmployeeInFieldValueTypeMatch
} }
) )
} }

View File

@ -56,7 +56,7 @@ export function createModel (builder: Builder): void {
notification.class.NotificationType, notification.class.NotificationType,
serverNotification.mixin.TypeMatch, serverNotification.mixin.TypeMatch,
{ {
func: serverTelegram.function.IsIncomingMessage func: serverTelegram.function.IsIncomingMessageTypeMatch
} }
) )

View File

@ -77,7 +77,7 @@ export function createModel (builder: Builder): void {
notification.class.NotificationType, notification.class.NotificationType,
serverNotification.mixin.TypeMatch, serverNotification.mixin.TypeMatch,
{ {
func: serverNotification.function.IsUserEmployeeInFieldValue func: serverNotification.function.IsUserEmployeeInFieldValueTypeMatch
} }
) )
} }

View File

@ -187,7 +187,7 @@ export function genMinModel (): TxCUD<Doc>[] {
txes.push( txes.push(
createClass(core.class.Blob, { createClass(core.class.Blob, {
label: 'Blob' as IntlString, label: 'Blob' as IntlString,
extends: core.class.Blob, extends: core.class.Doc,
kind: ClassifierKind.CLASS kind: ClassifierKind.CLASS
}) })
) )

View File

@ -440,6 +440,8 @@ export interface Permission extends Doc {
export interface Account extends Doc { export interface Account extends Doc {
email: string email: string
role: AccountRole role: AccountRole
person?: Ref<Doc>
} }
/** /**

View File

@ -30,7 +30,7 @@ export class Hierarchy {
private readonly attributes = new Map<Ref<Classifier>, Map<string, AnyAttribute>>() private readonly attributes = new Map<Ref<Classifier>, Map<string, AnyAttribute>>()
private readonly attributesById = new Map<Ref<AnyAttribute>, AnyAttribute>() private readonly attributesById = new Map<Ref<AnyAttribute>, AnyAttribute>()
private readonly descendants = new Map<Ref<Classifier>, Ref<Classifier>[]>() private readonly descendants = new Map<Ref<Classifier>, Ref<Classifier>[]>()
private readonly ancestors = new Map<Ref<Classifier>, Ref<Classifier>[]>() private readonly ancestors = new Map<Ref<Classifier>, { ordered: Ref<Classifier>[], set: Set<Ref<Classifier>> }>()
private readonly proxies = new Map<Ref<Mixin<Doc>>, ProxyHandler<Doc>>() private readonly proxies = new Map<Ref<Mixin<Doc>>, ProxyHandler<Doc>>()
private readonly classifierProperties = new Map<Ref<Classifier>, Record<string, any>>() private readonly classifierProperties = new Map<Ref<Classifier>, Record<string, any>>()
@ -166,7 +166,7 @@ export class Hierarchy {
if (result === undefined) { if (result === undefined) {
throw new Error('ancestors not found: ' + _class) throw new Error('ancestors not found: ' + _class)
} }
return result return result.ordered
} }
getClass<T extends Obj = Obj>(_class: Ref<Class<T>>): Class<T> { getClass<T extends Obj = Obj>(_class: Ref<Class<T>>): Class<T> {
@ -301,17 +301,7 @@ export class Hierarchy {
* It will iterate over parents. * It will iterate over parents.
*/ */
isDerived<T extends Obj>(_class: Ref<Class<T>>, from: Ref<Class<T>>): boolean { isDerived<T extends Obj>(_class: Ref<Class<T>>, from: Ref<Class<T>>): boolean {
let cl: Ref<Class<T>> | undefined = _class return this.ancestors.get(_class)?.set?.has(from) ?? false
while (cl !== undefined) {
if (cl === from) return true
const cll = this.classifiers.get(cl)
if (cll === undefined || this.isInterface(cll)) {
return false
}
cl = (cll as Class<T>).extends
}
return false
} }
/** /**
@ -398,15 +388,19 @@ export class Hierarchy {
const list = this.ancestors.get(_class) const list = this.ancestors.get(_class)
if (list === undefined) { if (list === undefined) {
if (add) { if (add) {
this.ancestors.set(_class, [classifier]) this.ancestors.set(_class, { ordered: [classifier], set: new Set([classifier]) })
} }
} else { } else {
if (add) { if (add) {
addIf(list, classifier) if (!list.set.has(classifier)) {
list.ordered.push(classifier)
list.set.add(classifier)
}
} else { } else {
const pos = list.indexOf(classifier) const pos = list.ordered.indexOf(classifier)
if (pos !== -1) { if (pos !== -1) {
list.splice(pos, 1) list.ordered.splice(pos, 1)
list.set.delete(classifier)
} }
} }
} }

View File

@ -15,7 +15,7 @@
import { PlatformError, Severity, Status } from '@hcengineering/platform' import { PlatformError, Severity, Status } from '@hcengineering/platform'
import { Lookup, MeasureContext, ReverseLookups, getObjectValue } from '.' import { Lookup, MeasureContext, ReverseLookups, getObjectValue } from '.'
import type { AttachedDoc, Class, Doc, Ref } from './classes' import type { Account, AttachedDoc, Class, Doc, Ref } from './classes'
import core from './component' import core from './component'
import { Hierarchy } from './hierarchy' import { Hierarchy } from './hierarchy'
import { checkMixinKey, matchQuery, resultSort } from './query' import { checkMixinKey, matchQuery, resultSort } from './query'
@ -31,6 +31,8 @@ export abstract class MemDb extends TxProcessor implements Storage {
private readonly objectsByClass = new Map<Ref<Class<Doc>>, Map<Ref<Doc>, Doc>>() private readonly objectsByClass = new Map<Ref<Class<Doc>>, Map<Ref<Doc>, Doc>>()
private readonly objectById = new Map<Ref<Doc>, Doc>() private readonly objectById = new Map<Ref<Doc>, Doc>()
private readonly accountByPersonId = new Map<Ref<Doc>, Account[]>()
constructor (protected readonly hierarchy: Hierarchy) { constructor (protected readonly hierarchy: Hierarchy) {
super() super()
} }
@ -75,6 +77,10 @@ export abstract class MemDb extends TxProcessor implements Storage {
return doc as T return doc as T
} }
getAccountByPersonId (ref: Ref<Doc>): Account[] {
return this.accountByPersonId.get(ref) ?? []
}
findObject<T extends Doc>(_id: Ref<T>): T | undefined { findObject<T extends Doc>(_id: Ref<T>): T | undefined {
const doc = this.objectById.get(_id) const doc = this.objectById.get(_id)
return doc as T return doc as T
@ -214,6 +220,12 @@ export abstract class MemDb extends TxProcessor implements Storage {
const arr = this.getObjectsByClass(_class) const arr = this.getObjectsByClass(_class)
arr.set(doc._id, doc) arr.set(doc._id, doc)
}) })
if (this.hierarchy.isDerived(doc._class, core.class.Account)) {
const account = doc as Account
if (account.person !== undefined) {
this.accountByPersonId.set(account.person, [...(this.accountByPersonId.get(account.person) ?? []), account])
}
}
this.objectById.set(doc._id, doc) this.objectById.set(doc._id, doc)
} }
@ -226,6 +238,37 @@ export abstract class MemDb extends TxProcessor implements Storage {
this.hierarchy.getAncestors(doc._class).forEach((_class) => { this.hierarchy.getAncestors(doc._class).forEach((_class) => {
this.cleanObjectByClass(_class, _id) this.cleanObjectByClass(_class, _id)
}) })
if (this.hierarchy.isDerived(doc._class, core.class.Account)) {
const account = doc as Account
if (account.person !== undefined) {
const acc = this.accountByPersonId.get(account.person) ?? []
this.accountByPersonId.set(
account.person,
acc.filter((it) => it._id !== _id)
)
}
}
}
updateDoc (_id: Ref<Doc>, doc: Doc, update: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>): void {
if (
this.hierarchy.isDerived(doc._class, core.class.Account) &&
update._class === core.class.TxUpdateDoc &&
(update as TxUpdateDoc<Account>).operations.person !== undefined
) {
const account = doc as Account
if (account.person !== undefined) {
const acc = this.accountByPersonId.get(account.person) ?? []
this.accountByPersonId.set(
account.person,
acc.filter((it) => it._id !== _id)
)
}
const newPerson = (update as TxUpdateDoc<Account>).operations.person
if (newPerson !== undefined) {
this.accountByPersonId.set(newPerson, [...(this.accountByPersonId.get(newPerson) ?? []), account])
}
}
} }
} }
@ -297,6 +340,7 @@ export class ModelDb extends MemDb {
const cud = tx as TxUpdateDoc<Doc> const cud = tx as TxUpdateDoc<Doc>
const doc = this.findObject(cud.objectId) const doc = this.findObject(cud.objectId)
if (doc !== undefined) { if (doc !== undefined) {
this.updateDoc(cud.objectId, doc, cud)
TxProcessor.updateDoc2Doc(doc, cud) TxProcessor.updateDoc2Doc(doc, cud)
} else { } else {
ctx.error('no document found, failed to apply model transaction, skipping', { ctx.error('no document found, failed to apply model transaction, skipping', {
@ -320,9 +364,10 @@ export class ModelDb extends MemDb {
break break
case core.class.TxMixin: { case core.class.TxMixin: {
const mix = tx as TxMixin<Doc, Doc> const mix = tx as TxMixin<Doc, Doc>
const obj = this.findObject(mix.objectId) const doc = this.findObject(mix.objectId)
if (obj !== undefined) { if (doc !== undefined) {
TxProcessor.updateMixin4Doc(obj, mix) this.updateDoc(mix.objectId, doc, mix)
TxProcessor.updateMixin4Doc(doc, mix)
} else { } else {
ctx.error('no document found, failed to apply model transaction, skipping', { ctx.error('no document found, failed to apply model transaction, skipping', {
_id: tx._id, _id: tx._id,
@ -339,6 +384,7 @@ export class ModelDb extends MemDb {
protected async txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult> { protected async txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult> {
try { try {
const doc = this.getObject(tx.objectId) as any const doc = this.getObject(tx.objectId) as any
this.updateDoc(tx.objectId, doc, tx)
TxProcessor.updateDoc2Doc(doc, tx) TxProcessor.updateDoc2Doc(doc, tx)
return tx.retrieve === true ? { object: doc } : {} return tx.retrieve === true ? { object: doc } : {}
} catch (err: any) {} } catch (err: any) {}
@ -354,8 +400,9 @@ export class ModelDb extends MemDb {
// TODO: process ancessor mixins // TODO: process ancessor mixins
protected async txMixin (tx: TxMixin<Doc, Doc>): Promise<TxResult> { protected async txMixin (tx: TxMixin<Doc, Doc>): Promise<TxResult> {
const obj = this.getObject(tx.objectId) as any const doc = this.getObject(tx.objectId) as any
TxProcessor.updateMixin4Doc(obj, tx) this.updateDoc(tx.objectId, doc, tx)
TxProcessor.updateMixin4Doc(doc, tx)
return {} return {}
} }
} }

View File

@ -18,10 +18,10 @@ import { Editor, Extensions, getSchema } from '@tiptap/core'
import { generateHTML, generateJSON } from '@tiptap/html' import { generateHTML, generateJSON } from '@tiptap/html'
import { Node as ProseMirrorNode, Schema } from '@tiptap/pm/model' import { Node as ProseMirrorNode, Schema } from '@tiptap/pm/model'
import { defaultExtensions } from '../extensions'
import { MarkupMark, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model'
import { nodeDoc, nodeParagraph, nodeText } from './dsl'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { defaultExtensions } from '../extensions'
import { nodeDoc, nodeParagraph, nodeText } from './dsl'
import { MarkupMark, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model'
/** @public */ /** @public */
export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode()) export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode())
@ -226,9 +226,11 @@ export function pmNodeToHTML (node: ProseMirrorNode, extensions?: Extensions): s
const ELLIPSIS_CHAR = '…' const ELLIPSIS_CHAR = '…'
const WHITESPACE = ' ' const WHITESPACE = ' '
const defaultSchema = getSchema(defaultExtensions)
/** @public */ /** @public */
export function stripTags (markup: Markup, textLimit = 0, extensions: Extensions | undefined = undefined): string { export function stripTags (markup: Markup, textLimit = 0, extensions: Extensions | undefined = undefined): string {
const schema = getSchema(extensions ?? defaultExtensions) const schema = extensions === undefined ? defaultSchema : getSchema(extensions)
const parsed = markupToPmNode(markup, schema) const parsed = markupToPmNode(markup, schema)
const textParts: string[] = [] const textParts: string[] = []

View File

@ -56,13 +56,15 @@ export function yDocContentToNode (
return yDocToNode(ydoc, field, schema, extensions) return yDocToNode(ydoc, field, schema, extensions)
} }
const defaultSchema = getSchema(defaultExtensions)
/** /**
* Get ProseMirror node from Y.Doc * Get ProseMirror node from Y.Doc
* *
* @public * @public
*/ */
export function yDocToNode (ydoc: YDoc, field?: string, schema?: Schema, extensions?: Extensions): Node { export function yDocToNode (ydoc: YDoc, field?: string, schema?: Schema, extensions?: Extensions): Node {
schema ??= getSchema(extensions ?? defaultExtensions) schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions)
try { try {
const body = yDocToProsemirrorJSON(ydoc, field) const body = yDocToProsemirrorJSON(ydoc, field)
@ -79,7 +81,7 @@ export function yDocToNode (ydoc: YDoc, field?: string, schema?: Schema, extensi
* @public * @public
*/ */
export function yDocContentToNodes (content: ArrayBuffer, schema?: Schema, extensions?: Extensions): Node[] { export function yDocContentToNodes (content: ArrayBuffer, schema?: Schema, extensions?: Extensions): Node[] {
schema ??= getSchema(extensions ?? defaultExtensions) schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions)
const nodes: Node[] = [] const nodes: Node[] = []
@ -112,7 +114,7 @@ export function updateYDocContent (
schema?: Schema, schema?: Schema,
extensions?: Extensions extensions?: Extensions
): YDoc | undefined { ): YDoc | undefined {
schema ??= getSchema(extensions ?? defaultExtensions) schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions)
try { try {
const ydoc = new YDoc() const ydoc = new YDoc()
@ -140,7 +142,7 @@ export function updateYDocContent (
* @public * @public
*/ */
export function YDocFromContent (content: MarkupNode, field: string, schema?: Schema, extensions?: Extensions): YDoc { export function YDocFromContent (content: MarkupNode, field: string, schema?: Schema, extensions?: Extensions): YDoc {
schema ??= getSchema(extensions ?? defaultExtensions) schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions)
const res = new YDoc({ gc: false }) const res = new YDoc({ gc: false })

View File

@ -268,8 +268,8 @@ async function kickEmployee (doc: Person): Promise<void> {
const client = getClient() const client = getClient()
const employee = client.getHierarchy().as(doc, contact.mixin.Employee) const employee = client.getHierarchy().as(doc, contact.mixin.Employee)
const email = await client.findOne(contact.class.PersonAccount, { person: doc._id }) const accounts = client.getModel().getAccountByPersonId(doc._id)
if (email === undefined) { if (accounts.length === 0) {
await client.update(employee, { active: false }) await client.update(employee, { active: false })
} else { } else {
showPopup( showPopup(
@ -282,9 +282,12 @@ async function kickEmployee (doc: Person): Promise<void> {
(res?: boolean) => { (res?: boolean) => {
if (res === true) { if (res === true) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
getResource(login.function.LeaveWorkspace).then(async (f) => { const p = getResource(login.function.LeaveWorkspace)
await f(email.email) for (const i of accounts) {
}) void p.then(async (f) => {
await f(i.email)
})
}
} }
} }
) )

View File

@ -320,7 +320,7 @@ function fillStores (): void {
const accountPersonQuery = createQuery(true) const accountPersonQuery = createQuery(true)
const query = createQuery(true) const query = createQuery(true)
query.query(contact.mixin.Employee, { [contact.mixin.Employee + '.active']: { $in: [true, false] } }, (res) => { query.query(contact.mixin.Employee, { active: { $in: [true, false] } }, (res) => {
employeesStore.set(res) employeesStore.set(res)
employeeByIdStore.set(toIdMap(res)) employeeByIdStore.set(toIdMap(res))
}) })

View File

@ -13,7 +13,9 @@
// limitations under the License. // limitations under the License.
// //
import activity, { ActivityMessage, ActivityReference, UserMentionInfo } from '@hcengineering/activity'
import { loadCollaborativeDoc, yDocToBuffer } from '@hcengineering/collaboration' import { loadCollaborativeDoc, yDocToBuffer } from '@hcengineering/collaboration'
import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact'
import core, { import core, {
Account, Account,
AttachedDoc, AttachedDoc,
@ -34,41 +36,30 @@ import core, {
TxProcessor, TxProcessor,
TxRemoveDoc, TxRemoveDoc,
TxUpdateDoc, TxUpdateDoc,
Type Type,
type MeasureContext
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { CommonInboxNotification, MentionInboxNotification } from '@hcengineering/notification' import notification, { CommonInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
import { StorageAdapter, TriggerControl } from '@hcengineering/server-core'
import { import {
applyNotificationProviders,
getCommonNotificationTxes,
getNotificationContent,
getNotificationProviderControl,
getPushCollaboratorTx,
isShouldNotifyTx,
NotifyResult,
shouldNotifyCommon,
toReceiverInfo,
type NotificationProviderControl
} from '@hcengineering/server-notification-resources'
import {
areEqualJson,
extractReferences, extractReferences,
markupToPmNode, markupToPmNode,
pmNodeToMarkup, pmNodeToMarkup,
yDocContentToNodes, yDocContentToNodes
areEqualJson
} from '@hcengineering/text' } from '@hcengineering/text'
import { StorageAdapter, TriggerControl } from '@hcengineering/server-core'
import activity, { ActivityMessage, ActivityReference, UserMentionInfo } from '@hcengineering/activity'
import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact'
import {
getCommonNotificationTxes,
getPushCollaboratorTx,
shouldNotifyCommon,
isShouldNotifyTx,
NotifyResult,
applyNotificationProviders,
getNotificationContent,
toReceiverInfo
} from '@hcengineering/server-notification-resources'
async function getPersonAccount (person: Ref<Person>, control: TriggerControl): Promise<PersonAccount | undefined> {
return (
await control.modelDb.findAll(
contact.class.PersonAccount,
{
person
},
{ limit: 1 }
)
)[0]
}
export function isDocMentioned (doc: Ref<Doc>, content: string | Buffer): boolean { export function isDocMentioned (doc: Ref<Doc>, content: string | Buffer): boolean {
const references = [] const references = []
@ -93,20 +84,22 @@ export function isDocMentioned (doc: Ref<Doc>, content: string | Buffer): boolea
} }
export async function getPersonNotificationTxes ( export async function getPersonNotificationTxes (
ctx: MeasureContext,
reference: Data<ActivityReference>, reference: Data<ActivityReference>,
control: TriggerControl, control: TriggerControl,
senderId: Ref<Account>, senderId: Ref<Account>,
space: Ref<Space>, space: Ref<Space>,
originTx: TxCUD<Doc> originTx: TxCUD<Doc>,
notificationControl: NotificationProviderControl
): Promise<Tx[]> { ): Promise<Tx[]> {
const receiverPersonId = reference.attachedTo as Ref<Person> const receiverPersonId = reference.attachedTo as Ref<Person>
const receiver = await getPersonAccount(receiverPersonId, control) const receiver = control.modelDb.getAccountByPersonId(receiverPersonId) as PersonAccount[]
if (receiver === undefined) { if (receiver.length === 0) {
return [] return []
} }
if (receiver._id === senderId) { if (receiver.some((it) => it._id === senderId)) {
return [] return []
} }
@ -120,14 +113,21 @@ export async function getPersonNotificationTxes (
const doc = (await control.findAll(reference.srcDocClass, { _id: reference.srcDocId }))[0] const doc = (await control.findAll(reference.srcDocClass, { _id: reference.srcDocId }))[0]
const receiverPerson = ( const receiverPerson = (
await control.findAll(contact.mixin.Employee, { _id: receiver.person as Ref<Employee>, active: true }, { limit: 1 }) await control.findAll(
contact.mixin.Employee,
{ _id: receiverPersonId as Ref<Employee>, active: true },
{ limit: 1 }
)
)[0] )[0]
if (receiverPerson === undefined) return res if (receiverPerson === undefined) return res
const receiverSpace = (await control.findAll(contact.class.PersonSpace, { person: receiver.person }, { limit: 1 }))[0] const receiverSpace = (
await control.findAll(contact.class.PersonSpace, { person: receiverPersonId }, { limit: 1 })
)[0]
if (receiverSpace === undefined) return res if (receiverSpace === undefined) return res
const collaboratorsTx = await getCollaboratorsTxes(reference, control, receiver, doc) // TODO: Do we need for all or just one?
const collaboratorsTx = await getCollaboratorsTxes(reference, control, receiver[0], doc)
res.push(...collaboratorsTx) res.push(...collaboratorsTx)
@ -159,13 +159,13 @@ export async function getPersonNotificationTxes (
}) })
) )
} }
// TODO: Select a proper reciever
const data: Omit<Data<MentionInboxNotification>, 'docNotifyContext'> = { const data: Omit<Data<MentionInboxNotification>, 'docNotifyContext'> = {
header: activity.string.MentionedYouIn, header: activity.string.MentionedYouIn,
messageHtml: reference.message, messageHtml: reference.message,
mentionedIn: reference.attachedDocId ?? reference.srcDocId, mentionedIn: reference.attachedDocId ?? reference.srcDocId,
mentionedInClass: reference.attachedDocClass ?? reference.srcDocClass, mentionedInClass: reference.attachedDocClass ?? reference.srcDocClass,
user: receiver._id, user: receiver[0]._id,
isViewed: false, isViewed: false,
archived: false archived: false
} }
@ -180,8 +180,8 @@ export async function getPersonNotificationTxes (
: undefined : undefined
const receiverInfo = toReceiverInfo(control.hierarchy, { const receiverInfo = toReceiverInfo(control.hierarchy, {
_id: receiver._id, _id: receiver[0]._id,
account: receiver, account: receiver[0],
person: receiverPerson, person: receiverPerson,
space: receiverSpace._id space: receiverSpace._id
}) })
@ -193,8 +193,20 @@ export async function getPersonNotificationTxes (
person: senderPerson person: senderPerson
} }
const notifyResult = await shouldNotifyCommon(control, receiver._id, notification.ids.MentionCommonNotificationType) const notifyResult = await shouldNotifyCommon(
const messageNotifyResult = await getMessageNotifyResult(reference, receiver, control, originTx, doc) control,
receiver.map((it) => it._id),
notification.ids.MentionCommonNotificationType,
notificationControl
)
const messageNotifyResult = await getMessageNotifyResult(
reference,
receiver,
control,
originTx,
doc,
notificationControl
)
for (const [provider] of messageNotifyResult.entries()) { for (const [provider] of messageNotifyResult.entries()) {
if (notifyResult.has(provider)) { if (notifyResult.has(provider)) {
@ -204,6 +216,7 @@ export async function getPersonNotificationTxes (
if (notifyResult.has(notification.providers.InboxNotificationProvider)) { if (notifyResult.has(notification.providers.InboxNotificationProvider)) {
const txes = await getCommonNotificationTxes( const txes = await getCommonNotificationTxes(
ctx,
control, control,
doc, doc,
data, data,
@ -221,7 +234,7 @@ export async function getPersonNotificationTxes (
const context = ( const context = (
await control.findAll( await control.findAll(
notification.class.DocNotifyContext, notification.class.DocNotifyContext,
{ objectId: reference.srcDocId, user: receiver._id }, { objectId: reference.srcDocId, user: { $in: receiver.map((it) => it._id) } },
{ projection: { _id: 1 } } { projection: { _id: 1 } }
) )
)[0] )[0]
@ -255,21 +268,24 @@ export async function getPersonNotificationTxes (
} }
async function checkSpace ( async function checkSpace (
user: PersonAccount, users: PersonAccount[],
spaceId: Ref<Space>, spaceId: Ref<Space>,
control: TriggerControl, control: TriggerControl,
res: Tx[] res: Tx[]
): Promise<boolean> { ): Promise<boolean> {
const space = (await control.findAll<Space>(core.class.Space, { _id: spaceId }, { limit: 1 }))[0] const space = (await control.findAll<Space>(core.class.Space, { _id: spaceId }, { limit: 1 }))[0]
const isMember = space.members.includes(user._id) const toAdd = users.filter((user) => !space.members.includes(user._id))
const isMember = toAdd.length === 0
if (space.private) { if (space.private) {
return isMember return isMember
} }
if (!isMember) { if (!isMember) {
res.push( for (const user of toAdd) {
control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { $push: { members: user._id } }) res.push(
) control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { $push: { members: user._id } })
)
}
} }
return true return true
@ -327,10 +343,11 @@ async function getCollaboratorsTxes (
async function getMessageNotifyResult ( async function getMessageNotifyResult (
reference: Data<ActivityReference>, reference: Data<ActivityReference>,
account: PersonAccount, account: PersonAccount[],
control: TriggerControl, control: TriggerControl,
originTx: TxCUD<Doc>, originTx: TxCUD<Doc>,
doc: Doc doc: Doc,
notificationControl: NotificationProviderControl
): Promise<NotifyResult> { ): Promise<NotifyResult> {
const { hierarchy } = control const { hierarchy } = control
const tx = TxProcessor.extractTx(originTx) as TxCUD<Doc> const tx = TxProcessor.extractTx(originTx) as TxCUD<Doc>
@ -345,7 +362,7 @@ async function getMessageNotifyResult (
const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators) const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators)
if (mixin === undefined || !mixin.collaborators.includes(account._id)) { if (mixin === undefined || !account.some((account) => mixin.collaborators.includes(account._id))) {
return new Map() return new Map()
} }
@ -353,7 +370,7 @@ async function getMessageNotifyResult (
return new Map() return new Map()
} }
return await isShouldNotifyTx(control, tx, originTx, doc, account, false, false, undefined) return await isShouldNotifyTx(control, tx, originTx, doc, account, false, false, notificationControl, undefined)
} }
function isMarkupType (type: Ref<Class<Type<any>>>): boolean { function isMarkupType (type: Ref<Class<Type<any>>>): boolean {
@ -365,6 +382,7 @@ function isCollaborativeType (type: Ref<Class<Type<any>>>): boolean {
} }
async function getCreateReferencesTxes ( async function getCreateReferencesTxes (
ctx: MeasureContext,
control: TriggerControl, control: TriggerControl,
storage: StorageAdapter, storage: StorageAdapter,
txFactory: TxFactory, txFactory: TxFactory,
@ -410,10 +428,11 @@ async function getCreateReferencesTxes (
? (srcDocId as Ref<Space>) ? (srcDocId as Ref<Space>)
: srcDocSpace : srcDocSpace
return await getReferencesTxes(control, txFactory, refs, refSpace, [], [], originTx) return await getReferencesTxes(ctx, control, txFactory, refs, refSpace, [], [], originTx)
} }
async function getUpdateReferencesTxes ( async function getUpdateReferencesTxes (
ctx: MeasureContext,
control: TriggerControl, control: TriggerControl,
storage: StorageAdapter, storage: StorageAdapter,
txFactory: TxFactory, txFactory: TxFactory,
@ -474,7 +493,7 @@ async function getUpdateReferencesTxes (
? (srcDocId as Ref<Space>) ? (srcDocId as Ref<Space>)
: srcDocSpace : srcDocSpace
return await getReferencesTxes(control, txFactory, references, refSpace, current, userMentions, originTx) return await getReferencesTxes(ctx, control, txFactory, references, refSpace, current, userMentions, originTx)
} }
return [] return []
@ -519,14 +538,16 @@ export function getReferencesData (
} }
async function createReferenceTxes ( async function createReferenceTxes (
ctx: MeasureContext,
control: TriggerControl, control: TriggerControl,
txFactory: TxFactory, txFactory: TxFactory,
ref: Data<ActivityReference>, ref: Data<ActivityReference>,
space: Ref<Space>, space: Ref<Space>,
originTx: TxCUD<Doc> originTx: TxCUD<Doc>,
notificationControl: NotificationProviderControl
): Promise<Tx[]> { ): Promise<Tx[]> {
if (control.hierarchy.isDerived(ref.attachedToClass, contact.class.Person)) { if (control.hierarchy.isDerived(ref.attachedToClass, contact.class.Person)) {
return await getPersonNotificationTxes(ref, control, txFactory.account, space, originTx) return await getPersonNotificationTxes(ctx, ref, control, txFactory.account, space, originTx, notificationControl)
} }
const refTx = control.txFactory.createTxCreateDoc(activity.class.ActivityReference, space, ref) const refTx = control.txFactory.createTxCreateDoc(activity.class.ActivityReference, space, ref)
@ -536,6 +557,7 @@ async function createReferenceTxes (
} }
async function getReferencesTxes ( async function getReferencesTxes (
ctx: MeasureContext,
control: TriggerControl, control: TriggerControl,
txFactory: TxFactory, txFactory: TxFactory,
references: Data<ActivityReference>[], references: Data<ActivityReference>[],
@ -568,6 +590,8 @@ async function getReferencesTxes (
} }
} }
const notificationControl = await getNotificationProviderControl(ctx, control)
for (const mention of mentions) { for (const mention of mentions) {
const refIndex = references.findIndex( const refIndex = references.findIndex(
(r) => mention.user === r.attachedTo && mention.attachedTo === r.attachedDocId (r) => mention.user === r.attachedTo && mention.attachedTo === r.attachedDocId
@ -588,7 +612,7 @@ async function getReferencesTxes (
// Add missing references // Add missing references
for (const ref of references) { for (const ref of references) {
txes.push(...(await createReferenceTxes(control, txFactory, ref, space, originTx))) txes.push(...(await createReferenceTxes(ctx, control, txFactory, ref, space, originTx, notificationControl)))
} }
return txes return txes
@ -659,6 +683,7 @@ async function ActivityReferenceCreate (tx: TxCUD<Doc>, etx: TxCUD<Doc>, control
const targetTx = guessReferenceTx(control.hierarchy, tx) const targetTx = guessReferenceTx(control.hierarchy, tx)
const txes: Tx[] = await getCreateReferencesTxes( const txes: Tx[] = await getCreateReferencesTxes(
control.ctx,
control, control,
control.storageAdapter, control.storageAdapter,
txFactory, txFactory,
@ -706,6 +731,7 @@ async function ActivityReferenceUpdate (tx: TxCUD<Doc>, etx: TxCUD<Doc>, control
const targetTx = guessReferenceTx(control.hierarchy, tx) const targetTx = guessReferenceTx(control.hierarchy, tx)
const txes: Tx[] = await getUpdateReferencesTxes( const txes: Tx[] = await getUpdateReferencesTxes(
control.ctx,
control, control,
control.storageAdapter, control.storageAdapter,
txFactory, txFactory,

View File

@ -22,7 +22,7 @@ import chunter, {
ChunterSpace, ChunterSpace,
ThreadMessage ThreadMessage
} from '@hcengineering/chunter' } from '@hcengineering/chunter'
import contact, { Person, PersonAccount } from '@hcengineering/contact' import { Person, PersonAccount } from '@hcengineering/contact'
import core, { import core, {
Account, Account,
AttachedDoc, AttachedDoc,
@ -56,7 +56,7 @@ import {
import { markupToHTML, markupToText, stripTags } from '@hcengineering/text' import { markupToHTML, markupToText, stripTags } from '@hcengineering/text'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification' import { getPersonAccountById, NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
import { encodeObjectURI } from '@hcengineering/view' import { encodeObjectURI } from '@hcengineering/view'
const updateChatInfoDelay = 12 * 60 * 60 * 1000 // 12 hours const updateChatInfoDelay = 12 * 60 * 60 * 1000 // 12 hours
@ -121,13 +121,8 @@ export async function CommentRemove (
} }
async function OnThreadMessageCreated (originTx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> { async function OnThreadMessageCreated (originTx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
const tx = TxProcessor.extractTx(originTx) as TxCreateDoc<ThreadMessage> const tx = TxProcessor.extractTx(originTx) as TxCreateDoc<ThreadMessage>
if (tx._class !== core.class.TxCreateDoc || !hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage)) {
return []
}
const threadMessage = TxProcessor.createDoc2Doc(tx) const threadMessage = TxProcessor.createDoc2Doc(tx)
const message = (await control.findAll(activity.class.ActivityMessage, { _id: threadMessage.attachedTo }))[0] const message = (await control.findAll(activity.class.ActivityMessage, { _id: threadMessage.attachedTo }))[0]
@ -166,13 +161,6 @@ async function OnChatMessageCreated (tx: TxCUD<Doc>, control: TriggerControl): P
const hierarchy = control.hierarchy const hierarchy = control.hierarchy
const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<ChatMessage> const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<ChatMessage>
if (
actualTx._class !== core.class.TxCreateDoc ||
!hierarchy.isDerived(actualTx.objectClass, chunter.class.ChatMessage)
) {
return []
}
const message = TxProcessor.createDoc2Doc(actualTx) const message = TxProcessor.createDoc2Doc(actualTx)
const mixin = hierarchy.classHierarchyMixin(message.attachedToClass, notification.mixin.ClassCollaborators) const mixin = hierarchy.classHierarchyMixin(message.attachedToClass, notification.mixin.ClassCollaborators)
@ -244,13 +232,8 @@ function joinChannel (control: TriggerControl, channel: Channel, user: Ref<Accou
} }
async function OnThreadMessageDeleted (tx: Tx, control: TriggerControl): Promise<Tx[]> { async function OnThreadMessageDeleted (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
const removeTx = TxProcessor.extractTx(tx) as TxRemoveDoc<ThreadMessage> const removeTx = TxProcessor.extractTx(tx) as TxRemoveDoc<ThreadMessage>
if (!hierarchy.isDerived(removeTx.objectClass, chunter.class.ThreadMessage)) {
return []
}
const message = control.removedMap.get(removeTx.objectId) as ThreadMessage const message = control.removedMap.get(removeTx.objectId) as ThreadMessage
if (message === undefined) { if (message === undefined) {
@ -286,15 +269,40 @@ async function OnThreadMessageDeleted (tx: Tx, control: TriggerControl): Promise
*/ */
export async function ChunterTrigger (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> { export async function ChunterTrigger (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = [] const res: Tx[] = []
res.push( const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<ChatMessage>
...(await control.ctx.with('OnThreadMessageCreated', {}, async (ctx) => await OnThreadMessageCreated(tx, control)))
) if (
res.push( actualTx._class === core.class.TxCreateDoc &&
...(await control.ctx.with('OnThreadMessageDeleted', {}, async (ctx) => await OnThreadMessageDeleted(tx, control))) control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage)
) ) {
res.push( res.push(
...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control))) ...(await control.ctx.with(
) 'OnThreadMessageCreated',
{},
async (ctx) => await OnThreadMessageCreated(tx, control)
))
)
}
if (
actualTx._class === core.class.TxRemoveDoc &&
control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage)
) {
res.push(
...(await control.ctx.with(
'OnThreadMessageDeleted',
{},
async (ctx) => await OnThreadMessageDeleted(tx, control)
))
)
}
if (
actualTx._class === core.class.TxCreateDoc &&
control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ChatMessage)
) {
res.push(
...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control)))
)
}
return res return res
} }
@ -455,7 +463,7 @@ async function hideOldChannels (
} }
export async function updateChatInfo (control: TriggerControl, status: UserStatus, date: Timestamp): Promise<void> { export async function updateChatInfo (control: TriggerControl, status: UserStatus, date: Timestamp): Promise<void> {
const account = await control.modelDb.findOne(contact.class.PersonAccount, { _id: status.user as Ref<PersonAccount> }) const account = getPersonAccountById(status.user as Ref<PersonAccount>, control)
if (account === undefined) return if (account === undefined) return
const update = (await control.findAll(chunter.class.ChatInfo, { user: account.person })).shift() const update = (await control.findAll(chunter.class.ChatInfo, { user: account.person })).shift()
@ -559,14 +567,14 @@ async function OnContextUpdate (tx: TxUpdateDoc<DocNotifyContext>, control: Trig
return [] return []
} }
async function JoinChannelTypeMatch (originTx: Tx, _: Doc, user: Ref<Account>): Promise<boolean> { function JoinChannelTypeMatch (originTx: Tx, _: Doc, user: Ref<Account>[]): boolean {
if (originTx.modifiedBy === user) return false if (user.some((it) => originTx.modifiedBy === it)) return false
if (originTx._class !== core.class.TxUpdateDoc) return false if (originTx._class !== core.class.TxUpdateDoc) return false
const tx = originTx as TxUpdateDoc<Channel> const tx = originTx as TxUpdateDoc<Channel>
const added = combineAttributes([tx.operations], 'members', '$push', '$each') const added = combineAttributes([tx.operations], 'members', '$push', '$each')
return added.includes(user) return user.some((it) => added.includes(it))
} }
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type

View File

@ -21,13 +21,13 @@ import contact, {
Organization, Organization,
Person, Person,
PersonAccount, PersonAccount,
PersonSpace,
contactId, contactId,
formatContactName, formatContactName,
formatName, formatName,
getFirstName, getFirstName,
getLastName, getLastName,
getName, getName
PersonSpace
} from '@hcengineering/contact' } from '@hcengineering/contact'
import core, { import core, {
Account, Account,
@ -37,13 +37,13 @@ import core, {
Ref, Ref,
SpaceType, SpaceType,
Tx, Tx,
TxCUD,
TxCreateDoc, TxCreateDoc,
TxMixin, TxMixin,
TxProcessor, TxProcessor,
TxRemoveDoc, TxRemoveDoc,
TxUpdateDoc, TxUpdateDoc,
concatLink, concatLink
TxCUD
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { Collaborators } from '@hcengineering/notification' import notification, { Collaborators } from '@hcengineering/notification'
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
@ -85,41 +85,49 @@ export async function OnSpaceTypeMembers (tx: Tx, control: TriggerControl): Prom
export async function OnEmployeeCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnEmployeeCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const mixinTx = tx as TxMixin<Person, Employee> const mixinTx = tx as TxMixin<Person, Employee>
if (mixinTx.attributes.active !== true) return [] if (mixinTx.attributes.active !== true) return []
const acc = await control.modelDb.findOne(contact.class.PersonAccount, { person: mixinTx.objectId }) const acc = control.modelDb.getAccountByPersonId(mixinTx.objectId)
if (acc === undefined) return [] if (acc.length === 0) return []
const spaces = await control.findAll(core.class.Space, { autoJoin: true }) const spaces = await control.findAll(core.class.Space, { autoJoin: true })
const result: Tx[] = [] const result: Tx[] = []
const txes = await createPersonSpace(acc._id, mixinTx.objectId, control) const txes = await createPersonSpace(
acc.map((it) => it._id),
mixinTx.objectId,
control
)
result.push(...txes) result.push(...txes)
for (const space of spaces) { for (const space of spaces) {
if (space.members.includes(acc._id)) continue const toAdd = acc.filter((it) => !space.members.includes(it._id))
const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { if (toAdd.length === 0) continue
$push: { for (const a of toAdd) {
members: acc._id const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
} $push: {
}) members: a._id
result.push(pushTx) }
})
result.push(pushTx)
}
} }
return result return result
} }
async function createPersonSpace ( async function createPersonSpace (
account: Ref<Account>, account: Ref<Account>[],
person: Ref<Person>, person: Ref<Person>,
control: TriggerControl control: TriggerControl
): Promise<TxCUD<PersonSpace>[]> { ): Promise<TxCUD<PersonSpace>[]> {
const personSpace = (await control.findAll(contact.class.PersonSpace, { person }, { limit: 1 })).shift() const personSpace = (await control.findAll(contact.class.PersonSpace, { person }, { limit: 1 })).shift()
if (personSpace !== undefined) { if (personSpace !== undefined) {
if (personSpace.members.includes(account)) return [] const toAdd = account.filter((it) => !personSpace.members.includes(it))
return [ if (toAdd.length === 0) return []
return toAdd.map((it) =>
control.txFactory.createTxUpdateDoc(personSpace._class, personSpace.space, personSpace._id, { control.txFactory.createTxUpdateDoc(personSpace._class, personSpace.space, personSpace._id, {
$push: { $push: {
members: account members: it
} }
}) })
] )
} }
return [ return [
@ -129,7 +137,7 @@ async function createPersonSpace (
private: true, private: true,
archived: false, archived: false,
person, person,
members: [account] members: account
}) })
] ]
} }
@ -143,7 +151,7 @@ export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): P
const spaces = await control.findAll(core.class.Space, { autoJoin: true }) const spaces = await control.findAll(core.class.Space, { autoJoin: true })
const result: Tx[] = [] const result: Tx[] = []
const txes = await createPersonSpace(acc._id, person._id, control) const txes = await createPersonSpace([acc._id], person._id, control)
result.push(...txes) result.push(...txes)

View File

@ -88,13 +88,13 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
/** /**
* @public * @public
*/ */
export async function IsIncomingMessage ( export function IsIncomingMessageTypeMatch (
tx: Tx, tx: Tx,
doc: Doc, doc: Doc,
user: Ref<Account>, user: Ref<Account>,
type: NotificationType, type: NotificationType,
control: TriggerControl control: TriggerControl
): Promise<boolean> { ): boolean {
const message = TxProcessor.createDoc2Doc(TxProcessor.extractTx(tx) as TxCreateDoc<Message>) const message = TxProcessor.createDoc2Doc(TxProcessor.extractTx(tx) as TxCreateDoc<Message>)
return message.incoming && message.sendOn > (doc.createdOn ?? doc.modifiedOn) return message.incoming && message.sendOn > (doc.createdOn ?? doc.modifiedOn)
} }
@ -183,7 +183,7 @@ export default async () => ({
OnMessageCreate OnMessageCreate
}, },
function: { function: {
IsIncomingMessage, IsIncomingMessageTypeMatch,
FindMessages, FindMessages,
SendEmailNotifications SendEmailNotifications
} }

View File

@ -32,7 +32,7 @@ export default plugin(serverGmailId, {
OnMessageCreate: '' as Resource<TriggerFunc> OnMessageCreate: '' as Resource<TriggerFunc>
}, },
function: { function: {
IsIncomingMessage: '' as TypeMatchFunc, IsIncomingMessageTypeMatch: '' as TypeMatchFunc,
FindMessages: '' as Resource<ObjectDDParticipantFunc>, FindMessages: '' as Resource<ObjectDDParticipantFunc>,
SendEmailNotifications: '' as Resource<NotificationProviderFunc> SendEmailNotifications: '' as Resource<NotificationProviderFunc>
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import contact, { Contact, Employee, Person, PersonAccount, formatName, getName } from '@hcengineering/contact' import contact, { Contact, Employee, formatName, getName, Person, PersonAccount } from '@hcengineering/contact'
import core, { import core, {
Doc, Doc,
Ref, Ref,
@ -27,6 +27,7 @@ import core, {
TxRemoveDoc, TxRemoveDoc,
TxUpdateDoc TxUpdateDoc
} from '@hcengineering/core' } from '@hcengineering/core'
import gmail from '@hcengineering/gmail'
import hr, { import hr, {
Department, Department,
DepartmentMember, DepartmentMember,
@ -39,10 +40,13 @@ import hr, {
import notification, { NotificationType } from '@hcengineering/notification' import notification, { NotificationType } from '@hcengineering/notification'
import { translate } from '@hcengineering/platform' import { translate } from '@hcengineering/platform'
import { TriggerControl } from '@hcengineering/server-core' import { TriggerControl } from '@hcengineering/server-core'
import { getEmployee, getPersonAccountById } from '@hcengineering/server-notification'
import { getContentByTemplate, isAllowed } from '@hcengineering/server-notification-resources'
import gmail from '@hcengineering/gmail'
import { sendEmailNotification } from '@hcengineering/server-gmail-resources' import { sendEmailNotification } from '@hcengineering/server-gmail-resources'
import { getEmployee, getPersonAccountById } from '@hcengineering/server-notification'
import {
getContentByTemplate,
getNotificationProviderControl,
isAllowed
} from '@hcengineering/server-notification-resources'
async function getOldDepartment ( async function getOldDepartment (
currentTx: TxMixin<Employee, Staff> | TxUpdateDoc<Employee>, currentTx: TxMixin<Employee, Staff> | TxUpdateDoc<Employee>,
@ -97,21 +101,29 @@ function exlude (first: Ref<Department>[], second: Ref<Department>[]): Ref<Depar
function getTxes ( function getTxes (
factory: TxFactory, factory: TxFactory,
account: Ref<DepartmentMember>, account: Ref<DepartmentMember>[],
added: Ref<Department>[], added: Ref<Department>[],
removed?: Ref<Department>[] removed?: Ref<Department>[]
): Tx[] { ): Tx[] {
const pushTxes = added.map((dep) => const pushTxes = added
factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep, { .map((dep) =>
$push: { members: account } account.map((it) =>
}) factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep, {
) $push: { members: it }
})
)
)
.flat()
if (removed === undefined) return pushTxes if (removed === undefined) return pushTxes
const pullTxes = removed.map((dep) => const pullTxes = removed
factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep, { .map((dep) =>
$pull: { members: account } account.map((it) =>
}) factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep, {
) $pull: { members: it }
})
)
)
.flat()
return [...pullTxes, ...pushTxes] return [...pullTxes, ...pushTxes]
} }
@ -121,12 +133,8 @@ function getTxes (
export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxMixin<Employee, Staff> const ctx = TxProcessor.extractTx(tx) as TxMixin<Employee, Staff>
const targetAccount = ( const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[]
await control.modelDb.findAll(contact.class.PersonAccount, { if (targetAccount.length === 0) return []
person: ctx.objectId
})
)[0]
if (targetAccount === undefined) return []
if (ctx.attributes.department !== undefined) { if (ctx.attributes.department !== undefined) {
const lastDepartment = await getOldDepartment(ctx, control) const lastDepartment = await getOldDepartment(ctx, control)
@ -137,7 +145,7 @@ export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promi
const removed = await buildHierarchy(lastDepartment, control) const removed = await buildHierarchy(lastDepartment, control)
return getTxes( return getTxes(
control.txFactory, control.txFactory,
targetAccount._id, targetAccount.map((it) => it._id),
[], [],
removed.map((p) => p._id) removed.map((p) => p._id)
) )
@ -146,13 +154,22 @@ export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promi
const push = (await buildHierarchy(departmentId, control)).map((p) => p._id) const push = (await buildHierarchy(departmentId, control)).map((p) => p._id)
if (lastDepartment === undefined) { if (lastDepartment === undefined) {
return getTxes(control.txFactory, targetAccount._id, push) return getTxes(
control.txFactory,
targetAccount.map((it) => it._id),
push
)
} }
let removed = (await buildHierarchy(lastDepartment, control)).map((p) => p._id) let removed = (await buildHierarchy(lastDepartment, control)).map((p) => p._id)
const added = exlude(removed, push) const added = exlude(removed, push)
removed = exlude(push, removed) removed = exlude(push, removed)
return getTxes(control.txFactory, targetAccount._id, added, removed) return getTxes(
control.txFactory,
targetAccount.map((it) => it._id),
added,
removed
)
} }
return [] return []
@ -183,16 +200,14 @@ export async function OnDepartmentRemove (tx: Tx, control: TriggerControl): Prom
employee.forEach((em) => { employee.forEach((em) => {
res.push(control.txFactory.createTxMixin(em._id, em._class, em.space, hr.mixin.Staff, { department: undefined })) res.push(control.txFactory.createTxMixin(em._id, em._class, em.space, hr.mixin.Staff, { department: undefined }))
}) })
targetAccounts.forEach((acc) => { res.push(
res.push( ...getTxes(
...getTxes( control.txFactory,
control.txFactory, targetAccounts.map((it) => it._id),
acc._id, [],
[], removed.map((p) => p._id)
removed.map((p) => p._id)
)
) )
}) )
return res return res
} }
@ -232,19 +247,15 @@ export async function OnEmployeeDeactivate (tx: Tx, control: TriggerControl): Pr
return [] return []
} }
const targetAccount = ( const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[]
await control.modelDb.findAll(contact.class.PersonAccount, { if (targetAccount.length === 0) return []
person: ctx.objectId
})
)[0]
if (targetAccount === undefined) return []
const lastDepartment = await getOldDepartment(ctx, control) const lastDepartment = await getOldDepartment(ctx, control)
if (lastDepartment === undefined) return [] if (lastDepartment === undefined) return []
const removed = await buildHierarchy(lastDepartment, control) const removed = await buildHierarchy(lastDepartment, control)
return getTxes( return getTxes(
control.txFactory, control.txFactory,
targetAccount._id, targetAccount.map((it) => it._id),
[], [],
removed.map((p) => p._id) removed.map((p) => p._id)
) )
@ -268,19 +279,21 @@ async function sendEmailNotifications (
} }
// should respect employee settings // should respect employee settings
const accounts = await control.modelDb.findAll(contact.class.PersonAccount, {
person: { $in: Array.from(contacts.values()) as Ref<Employee>[] }
})
const type = await control.modelDb.findOne(notification.class.NotificationType, { _id: typeId }) const type = await control.modelDb.findOne(notification.class.NotificationType, { _id: typeId })
if (type === undefined) return if (type === undefined) return
const provider = await control.modelDb.findOne(notification.class.NotificationProvider, { const provider = await control.modelDb.findOne(notification.class.NotificationProvider, {
_id: gmail.providers.EmailNotificationProvider _id: gmail.providers.EmailNotificationProvider
}) })
if (provider === undefined) return if (provider === undefined) return
for (const account of accounts) {
const allowed = await isAllowed(control, account._id, type, provider) const notificationControl = await getNotificationProviderControl(control.ctx, control)
if (!allowed) { for (const accountId of contacts.values()) {
contacts.delete(account.person) const accounts = control.modelDb.getAccountByPersonId(accountId) as PersonAccount[]
for (const account of accounts) {
const allowed = isAllowed(control, account._id, type, provider, notificationControl)
if (!allowed) {
contacts.delete(account.person)
}
} }
} }
@ -306,7 +319,7 @@ async function sendEmailNotifications (
export async function OnRequestCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnRequestCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Request> const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Request>
const sender = await getPersonAccountById(ctx.modifiedBy, control) const sender = getPersonAccountById(ctx.modifiedBy, control)
if (sender === undefined) return [] if (sender === undefined) return []
const request = TxProcessor.createDoc2Doc(ctx) const request = TxProcessor.createDoc2Doc(ctx)
@ -321,7 +334,7 @@ export async function OnRequestCreate (tx: Tx, control: TriggerControl): Promise
export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc<Request> const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc<Request>
const sender = await getPersonAccountById(ctx.modifiedBy, control) const sender = getPersonAccountById(ctx.modifiedBy, control)
if (sender === undefined) return [] if (sender === undefined) return []
const request = (await control.findAll(hr.class.Request, { _id: ctx.objectId }))[0] as Request const request = (await control.findAll(hr.class.Request, { _id: ctx.objectId }))[0] as Request
@ -337,7 +350,7 @@ export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise
export async function OnRequestRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnRequestRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Request> const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Request>
const sender = await getPersonAccountById(ctx.modifiedBy, control) const sender = getPersonAccountById(ctx.modifiedBy, control)
if (sender === undefined) return [] if (sender === undefined) return []
const request = control.removedMap.get(ctx.objectId) as Request const request = control.removedMap.get(ctx.objectId) as Request
@ -389,7 +402,7 @@ export async function RequestTextPresenter (doc: Doc, control: TriggerControl):
export async function OnPublicHolidayCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnPublicHolidayCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<PublicHoliday> const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<PublicHoliday>
const sender = await getPersonAccountById(ctx.modifiedBy, control) const sender = getPersonAccountById(ctx.modifiedBy, control)
if (sender === undefined) return [] if (sender === undefined) return []
const employee = await getEmployee(sender.person as Ref<Employee>, control) const employee = await getEmployee(sender.person as Ref<Employee>, control)
if (employee === undefined) return [] if (employee === undefined) return []
@ -410,7 +423,7 @@ export async function OnPublicHolidayCreate (tx: Tx, control: TriggerControl): P
*/ */
export async function PublicHolidayHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> { export async function PublicHolidayHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const holiday = doc as PublicHoliday const holiday = doc as PublicHoliday
const sender = await getPersonAccountById(holiday.modifiedBy, control) const sender = getPersonAccountById(holiday.modifiedBy, control)
if (sender === undefined) return '' if (sender === undefined) return ''
const employee = await getEmployee(sender.person as Ref<Employee>, control) const employee = await getEmployee(sender.person as Ref<Employee>, control)
if (employee === undefined) return '' if (employee === undefined) return ''
@ -426,7 +439,7 @@ export async function PublicHolidayHTMLPresenter (doc: Doc, control: TriggerCont
*/ */
export async function PublicHolidayTextPresenter (doc: Doc, control: TriggerControl): Promise<string> { export async function PublicHolidayTextPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const holiday = doc as PublicHoliday const holiday = doc as PublicHoliday
const sender = await getPersonAccountById(holiday.modifiedBy, control) const sender = getPersonAccountById(holiday.modifiedBy, control)
if (sender === undefined) return '' if (sender === undefined) return ''
const employee = await getEmployee(sender.person as Ref<Employee>, control) const employee = await getEmployee(sender.person as Ref<Employee>, control)
if (employee === undefined) return '' if (employee === undefined) return ''

View File

@ -37,7 +37,11 @@ import love, {
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { translate } from '@hcengineering/platform' import { translate } from '@hcengineering/platform'
import { TriggerControl } from '@hcengineering/server-core' import { TriggerControl } from '@hcengineering/server-core'
import { createPushNotification, isAllowed } from '@hcengineering/server-notification-resources' import {
createPushNotification,
getNotificationProviderControl,
isAllowed
} from '@hcengineering/server-notification-resources'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
export async function OnEmployee (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnEmployee (tx: Tx, control: TriggerControl): Promise<Tx[]> {
@ -267,16 +271,19 @@ export async function OnKnock (tx: Tx, control: TriggerControl): Promise<Tx[]> {
_id: notification.providers.PushNotificationProvider _id: notification.providers.PushNotificationProvider
}) })
if (provider === undefined) return [] if (provider === undefined) return []
const notificationControl = await getNotificationProviderControl(control.ctx, control)
for (const user of roomInfo.persons) { for (const user of roomInfo.persons) {
const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: user }) const userAcc = control.modelDb.getAccountByPersonId(user) as PersonAccount[]
if (userAcc === undefined) continue if (userAcc.length === 0) continue
if (await isAllowed(control, userAcc._id, type, provider)) { if (userAcc.some((it) => isAllowed(control, it._id, type, provider, notificationControl))) {
const path = [workbenchId, control.workspace.workspaceUrl, loveId] const path = [workbenchId, control.workspace.workspaceUrl, loveId]
const title = await translate(love.string.KnockingLabel, {}) const title = await translate(love.string.KnockingLabel, {})
const body = await translate(love.string.IsKnocking, { const body = await translate(love.string.IsKnocking, {
name: formatName(from.name, control.branding?.lastNameFirst) name: formatName(from.name, control.branding?.lastNameFirst)
}) })
await createPushNotification(control, userAcc._id, title, body, request._id, from, path) // TODO: Select proper account target
await createPushNotification(control, userAcc[0]._id, title, body, request._id, from, path)
} }
} }
return res return res
@ -293,8 +300,8 @@ export async function OnInvite (tx: Tx, control: TriggerControl): Promise<Tx[]>
if (invite.status === RequestStatus.Pending) { if (invite.status === RequestStatus.Pending) {
const target = (await control.findAll(contact.class.Person, { _id: invite.target }))[0] const target = (await control.findAll(contact.class.Person, { _id: invite.target }))[0]
if (target === undefined) return [] if (target === undefined) return []
const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: target._id }) const userAcc = control.modelDb.getAccountByPersonId(target._id) as PersonAccount[]
if (userAcc === undefined) return [] if (userAcc.length === 0) return []
const from = (await control.findAll(contact.class.Person, { _id: invite.from }))[0] const from = (await control.findAll(contact.class.Person, { _id: invite.from }))[0]
const type = await control.modelDb.findOne(notification.class.NotificationType, { const type = await control.modelDb.findOne(notification.class.NotificationType, {
_id: love.ids.InviteNotification _id: love.ids.InviteNotification
@ -304,7 +311,8 @@ export async function OnInvite (tx: Tx, control: TriggerControl): Promise<Tx[]>
_id: notification.providers.PushNotificationProvider _id: notification.providers.PushNotificationProvider
}) })
if (provider === undefined) return [] if (provider === undefined) return []
if (await isAllowed(control, userAcc._id, type, provider)) { const notificationControl = await getNotificationProviderControl(control.ctx, control)
if (userAcc.some((it) => isAllowed(control, it._id, type, provider, notificationControl))) {
const path = [workbenchId, control.workspace.workspaceUrl, loveId] const path = [workbenchId, control.workspace.workspaceUrl, loveId]
const title = await translate(love.string.InivitingLabel, {}) const title = await translate(love.string.InivitingLabel, {})
const body = const body =
@ -313,7 +321,8 @@ export async function OnInvite (tx: Tx, control: TriggerControl): Promise<Tx[]>
name: formatName(from.name, control.branding?.lastNameFirst) name: formatName(from.name, control.branding?.lastNameFirst)
}) })
: await translate(love.string.InivitingLabel, {}) : await translate(love.string.InivitingLabel, {})
await createPushNotification(control, userAcc._id, title, body, invite._id, from, path) // TODO: Select a proper user
await createPushNotification(control, userAcc[0]._id, title, body, invite._id, from, path)
} }
} }
} }

View File

@ -17,11 +17,11 @@
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
import chunter, { ChatMessage } from '@hcengineering/chunter' import chunter, { ChatMessage } from '@hcengineering/chunter'
import contact, { import contact, {
type AvatarInfo,
getAvatarProviderId, getAvatarProviderId,
getGravatarUrl, getGravatarUrl,
Person, Person,
PersonAccount PersonAccount,
type AvatarInfo
} from '@hcengineering/contact' } from '@hcengineering/contact'
import core, { import core, {
Account, Account,
@ -72,7 +72,6 @@ import { getMetadata, getResource, translate } from '@hcengineering/platform'
import { type TriggerControl } from '@hcengineering/server-core' import { type TriggerControl } from '@hcengineering/server-core'
import serverCore from '@hcengineering/server-core' import serverCore from '@hcengineering/server-core'
import serverNotification, { import serverNotification, {
getPersonAccount,
getPersonAccountById, getPersonAccountById,
NOTIFICATION_BODY_SIZE, NOTIFICATION_BODY_SIZE,
PUSH_NOTIFICATION_TITLE_SIZE, PUSH_NOTIFICATION_TITLE_SIZE,
@ -92,17 +91,19 @@ import {
getHTMLPresenter, getHTMLPresenter,
getNotificationContent, getNotificationContent,
getNotificationLink, getNotificationLink,
getNotificationProviderControl,
getTextPresenter, getTextPresenter,
getUsersInfo, getUsersInfo,
isAllowed, isAllowed,
isMixinTx, isMixinTx,
isShouldNotifyTx, isShouldNotifyTx,
isUserEmployeeInFieldValue, isUserEmployeeInFieldValueTypeMatch,
isUserInFieldValue, isUserInFieldValueTypeMatch,
messageToMarkup, messageToMarkup,
replaceAll, replaceAll,
toReceiverInfo, toReceiverInfo,
updateNotifyContextsSpace updateNotifyContextsSpace,
type NotificationProviderControl
} from './utils' } from './utils'
export function getPushCollaboratorTx ( export function getPushCollaboratorTx (
@ -124,6 +125,7 @@ export function getPushCollaboratorTx (
} }
export async function getCommonNotificationTxes ( export async function getCommonNotificationTxes (
ctx: MeasureContext,
control: TriggerControl, control: TriggerControl,
doc: Doc, doc: Doc,
data: Partial<Data<CommonInboxNotification>>, data: Partial<Data<CommonInboxNotification>>,
@ -144,6 +146,7 @@ export async function getCommonNotificationTxes (
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { objectId: attachedTo }) const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { objectId: attachedTo })
const notificationTx = await pushInboxNotifications( const notificationTx = await pushInboxNotifications(
ctx,
control, control,
res, res,
receiver, receiver,
@ -255,15 +258,14 @@ export async function getContentByTemplate (
} }
} }
async function getValueCollaborators (value: any, attr: AnyAttribute, control: TriggerControl): Promise<Ref<Account>[]> { function getValueCollaborators (value: any, attr: AnyAttribute, control: TriggerControl): Ref<Account>[] {
const hierarchy = control.hierarchy const hierarchy = control.hierarchy
if (attr.type._class === core.class.RefTo) { if (attr.type._class === core.class.RefTo) {
const to = (attr.type as RefTo<Doc>).to const to = (attr.type as RefTo<Doc>).to
if (hierarchy.isDerived(to, contact.class.Person)) { if (hierarchy.isDerived(to, contact.class.Person)) {
const acc = await getPersonAccount(value, control) return (control.modelDb.getAccountByPersonId(value) as PersonAccount[]).map((it) => it._id)
return acc !== undefined ? [acc._id] : []
} else if (hierarchy.isDerived(to, core.class.Account)) { } else if (hierarchy.isDerived(to, core.class.Account)) {
const acc = await getPersonAccountById(value, control) const acc = getPersonAccountById(value, control)
return acc !== undefined ? [acc._id] : [] return acc !== undefined ? [acc._id] : []
} }
} else if (attr.type._class === core.class.ArrOf) { } else if (attr.type._class === core.class.ArrOf) {
@ -271,12 +273,12 @@ async function getValueCollaborators (value: any, attr: AnyAttribute, control: T
if (arrOf._class === core.class.RefTo) { if (arrOf._class === core.class.RefTo) {
const to = (arrOf as RefTo<Doc>).to const to = (arrOf as RefTo<Doc>).to
if (hierarchy.isDerived(to, contact.class.Person)) { if (hierarchy.isDerived(to, contact.class.Person)) {
const employeeAccounts = await control.modelDb.findAll(contact.class.PersonAccount, { const employeeAccounts = control.modelDb.findAllSync(contact.class.PersonAccount, {
person: { $in: Array.isArray(value) ? value : [value] } person: { $in: Array.isArray(value) ? value : [value] }
}) })
return employeeAccounts.map((p) => p._id) return employeeAccounts.map((p) => p._id)
} else if (hierarchy.isDerived(to, core.class.Account)) { } else if (hierarchy.isDerived(to, core.class.Account)) {
const employeeAccounts = await control.modelDb.findAll(contact.class.PersonAccount, { const employeeAccounts = control.modelDb.findAllSync(contact.class.PersonAccount, {
_id: { $in: Array.isArray(value) ? value : [value] } _id: { $in: Array.isArray(value) ? value : [value] }
}) })
return employeeAccounts.map((p) => p._id) return employeeAccounts.map((p) => p._id)
@ -295,7 +297,7 @@ async function getKeyCollaborators (
if (value !== undefined && value !== null) { if (value !== undefined && value !== null) {
const attr = control.hierarchy.findAttribute(docClass, field) const attr = control.hierarchy.findAttribute(docClass, field)
if (attr !== undefined) { if (attr !== undefined) {
return await getValueCollaborators(value, attr, control) return getValueCollaborators(value, attr, control)
} }
} }
} }
@ -315,7 +317,7 @@ export async function getDocCollaborators (
const newCollaborators = await ctx.with( const newCollaborators = await ctx.with(
'getKeyCollaborators', 'getKeyCollaborators',
{}, {},
async () => await getKeyCollaborators(doc._class, value, field, control) async (ctx) => await getKeyCollaborators(doc._class, value, field, control)
) )
if (newCollaborators !== undefined) { if (newCollaborators !== undefined) {
for (const newCollaborator of newCollaborators) { for (const newCollaborator of newCollaborators) {
@ -327,6 +329,7 @@ export async function getDocCollaborators (
} }
export async function pushInboxNotifications ( export async function pushInboxNotifications (
ctx: MeasureContext,
control: TriggerControl, control: TriggerControl,
res: Tx[], res: Tx[],
receiver: ReceiverInfo, receiver: ReceiverInfo,
@ -344,6 +347,7 @@ export async function pushInboxNotifications (
if (context === undefined) { if (context === undefined) {
docNotifyContextId = await createNotifyContext( docNotifyContextId = await createNotifyContext(
ctx,
control, control,
objectId, objectId,
objectClass, objectClass,
@ -584,6 +588,7 @@ async function sendPushToSubscription (
* @public * @public
*/ */
export async function pushActivityInboxNotifications ( export async function pushActivityInboxNotifications (
ctx: MeasureContext,
originTx: TxCUD<Doc>, originTx: TxCUD<Doc>,
control: TriggerControl, control: TriggerControl,
res: Tx[], res: Tx[],
@ -594,7 +599,7 @@ export async function pushActivityInboxNotifications (
activityMessage: ActivityMessage, activityMessage: ActivityMessage,
shouldUpdateTimestamp: boolean shouldUpdateTimestamp: boolean
): Promise<TxCreateDoc<InboxNotification> | undefined> { ): Promise<TxCreateDoc<InboxNotification> | undefined> {
const content = await getNotificationContent(originTx, receiver.account, sender, object, control) const content = await getNotificationContent(originTx, [receiver.account], sender, object, control)
const data: Partial<Data<ActivityInboxNotification>> = { const data: Partial<Data<ActivityInboxNotification>> = {
...content, ...content,
attachedTo: activityMessage._id, attachedTo: activityMessage._id,
@ -602,6 +607,7 @@ export async function pushActivityInboxNotifications (
} }
return await pushInboxNotifications( return await pushInboxNotifications(
ctx,
control, control,
res, res,
receiver, receiver,
@ -628,7 +634,7 @@ export async function applyNotificationProviders (
sender: SenderInfo, sender: SenderInfo,
message?: ActivityMessage message?: ActivityMessage
): Promise<void> { ): Promise<void> {
const resources = await control.modelDb.findAll(serverNotification.class.NotificationProviderResources, {}) const resources = control.modelDb.findAllSync(serverNotification.class.NotificationProviderResources, {})
for (const [provider, types] of notifyResult.entries()) { for (const [provider, types] of notifyResult.entries()) {
if (provider === notification.providers.PushNotificationProvider) { if (provider === notification.providers.PushNotificationProvider) {
// const now = Date.now() // const now = Date.now()
@ -663,6 +669,7 @@ export async function applyNotificationProviders (
} }
async function createNotifyContext ( async function createNotifyContext (
ctx: MeasureContext,
control: TriggerControl, control: TriggerControl,
objectId: Ref<Doc>, objectId: Ref<Doc>,
objectClass: Ref<Class<Doc>>, objectClass: Ref<Class<Doc>>,
@ -678,7 +685,7 @@ async function createNotifyContext (
isPinned: false, isPinned: false,
lastUpdateTimestamp: updateTimestamp lastUpdateTimestamp: updateTimestamp
}) })
await control.apply([createTx]) await ctx.with('apply', {}, () => control.apply([createTx]))
if (receiver.account?.email !== undefined) { if (receiver.account?.email !== undefined) {
control.operationContext.derived.targets['docNotifyContext' + createTx._id] = (it) => { control.operationContext.derived.targets['docNotifyContext' + createTx._id] = (it) => {
if (it._id === createTx._id) { if (it._id === createTx._id) {
@ -690,6 +697,7 @@ async function createNotifyContext (
} }
export async function getNotificationTxes ( export async function getNotificationTxes (
ctx: MeasureContext,
control: TriggerControl, control: TriggerControl,
object: Doc, object: Doc,
tx: TxCUD<Doc>, tx: TxCUD<Doc>,
@ -698,7 +706,8 @@ export async function getNotificationTxes (
sender: SenderInfo, sender: SenderInfo,
params: NotifyParams, params: NotifyParams,
docNotifyContexts: DocNotifyContext[], docNotifyContexts: DocNotifyContext[],
activityMessages: ActivityMessage[] activityMessages: ActivityMessage[],
settings: NotificationProviderControl
): Promise<Tx[]> { ): Promise<Tx[]> {
if (receiver.account === undefined) { if (receiver.account === undefined) {
return [] return []
@ -713,14 +722,16 @@ export async function getNotificationTxes (
tx, tx,
originTx, originTx,
object, object,
receiver.account, [receiver.account],
params.isOwn, params.isOwn,
params.isSpace, params.isSpace,
settings,
docMessage docMessage
) )
if (notifyResult.has(notification.providers.InboxNotificationProvider)) { if (notifyResult.has(notification.providers.InboxNotificationProvider)) {
const notificationTx = await pushActivityInboxNotifications( const notificationTx = await pushActivityInboxNotifications(
ctx,
originTx, originTx,
control, control,
res, res,
@ -753,6 +764,7 @@ export async function getNotificationTxes (
if (context === undefined) { if (context === undefined) {
await createNotifyContext( await createNotifyContext(
ctx,
control, control,
message.attachedTo, message.attachedTo,
message.attachedToClass, message.attachedToClass,
@ -767,20 +779,17 @@ export async function getNotificationTxes (
} }
async function updateContextsTimestamp ( async function updateContextsTimestamp (
ctx: MeasureContext,
contexts: DocNotifyContext[], contexts: DocNotifyContext[],
timestamp: Timestamp, timestamp: Timestamp,
control: TriggerControl, control: TriggerControl,
modifiedBy: Ref<Account> modifiedBy: Ref<Account>
): Promise<void> { ): Promise<void> {
if (contexts.length === 0) return if (contexts.length === 0) return
const accounts = await control.modelDb.findAll(contact.class.PersonAccount, {
_id: { $in: contexts.map((it) => it.user as Ref<PersonAccount>) }
})
const res: Tx[] = [] const res: Tx[] = []
for (const context of contexts) { for (const context of contexts) {
const account = accounts.find(({ _id }) => _id === context.user) const account = getPersonAccountById(context.user, control) // accounts.find(({ _id }) => _id === context.user)
const isViewed = const isViewed =
context.lastViewedTimestamp !== undefined && (context.lastUpdateTimestamp ?? 0) <= context.lastViewedTimestamp context.lastViewedTimestamp !== undefined && (context.lastUpdateTimestamp ?? 0) <= context.lastViewedTimestamp
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
@ -803,10 +812,13 @@ async function updateContextsTimestamp (
} }
} }
await control.apply(res) if (res.length > 0) {
await ctx.with('apply', {}, () => control.apply(res))
}
} }
async function removeContexts ( async function removeContexts (
ctx: MeasureContext,
contexts: DocNotifyContext[], contexts: DocNotifyContext[],
unsubscribe: Ref<PersonAccount>[], unsubscribe: Ref<PersonAccount>[],
control: TriggerControl control: TriggerControl
@ -814,13 +826,13 @@ async function removeContexts (
if (contexts.length === 0) return if (contexts.length === 0) return
if (unsubscribe.length === 0) return if (unsubscribe.length === 0) return
const unsubscribeAccounts = await control.modelDb.findAll(contact.class.PersonAccount, {
_id: { $in: unsubscribe }
})
const res: Tx[] = [] const res: Tx[] = []
for (const context of contexts) { for (const context of contexts) {
const account = unsubscribeAccounts.find(({ _id }) => _id === context.user) if (!unsubscribe.includes(context.user as Ref<PersonAccount>)) {
continue
}
const account = control.modelDb.getObject(context.user)
if (account === undefined) continue if (account === undefined) continue
const removeTx = control.txFactory.createTxRemoveDoc(context._class, context.space, context._id) const removeTx = control.txFactory.createTxRemoveDoc(context._class, context.space, context._id)
@ -864,23 +876,31 @@ export async function createCollabDocInfo (
objectId: object._id, objectId: object._id,
user: { $in: unsubscribe } user: { $in: unsubscribe }
}) })
await removeContexts(notifyContexts, unsubscribe, control) await removeContexts(ctx, notifyContexts, unsubscribe, control)
} }
return res return res
} }
const notifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { objectId: object._id }) let notifyContexts: DocNotifyContext[] = []
await removeContexts(notifyContexts, unsubscribe, control) if (!(object._id === tx.objectId && tx._class === core.class.TxCreateDoc)) {
await updateContextsTimestamp(notifyContexts, originTx.modifiedOn, control, originTx.modifiedBy) notifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { objectId: object._id })
}
if (notifyContexts.length > 0) {
await updateContextsTimestamp(ctx, notifyContexts, originTx.modifiedOn, control, originTx.modifiedBy)
}
if (notifyContexts.length > 0 && unsubscribe.length > 0) {
await removeContexts(ctx, notifyContexts, unsubscribe, control)
}
const targets = new Set(collaborators) const targets = new Set(collaborators)
// user is not collaborator of himself, but we should notify user of changes related to users account (mentions, comments etc) // user is not collaborator of himself, but we should notify user of changes related to users account (mentions, comments etc)
if (control.hierarchy.isDerived(object._class, contact.class.Person)) { if (control.hierarchy.isDerived(object._class, contact.class.Person)) {
const acc = await getPersonAccount(object._id as Ref<Person>, control) const acc = control.modelDb.getAccountByPersonId(object._id as Ref<Person>) as PersonAccount[]
if (acc !== undefined) { for (const a of acc.map((it) => it._id)) {
targets.add(acc._id) targets.add(a)
} }
} }
@ -893,19 +913,19 @@ export async function createCollabDocInfo (
{}, {},
async (ctx) => await getUsersInfo(ctx, [...Array.from(targets), originTx.modifiedBy as Ref<PersonAccount>], control) async (ctx) => await getUsersInfo(ctx, [...Array.from(targets), originTx.modifiedBy as Ref<PersonAccount>], control)
) )
const sender: SenderInfo = usersInfo.find(({ _id }) => _id === originTx.modifiedBy) ?? { const sender: SenderInfo = usersInfo.get(originTx.modifiedBy) ?? {
_id: originTx.modifiedBy _id: originTx.modifiedBy
} }
const settings = await getNotificationProviderControl(ctx, control)
for (const target of targets) { for (const target of targets) {
const info: ReceiverInfo | undefined = toReceiverInfo( const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(target))
control.hierarchy,
usersInfo.find(({ _id }) => _id === target)
)
if (info === undefined) continue if (info === undefined) continue
const targetRes = await getNotificationTxes( const targetRes = await getNotificationTxes(
ctx,
control, control,
object, object,
tx, tx,
@ -914,7 +934,8 @@ export async function createCollabDocInfo (
sender, sender,
params, params,
notifyContexts, notifyContexts,
docMessages docMessages,
settings
) )
const ids = new Set(targetRes.map((it) => it._id)) const ids = new Set(targetRes.map((it) => it._id))
if (info.account?.email !== undefined) { if (info.account?.email !== undefined) {
@ -994,6 +1015,7 @@ async function getTxCollabs (
} }
async function getSpaceCollabTxes ( async function getSpaceCollabTxes (
ctx: MeasureContext,
control: TriggerControl, control: TriggerControl,
doc: Doc, doc: Doc,
tx: TxCUD<Doc>, tx: TxCUD<Doc>,
@ -1005,7 +1027,8 @@ async function getSpaceCollabTxes (
return [] return []
} }
const space = cache.get(doc.space) ?? (await control.findAll(core.class.Space, { _id: doc.space }, { limit: 1 }))[0] const space =
cache.get(doc.space) ?? (await control.findAllCtx(ctx, core.class.Space, { _id: doc.space }, { limit: 1 }))[0]
if (space === undefined) return [] if (space === undefined) return []
cache.set(space._id, space) cache.set(space._id, space)
@ -1018,7 +1041,7 @@ async function getSpaceCollabTxes (
const collabs = control.hierarchy.as<Doc, Collaborators>(space, notification.mixin.Collaborators) const collabs = control.hierarchy.as<Doc, Collaborators>(space, notification.mixin.Collaborators)
if (collabs.collaborators !== undefined) { if (collabs.collaborators !== undefined) {
return await createCollabDocInfo( return await createCollabDocInfo(
control.ctx, ctx,
collabs.collaborators as Ref<PersonAccount>[], collabs.collaborators as Ref<PersonAccount>[],
control, control,
tx, tx,
@ -1059,7 +1082,7 @@ async function createCollaboratorDoc (
const notificationTxes = await ctx.with( const notificationTxes = await ctx.with(
'create-collabdocinfo', 'create-collabdocinfo',
{}, {},
async () => async (ctx) =>
await createCollabDocInfo( await createCollabDocInfo(
ctx, ctx,
collaborators as Ref<PersonAccount>[], collaborators as Ref<PersonAccount>[],
@ -1078,7 +1101,7 @@ async function createCollaboratorDoc (
...(await ctx.with( ...(await ctx.with(
'get-space-collabtxes', 'get-space-collabtxes',
{}, {},
async () => await getSpaceCollabTxes(control, doc, tx, originTx, activityMessage, cache) async (ctx) => await getSpaceCollabTxes(ctx, control, doc, tx, originTx, activityMessage, cache)
)) ))
) )
@ -1100,6 +1123,8 @@ async function updateCollaboratorsMixin (
const res: Tx[] = [] const res: Tx[] = []
const notificationControl = await getNotificationProviderControl(ctx, control)
if (tx.attributes.collaborators !== undefined) { if (tx.attributes.collaborators !== undefined) {
const createTx = hierarchy.isDerived(tx.objectClass, core.class.AttachedDoc) const createTx = hierarchy.isDerived(tx.objectClass, core.class.AttachedDoc)
? ( ? (
@ -1142,7 +1167,7 @@ async function updateCollaboratorsMixin (
for (const collab of tx.attributes.collaborators) { for (const collab of tx.attributes.collaborators) {
if (!prevCollabs.has(collab) && tx.modifiedBy !== collab) { if (!prevCollabs.has(collab) && tx.modifiedBy !== collab) {
for (const provider of providers) { for (const provider of providers) {
if (await isAllowed(control, collab as Ref<PersonAccount>, type, provider)) { if (isAllowed(control, collab as Ref<PersonAccount>, type, provider, notificationControl)) {
newCollabs.push(collab) newCollabs.push(collab)
break break
} }
@ -1161,17 +1186,15 @@ async function updateCollaboratorsMixin (
{}, {},
async (ctx) => await getUsersInfo(ctx, [...newCollabs, originTx.modifiedBy] as Ref<PersonAccount>[], control) async (ctx) => await getUsersInfo(ctx, [...newCollabs, originTx.modifiedBy] as Ref<PersonAccount>[], control)
) )
const sender: SenderInfo = infos.find(({ _id }) => _id === originTx.modifiedBy) ?? { _id: originTx.modifiedBy } const sender: SenderInfo = infos.get(originTx.modifiedBy) ?? { _id: originTx.modifiedBy }
for (const collab of newCollabs) { for (const collab of newCollabs) {
const target = toReceiverInfo( const target = toReceiverInfo(hierarchy, infos.get(collab))
hierarchy,
infos.find(({ _id }) => _id === collab)
)
if (target === undefined) continue if (target === undefined) continue
for (const message of activityMessages) { for (const message of activityMessages) {
await pushActivityInboxNotifications( await pushActivityInboxNotifications(
ctx,
originTx, originTx,
control, control,
res, res,
@ -1225,7 +1248,7 @@ async function collectionCollabDoc (
const collaborators = await ctx.with( const collaborators = await ctx.with(
'get-collaborators', 'get-collaborators',
{}, {},
async () => await getCollaborators(doc, control, tx, res) async (ctx) => await getCollaborators(ctx, doc, control, tx, res)
) )
res = res.concat( res = res.concat(
@ -1377,7 +1400,7 @@ async function updateCollaboratorDoc (
const doc = await ctx.with( const doc = await ctx.with(
'find-doc', 'find-doc',
{ _class: tx.objectClass }, { _class: tx.objectClass },
async () => async (ctx) =>
cache.get(tx.objectId) ?? (await control.findAllCtx(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] cache.get(tx.objectId) ?? (await control.findAllCtx(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
) )
if (doc === undefined) return [] if (doc === undefined) return []
@ -1388,7 +1411,7 @@ async function updateCollaboratorDoc (
const collabsInfo = await ctx.with( const collabsInfo = await ctx.with(
'get-tx-collaborators', 'get-tx-collaborators',
{}, {},
async () => await getTxCollabs(ctx, tx, control, doc) async (ctx) => await getTxCollabs(ctx, tx, control, doc)
) )
if (collabsInfo.added.length > 0) { if (collabsInfo.added.length > 0) {
@ -1403,7 +1426,7 @@ async function updateCollaboratorDoc (
await ctx.with( await ctx.with(
'create-collab-docinfo', 'create-collab-docinfo',
{}, {},
async () => async (ctx) =>
await createCollabDocInfo( await createCollabDocInfo(
ctx, ctx,
collabsInfo.result as Ref<PersonAccount>[], collabsInfo.result as Ref<PersonAccount>[],
@ -1421,7 +1444,7 @@ async function updateCollaboratorDoc (
const collaborators = await ctx.with( const collaborators = await ctx.with(
'get-doc-collaborators', 'get-doc-collaborators',
{}, {},
async () => await getDocCollaborators(ctx, doc, mixin, control) async (ctx) => await getDocCollaborators(ctx, doc, mixin, control)
) )
res.push(getMixinTx(tx, control, collaborators)) res.push(getMixinTx(tx, control, collaborators))
res = res.concat( res = res.concat(
@ -1442,11 +1465,11 @@ async function updateCollaboratorDoc (
await ctx.with( await ctx.with(
'get-space-collabtxes', 'get-space-collabtxes',
{}, {},
async () => await getSpaceCollabTxes(control, doc, tx, originTx, activityMessages, cache) async (ctx) => await getSpaceCollabTxes(ctx, control, doc, tx, originTx, activityMessages, cache)
) )
) )
res = res.concat( res = res.concat(
await ctx.with('update-notify-context-space', {}, async () => await updateNotifyContextsSpace(control, tx)) await ctx.with('update-notify-context-space', {}, async (ctx) => await updateNotifyContextsSpace(ctx, control, tx))
) )
return res return res
@ -1546,7 +1569,7 @@ async function applyUserTxes (
for (const [user, txs] of map.entries()) { for (const [user, txs] of map.entries()) {
const account = const account =
(cache.get(user) as PersonAccount) ?? (cache.get(user) as PersonAccount) ??
(await ctx.with('get-person-account', {}, async () => await getPersonAccountById(user, control))) ctx.withSync('get-person-account', {}, () => getPersonAccountById(user, control))
if (account !== undefined) { if (account !== undefined) {
cache.set(account._id, account) cache.set(account._id, account)
@ -1604,15 +1627,15 @@ async function updateCollaborators (ctx: MeasureContext, control: TriggerControl
const contexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: objectId }) const contexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: objectId })
const addedInfo = await getUsersInfo(ctx, toAdd as Ref<PersonAccount>[], control) const addedInfo = await getUsersInfo(ctx, toAdd as Ref<PersonAccount>[], control)
for (const addedUser of addedInfo) { for (const addedUser of addedInfo.values()) {
const info = toReceiverInfo(hierarchy, addedUser) const info = toReceiverInfo(hierarchy, addedUser)
if (info === undefined) continue if (info === undefined) continue
const context = getDocNotifyContext(control, contexts, objectId, info._id) const context = getDocNotifyContext(control, contexts, objectId, info._id)
if (context !== undefined) continue if (context !== undefined) continue
await createNotifyContext(control, objectId, objectClass, objectSpace, info) await createNotifyContext(ctx, control, objectId, objectClass, objectSpace, info)
} }
await removeContexts(contexts, removedCollaborators as Ref<PersonAccount>[], control) await removeContexts(ctx, contexts, removedCollaborators as Ref<PersonAccount>[], control)
return res return res
} }
@ -1630,7 +1653,7 @@ export async function createCollaboratorNotifications (
return await ctx.with( return await ctx.with(
'updateDerivedCollaborators', 'updateDerivedCollaborators',
{}, {},
async () => await updateCollaborators(ctx, control, TxProcessor.extractTx(tx) as TxCUD<Doc>) async (ctx) => await updateCollaborators(ctx, control, TxProcessor.extractTx(tx) as TxCUD<Doc>)
) )
} }
@ -1643,7 +1666,7 @@ export async function createCollaboratorNotifications (
const res = await ctx.with( const res = await ctx.with(
'createCollaboratorDoc', 'createCollaboratorDoc',
{}, {},
async () => async (ctx) =>
await createCollaboratorDoc(ctx, tx as TxCreateDoc<Doc>, control, activityMessages, originTx ?? tx, cache) await createCollaboratorDoc(ctx, tx as TxCreateDoc<Doc>, control, activityMessages, originTx ?? tx, cache)
) )
@ -1654,14 +1677,14 @@ export async function createCollaboratorNotifications (
let res = await ctx.with( let res = await ctx.with(
'updateCollaboratorDoc', 'updateCollaboratorDoc',
{}, {},
async () => async (ctx) =>
await updateCollaboratorDoc(ctx, tx as TxUpdateDoc<Doc>, control, originTx ?? tx, activityMessages, cache) await updateCollaboratorDoc(ctx, tx as TxUpdateDoc<Doc>, control, originTx ?? tx, activityMessages, cache)
) )
res = res.concat( res = res.concat(
await ctx.with( await ctx.with(
'updateCollaboratorMixin', 'updateCollaboratorMixin',
{}, {},
async () => async (ctx) =>
await updateCollaboratorsMixin( await updateCollaboratorsMixin(
ctx, ctx,
tx as TxMixin<Doc, Collaborators>, tx as TxMixin<Doc, Collaborators>,
@ -1703,6 +1726,7 @@ export async function removeDocInboxNotifications (_id: Ref<ActivityMessage>, co
} }
export async function getCollaborators ( export async function getCollaborators (
ctx: MeasureContext,
doc: Doc, doc: Doc,
control: TriggerControl, control: TriggerControl,
tx: TxCUD<Doc>, tx: TxCUD<Doc>,
@ -1717,7 +1741,7 @@ export async function getCollaborators (
if (control.hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { if (control.hierarchy.hasMixin(doc, notification.mixin.Collaborators)) {
return control.hierarchy.as(doc, notification.mixin.Collaborators).collaborators return control.hierarchy.as(doc, notification.mixin.Collaborators).collaborators
} else { } else {
const collaborators = await getDocCollaborators(control.ctx, doc, mixin, control) const collaborators = await getDocCollaborators(ctx, doc, mixin, control)
res.push(getMixinTx(tx, control, collaborators)) res.push(getMixinTx(tx, control, collaborators))
return collaborators return collaborators
@ -1821,7 +1845,7 @@ export default async () => ({
OnDocRemove OnDocRemove
}, },
function: { function: {
IsUserInFieldValue: isUserInFieldValue, IsUserInFieldValueTypeMatch: isUserInFieldValueTypeMatch,
IsUserEmployeeInFieldValue: isUserEmployeeInFieldValue IsUserEmployeeInFieldValueTypeMatch: isUserEmployeeInFieldValueTypeMatch
} }
}) })

View File

@ -12,24 +12,19 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import notification, { import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
BaseNotificationType,
Collaborators,
CommonNotificationType,
NotificationContent,
notificationId,
NotificationProvider,
NotificationType
} from '@hcengineering/notification'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import contact, { formatName, PersonAccount } from '@hcengineering/contact' import chunter, { ChatMessage } from '@hcengineering/chunter'
import contact, { formatName, PersonAccount, type Person, type PersonSpace } from '@hcengineering/contact'
import core, { import core, {
Account, Account,
Class, Class,
concatLink,
Doc, Doc,
DocumentUpdate, DocumentUpdate,
groupByArray,
Hierarchy, Hierarchy,
Markup,
matchQuery, matchQuery,
MixinUpdate, MixinUpdate,
Ref, Ref,
@ -42,11 +37,23 @@ import core, {
TxProcessor, TxProcessor,
TxRemoveDoc, TxRemoveDoc,
TxUpdateDoc, TxUpdateDoc,
type IdMap,
type MeasureContext, type MeasureContext,
Markup, type WithLookup
concatLink
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, {
BaseNotificationType,
Collaborators,
CommonNotificationType,
NotificationContent,
notificationId,
NotificationProvider,
NotificationType,
type NotificationProviderSetting,
type NotificationTypeSetting
} from '@hcengineering/notification'
import { getMetadata, getResource, IntlString, translate } from '@hcengineering/platform' import { getMetadata, getResource, IntlString, translate } from '@hcengineering/platform'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import serverNotification, { import serverNotification, {
getPersonAccountById, getPersonAccountById,
HTMLPresenter, HTMLPresenter,
@ -55,28 +62,26 @@ import serverNotification, {
SenderInfo, SenderInfo,
TextPresenter TextPresenter
} from '@hcengineering/server-notification' } from '@hcengineering/server-notification'
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
import serverView from '@hcengineering/server-view' import serverView from '@hcengineering/server-view'
import { workbenchId } from '@hcengineering/workbench'
import { encodeObjectURI } from '@hcengineering/view' import { encodeObjectURI } from '@hcengineering/view'
import chunter, { ChatMessage } from '@hcengineering/chunter' import { workbenchId } from '@hcengineering/workbench'
import { NotifyResult } from './types' import { NotifyResult } from './types'
/** /**
* @public * @public
*/ */
export async function isUserEmployeeInFieldValue ( export function isUserEmployeeInFieldValueTypeMatch (
_: Tx, _: Tx,
doc: Doc, doc: Doc,
user: Ref<Account>, user: Ref<Account>[],
type: NotificationType, type: NotificationType,
control: TriggerControl control: TriggerControl
): Promise<boolean> { ): boolean {
if (type.field === undefined) return false if (type.field === undefined) return false
const value = (doc as any)[type.field] const value = (doc as any)[type.field]
if (value == null) return false if (value == null) return false
const employee = (await control.modelDb.findAll(contact.class.PersonAccount, { _id: user as Ref<PersonAccount> }))[0] const employee = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: user[0] as Ref<PersonAccount> })[0]
if (employee === undefined) return false if (employee === undefined) return false
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.includes(employee.person) return value.includes(employee.person)
@ -88,12 +93,7 @@ export async function isUserEmployeeInFieldValue (
/** /**
* @public * @public
*/ */
export async function isUserInFieldValue ( export function isUserInFieldValueTypeMatch (_: Tx, doc: Doc, user: Ref<Account>[], type: NotificationType): boolean {
_: Tx,
doc: Doc,
user: Ref<Account>,
type: NotificationType
): Promise<boolean> {
if (type.field === undefined) { if (type.field === undefined) {
return false return false
} }
@ -104,7 +104,7 @@ export async function isUserInFieldValue (
return false return false
} }
return Array.isArray(value) ? value.includes(user) : value === user return Array.isArray(value) ? user.some((it) => value.includes(it)) : user.includes(value)
} }
export function replaceAll (str: string, find: string, replace: string): string { export function replaceAll (str: string, find: string, replace: string): string {
@ -117,8 +117,9 @@ function escapeRegExp (str: string): string {
export async function shouldNotifyCommon ( export async function shouldNotifyCommon (
control: TriggerControl, control: TriggerControl,
user: Ref<Account>, user: Ref<Account>[],
typeId: Ref<CommonNotificationType> typeId: Ref<CommonNotificationType>,
notificationControl: NotificationProviderControl
): Promise<NotifyResult> { ): Promise<NotifyResult> {
const type = (await control.modelDb.findAll(notification.class.CommonNotificationType, { _id: typeId }))[0] const type = (await control.modelDb.findAll(notification.class.CommonNotificationType, { _id: typeId }))[0]
@ -130,7 +131,9 @@ export async function shouldNotifyCommon (
const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {}) const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {})
for (const provider of providers) { for (const provider of providers) {
const allowed = await isAllowed(control, user as Ref<PersonAccount>, type, provider) const allowed = user.some((user) =>
isAllowed(control, user as Ref<PersonAccount>, type, provider, notificationControl)
)
if (allowed) { if (allowed) {
const cur = result.get(provider._id) ?? [] const cur = result.get(provider._id) ?? []
result.set(provider._id, [...cur, type]) result.set(provider._id, [...cur, type])
@ -140,17 +143,15 @@ export async function shouldNotifyCommon (
return result return result
} }
export async function isAllowed ( export function isAllowed (
control: TriggerControl, control: TriggerControl,
receiver: Ref<PersonAccount>, receiver: Ref<PersonAccount>,
type: BaseNotificationType, type: BaseNotificationType,
provider: NotificationProvider provider: NotificationProvider,
): Promise<boolean> { notificationControl: NotificationProviderControl
const providersSettings = await control.queryFind(control.ctx, notification.class.NotificationProviderSetting, { ): boolean {
space: core.space.Workspace const providerSetting = (notificationControl.byProvider.get(provider._id) ?? []).find(
}) ({ attachedTo, modifiedBy }) => modifiedBy === receiver
const providerSetting = providersSettings.find(
({ attachedTo, modifiedBy }) => attachedTo === provider._id && modifiedBy === receiver
) )
if (providerSetting !== undefined && !providerSetting.enabled) { if (providerSetting !== undefined && !providerSetting.enabled) {
@ -161,17 +162,13 @@ export async function isAllowed (
return false return false
} }
const providerDefaults = await control.modelDb.findAll(notification.class.NotificationProviderDefaults, {}) const providerDefaults = control.modelDb.findAllSync(notification.class.NotificationProviderDefaults, {})
if (providerDefaults.some((it) => it.provider === provider._id && it.ignoredTypes.includes(type._id))) { if (providerDefaults.some((it) => it.provider === provider._id && it.ignoredTypes.includes(type._id))) {
return false return false
} }
const setting = (notificationControl.settingsByProvider.get(provider._id) ?? []).find(
const typesSettings = await control.queryFind(control.ctx, notification.class.NotificationTypeSetting, { (it) => it.type === type._id && it.modifiedBy === receiver
space: core.space.Workspace
})
const setting = typesSettings.find(
(it) => it.attachedTo === provider._id && it.type === type._id && it.modifiedBy === receiver
) )
if (setting !== undefined) { if (setting !== undefined) {
@ -192,29 +189,23 @@ export async function isShouldNotifyTx (
tx: TxCUD<Doc>, tx: TxCUD<Doc>,
originTx: TxCUD<Doc>, originTx: TxCUD<Doc>,
object: Doc, object: Doc,
user: PersonAccount, user: PersonAccount[],
isOwn: boolean, isOwn: boolean,
isSpace: boolean, isSpace: boolean,
notificationControl: NotificationProviderControl,
docUpdateMessage?: DocUpdateMessage docUpdateMessage?: DocUpdateMessage
): Promise<NotifyResult> { ): Promise<NotifyResult> {
const types = await getMatchedTypes( const types = getMatchedTypes(control, tx, originTx, isOwn, isSpace, docUpdateMessage?.attributeUpdates?.attrKey)
control, const modifiedAccount = getPersonAccountById(tx.modifiedBy, control)
tx,
originTx,
isOwn,
isSpace,
docUpdateMessage?.attributeUpdates?.attrKey
)
const modifiedAccount = await getPersonAccountById(tx.modifiedBy, control)
const result = new Map<Ref<NotificationProvider>, BaseNotificationType[]>() const result = new Map<Ref<NotificationProvider>, BaseNotificationType[]>()
const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {}) const providers = control.modelDb.findAllSync(notification.class.NotificationProvider, {})
for (const type of types) { for (const type of types) {
if ( if (
type.allowedForAuthor !== true && type.allowedForAuthor !== true &&
(tx.modifiedBy === user._id || (user.some((it) => tx.modifiedBy === it._id) ||
// Also check if we have different account for same user. // Also check if we have different account for same user.
(user?.person !== undefined && user?.person === modifiedAccount?.person)) (user?.[0].person !== undefined && user?.[0]?.person === modifiedAccount?.person))
) { ) {
continue continue
} }
@ -222,12 +213,18 @@ export async function isShouldNotifyTx (
const mixin = control.hierarchy.as(type, serverNotification.mixin.TypeMatch) const mixin = control.hierarchy.as(type, serverNotification.mixin.TypeMatch)
if (mixin.func !== undefined) { if (mixin.func !== undefined) {
const f = await getResource(mixin.func) const f = await getResource(mixin.func)
const res = await f(tx, object, user._id, type, control) const res = f(
tx,
object,
user.map((it) => it._id),
type,
control
)
if (!res) continue if (!res) continue
} }
} }
for (const provider of providers) { for (const provider of providers) {
const allowed = await isAllowed(control, user._id, type, provider) const allowed = user.some((it) => isAllowed(control, it._id, type, provider, notificationControl))
if (allowed) { if (allowed) {
const cur = result.get(provider._id) ?? [] const cur = result.get(provider._id) ?? []
@ -239,17 +236,17 @@ export async function isShouldNotifyTx (
return result return result
} }
async function getMatchedTypes ( function getMatchedTypes (
control: TriggerControl, control: TriggerControl,
tx: TxCUD<Doc>, tx: TxCUD<Doc>,
originTx: TxCUD<Doc>, originTx: TxCUD<Doc>,
isOwn: boolean, isOwn: boolean,
isSpace: boolean, isSpace: boolean,
field?: string field?: string
): Promise<NotificationType[]> { ): NotificationType[] {
const allTypes = ( const allTypes = control.modelDb
await control.modelDb.findAll(notification.class.NotificationType, { ...(field !== undefined ? { field } : {}) }) .findAllSync(notification.class.NotificationType, { ...(field !== undefined ? { field } : {}) })
).filter((p) => (isSpace ? p.spaceSubscribe === true : p.spaceSubscribe !== true)) .filter((p) => (isSpace ? p.spaceSubscribe === true : p.spaceSubscribe !== true))
const filtered: NotificationType[] = [] const filtered: NotificationType[] = []
for (const type of allTypes) { for (const type of allTypes) {
if (isTypeMatched(control, type, tx, originTx, isOwn)) { if (isTypeMatched(control, type, tx, originTx, isOwn)) {
@ -300,6 +297,7 @@ function fieldUpdated (field: string, ops: DocumentUpdate<Doc> | MixinUpdate<Doc
} }
export async function updateNotifyContextsSpace ( export async function updateNotifyContextsSpace (
ctx: MeasureContext,
control: TriggerControl, control: TriggerControl,
tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc> tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>
): Promise<Tx[]> { ): Promise<Tx[]> {
@ -312,7 +310,7 @@ export async function updateNotifyContextsSpace (
return [] return []
} }
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { objectId: tx.objectId }) const notifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { objectId: tx.objectId })
return notifyContexts.map((value) => return notifyContexts.map((value) =>
control.txFactory.createTxUpdateDoc(value._class, value.space, value._id, { control.txFactory.createTxUpdateDoc(value._class, value.space, value._id, {
@ -421,7 +419,7 @@ function getNotificationPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy
export async function getNotificationContent ( export async function getNotificationContent (
originTx: TxCUD<Doc>, originTx: TxCUD<Doc>,
targetUser: PersonAccount, targetUser: PersonAccount[],
sender: SenderInfo, sender: SenderInfo,
object: Doc, object: Doc,
control: TriggerControl control: TriggerControl
@ -440,7 +438,7 @@ export async function getNotificationContent (
if (notificationPresenter !== undefined) { if (notificationPresenter !== undefined) {
const getFuillfillmentParams = await getResource(notificationPresenter.presenter) const getFuillfillmentParams = await getResource(notificationPresenter.presenter)
const updateParams = await getFuillfillmentParams(object, originTx, targetUser._id, control) const updateParams = await getFuillfillmentParams(object, originTx, targetUser[0]._id, control)
title = updateParams.title title = updateParams.title
body = updateParams.body body = updateParams.body
data = updateParams.data data = updateParams.data
@ -474,33 +472,57 @@ export async function getUsersInfo (
ctx: MeasureContext, ctx: MeasureContext,
ids: Ref<PersonAccount>[], ids: Ref<PersonAccount>[],
control: TriggerControl control: TriggerControl
): Promise<(ReceiverInfo | SenderInfo)[]> { ): Promise<Map<Ref<Account>, ReceiverInfo | SenderInfo>> {
if (ids.length === 0) return [] if (ids.length === 0) return new Map()
const accounts = await control.modelDb.findAll(contact.class.PersonAccount, { _id: { $in: ids } }) const accounts = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: { $in: ids } })
const personIds = accounts.map((it) => it.person) const personIds = accounts.map((it) => it.person)
const personIdsMap = new Set(personIds)
const accountById = toIdMap(accounts) const accountById = toIdMap(accounts)
const persons = toIdMap( const persons: IdMap<WithLookup<Person>> = new Map()
await ctx.with( const spaces = new Map<Ref<Person>, PersonSpace>()
'find-persons',
{}, const p = await ctx.with('find-persons', {}, async (ctx) =>
async () => await control.findAll(contact.class.Person, { _id: { $in: personIds } }) (await control.queryFind(ctx, contact.mixin.Employee, { active: { $in: [true, false] } }, {})).filter((it) =>
personIdsMap.has(it._id)
) )
) )
const spaces = await ctx.with('find-person-spaces', {}, async () => { for (const pp of p) {
const res = await control.findAll(contact.class.PersonSpace, { person: { $in: personIds } }) persons.set(pp._id, pp)
}
return new Map(res.map((s) => [s.person, s])) const nonEmployee = personIds.filter((it) => !persons.has(it))
})
return ids.map((_id) => { const p2 = await ctx.with('find-persons', {}, async (ctx) =>
const account = accountById.get(_id) (await control.queryFind(ctx, contact.class.Person, { _id: { $in: Array.from(nonEmployee) } }, {})).filter((it) =>
return { personIdsMap.has(it._id)
_id, )
account, )
person: account !== undefined ? persons.get(account.person) : undefined, for (const pp of p2) {
space: account !== undefined ? spaces.get(account.person)?._id : undefined persons.set(pp._id, pp)
} }
})
const res = await ctx.with('find-person-spaces', {}, async (ctx) =>
(await control.queryFind(ctx, contact.class.PersonSpace, {}, {})).filter((it) => personIdsMap.has(it.person))
)
for (const r of res) {
spaces.set(r.person, r)
}
return new Map(
ids.map((_id) => {
const account = accountById.get(_id)
return [
_id,
{
_id,
account,
person: account !== undefined ? persons.get(account.person) : undefined,
space: account !== undefined ? spaces.get(account.person)?._id : undefined
}
]
})
)
} }
export function toReceiverInfo (hierarchy: Hierarchy, info?: SenderInfo | ReceiverInfo): ReceiverInfo | undefined { export function toReceiverInfo (hierarchy: Hierarchy, info?: SenderInfo | ReceiverInfo): ReceiverInfo | undefined {
@ -611,3 +633,36 @@ export async function messageToMarkup (control: TriggerControl, message: Activit
return undefined return undefined
} }
export class NotificationProviderControl {
public byProvider: Map<Ref<NotificationProvider>, NotificationProviderSetting[]>
public settingsByProvider: Map<Ref<NotificationProvider>, NotificationTypeSetting[]>
constructor (
readonly providersSettings: NotificationProviderSetting[],
readonly typesSettings: NotificationTypeSetting[]
) {
this.byProvider = groupByArray(providersSettings, (it) => it.attachedTo)
this.settingsByProvider = groupByArray(typesSettings, (it) => it.attachedTo)
}
}
const notificationProvidersKey = 'notification_provider_settings'
const typesSettingsKey = 'notification_type_settings'
export async function getNotificationProviderControl (
ctx: MeasureContext,
control: TriggerControl
): Promise<NotificationProviderControl> {
let providersSettings: NotificationProviderSetting[] = control.contextCache.get(notificationProvidersKey)
if (providersSettings === undefined) {
providersSettings = await control.queryFind(ctx, notification.class.NotificationProviderSetting, {
space: core.space.Workspace
})
control.contextCache.set(notificationProvidersKey, providersSettings)
}
let typesSettings: NotificationTypeSetting[] = control.contextCache.get(typesSettingsKey)
if (typesSettings === undefined) {
typesSettings = await control.queryFind(ctx, notification.class.NotificationTypeSetting, {
space: core.space.Workspace
})
control.contextCache.set(typesSettingsKey, typesSettings)
}
return new NotificationProviderControl(providersSettings, typesSettings)
}

View File

@ -14,6 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import { ActivityMessage } from '@hcengineering/activity'
import contact, { Employee, Person, PersonAccount, PersonSpace } from '@hcengineering/contact' import contact, { Employee, Person, PersonAccount, PersonSpace } from '@hcengineering/contact'
import { Account, Class, Doc, Mixin, Ref, Tx, TxCUD } from '@hcengineering/core' import { Account, Class, Doc, Mixin, Ref, Tx, TxCUD } from '@hcengineering/core'
import { import {
@ -25,7 +26,6 @@ import {
} from '@hcengineering/notification' } from '@hcengineering/notification'
import { Metadata, Plugin, Resource, plugin } from '@hcengineering/platform' import { Metadata, Plugin, Resource, plugin } from '@hcengineering/platform'
import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core' import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core'
import { ActivityMessage } from '@hcengineering/activity'
/** /**
* @public * @public
@ -35,37 +35,13 @@ export const serverNotificationId = 'server-notification' as Plugin
/** /**
* @public * @public
*/ */
export async function getPersonAccount ( export function getPersonAccountById (_id: Ref<Account>, control: TriggerControl): PersonAccount | undefined {
person: Ref<Person>, const account = control.modelDb.findAllSync(
control: TriggerControl contact.class.PersonAccount,
): Promise<PersonAccount | undefined> { {
const account = ( _id: _id as Ref<PersonAccount>
await control.modelDb.findAll( },
contact.class.PersonAccount, { limit: 1 }
{
person
},
{ limit: 1 }
)
)[0]
return account
}
/**
* @public
*/
export async function getPersonAccountById (
_id: Ref<Account>,
control: TriggerControl
): Promise<PersonAccount | undefined> {
const account = (
await control.modelDb.findAll(
contact.class.PersonAccount,
{
_id: _id as Ref<PersonAccount>
},
{ limit: 1 }
)
)[0] )[0]
return account return account
} }
@ -109,7 +85,7 @@ export interface TextPresenter<T extends Doc = any> extends Class<T> {
* @public * @public
*/ */
export type TypeMatchFunc = Resource< export type TypeMatchFunc = Resource<
(tx: Tx, doc: Doc, user: Ref<Account>, type: NotificationType, control: TriggerControl) => Promise<boolean> (tx: Tx, doc: Doc, user: Ref<Account>[], type: NotificationType, control: TriggerControl) => boolean
> >
/** /**
@ -192,7 +168,7 @@ export default plugin(serverNotificationId, {
OnDocRemove: '' as Resource<TriggerFunc> OnDocRemove: '' as Resource<TriggerFunc>
}, },
function: { function: {
IsUserInFieldValue: '' as TypeMatchFunc, IsUserInFieldValueTypeMatch: '' as TypeMatchFunc,
IsUserEmployeeInFieldValue: '' as TypeMatchFunc IsUserEmployeeInFieldValueTypeMatch: '' as TypeMatchFunc
} }
}) })

View File

@ -13,7 +13,17 @@
// limitations under the License. // limitations under the License.
// //
import core, { Doc, Tx, TxCUD, TxCollectionCUD, TxCreateDoc, TxUpdateDoc, TxProcessor, Ref } from '@hcengineering/core' import core, {
Doc,
Tx,
TxCUD,
TxCollectionCUD,
TxCreateDoc,
TxUpdateDoc,
TxProcessor,
Ref,
type MeasureContext
} from '@hcengineering/core'
import request, { Request, RequestStatus } from '@hcengineering/request' import request, { Request, RequestStatus } from '@hcengineering/request'
import { getResource, translate } from '@hcengineering/platform' import { getResource, translate } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core' import type { TriggerControl } from '@hcengineering/server-core'
@ -25,7 +35,8 @@ import {
getCollaborators, getCollaborators,
getTextPresenter, getTextPresenter,
getUsersInfo, getUsersInfo,
toReceiverInfo toReceiverInfo,
getNotificationProviderControl
} from '@hcengineering/server-notification-resources' } from '@hcengineering/server-notification-resources'
import { PersonAccount } from '@hcengineering/contact' import { PersonAccount } from '@hcengineering/contact'
@ -46,7 +57,7 @@ export async function OnRequest (tx: Tx, control: TriggerControl): Promise<Tx[]>
let res: Tx[] = [] let res: Tx[] = []
res = res.concat(await getRequestNotificationTx(ptx, control)) res = res.concat(await getRequestNotificationTx(control.ctx, ptx, control))
if (ptx.tx._class === core.class.TxUpdateDoc) { if (ptx.tx._class === core.class.TxUpdateDoc) {
res = res.concat(await OnRequestUpdate(ptx, control)) res = res.concat(await OnRequestUpdate(ptx, control))
@ -121,7 +132,11 @@ async function getRequest (tx: TxCUD<Request>, control: TriggerControl): Promise
} }
// We need request-specific logic to attach a activity message on request create/update to parent, but use request collaborators for notifications // We need request-specific logic to attach a activity message on request create/update to parent, but use request collaborators for notifications
async function getRequestNotificationTx (tx: TxCollectionCUD<Doc, Request>, control: TriggerControl): Promise<Tx[]> { async function getRequestNotificationTx (
ctx: MeasureContext,
tx: TxCollectionCUD<Doc, Request>,
control: TriggerControl
): Promise<Tx[]> {
const request = await getRequest(tx.tx, control) const request = await getRequest(tx.tx, control)
if (request === undefined) return [] if (request === undefined) return []
@ -140,7 +155,7 @@ async function getRequestNotificationTx (tx: TxCollectionCUD<Doc, Request>, cont
const messages = messagesTxes.map((messageTx) => const messages = messagesTxes.map((messageTx) =>
TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc<DocUpdateMessage>) TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc<DocUpdateMessage>)
) )
const collaborators = await getCollaborators(request, control, tx.tx, res) const collaborators = await getCollaborators(control.ctx, request, control, tx.tx, res)
if (collaborators.length === 0) return res if (collaborators.length === 0) return res
@ -148,18 +163,18 @@ async function getRequestNotificationTx (tx: TxCollectionCUD<Doc, Request>, cont
objectId: doc._id objectId: doc._id
}) })
const usersInfo = await getUsersInfo(control.ctx, [...collaborators, tx.modifiedBy] as Ref<PersonAccount>[], control) const usersInfo = await getUsersInfo(control.ctx, [...collaborators, tx.modifiedBy] as Ref<PersonAccount>[], control)
const senderInfo = usersInfo.find(({ _id }) => _id === tx.modifiedBy) ?? { const senderInfo = usersInfo.get(tx.modifiedBy) ?? {
_id: tx.modifiedBy _id: tx.modifiedBy
} }
const notificationControl = await getNotificationProviderControl(ctx, control)
for (const target of collaborators) { for (const target of collaborators) {
const targetInfo = toReceiverInfo( const targetInfo = toReceiverInfo(control.hierarchy, usersInfo.get(target))
control.hierarchy,
usersInfo.find(({ _id }) => _id === target)
)
if (targetInfo === undefined) continue if (targetInfo === undefined) continue
const txes = await getNotificationTxes( const txes = await getNotificationTxes(
ctx,
control, control,
request, request,
tx.tx, tx.tx,
@ -168,7 +183,8 @@ async function getRequestNotificationTx (tx: TxCollectionCUD<Doc, Request>, cont
senderInfo, senderInfo,
{ isOwn: true, isSpace: false, shouldUpdateTimestamp: true }, { isOwn: true, isSpace: false, shouldUpdateTimestamp: true },
notifyContexts, notifyContexts,
messages messages,
notificationControl
) )
res.push(...txes) res.push(...txes)
} }

View File

@ -90,13 +90,13 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
/** /**
* @public * @public
*/ */
export async function IsIncomingMessage ( export function IsIncomingMessageTypeMatch (
tx: Tx, tx: Tx,
doc: Doc, doc: Doc,
user: Ref<Account>, user: Ref<Account>[],
type: NotificationType, type: NotificationType,
control: TriggerControl control: TriggerControl
): Promise<boolean> { ): boolean {
const message = TxProcessor.createDoc2Doc(TxProcessor.extractTx(tx) as TxCreateDoc<TelegramMessage>) const message = TxProcessor.createDoc2Doc(TxProcessor.extractTx(tx) as TxCreateDoc<TelegramMessage>)
return message.incoming && message.sendOn > (doc.createdOn ?? doc.modifiedOn) return message.incoming && message.sendOn > (doc.createdOn ?? doc.modifiedOn)
} }
@ -280,7 +280,7 @@ export default async () => ({
OnMessageCreate OnMessageCreate
}, },
function: { function: {
IsIncomingMessage, IsIncomingMessageTypeMatch,
FindMessages, FindMessages,
GetCurrentEmployeeTG, GetCurrentEmployeeTG,
GetIntegrationOwnerTG, GetIntegrationOwnerTG,

View File

@ -35,7 +35,7 @@ export default plugin(serverTelegramId, {
OnMessageCreate: '' as Resource<TriggerFunc> OnMessageCreate: '' as Resource<TriggerFunc>
}, },
function: { function: {
IsIncomingMessage: '' as TypeMatchFunc, IsIncomingMessageTypeMatch: '' as TypeMatchFunc,
FindMessages: '' as Resource<ObjectDDParticipantFunc>, FindMessages: '' as Resource<ObjectDDParticipantFunc>,
GetCurrentEmployeeTG: '' as Resource<TemplateFieldServerFunc>, GetCurrentEmployeeTG: '' as Resource<TemplateFieldServerFunc>,
GetIntegrationOwnerTG: '' as Resource<TemplateFieldServerFunc>, GetIntegrationOwnerTG: '' as Resource<TemplateFieldServerFunc>,

View File

@ -34,17 +34,18 @@ import core, {
import notification, { CommonInboxNotification } from '@hcengineering/notification' import notification, { CommonInboxNotification } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core' import type { TriggerControl } from '@hcengineering/server-core'
import { ReceiverInfo, SenderInfo } from '@hcengineering/server-notification'
import { import {
getCommonNotificationTxes, getCommonNotificationTxes,
getNotificationContent, getNotificationContent,
getNotificationProviderControl,
isShouldNotifyTx isShouldNotifyTx
} from '@hcengineering/server-notification-resources' } from '@hcengineering/server-notification-resources'
import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time'
import task, { makeRank } from '@hcengineering/task' import task, { makeRank } from '@hcengineering/task'
import { jsonToMarkup, nodeDoc, nodeParagraph, nodeText } from '@hcengineering/text' import { jsonToMarkup, nodeDoc, nodeParagraph, nodeText } from '@hcengineering/text'
import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker'
import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time'
import time, { ProjectToDo, ToDo, ToDoPriority, TodoAutomationHelper, WorkSlot } from '@hcengineering/time' import time, { ProjectToDo, ToDo, ToDoPriority, TodoAutomationHelper, WorkSlot } from '@hcengineering/time'
import { ReceiverInfo, SenderInfo } from '@hcengineering/server-notification' import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker'
/** /**
* @public * @public
@ -179,9 +180,9 @@ export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Pr
} }
const todo = TxProcessor.createDoc2Doc(createTx) const todo = TxProcessor.createDoc2Doc(createTx)
const account = await getPersonAccount(todo.user, control) const account = control.modelDb.getAccountByPersonId(todo.user) as PersonAccount[]
if (account === undefined) { if (account.length === 0) {
return [] return []
} }
@ -189,23 +190,24 @@ export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Pr
if (object === undefined) return [] if (object === undefined) return []
const person = ( const person = (
await control.findAll(contact.mixin.Employee, { _id: account.person as Ref<Employee>, active: true }, { limit: 1 }) await control.findAll(contact.mixin.Employee, { _id: todo.user as Ref<Employee>, active: true }, { limit: 1 })
)[0] )[0]
if (person === undefined) return [] if (person === undefined) return []
const personSpace = (await control.findAll(contact.class.PersonSpace, { person: account.person }, { limit: 1 }))[0] const personSpace = (await control.findAll(contact.class.PersonSpace, { person: todo.user }, { limit: 1 }))[0]
if (personSpace === undefined) return [] if (personSpace === undefined) return []
// TODO: Select a proper account
const receiverInfo: ReceiverInfo = { const receiverInfo: ReceiverInfo = {
_id: account._id, _id: account[0]._id,
account, account: account[0],
person, person,
space: personSpace._id space: personSpace._id
} }
const senderAccount = await control.modelDb.findOne(contact.class.PersonAccount, { const senderAccount = control.modelDb.findAllSync(contact.class.PersonAccount, {
_id: tx.modifiedBy as Ref<PersonAccount> _id: tx.modifiedBy as Ref<PersonAccount>
}) })[0]
const senderPerson = const senderPerson =
senderAccount !== undefined senderAccount !== undefined
? (await control.findAll(contact.class.Person, { _id: senderAccount.person }))[0] ? (await control.findAll(contact.class.Person, { _id: senderAccount.person }))[0]
@ -216,8 +218,8 @@ export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Pr
account: senderAccount, account: senderAccount,
person: senderPerson person: senderPerson
} }
const notificationControl = await getNotificationProviderControl(control.ctx, control)
const notifyResult = await isShouldNotifyTx(control, createTx, tx, todo, account, true, false) const notifyResult = await isShouldNotifyTx(control, createTx, tx, todo, account, true, false, notificationControl)
const content = await getNotificationContent(tx, account, senderInfo, todo, control) const content = await getNotificationContent(tx, account, senderInfo, todo, control)
const data: Partial<Data<CommonInboxNotification>> = { const data: Partial<Data<CommonInboxNotification>> = {
...content, ...content,
@ -229,6 +231,7 @@ export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Pr
} }
const txes = await getCommonNotificationTxes( const txes = await getCommonNotificationTxes(
control.ctx,
control, control,
object, object,
data, data,
@ -465,31 +468,18 @@ async function createIssueHandler (issue: Issue, control: TriggerControl): Promi
return [] return []
} }
async function getPersonAccount (person: Ref<Person>, control: TriggerControl): Promise<PersonAccount | undefined> {
const account = (
await control.modelDb.findAll(
contact.class.PersonAccount,
{
person
},
{ limit: 1 }
)
)[0]
return account
}
async function getIssueToDoData ( async function getIssueToDoData (
issue: Issue, issue: Issue,
user: Ref<Person>, user: Ref<Person>,
control: TriggerControl control: TriggerControl
): Promise<AttachedData<ProjectToDo> | undefined> { ): Promise<AttachedData<ProjectToDo> | undefined> {
const acc = await getPersonAccount(user, control) const acc = control.modelDb.getAccountByPersonId(user) as PersonAccount[]
if (acc === undefined) return if (acc.length === 0) return
const firstTodoItem = ( const firstTodoItem = (
await control.findAll( await control.findAll(
time.class.ToDo, time.class.ToDo,
{ {
user: acc.person, user: { $in: acc.map((it) => it.person) },
doneOn: null doneOn: null
}, },
{ {
@ -506,7 +496,7 @@ async function getIssueToDoData (
priority: ToDoPriority.NoPriority, priority: ToDoPriority.NoPriority,
visibility: 'public', visibility: 'public',
title: issue.title, title: issue.title,
user: acc.person, user,
rank rank
} }
return data return data

View File

@ -13,25 +13,25 @@
// limitations under the License. // limitations under the License.
// //
import contact, { type Employee, type PersonAccount } from '@hcengineering/contact'
import { type Account, type Ref, type TxCUD } from '@hcengineering/core' import { type Account, type Ref, type TxCUD } from '@hcengineering/core'
import type { NotificationType } from '@hcengineering/notification' import type { NotificationType } from '@hcengineering/notification'
import type { TriggerControl } from '@hcengineering/server-core' import type { TriggerControl } from '@hcengineering/server-core'
import type { TrainingRequest } from '@hcengineering/training' import type { TrainingRequest } from '@hcengineering/training'
import contact, { type Employee, type PersonAccount } from '@hcengineering/contact'
import { isTxCreateDoc } from '../utils/isTxCreateDoc' import { isTxCreateDoc } from '../utils/isTxCreateDoc'
import { isTxUpdateDoc } from '../utils/isTxUpdateDoc' import { isTxUpdateDoc } from '../utils/isTxUpdateDoc'
export async function TrainingRequestNotificationTypeMatch ( export function TrainingRequestNotificationTypeMatch (
tx: TxCUD<TrainingRequest>, tx: TxCUD<TrainingRequest>,
doc: TrainingRequest, doc: TrainingRequest,
user: Ref<Account>, user: Ref<Account>[],
type: NotificationType, type: NotificationType,
control: TriggerControl control: TriggerControl
): Promise<boolean> { ): boolean {
if (isTxCreateDoc(tx)) { if (isTxCreateDoc(tx)) {
const employeeRef: Ref<Employee> = ( const employeeRef: Ref<Employee> = control.modelDb.findAllSync(contact.class.PersonAccount, {
await control.modelDb.findAll(contact.class.PersonAccount, { _id: user as Ref<PersonAccount> }) _id: user[0] as Ref<PersonAccount>
)[0]?.person as Ref<Employee> })[0]?.person as Ref<Employee>
return doc.trainees.includes(employeeRef) return doc.trainees.includes(employeeRef)
} }
@ -41,9 +41,9 @@ export async function TrainingRequestNotificationTypeMatch (
return false return false
} }
const employeeRef: Ref<Employee> = ( const employeeRef: Ref<Employee> = control.modelDb.findAllSync(contact.class.PersonAccount, {
await control.modelDb.findAll(contact.class.PersonAccount, { _id: user as Ref<PersonAccount> }) _id: user[0] as Ref<PersonAccount>
)[0]?.person as Ref<Employee> })[0]?.person as Ref<Employee>
const newTrainees = typeof pushed === 'object' ? pushed.$each : [pushed] const newTrainees = typeof pushed === 'object' ? pushed.$each : [pushed]
return newTrainees.includes(employeeRef) return newTrainees.includes(employeeRef)
} }

View File

@ -1983,7 +1983,7 @@ async function createPersonAccount (
client ?? client ??
(await connect(getEndpoint(ctx, workspaceInfo, EndpointKind.Internal), getWorkspaceId(workspace, productId))) (await connect(getEndpoint(ctx, workspaceInfo, EndpointKind.Internal), getWorkspaceId(workspace, productId)))
try { try {
const ops = new TxOperations(connection, core.account.System) const ops = new TxOperations(connection, core.account.System).apply('create-person' + generateId())
const name = combineName(account.first, account.last) const name = combineName(account.first, account.last)
// Check if PersonAccount is not exists // Check if PersonAccount is not exists
@ -2033,6 +2033,7 @@ async function createPersonAccount (
} }
} }
} }
await ops.commit()
} finally { } finally {
if (client === undefined) { if (client === undefined) {
await connection.close() await connection.close()

View File

@ -879,12 +879,10 @@ export class TServerStorage implements ServerStorage {
ctx: SessionOperationContext, ctx: SessionOperationContext,
findAll: ServerStorage['findAll'] findAll: ServerStorage['findAll']
): Promise<Tx[]> { ): Promise<Tx[]> {
derived.sort((a, b) => a.modifiedOn - b.modifiedOn)
await ctx.with('derived-route-tx', {}, (ctx) => this.routeTx(ctx, ...derived))
const nestedTxes: Tx[] = [] const nestedTxes: Tx[] = []
if (derived.length > 0) { if (derived.length > 0) {
derived.sort((a, b) => a.modifiedOn - b.modifiedOn)
await ctx.with('derived-route-tx', {}, (ctx) => this.routeTx(ctx, ...derived))
nestedTxes.push(...(await this.processDerived(ctx, derived, findAll))) nestedTxes.push(...(await this.processDerived(ctx, derived, findAll)))
} }
@ -992,6 +990,7 @@ export class TServerStorage implements ServerStorage {
const onEnds: (() => void)[] = [] const onEnds: (() => void)[] = []
const result: TxResult[] = [] const result: TxResult[] = []
const derived: Tx[] = [...txes].filter((it) => it._class !== core.class.TxApplyIf) const derived: Tx[] = [...txes].filter((it) => it._class !== core.class.TxApplyIf)
let txPromise: Promise<TxResult[]> | undefined
try { try {
this.fillTxes(txes, txToStore, modelTx, txToProcess, applyTxes) this.fillTxes(txes, txToStore, modelTx, txToProcess, applyTxes)
@ -1023,7 +1022,7 @@ export class TServerStorage implements ServerStorage {
await this.modelDb.tx(tx) await this.modelDb.tx(tx)
} }
if (txToStore.length > 0) { if (txToStore.length > 0) {
await ctx.with( txPromise = ctx.with(
'domain-tx', 'domain-tx',
{}, {},
async (ctx) => await this.getAdapter(DOMAIN_TX, true).tx(ctx.ctx, ...txToStore), async (ctx) => await this.getAdapter(DOMAIN_TX, true).tx(ctx.ctx, ...txToStore),
@ -1034,9 +1033,8 @@ export class TServerStorage implements ServerStorage {
) )
} }
if (txToProcess.length > 0) { if (txToProcess.length > 0) {
result.push(...(await ctx.with('routeTx', {}, (ctx) => this.routeTx(ctx, ...txToProcess))), { const routerResult = await ctx.with('routeTx', {}, (ctx) => this.routeTx(ctx, ...txToProcess))
count: txToProcess.length result.push(...routerResult, { count: txToProcess.length })
})
// invoke triggers and store derived objects // invoke triggers and store derived objects
derived.push(...(await this.processDerived(ctx, txToProcess, _findAll))) derived.push(...(await this.processDerived(ctx, txToProcess, _findAll)))
} }
@ -1053,6 +1051,10 @@ export class TServerStorage implements ServerStorage {
{ count: txToProcess.length + derived.length } { count: txToProcess.length + derived.length }
) )
} }
if (txPromise !== undefined) {
// Wait for main Tx to be stored
await txPromise
}
} catch (err: any) { } catch (err: any) {
ctx.ctx.error('error process tx', { error: err }) ctx.ctx.error('error process tx', { error: err })
Analytics.handleError(err) Analytics.handleError(err)

View File

@ -16,7 +16,6 @@
import core, { import core, {
DOMAIN_MODEL, DOMAIN_MODEL,
DOMAIN_TX, DOMAIN_TX,
RateLimiter,
SortingOrder, SortingOrder,
TxProcessor, TxProcessor,
addOperation, addOperation,
@ -76,6 +75,7 @@ import { createHash } from 'crypto'
import { import {
type AbstractCursor, type AbstractCursor,
type AnyBulkWriteOperation, type AnyBulkWriteOperation,
type BulkWriteResult,
type Collection, type Collection,
type Db, type Db,
type Document, type Document,
@ -141,9 +141,6 @@ export interface DbAdapterOptions {
abstract class MongoAdapterBase implements DbAdapter { abstract class MongoAdapterBase implements DbAdapter {
_db: DBCollectionHelper _db: DBCollectionHelper
findRateLimit = new RateLimiter(parseInt(process.env.FIND_RLIMIT ?? '1000'))
rateLimit = new RateLimiter(parseInt(process.env.TX_RLIMIT ?? '5'))
handlers: DbAdapterHandler[] = [] handlers: DbAdapterHandler[] = []
on (handler: DbAdapterHandler): void { on (handler: DbAdapterHandler): void {
@ -706,130 +703,119 @@ abstract class MongoAdapterBase implements DbAdapter {
const stTime = Date.now() const stTime = Date.now()
const mongoQuery = this.translateQuery(_class, query, options) const mongoQuery = this.translateQuery(_class, query, options)
const fQuery = { ...mongoQuery.base, ...mongoQuery.lookup } const fQuery = { ...mongoQuery.base, ...mongoQuery.lookup }
return await addOperation( return await addOperation(ctx, 'find-all', {}, async () => {
ctx, const st = Date.now()
'find-all', let result: FindResult<T>
{}, const domain = options?.domain ?? this.hierarchy.getDomain(_class)
async () => if (
await this.findRateLimit.exec(async () => { options != null &&
const st = Date.now() (options?.lookup != null || this.isEnumSort(_class, options) || this.isRulesSort(options))
let result: FindResult<T> ) {
const domain = options?.domain ?? this.hierarchy.getDomain(_class) return await this.findWithPipeline(ctx, domain, _class, query, options, stTime)
if (
options != null &&
(options?.lookup != null || this.isEnumSort(_class, options) || this.isRulesSort(options))
) {
return await this.findWithPipeline(ctx, domain, _class, query, options, stTime)
}
const coll = this.collection(domain)
if (options?.limit === 1 || typeof query._id === 'string') {
// Skip sort/projection/etc.
return await ctx.with(
'find-one',
{ domain },
async (ctx) => {
const findOptions: MongoFindOptions = {}
if (options?.sort !== undefined) {
findOptions.sort = this.collectSort<T>(options, _class)
}
if (options?.projection !== undefined) {
findOptions.projection = this.calcProjection<T>(options, _class)
}
let doc: WithId<Document> | null
if (typeof fQuery._id === 'string') {
doc = await coll.findOne({ _id: fQuery._id }, findOptions)
if (doc != null && matchQuery([doc as unknown as Doc], query, _class, this.hierarchy).length === 0) {
doc = null
}
} else {
doc = await coll.findOne(fQuery, findOptions)
}
let total = -1
if (options?.total === true) {
total = await coll.countDocuments({ ...mongoQuery.base, ...mongoQuery.lookup })
}
if (doc != null) {
return toFindResult([this.stripHash<T>(doc as unknown as T) as T], total)
}
return toFindResult([], total)
},
{ domain, mongoQuery, _idOnly: typeof fQuery._id === 'string' }
)
}
let cursor = coll.find<T>(fQuery)
if (options?.projection !== undefined) {
const projection = this.calcProjection<T>(options, _class)
if (projection != null) {
cursor = cursor.project(projection)
}
}
let total: number = -1
if (options != null) {
if (options.sort !== undefined) {
const sort = this.collectSort<T>(options, _class)
if (sort !== undefined) {
cursor = cursor.sort(sort)
}
}
if (options.limit !== undefined || typeof query._id === 'string') {
if (options.total === true) {
total = await coll.countDocuments(fQuery)
}
cursor = cursor.limit(options.limit ?? 1)
}
}
// Error in case of timeout
try {
let res: T[] = []
await ctx.with(
'find-all',
{},
async (ctx) => {
res = await toArray(cursor)
},
() => ({
size: res.length,
queueTime: stTime - st,
mongoQuery,
options,
domain
})
)
if (options?.total === true && options?.limit === undefined) {
total = res.length
}
result = toFindResult(this.stripHash(res) as T[], total)
} catch (e) {
console.error('error during executing cursor in findAll', _class, cutObjectArray(query), options, e)
throw e
}
const edTime = Date.now()
if (edTime - st > 1000 || st - stTime > 1000) {
ctx.error('FindAll', {
time: edTime - st,
_class,
query: fQuery,
options,
queueTime: st - stTime
})
}
this.handleEvent(domain, 'read', result.length)
return result
}),
{
_class,
query: fQuery,
options
} }
) const coll = this.collection(domain)
if (options?.limit === 1 || typeof query._id === 'string') {
// Skip sort/projection/etc.
return await ctx.with(
'find-one',
{ domain },
async (ctx) => {
const findOptions: MongoFindOptions = {}
if (options?.sort !== undefined) {
findOptions.sort = this.collectSort<T>(options, _class)
}
if (options?.projection !== undefined) {
findOptions.projection = this.calcProjection<T>(options, _class)
}
let doc: WithId<Document> | null
if (typeof fQuery._id === 'string') {
doc = await coll.findOne({ _id: fQuery._id }, findOptions)
if (doc != null && matchQuery([doc as unknown as Doc], query, _class, this.hierarchy).length === 0) {
doc = null
}
} else {
doc = await coll.findOne(fQuery, findOptions)
}
let total = -1
if (options?.total === true) {
total = await coll.countDocuments({ ...mongoQuery.base, ...mongoQuery.lookup })
}
if (doc != null) {
return toFindResult([this.stripHash<T>(doc as unknown as T) as T], total)
}
return toFindResult([], total)
},
{ domain, mongoQuery, _idOnly: typeof fQuery._id === 'string' }
)
}
let cursor = coll.find<T>(fQuery)
if (options?.projection !== undefined) {
const projection = this.calcProjection<T>(options, _class)
if (projection != null) {
cursor = cursor.project(projection)
}
}
let total: number = -1
if (options != null) {
if (options.sort !== undefined) {
const sort = this.collectSort<T>(options, _class)
if (sort !== undefined) {
cursor = cursor.sort(sort)
}
}
if (options.limit !== undefined || typeof query._id === 'string') {
if (options.total === true) {
total = await coll.countDocuments(fQuery)
}
cursor = cursor.limit(options.limit ?? 1)
}
}
// Error in case of timeout
try {
let res: T[] = []
await ctx.with(
'find-all',
{},
async (ctx) => {
res = await toArray(cursor)
},
() => ({
size: res.length,
queueTime: stTime - st,
mongoQuery,
options,
domain
})
)
if (options?.total === true && options?.limit === undefined) {
total = res.length
}
result = toFindResult(this.stripHash(res) as T[], total)
} catch (e) {
console.error('error during executing cursor in findAll', _class, cutObjectArray(query), options, e)
throw e
}
const edTime = Date.now()
if (edTime - st > 1000 || st - stTime > 1000) {
ctx.error('FindAll', {
time: edTime - st,
_class,
query: fQuery,
options,
queueTime: st - stTime
})
}
this.handleEvent(domain, 'read', result.length)
return result
})
} }
private collectSort<T extends Doc>( private collectSort<T extends Doc>(
@ -1129,13 +1115,14 @@ class MongoAdapter extends MongoAdapterBase {
if (bulk.length === 0) { if (bulk.length === 0) {
return return
} }
const promises: Promise<BulkWriteResult>[] = []
for (const [domain, ops] of bulk) { for (const [domain, ops] of bulk) {
if (ops === undefined || ops.length === 0) { if (ops === undefined || ops.length === 0) {
continue continue
} }
const coll = this.db.collection<Doc>(domain) const coll = this.db.collection<Doc>(domain)
await this.rateLimit.exec(() => promises.push(
addOperation( addOperation(
ctx, ctx,
'bulk-write', 'bulk-write',
@ -1146,8 +1133,7 @@ class MongoAdapter extends MongoAdapterBase {
{ domain }, { domain },
() => () =>
coll.bulkWrite(ops, { coll.bulkWrite(ops, {
ordered: false, ordered: false
writeConcern: { w: 0 }
}), }),
{ {
domain, domain,
@ -1157,6 +1143,7 @@ class MongoAdapter extends MongoAdapterBase {
) )
) )
} }
await Promise.all(promises)
} }
async pushBulk (ctx: MeasureContext, domain: Domain, ops: AnyBulkWriteOperation<Doc>[]): Promise<void> { async pushBulk (ctx: MeasureContext, domain: Domain, ops: AnyBulkWriteOperation<Doc>[]): Promise<void> {
@ -1166,6 +1153,8 @@ class MongoAdapter extends MongoAdapterBase {
} else { } else {
this.bulkOps.set(domain, ops) this.bulkOps.set(domain, ops)
} }
// We need to wait next cycle to send request
await new Promise<void>((resolve) => setImmediate(resolve))
await this._pushBulk(ctx) await this._pushBulk(ctx)
} }
@ -1528,6 +1517,10 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
const txes = this.txBulk const txes = this.txBulk
this.txBulk = [] this.txBulk = []
if (txes.length === 0) {
return
}
const opName = txes.length === 1 ? 'tx-one' : 'tx' const opName = txes.length === 1 ? 'tx-one' : 'tx'
await addOperation( await addOperation(
ctx, ctx,
@ -1541,8 +1534,7 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
this.txCollection().insertMany( this.txCollection().insertMany(
txes.map((it) => translateDoc(it)), txes.map((it) => translateDoc(it)),
{ {
ordered: false, ordered: false
writeConcern: { w: 0 }
} }
), ),
{ {
@ -1561,6 +1553,9 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
return [] return []
} }
this.txBulk.push(...tx) this.txBulk.push(...tx)
// We need to wait next cycle to send request
await new Promise<void>((resolve) => setImmediate(resolve))
await this._bulkTx(ctx) await this._bulkTx(ctx)
return [] return []
} }

View File

@ -27,8 +27,8 @@
"compression-webpack-plugin": "^10.0.0", "compression-webpack-plugin": "^10.0.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"fork-ts-checker-webpack-plugin": "~7.3.0", "fork-ts-checker-webpack-plugin": "~7.3.0",
"update-browserslist-db": "~1.0.11", "update-browserslist-db": "^1.1.0",
"browserslist": "4.21.5", "browserslist": "^4.23.3",
"esbuild": "^0.20.0", "esbuild": "^0.20.0",
"esbuild-loader": "^4.0.3", "esbuild-loader": "^4.0.3",
"typescript": "^5.3.3", "typescript": "^5.3.3",

View File

@ -27,6 +27,9 @@ export class ChannelPage extends CommonPage {
readonly chooseChannel = (channel: string): Locator => this.page.getByRole('button', { name: channel }) readonly chooseChannel = (channel: string): Locator => this.page.getByRole('button', { name: channel })
readonly closePopupWindow = (): Locator => this.page.locator('.notifyPopup button[data-id="btnNotifyClose"]') readonly closePopupWindow = (): Locator => this.page.locator('.notifyPopup button[data-id="btnNotifyClose"]')
readonly openAddMemberToChannel = (userName: string): Locator => this.page.getByRole('button', { name: userName }) readonly openAddMemberToChannel = (userName: string): Locator => this.page.getByRole('button', { name: userName })
readonly addMemberToChannelTableButton = (userName: string): Locator =>
this.page.locator('.antiTable-body__row').getByText(userName)
readonly addMemberToChannelButton = (userName: string): Locator => this.page.getByText(userName) readonly addMemberToChannelButton = (userName: string): Locator => this.page.getByText(userName)
readonly joinChannelButton = (): Locator => this.page.getByRole('button', { name: 'Join' }) readonly joinChannelButton = (): Locator => this.page.getByRole('button', { name: 'Join' })
readonly addEmojiButton = (): Locator => readonly addEmojiButton = (): Locator =>
@ -204,7 +207,7 @@ export class ChannelPage extends CommonPage {
} }
async clickOnUser (user: string): Promise<void> { async clickOnUser (user: string): Promise<void> {
await this.addMemberToChannelButton(user).click() await this.addMemberToChannelTableButton(user).click()
} }
async addMemberToChannel (user: string): Promise<void> { async addMemberToChannel (user: string): Promise<void> {