Merge branch 'develop' into staging

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-01-21 23:50:08 +07:00
75 changed files with 1154 additions and 424 deletions
+131 -79
View File
@@ -1069,6 +1069,9 @@ importers:
'@rush-temp/text':
specifier: file:./projects/text.tgz
version: file:projects/text.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.24.2)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.22.3)(prosemirror-state@1.4.3)(prosemirror-view@1.34.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))(utf-8-validate@6.0.4)
'@rush-temp/text-core':
specifier: file:./projects/text-core.tgz
version: file:projects/text-core.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))(utf-8-validate@6.0.4)
'@rush-temp/text-editor':
specifier: file:./projects/text-editor.tgz
version: file:projects/text-editor.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
@@ -1273,9 +1276,6 @@ importers:
'@types/cors':
specifier: ^2.8.12
version: 2.8.17
'@types/crypto-js':
specifier: ^4.1.1
version: 4.2.2
'@types/csvtojson':
specifier: ^2.0.0
version: 2.0.3
@@ -1394,8 +1394,8 @@ importers:
specifier: ^8.3.1
version: 8.3.4
'@types/web-push':
specifier: ~3.6.3
version: 3.6.3
specifier: ^3.6.4
version: 3.6.4
'@types/ws':
specifier: ^8.5.11
version: 8.5.11
@@ -1490,7 +1490,7 @@ importers:
specifier: ^3.1.5
version: 3.1.8(encoding@0.1.13)
crypto-js:
specifier: ^4.1.1
specifier: ^4.2.0
version: 4.2.0
css-loader:
specifier: ^5.2.1
@@ -1667,8 +1667,8 @@ importers:
specifier: ^1.1.1
version: 1.1.1
intl-messageformat:
specifier: ^9.7.1
version: 9.13.0
specifier: ^10.7.14
version: 10.7.14
itty-router:
specifier: ^5.0.18
version: 5.0.18
@@ -1729,6 +1729,9 @@ importers:
markdown-it:
specifier: ^14.0.0
version: 14.0.0
md5.js:
specifier: ^1.3.5
version: 1.3.5
mermaid:
specifier: ~11.4.1
version: 11.4.1
@@ -1928,7 +1931,7 @@ importers:
specifier: ^8.3.2
version: 8.3.2
web-push:
specifier: ~3.6.7
specifier: ^3.6.7
version: 3.6.7
webpack:
specifier: ^5.97.1
@@ -2928,20 +2931,20 @@ packages:
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'}
'@formatjs/ecma402-abstract@1.11.4':
resolution: {integrity: sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==}
'@formatjs/ecma402-abstract@2.3.2':
resolution: {integrity: sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg==}
'@formatjs/fast-memoize@1.2.1':
resolution: {integrity: sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==}
'@formatjs/fast-memoize@2.2.6':
resolution: {integrity: sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==}
'@formatjs/icu-messageformat-parser@2.1.0':
resolution: {integrity: sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==}
'@formatjs/icu-messageformat-parser@2.11.0':
resolution: {integrity: sha512-Hp81uTjjdTk3FLh/dggU5NK7EIsVWc5/ZDWrIldmf2rBuPejuZ13CZ/wpVE2SToyi4EiroPTQ1XJcJuZFIxTtw==}
'@formatjs/icu-skeleton-parser@1.3.6':
resolution: {integrity: sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==}
'@formatjs/icu-skeleton-parser@1.8.12':
resolution: {integrity: sha512-QRAY2jC1BomFQHYDMcZtClqHR55EEnB96V7Xbk/UiBodsuFc5kujybzt87+qj1KqmJozFhk6n4KiT1HKwAkcfg==}
'@formatjs/intl-localematcher@0.2.25':
resolution: {integrity: sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==}
'@formatjs/intl-localematcher@0.5.10':
resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
'@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
@@ -3808,7 +3811,7 @@ packages:
version: 0.0.0
'@rush-temp/account@file:projects/account.tgz':
resolution: {integrity: sha512-aFUxA1twR0m+7sDVFXyKoeVQtVLMrr973LpLK6LHrRa+PD010X6+AyoOkkyVJKFN0Q6zu2Oqz88PsEt50Z2Mjg==, tarball: file:projects/account.tgz}
resolution: {integrity: sha512-xWdmeIZiSKL5eyvVHNUEnn/GXGsaNkayjxoDW+ydFB1GwUWExvSTA7OOs9mfG+IwLNU08GNKNGBYGM3XKZ2PsA==, tarball: file:projects/account.tgz}
version: 0.0.0
'@rush-temp/activity-assets@file:projects/activity-assets.tgz':
@@ -3936,7 +3939,7 @@ packages:
version: 0.0.0
'@rush-temp/cloud-transactor@file:projects/cloud-transactor.tgz':
resolution: {integrity: sha512-sz4uAEfFOZfE7TOae+sv+RWlxunC4WlsQ7GTOX+kE+KDTNJK/r3d7+yIR/O3iDd4nsbLO/Al/9Z+wMZDXsmJcw==, tarball: file:projects/cloud-transactor.tgz}
resolution: {integrity: sha512-RmuejZDmaNRcHOCurZBKIHOgqyfTwUNtJdhzJEANVw3XAbuapCjlaCwmtK5bad2D6S1+VrHrXoF4vw4FamxZkw==, tarball: file:projects/cloud-transactor.tgz}
version: 0.0.0
'@rush-temp/collaboration@file:projects/collaboration.tgz':
@@ -3956,11 +3959,11 @@ packages:
version: 0.0.0
'@rush-temp/contact-resources@file:projects/contact-resources.tgz':
resolution: {integrity: sha512-NwGEZHpomoqLYe2i/Z3Wm9xLB1Cl7okFToLBn51JWhFebje+20w47PFde2tUX77f5bfYuLJ0ykZbKnfDbFz9TA==, tarball: file:projects/contact-resources.tgz}
resolution: {integrity: sha512-sOoou1UaCHSN/qYHhEiWyiPICxmqP2p+ACDAeVzhU8fuUa5tejlzuANOdiEYqz4c8+u/NhlCQHd7G7G5WjMPpg==, tarball: file:projects/contact-resources.tgz}
version: 0.0.0
'@rush-temp/contact@file:projects/contact.tgz':
resolution: {integrity: sha512-sjRtgKszoagGm79PFQKes/sfRMcddH1qEMG3/xKuRLKlVcVJJ56twJaCCUeXP+7MsXMH5eEcS3sNCIdtznuASg==, tarball: file:projects/contact.tgz}
resolution: {integrity: sha512-59CabCDtmhSMmziQMA8E7sB/xLjRibcfTjx/EuYh4trtpfOawQ6lVZSljJ3Fr3huHHg4A8A14L71MC4PgPSepQ==, tarball: file:projects/contact.tgz}
version: 0.0.0
'@rush-temp/controlled-documents-assets@file:projects/controlled-documents-assets.tgz':
@@ -4536,7 +4539,7 @@ packages:
version: 0.0.0
'@rush-temp/platform@file:projects/platform.tgz':
resolution: {integrity: sha512-GyVd1NUl00O73Bwq0b/KFXhAfrko5BnqcN9ak+WWmDjHqi4CsWUfYm6ol5B7rbC38dqXXD4yBad/iEgywLliXA==, tarball: file:projects/platform.tgz}
resolution: {integrity: sha512-BlslXnvEEebr3t7GEdTHD5HcoOTdez1j6Fb8Q6KctCunFjTeRtN5vtsutv2ZR+ChIGgZ5seZ6DOUFNM1MJf4ew==, tarball: file:projects/platform.tgz}
version: 0.0.0
'@rush-temp/pod-account@file:projects/pod-account.tgz':
@@ -4592,7 +4595,7 @@ packages:
version: 0.0.0
'@rush-temp/pod-ses@file:projects/pod-ses.tgz':
resolution: {integrity: sha512-P+HVwQR4SO6F4EXl5ReGR6hbng8CKsl4IK6Ww6u7yhNh1x7YGKzmV7UMRjSUc7qsNu4CzOhF7AYatCB602ANjA==, tarball: file:projects/pod-ses.tgz}
resolution: {integrity: sha512-gVCpGeUc+IsGfdCD84Sp+PcY21yRBkUhwfMUUC8VXcjf5MiayypLcc8pIGD2ufGGRJRAI3+clxIfiVc29ofBxw==, tarball: file:projects/pod-ses.tgz}
version: 0.0.0
'@rush-temp/pod-sign@file:projects/pod-sign.tgz':
@@ -4740,7 +4743,7 @@ packages:
version: 0.0.0
'@rush-temp/server-activity-resources@file:projects/server-activity-resources.tgz':
resolution: {integrity: sha512-+UU7f65uCYtxuk56pj1bJ2ZaE6YHxVubtTfbsrLa8ZAGUOvWjrxY37B8j8GfcgGioL6PESsB12uEZIvghnNNaQ==, tarball: file:projects/server-activity-resources.tgz}
resolution: {integrity: sha512-pzbXI3TbbMXYv/qVmx97uHJD8IOrI7glY8HsD1Np0PAGqzGpWBOm1j6oHKjO/WvfXPCvbmBRBvir22/+IdeXKA==, tarball: file:projects/server-activity-resources.tgz}
version: 0.0.0
'@rush-temp/server-activity@file:projects/server-activity.tgz':
@@ -4784,7 +4787,7 @@ packages:
version: 0.0.0
'@rush-temp/server-chunter-resources@file:projects/server-chunter-resources.tgz':
resolution: {integrity: sha512-Kpp8RTEJ+z5d8Rq7HbOhui8ZyV8OvKEXVkAXT80Ju/qhVnZ5Mnw/jk33dgwur1NPVjCWOK2SPECHf+eqzwXpBg==, tarball: file:projects/server-chunter-resources.tgz}
resolution: {integrity: sha512-anzs0C7AXicSpf9F+jjqdokc3cZunw5O8gC0095fQmEoOW94OmvTyboGrx8iK3i+iYam4Z6Bkt60rXRo3JvXZQ==, tarball: file:projects/server-chunter-resources.tgz}
version: 0.0.0
'@rush-temp/server-chunter@file:projects/server-chunter.tgz':
@@ -4812,7 +4815,7 @@ packages:
version: 0.0.0
'@rush-temp/server-controlled-documents-resources@file:projects/server-controlled-documents-resources.tgz':
resolution: {integrity: sha512-g27+rhbyGhRdYikNstWia5wPng1EUiDp4DoEu51KVFrE4MOeaEXvB85Zb/4Jxg1RL4H7P6o7VRABeVqv6fkqsg==, tarball: file:projects/server-controlled-documents-resources.tgz}
resolution: {integrity: sha512-mYK9xbNtII74MyRDJjutIBvUvdXfVeGwaixq9uRTVlqQsmb8oYvqVsTLLfmjL/e/eb1nmUPtnWh7sogkGYU/yA==, tarball: file:projects/server-controlled-documents-resources.tgz}
version: 0.0.0
'@rush-temp/server-controlled-documents@file:projects/server-controlled-documents.tgz':
@@ -4860,7 +4863,7 @@ packages:
version: 0.0.0
'@rush-temp/server-gmail-resources@file:projects/server-gmail-resources.tgz':
resolution: {integrity: sha512-V9M3+rquV4MPbPtmwWPdekguzA5WKJRDvU7xtpYKQSJ7J7hkLPKNah4GtSjin0opg6lTgtoAJUQaYLKheeCCbA==, tarball: file:projects/server-gmail-resources.tgz}
resolution: {integrity: sha512-B+iAjGb/dbs1BhVc2MmYZxv2LrHQk/IH3UazglCC7v3Nu5x6ny4oMPCHXqv7W7MMVjIzqt60Wsw7spkvyS1CSA==, tarball: file:projects/server-gmail-resources.tgz}
version: 0.0.0
'@rush-temp/server-gmail@file:projects/server-gmail.tgz':
@@ -4912,7 +4915,7 @@ packages:
version: 0.0.0
'@rush-temp/server-notification-resources@file:projects/server-notification-resources.tgz':
resolution: {integrity: sha512-DhxiRHwKgwqkX4WP2ZmqGL7fHs0Ev3rtxHT/uylk5SPCPbKmBe20xyEzheSahP0Vm6kIS9XerGQuxGrI31eVBw==, tarball: file:projects/server-notification-resources.tgz}
resolution: {integrity: sha512-sLf+6eAqeGkx+28mefySh7RB9mMnEh5Cuw/Q036PALbzj/1tihPIIxlst9kMZAYgGr7WRaZGDGK4Pq2yxfCrPQ==, tarball: file:projects/server-notification-resources.tgz}
version: 0.0.0
'@rush-temp/server-notification@file:projects/server-notification.tgz':
@@ -4972,7 +4975,7 @@ packages:
version: 0.0.0
'@rush-temp/server-telegram-resources@file:projects/server-telegram-resources.tgz':
resolution: {integrity: sha512-VsEVxoPM/hyelQa9/rQFzo+vbS8ixxU4RvVWdM0ae9HJuaEyszX4y9GheG/SDMa0NzCtyOOLf9KqL3ohGpkWRg==, tarball: file:projects/server-telegram-resources.tgz}
resolution: {integrity: sha512-2zGK/gI0mG8ZvEUYDK0lt8m525blawYUmk64vMfYF0K7hkOmKaE+aSECh6UJozxuxURpyNNzi/9R0SNnMyfXKw==, tarball: file:projects/server-telegram-resources.tgz}
version: 0.0.0
'@rush-temp/server-telegram@file:projects/server-telegram.tgz':
@@ -4984,7 +4987,7 @@ packages:
version: 0.0.0
'@rush-temp/server-time-resources@file:projects/server-time-resources.tgz':
resolution: {integrity: sha512-WePYLAz/QHfFL+jMa/ZE6JQIvNYK4tGBs4ur6UBG3inJigtt9p7AR4PO+nr7dtGZqG8RT0T0TrGREcdV7pX5Gw==, tarball: file:projects/server-time-resources.tgz}
resolution: {integrity: sha512-N9YLO1krTVoyAYV78E8sW2mDjSuJ/wHD9EHPAZO6AHxtoFmzlMPJaLprjGf7bWIemiGyAZKcAFhmpu0RSlaYGA==, tarball: file:projects/server-time-resources.tgz}
version: 0.0.0
'@rush-temp/server-time@file:projects/server-time.tgz':
@@ -5000,7 +5003,7 @@ packages:
version: 0.0.0
'@rush-temp/server-tracker-resources@file:projects/server-tracker-resources.tgz':
resolution: {integrity: sha512-VmLhXIzNRo1VeABIkcyYrRK7P2+OtDpXpYCrutGgmUqT+ioPHxaefntsS+wzItpblY78u4DJYSIkfOhtQXCTtg==, tarball: file:projects/server-tracker-resources.tgz}
resolution: {integrity: sha512-Npz6QfpPLP18rNxIGzNvjKf4QEhT8ywVdlYNt7e4QGvA1zetFDava2Cg0QpN31LZue8pTDCC3T94kBQMUiUfqg==, tarball: file:projects/server-tracker-resources.tgz}
version: 0.0.0
'@rush-temp/server-tracker@file:projects/server-tracker.tgz':
@@ -5139,6 +5142,10 @@ packages:
resolution: {integrity: sha512-noV3nlBP0OvM6i4C5YUevkhltHxG/2bct4pusP3O3AEQ7Za+A4qwzpsE08pHfA9f9pedEG677GE4EmG9x61rVQ==, tarball: file:projects/tests-sanity.tgz}
version: 0.0.0
'@rush-temp/text-core@file:projects/text-core.tgz':
resolution: {integrity: sha512-piWZZp7T/zx+IKTP7fQGdY2schRbTEF2k0YuwXHkipp5MCA3BQLfy9VaZxAo5D4jhg0xySc1grVUfAqoOJIv8g==, tarball: file:projects/text-core.tgz}
version: 0.0.0
'@rush-temp/text-editor-assets@file:projects/text-editor-assets.tgz':
resolution: {integrity: sha512-HrJXNpWkSNwguy4rSL6Cihc3/1eTmi0KQAmyLix1ci88uOK/9kKga7lMnKAsuobrB9DjbuLgPpcEMNpavMi13w==, tarball: file:projects/text-editor-assets.tgz}
version: 0.0.0
@@ -5156,7 +5163,7 @@ packages:
version: 0.0.0
'@rush-temp/text@file:projects/text.tgz':
resolution: {integrity: sha512-p2TRL6FgtqVBooDf+E5A4HB+45VS03IvNxudhC9gCotewS726ZLOxl2XdfeuYB2rHYbzdmmiG2zb8GbPwqTkOw==, tarball: file:projects/text.tgz}
resolution: {integrity: sha512-3aISTkQPGhn/Af7odByrglZazlbus1nu5kHvq77BZ0LJZ2lVZDZ5eTf0/EH6IpvGkmSvkEmkVJUl/bG/5Z7urA==, tarball: file:projects/text.tgz}
version: 0.0.0
'@rush-temp/theme@file:projects/theme.tgz':
@@ -6254,8 +6261,8 @@ packages:
'@types/verror@1.10.10':
resolution: {integrity: sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==}
'@types/web-push@3.6.3':
resolution: {integrity: sha512-v3oT4mMJsHeJ/rraliZ+7TbZtr5bQQuxcgD7C3/1q/zkAj29c8RE0F9lVZVu3hiQe5Z9fYcBreV7TLnfKR+4mg==}
'@types/web-push@3.6.4':
resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==}
'@types/webidl-conversions@7.0.3':
resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
@@ -9137,6 +9144,10 @@ packages:
has-unicode@2.0.1:
resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
hash-base@3.1.0:
resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==}
engines: {node: '>=4'}
hasown@2.0.1:
resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==}
engines: {node: '>= 0.4'}
@@ -9283,10 +9294,6 @@ packages:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
https-proxy-agent@7.0.4:
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
engines: {node: '>= 14'}
https-proxy-agent@7.0.5:
resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
engines: {node: '>= 14'}
@@ -9398,8 +9405,8 @@ packages:
resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==}
engines: {node: '>=10.13.0'}
intl-messageformat@9.13.0:
resolution: {integrity: sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==}
intl-messageformat@10.7.14:
resolution: {integrity: sha512-mMGnE4E1otdEutV5vLUdCxRJygHB5ozUBxsPB5qhitewssrS/qGruq9bmvIRkkGsNeK5ZWLfYRld18UHGTIifQ==}
invert-kv@2.0.0:
resolution: {integrity: sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==}
@@ -10351,6 +10358,9 @@ packages:
resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
engines: {node: '>=10'}
md5.js@1.3.5:
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
md5@2.3.0:
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
@@ -13590,7 +13600,7 @@ snapshots:
'@smithy/util-middleware': 3.0.0
'@smithy/util-retry': 3.0.0
'@smithy/util-utf8': 3.0.0
tslib: 2.6.2
tslib: 2.7.0
transitivePeerDependencies:
- aws-crt
@@ -13891,7 +13901,7 @@ snapshots:
'@smithy/property-provider': 3.0.0
'@smithy/shared-ini-file-loader': 3.0.0
'@smithy/types': 3.0.0
tslib: 2.6.2
tslib: 2.7.0
'@aws-sdk/types@3.577.0':
dependencies:
@@ -14541,29 +14551,31 @@ snapshots:
'@fastify/busboy@2.1.1': {}
'@formatjs/ecma402-abstract@1.11.4':
'@formatjs/ecma402-abstract@2.3.2':
dependencies:
'@formatjs/intl-localematcher': 0.2.25
tslib: 2.6.2
'@formatjs/fast-memoize': 2.2.6
'@formatjs/intl-localematcher': 0.5.10
decimal.js: 10.4.3
tslib: 2.7.0
'@formatjs/fast-memoize@1.2.1':
'@formatjs/fast-memoize@2.2.6':
dependencies:
tslib: 2.6.2
tslib: 2.7.0
'@formatjs/icu-messageformat-parser@2.1.0':
'@formatjs/icu-messageformat-parser@2.11.0':
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/icu-skeleton-parser': 1.3.6
tslib: 2.6.2
'@formatjs/ecma402-abstract': 2.3.2
'@formatjs/icu-skeleton-parser': 1.8.12
tslib: 2.7.0
'@formatjs/icu-skeleton-parser@1.3.6':
'@formatjs/icu-skeleton-parser@1.8.12':
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
tslib: 2.6.2
'@formatjs/ecma402-abstract': 2.3.2
tslib: 2.7.0
'@formatjs/intl-localematcher@0.2.25':
'@formatjs/intl-localematcher@0.5.10':
dependencies:
tslib: 2.6.2
tslib: 2.7.0
'@gar/promisify@1.1.3': {}
@@ -15760,10 +15772,12 @@ snapshots:
'@rush-temp/account@file:projects/account.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))':
dependencies:
'@types/crypto-js': 4.2.2
'@types/jest': 29.5.12
'@types/otp-generator': 4.0.2
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
crypto-js: 4.2.0
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.3.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
@@ -16925,9 +16939,11 @@ snapshots:
'@rush-temp/contact-resources@file:projects/contact-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(postcss-load-config@4.0.2(postcss@8.4.35)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(postcss@8.4.35)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))':
dependencies:
'@types/crypto-js': 4.2.2
'@types/jest': 29.5.12
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
crypto-js: 4.2.0
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.3.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
@@ -16969,13 +16985,13 @@ snapshots:
'@types/jest': 29.5.12
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
crypto-js: 4.2.0
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.3.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
md5.js: 1.3.5
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
typescript: 5.3.3
@@ -20207,7 +20223,7 @@ snapshots:
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
intl-messageformat: 9.13.0
intl-messageformat: 10.7.14
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
@@ -20816,6 +20832,7 @@ snapshots:
'@types/express': 4.17.21
'@types/jest': 29.5.12
'@types/node': 20.11.19
'@types/web-push': 3.6.4
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
aws-sdk: 2.1664.0
@@ -20835,6 +20852,7 @@ snapshots:
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3)
typescript: 5.3.3
web-push: 3.6.7
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
@@ -23122,7 +23140,6 @@ snapshots:
'@rush-temp/server-notification-resources@file:projects/server-notification-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))':
dependencies:
'@types/jest': 29.5.12
'@types/web-push': 3.6.3
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
eslint: 8.56.0
@@ -23134,7 +23151,6 @@ snapshots:
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
typescript: 5.3.3
web-push: 3.6.7
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
@@ -24705,6 +24721,37 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@rush-temp/text-core@file:projects/text-core.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))(utf-8-validate@6.0.4)':
dependencies:
'@types/jest': 29.5.12
'@types/markdown-it': 13.0.8
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.6.2)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
fast-equals: 5.0.1
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
jest-environment-jsdom: 29.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.6.2)
typescript: 5.6.2
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@types/node'
- babel-jest
- babel-plugin-macros
- bufferutil
- canvas
- esbuild
- node-notifier
- supports-color
- ts-node
- utf-8-validate
'@rush-temp/text-editor-assets@file:projects/text-editor-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))':
dependencies:
'@types/jest': 29.5.12
@@ -25866,7 +25913,7 @@ snapshots:
'@aws-crypto/crc32': 3.0.0
'@smithy/types': 3.0.0
'@smithy/util-hex-encoding': 3.0.0
tslib: 2.6.2
tslib: 2.7.0
'@smithy/eventstream-serde-browser@3.0.0':
dependencies:
@@ -26952,7 +26999,7 @@ snapshots:
'@types/verror@1.10.10':
optional: true
'@types/web-push@3.6.3':
'@types/web-push@3.6.4':
dependencies:
'@types/node': 20.11.19
@@ -29182,7 +29229,7 @@ snapshots:
dot-case@3.0.4:
dependencies:
no-case: 3.0.4
tslib: 2.6.2
tslib: 2.7.0
dot-prop@6.0.1:
dependencies:
@@ -30656,6 +30703,12 @@ snapshots:
has-unicode@2.0.1: {}
hash-base@3.1.0:
dependencies:
inherits: 2.0.4
readable-stream: 3.6.2
safe-buffer: 5.2.1
hasown@2.0.1:
dependencies:
function-bind: 1.1.2
@@ -30849,13 +30902,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.4:
dependencies:
agent-base: 7.1.1
debug: 4.3.5
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.5:
dependencies:
agent-base: 7.1.1
@@ -30956,12 +31002,12 @@ snapshots:
interpret@3.1.1: {}
intl-messageformat@9.13.0:
intl-messageformat@10.7.14:
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/fast-memoize': 1.2.1
'@formatjs/icu-messageformat-parser': 2.1.0
tslib: 2.6.2
'@formatjs/ecma402-abstract': 2.3.2
'@formatjs/fast-memoize': 2.2.6
'@formatjs/icu-messageformat-parser': 2.11.0
tslib: 2.7.0
invert-kv@2.0.0: {}
@@ -32189,6 +32235,12 @@ snapshots:
escape-string-regexp: 4.0.0
optional: true
md5.js@1.3.5:
dependencies:
hash-base: 3.1.0
inherits: 2.0.4
safe-buffer: 5.2.1
md5@2.3.0:
dependencies:
charenc: 0.0.2
@@ -32911,7 +32963,7 @@ snapshots:
pascal-case@3.1.2:
dependencies:
no-case: 3.0.4
tslib: 2.6.2
tslib: 2.7.0
passport-custom@1.1.1:
dependencies:
@@ -35104,7 +35156,7 @@ snapshots:
dependencies:
asn1.js: 5.4.1
http_ece: 1.2.0
https-proxy-agent: 7.0.4
https-proxy-agent: 7.0.5
jws: 4.0.0
minimist: 1.2.8
transitivePeerDependencies:
+4
View File
@@ -1,5 +1,9 @@
FROM hardcoreeng/base
# Chromium hangs when usging LD_PRELOAD and MALLOC_CONF
ENV LD_PRELOAD=
ENV MALLOC_CONF=
# We don't need the standalone Chromium
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
# Set executable path for puppeteer
+8 -1
View File
@@ -1197,12 +1197,14 @@ export function devTool (
.description('copy files from s3 to datalake')
.option('-w, --workspace <workspace>', 'Selected workspace only', '')
.option('-c, --concurrency <concurrency>', 'Number of files being processed concurrently', '10')
.option('-s, --skip <number>', 'Number of workspaces to skip', '0')
.option('-e, --existing', 'Copy existing blobs', false)
.action(async (cmd: { workspace: string, concurrency: string, existing: boolean }) => {
.action(async (cmd: { workspace: string, concurrency: string, existing: boolean, skip: string }) => {
const params = {
concurrency: parseInt(cmd.concurrency),
existing: cmd.existing
}
const skip = parseInt(cmd.skip)
const storageConfig = storageConfigFromEnv(process.env.STORAGE)
@@ -1245,6 +1247,11 @@ export function devTool (
let index = 0
for (const workspace of workspaces) {
index++
if (index <= skip) {
toolCtx.info('processing workspace', { workspace: workspace.workspace, index, count })
continue
}
toolCtx.info('processing workspace', {
workspace: workspace.workspace,
index,
+49 -6
View File
@@ -23,6 +23,7 @@ import {
RateLimiter
} from '@hcengineering/core'
import { type DatalakeClient } from '@hcengineering/datalake'
import { type UploadObjectParams } from '@hcengineering/datalake/types/client'
import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment'
import { type S3Config, type S3Service } from '@hcengineering/s3'
import {
@@ -32,7 +33,7 @@ import {
type UploadedObjectInfo
} from '@hcengineering/server-core'
import { type Db } from 'mongodb'
import { PassThrough } from 'stream'
import { PassThrough, type Readable } from 'stream'
export interface MoveFilesParams {
concurrency: number
@@ -400,14 +401,11 @@ export async function copyBlobToDatalake (
type: stat.contentType,
size: stat.size
}
const readable = await adapter.get(ctx, workspaceId, objectName)
try {
readable.on('end', () => {
readable.destroy()
})
console.log('uploading huge blob', objectName, Math.round(stat.size / 1024 / 1024), 'MB')
const stream = readable.pipe(new PassThrough())
await datalake.uploadMultipart(ctx, workspaceId, objectName, stream, metadata)
await uploadMultipart(ctx, datalake, workspaceId, objectName, readable, metadata)
console.log('done', objectName)
} finally {
readable.destroy()
@@ -416,6 +414,51 @@ export async function copyBlobToDatalake (
}
}
function uploadMultipart (
ctx: MeasureContext,
datalake: DatalakeClient,
workspaceId: WorkspaceId,
objectName: string,
stream: Readable,
metadata: UploadObjectParams
): Promise<void> {
return new Promise<void>((resolve, reject) => {
const passthrough = new PassThrough()
const cleanup = (): void => {
stream.removeAllListeners()
passthrough.removeAllListeners()
stream.destroy()
passthrough.destroy()
}
stream.on('error', (err) => {
ctx.error('error reading blob', { err })
cleanup()
reject(err)
})
passthrough.on('error', (err) => {
ctx.error('error reading blob', { err })
cleanup()
reject(err)
})
stream.pipe(passthrough)
datalake
.uploadMultipart(ctx, workspaceId, objectName, passthrough, metadata)
.then(() => {
cleanup()
resolve()
})
.catch((err) => {
ctx.error('failed to upload blob', { err })
cleanup()
reject(err)
})
})
}
export function formatSize (size: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const pow = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024))
+15
View File
@@ -904,3 +904,18 @@ export class TimeRateLimiter {
}
}
}
export function combineAttributes (
attributes: any[],
key: string,
operator: '$push' | '$pull',
arrayKey: '$each' | '$in'
): any[] {
return Array.from(
new Set(
attributes.flatMap((attr) =>
Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key]
)
)
).filter((v) => v != null)
}
+1 -1
View File
@@ -37,7 +37,7 @@
"typescript": "^5.3.3"
},
"dependencies": {
"intl-messageformat": "^9.7.1"
"intl-messageformat": "^10.7.14"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
@@ -103,7 +103,7 @@
{#await srcRef then src}
{#if !isLoading}
{#if src === ''}
{#if src === '' || src === undefined}
<div class="centered">
<Label label={presentation.string.FailedToPreview} />
</div>
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+49
View File
@@ -0,0 +1,49 @@
{
"name": "@hcengineering/text-core",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"files": [
"lib/**/*",
"types/**/*",
"tsconfig.json"
],
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"test": "jest --passWithNoTests --silent",
"build:watch": "compile",
"format": "format src",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"@types/markdown-it": "~13.0.0",
"jest-environment-jsdom": "^29.7.0"
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
"fast-equals": "^5.0.1"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
+20
View File
@@ -0,0 +1,20 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
export * from './markup/dsl'
export * from './markup/model'
export * from './markup/reference'
export * from './markup/traverse'
export * from './markup/utils'
+402
View File
@@ -0,0 +1,402 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Markup } from '@hcengineering/core'
import { deepEqual } from 'fast-equals'
import { nodeDoc, nodeParagraph, nodeText } from './dsl'
import { MarkupMark, MarkupMarkType, MarkupNode, MarkupNodeType, emptyMarkupNode, type AttrValue } from './model'
import { traverseNode } from './traverse'
/** @public */
export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode())
/** @public */
export function isEmptyMarkup (markup: Markup | undefined): boolean {
if (markup === undefined || markup === null || markup === '') {
return true
}
return isEmptyNode(markupToJSON(markup))
}
/** @public */
export function areEqualMarkups (markup1: Markup, markup2: Markup): boolean {
if (markup1 === markup2) {
return true
}
const node1 = markupToJSON(markup1)
const node2 = markupToJSON(markup2)
if (isEmptyNode(node1) && isEmptyNode(node2)) {
return true
}
return equalNodes(node1, node2)
}
/** @public */
export function areEqualJson (json1: MarkupNode, json2: MarkupNode): boolean {
return equalNodes(json1, json2)
}
function equalNodes (node1: MarkupNode, node2: MarkupNode): boolean {
if (node1.type !== node2.type) return false
const text1 = node1.text ?? ''
const text2 = node2.text ?? ''
if (text1 !== text2) return false
if (!equalArrays(node1.content, node2.content, equalNodes)) return false
if (!equalArrays(node1.marks, node2.marks, equalMarks)) return false
if (!equalRecords(node1.attrs, node2.attrs)) return false
return true
}
function equalArrays<T> (a: T[] | undefined, b: T[] | undefined, equal: (a: T, b: T) => boolean): boolean {
if (a === b) return true
const arr1 = a ?? []
const arr2 = b ?? []
if (arr1.length !== arr2.length) return false
return arr1.every((item1, i) => equal(item1, arr2[i]))
}
function equalRecords (a: Record<string, any> | undefined, b: Record<string, any> | undefined): boolean {
if (a === b) return true
a = Object.fromEntries(Object.entries(a ?? {}).filter(([_, v]) => v != null))
b = Object.fromEntries(Object.entries(b ?? {}).filter(([_, v]) => v != null))
return deepEqual(a, b)
}
export function equalMarks (a: MarkupMark, b: MarkupMark): boolean {
return a.type === b.type && equalRecords(a.attrs, b.attrs)
}
const emptyNodes = [MarkupNodeType.hard_break]
const nonEmptyNodes = [
MarkupNodeType.horizontal_rule,
MarkupNodeType.image,
MarkupNodeType.reference,
MarkupNodeType.subLink,
MarkupNodeType.table
]
/** @public */
export function isEmptyNode (node: MarkupNode): boolean {
if (emptyNodes.includes(node.type)) return true
if (nonEmptyNodes.includes(node.type)) return false
if (node.text !== undefined && node.text?.trim().length > 0) return false
const content = node.content ?? []
return content.every(isEmptyNode)
}
// Markup
/** @public */
export function jsonToMarkup (json: MarkupNode): Markup {
return JSON.stringify(json)
}
/** @public */
export function markupToJSON (markup: Markup): MarkupNode {
if (markup == null || markup === '') {
return emptyMarkupNode()
}
try {
// Ideally Markup should contain only serialized JSON
// But there seem to be some cases when it contains HTML or plain text
// So we need to handle those cases and produce valid MarkupNode
if (markup.startsWith('{')) {
return JSON.parse(markup) as MarkupNode
} else {
return nodeDoc(nodeParagraph(nodeText(markup)))
}
} catch (error) {
return emptyMarkupNode()
}
}
// UTILS
const ELLIPSIS_CHAR = '…'
const WHITESPACE = ' '
/** @public */
export function stripTags (markup: Markup, textLimit = 0): string {
const parsed = markupToJSON(markup)
const textParts: string[] = []
let charCount = 0
let isHardStop = false
const pushText = (text: string): void => {
if (textLimit > 0 && charCount + text.length > textLimit) {
const toAddCount = textLimit - charCount
const textPart = text.substring(0, toAddCount)
textParts.push(textPart)
textParts.push(ELLIPSIS_CHAR)
isHardStop = true
} else {
textParts.push(text)
charCount += text.length
}
}
traverseNode(parsed, (node, parent): boolean => {
if (isHardStop) {
return false
}
if (node.type === MarkupNodeType.text) {
const text = node.text ?? ''
pushText(text)
return false
} else if (
node.type === MarkupNodeType.paragraph ||
node.type === MarkupNodeType.table ||
node.type === MarkupNodeType.doc ||
node.type === MarkupNodeType.blockquote
) {
if (textParts.length > 0 && textParts[textParts.length - 1] !== WHITESPACE) {
textParts.push(WHITESPACE)
charCount++
}
} else if (node.type === MarkupNodeType.reference) {
const label = `${node.attrs?.label ?? ''}`
pushText(label.length > 0 ? `@${label}` : '')
}
return true
})
const result = textParts.join('')
return result
}
class NodeBuilder {
textParts: string[] = []
constructor (private readonly addTags: boolean) {}
addText (text: string): void {
this.textParts.push(text)
}
addTag (text: string, newLine: boolean = false): void {
if (this.addTags) {
this.textParts.push(text)
}
if (!this.addTags && newLine) {
this.textParts.push('\n')
}
}
toText (): string {
return this.textParts.join('')
}
}
function addMark (builder: NodeBuilder, mark?: MarkupMark, next?: () => void): void {
if (mark != null) {
const attrs = mark.attrs ?? {}
if (mark.type === MarkupMarkType.bold) {
builder.addTag('<strong>')
next?.()
builder.addTag('</strong>')
} else if (mark.type === MarkupMarkType.code) {
builder.addTag('<code class="proseCode">')
next?.()
builder.addTag('</code>')
} else if (mark.type === MarkupMarkType.em) {
builder.addTag('<em>')
next?.()
builder.addTag('</em>')
} else if (mark.type === MarkupMarkType.link) {
builder.addTag(`<a href=${attrs.href} target=${attrs.target}>`)
next?.()
builder.addTag('</a>')
} else if (mark.type === MarkupMarkType.strike) {
builder.addTag('<s>')
next?.()
builder.addTag('</s>')
} else if (mark.type === MarkupMarkType.underline) {
builder.addTag('<u>')
next?.()
builder.addTag('</u>')
} else {
builder.addTag(`unknown mark: "${mark.type as string}"`, false)
next?.()
}
}
}
function addMarks (builder: NodeBuilder, marks: MarkupMark[], next?: () => void): void {
if (marks.length > 0) {
const mark = marks[0]
const others = marks.slice(1)
if (others.length > 0) {
addMark(builder, mark, () => {
addMarks(builder, others, next)
})
}
}
}
function addNodeContent (builder: NodeBuilder, node?: MarkupNode): void {
if (node == null) return
const attrs = node.attrs ?? {}
const nodes = node.content ?? []
if (node.type === MarkupNodeType.doc) {
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
} else if (node.type === MarkupNodeType.text) {
builder.addText(node.text ?? '')
} else if (node.type === MarkupNodeType.paragraph) {
builder.addTag('<p>')
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag('</p>')
} else if (node.type === MarkupNodeType.blockquote) {
builder.addTag('<blockquote>')
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag('</blockquote>')
} else if (node.type === MarkupNodeType.horizontal_rule) {
builder.addTag('<hr/>')
} else if (node.type === MarkupNodeType.heading) {
const level = toNumber(node.attrs?.level) ?? 1
builder.addTag(`<h${level}>`)
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag(`</h${level}>`)
} else if (node.type === MarkupNodeType.code_block) {
builder.addTag('<pre><code>')
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag('</code></pre>')
} else if (node.type === MarkupNodeType.image) {
const src = toString(attrs.src)
const alt = toString(attrs.alt)
builder.addText(`<img src="${src}" alt="${alt}"/>`)
} else if (node.type === MarkupNodeType.reference) {
const label = toString(attrs.label)
builder.addText(label !== undefined ? `@${label}` : '')
} else if (node.type === MarkupNodeType.hard_break) {
builder.addTag('<br/>')
} else if (node.type === MarkupNodeType.ordered_list) {
builder.addTag('<ol>')
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag('</ol>')
} else if (node.type === MarkupNodeType.bullet_list) {
builder.addTag('<ul>')
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag('</ul>')
} else if (node.type === MarkupNodeType.list_item) {
builder.addTag('<li>')
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag('</li>')
} else if (node.type === MarkupNodeType.subLink) {
builder.addTag('<sub>')
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag('</sub>')
} else if (node.type === MarkupNodeType.table) {
builder.addTag('<table><tbody>')
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag('</tbody></table>')
} else if (node.type === MarkupNodeType.table_row) {
builder.addTag('<tr>')
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag('</tr>')
} else if (node.type === MarkupNodeType.table_cell) {
builder.addTag('<td>')
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag('</td>')
} else if (node.type === MarkupNodeType.table_header) {
builder.addTag('<th>')
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
builder.addTag('</th>')
} else {
builder.addText(`unknown node: "${node.type}"`)
nodes.forEach((childNode) => {
addNode(builder, childNode)
})
}
}
function addNode (builder: NodeBuilder, node: MarkupNode): void {
const marks = node.marks ?? []
if (marks.length > 0) {
addMarks(builder, marks, () => {
addNodeContent(builder, node)
})
} else {
addNodeContent(builder, node)
}
}
function toString (value: AttrValue | undefined): string | undefined {
return value !== undefined ? `${value}` : undefined
}
function toNumber (value: AttrValue | undefined): number | undefined {
if (typeof value === 'boolean') {
return value ? 1 : 0
}
return value !== undefined ? (typeof value === 'string' ? parseInt(value) : value) : undefined
}
export function markupToHTML (markup: Markup): string {
const jsonModel = markupToJSON(markup)
const builder = new NodeBuilder(true)
addNode(builder, jsonModel)
return builder.toText()
}
export function markupToText (markup: Markup): string {
const jsonModel = markupToJSON(markup)
const builder = new NodeBuilder(false)
addNode(builder, jsonModel)
return builder.toText()
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}
+1
View File
@@ -40,6 +40,7 @@
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
"@hcengineering/text-core": "^0.6.0",
"@tiptap/core": "^2.6.6",
"@tiptap/html": "^2.6.6",
"@tiptap/pm": "^2.6.6",
+2 -6
View File
@@ -14,13 +14,9 @@
//
export * from './extensions'
export * from './markup/dsl'
export * from './markup/model'
export * from './markup/reference'
export * from './markup/traverse'
export * from './markup/utils'
export * from '@hcengineering/text-core'
export * from './nodes'
// export * from './ydoc'
export * from './markup/utils'
export * from './marks/code'
export * from './marks/colors'
export * from './marks/noteBase'
+1 -2
View File
@@ -1,9 +1,8 @@
import { MarkupNode, markupToJSON } from '@hcengineering/text-core'
import { Extensions } from '@tiptap/core'
import { defaultExtensions } from '../extensions'
import { MarkdownParser } from './parser'
import { MarkdownState, storeMarks, storeNodes } from './serializer'
import { MarkupNode } from '../markup/model'
import { markupToJSON } from '../markup/utils'
/**
* @public
+1 -1
View File
@@ -1,5 +1,5 @@
import { deepEqual } from 'fast-equals'
import { MarkupMark, MarkupMarkType, MarkupNode } from '../markup/model'
import { MarkupMark, MarkupMarkType, MarkupNode } from '@hcengineering/text-core'
export function traverseMarks (node: MarkupNode, f: (el: MarkupMark) => void): void {
node.marks?.forEach(f)
+1 -1
View File
@@ -1,4 +1,4 @@
import { Attrs, MarkupNode } from '../markup/model'
import { Attrs, MarkupNode } from '@hcengineering/text-core'
export function traverseMarkupNode (node: MarkupNode, f: (el: MarkupNode) => void): void {
f(node)
+2 -2
View File
@@ -3,10 +3,10 @@ import MarkdownIt, { type Token } from 'markdown-it'
import type { RuleCore } from 'markdown-it/lib/parser_core'
import type StateCore from 'markdown-it/lib/rules_core/state_core'
import { Attrs, AttrValue, MarkupMark, MarkupMarkType, MarkupNode, MarkupNodeType } from '@hcengineering/text-core'
import { htmlToJSON } from '../markup/utils'
import { addToSet, removeFromSet, sameSet } from './marks'
import { messageContent } from './node'
import { Attrs, AttrValue, MarkupMark, MarkupMarkType, MarkupNode, MarkupNodeType } from '../markup/model'
import { htmlToJSON } from '../markup/utils'
interface ParsingBlockRule {
block: MarkupNodeType
+2 -2
View File
@@ -1,8 +1,8 @@
import { MarkupMark, MarkupNode, MarkupNodeType } from '@hcengineering/text-core'
import { generateHTML } from '@tiptap/html'
import { defaultExtensions } from '../extensions'
import { isInSet, markEq } from './marks'
import { messageContent, nodeAttrs } from './node'
import { MarkupMark, MarkupNode, MarkupNodeType } from '../markup/model'
import { defaultExtensions } from '../extensions'
type FirstDelim = (i: number, attrs?: Record<string, any>, parentAttrs?: Record<string, any>) => string
interface IState {
+10 -2
View File
@@ -1,5 +1,13 @@
import { nodeDoc, nodeImage, nodeParagraph, nodeReference, nodeText, markLink, markUnderline } from '../dsl'
import { MarkupNodeType } from '../model'
import {
markLink,
markUnderline,
MarkupNodeType,
nodeDoc,
nodeImage,
nodeParagraph,
nodeReference,
nodeText
} from '@hcengineering/text-core'
import { jsonToHTML } from '../utils'
describe('dsl', () => {
@@ -17,28 +17,34 @@
// limitations under the License.
//
import { Editor, getSchema } from '@tiptap/core'
import { MarkupMarkType, MarkupNode, MarkupNodeType } from '../model'
import {
areEqualMarkups,
isEmptyMarkup,
isEmptyNode,
jsonToMarkup,
MarkupMarkType,
MarkupNode,
MarkupNodeType,
markupToJSON,
nodeDoc,
nodeParagraph,
nodeText,
markupToHTML
} from '@hcengineering/text-core'
import { Editor, getSchema } from '@tiptap/core'
import { ServerKit } from '../../kits/server-kit'
import {
getMarkup,
htmlToJSON,
htmlToMarkup,
htmlToPmNode,
isEmptyMarkup,
isEmptyNode,
jsonToHTML,
jsonToMarkup,
jsonToText,
markupToHTML,
markupToJSON,
markupToPmNode,
pmNodeToHTML,
pmNodeToJSON,
pmNodeToMarkup
} from '../utils'
import { ServerKit } from '../../kits/server-kit'
import { nodeDoc, nodeParagraph, nodeText } from '../dsl'
// mock tiptap functions
jest.mock('@tiptap/html', () => ({
+19 -155
View File
@@ -18,13 +18,18 @@ import { Editor, Extensions, getSchema } from '@tiptap/core'
import { generateHTML, generateJSON } from '@tiptap/html'
import { Node as ProseMirrorNode, Schema } from '@tiptap/pm/model'
import { deepEqual } from 'fast-equals'
import {
MarkupNode,
emptyMarkupNode,
jsonToMarkup,
markupToJSON,
nodeDoc,
nodeParagraph,
nodeText
} from '@hcengineering/text-core'
import { defaultExtensions } from '../extensions'
import { nodeDoc, nodeParagraph, nodeText } from './dsl'
import { MarkupMark, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model'
/** @public */
export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode())
const defaultSchema = getSchema(defaultExtensions)
/** @public */
@@ -32,88 +37,6 @@ export function getMarkup (editor?: Editor): Markup {
return jsonToMarkup(editor?.getJSON() as MarkupNode)
}
/** @public */
export function isEmptyMarkup (markup: Markup | undefined): boolean {
if (markup === undefined || markup === null || markup === '') {
return true
}
return isEmptyNode(markupToJSON(markup))
}
/** @public */
export function areEqualMarkups (markup1: Markup, markup2: Markup): boolean {
if (markup1 === markup2) {
return true
}
const node1 = markupToJSON(markup1)
const node2 = markupToJSON(markup2)
if (isEmptyNode(node1) && isEmptyNode(node2)) {
return true
}
return equalNodes(node1, node2)
}
/** @public */
export function areEqualJson (json1: MarkupNode, json2: MarkupNode): boolean {
return equalNodes(json1, json2)
}
function equalNodes (node1: MarkupNode, node2: MarkupNode): boolean {
if (node1.type !== node2.type) return false
const text1 = node1.text ?? ''
const text2 = node2.text ?? ''
if (text1 !== text2) return false
if (!equalArrays(node1.content, node2.content, equalNodes)) return false
if (!equalArrays(node1.marks, node2.marks, equalMarks)) return false
if (!equalRecords(node1.attrs, node2.attrs)) return false
return true
}
function equalArrays<T> (a: T[] | undefined, b: T[] | undefined, equal: (a: T, b: T) => boolean): boolean {
if (a === b) return true
const arr1 = a ?? []
const arr2 = b ?? []
if (arr1.length !== arr2.length) return false
return arr1.every((item1, i) => equal(item1, arr2[i]))
}
function equalRecords (a: Record<string, any> | undefined, b: Record<string, any> | undefined): boolean {
if (a === b) return true
a = Object.fromEntries(Object.entries(a ?? {}).filter(([_, v]) => v != null))
b = Object.fromEntries(Object.entries(b ?? {}).filter(([_, v]) => v != null))
return deepEqual(a, b)
}
function equalMarks (a: MarkupMark, b: MarkupMark): boolean {
return a.type === b.type && equalRecords(a.attrs, b.attrs)
}
const emptyNodes = [MarkupNodeType.hard_break]
const nonEmptyNodes = [
MarkupNodeType.horizontal_rule,
MarkupNodeType.image,
MarkupNodeType.reference,
MarkupNodeType.subLink,
MarkupNodeType.table
]
/** @public */
export function isEmptyNode (node: MarkupNode): boolean {
if (emptyNodes.includes(node.type)) return true
if (nonEmptyNodes.includes(node.type)) return false
if (node.text !== undefined && node.text?.trim().length > 0) return false
const content = node.content ?? []
return content.every(isEmptyNode)
}
// Markup
/** @public */
@@ -127,15 +50,8 @@ export function markupToPmNode (markup: Markup, schema?: Schema, extensions?: Ex
return jsonToPmNode(json, schema, extensions)
}
// JSON
/** @public */
export function jsonToMarkup (json: MarkupNode): Markup {
return JSON.stringify(json)
}
/** @public */
export function markupToJSON (markup: Markup): MarkupNode {
export function markupHtmlToJSON (markup: Markup): MarkupNode {
if (markup == null || markup === '') {
return emptyMarkupNode()
}
@@ -178,10 +94,10 @@ export function pmNodeToText (node: ProseMirrorNode): string {
return jsonToText(node.toJSON())
}
export function markupToText (markup: Markup, schema?: Schema, extensions?: Extensions): string {
const pmNode = markupToPmNode(markup, schema, extensions)
return pmNode.textBetween(0, pmNode.content.size, '\n', '')
}
// export function markupToText (markup: Markup, schema?: Schema, extensions?: Extensions): string {
// const pmNode = markupToPmNode(markup, schema, extensions)
// return pmNode.textBetween(0, pmNode.content.size, '\n', '')
// }
// HTML
@@ -191,11 +107,11 @@ export function htmlToMarkup (html: string, extensions?: Extensions): Markup {
return jsonToMarkup(json)
}
/** @public */
export function markupToHTML (markup: Markup, extensions?: Extensions): string {
const json = markupToJSON(markup)
return jsonToHTML(json, extensions)
}
// /** @public */
// export function markupToHTML (markup: Markup, extensions?: Extensions): string {
// const json = markupToJSON(markup)
// return jsonToHTML(json, extensions)
// }
/** @public */
export function htmlToJSON (html: string, extensions?: Extensions): MarkupNode {
@@ -221,55 +137,3 @@ export function pmNodeToHTML (node: ProseMirrorNode, extensions?: Extensions): s
extensions ??= defaultExtensions
return generateHTML(node.toJSON(), extensions)
}
// UTILS
const ELLIPSIS_CHAR = '…'
const WHITESPACE = ' '
/** @public */
export function stripTags (markup: Markup, textLimit = 0, extensions: Extensions | undefined = undefined): string {
const schema = extensions === undefined ? defaultSchema : getSchema(extensions)
const parsed = markupToPmNode(markup, schema)
const textParts: string[] = []
let charCount = 0
let isHardStop = false
const pushText = (text: string): void => {
if (textLimit > 0 && charCount + text.length > textLimit) {
const toAddCount = textLimit - charCount
const textPart = text.substring(0, toAddCount)
textParts.push(textPart)
textParts.push(ELLIPSIS_CHAR)
isHardStop = true
} else {
textParts.push(text)
charCount += text.length
}
}
parsed.descendants((node, _pos, parent): boolean => {
if (isHardStop) {
return false
}
if (node.type.isText) {
const text = node.text ?? ''
pushText(text)
return false
} else if (node.type.isBlock) {
if (textParts.length > 0 && textParts[textParts.length - 1] !== WHITESPACE) {
textParts.push(WHITESPACE)
charCount++
}
} else if (node.type.name === 'reference') {
const label = node.attrs.label ?? ''
pushText(label.length > 0 ? `@${label}` : '')
}
return true
})
const result = textParts.join('')
return result
}
+4 -2
View File
@@ -35,7 +35,8 @@
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"svelte-eslint-parser": "^0.33.1"
"svelte-eslint-parser": "^0.33.1",
"@types/crypto-js": "^4.2.2"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
@@ -60,6 +61,7 @@
"@hcengineering/view": "^0.6.13",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/workbench": "^0.6.16",
"svelte": "^4.2.19"
"svelte": "^4.2.19",
"crypto-js": "^4.2.0"
}
}
@@ -15,7 +15,9 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { AvatarType, buildGravatarId, checkHasGravatar, type AvatarInfo } from '@hcengineering/contact'
import MD5 from 'crypto-js/md5'
import { AvatarType, checkHasGravatar, type AvatarInfo } from '@hcengineering/contact'
import type { Ref } from '@hcengineering/core'
import { Blob as PlatformBlob } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
@@ -38,6 +40,10 @@
import AvatarComponent from './Avatar.svelte'
import EditAvatarPopup from './EditAvatarPopup.svelte'
function buildGravatarId (email: string): string {
return MD5(email.trim().toLowerCase()).toString()
}
export let selectedAvatarType: AvatarType
export let selectedAvatar: AvatarInfo['avatar']
export let selectedAvatarProps: AvatarInfo['avatarProps']
+1 -3
View File
@@ -23,7 +23,6 @@
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@types/crypto-js": "^4.1.1",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
@@ -42,8 +41,7 @@
"@hcengineering/ui": "^0.6.15",
"@hcengineering/core": "^0.6.32",
"@hcengineering/templates": "^0.6.11",
"@hcengineering/view": "^0.6.13",
"crypto-js": "^4.1.1"
"@hcengineering/view": "^0.6.13"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
-8
View File
@@ -16,7 +16,6 @@
import { AttachedData, Class, Client, Doc, FindResult, Hierarchy, Ref } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import { ColorDefinition } from '@hcengineering/ui'
import MD5 from 'crypto-js/md5'
import { AvatarProvider, AvatarType, Channel, Contact, Person, contactPlugin } from '.'
import { AVATAR_COLORS, GravatarPlaceholderType } from './types'
@@ -48,13 +47,6 @@ export function getAvatarColorName (color: string): string {
return AVATAR_COLORS.find((col) => col.color === color)?.name ?? AVATAR_COLORS[0].name
}
/**
* @public
*/
export function buildGravatarId (email: string): string {
return MD5(email.trim().toLowerCase()).toString()
}
/**
* @public
*/
+1 -1
View File
@@ -126,7 +126,7 @@ export async function generateLocation (loc: Location, id: Ref<Document>): Promi
fragment: getPanelURI(document.component.EditDoc, doc._id, doc._class, 'content')
},
defaultLocation: {
path: [appComponent, workspace],
path: [appComponent, workspace, documentId],
fragment: getPanelURI(document.component.EditDoc, doc._id, doc._class, 'content')
}
}
@@ -37,7 +37,7 @@
ctx.drawImage(bitmap, 0, 0)
imgRef.src = canvas.toDataURL('image/jpeg', 1.0)
imgRef.src = canvas.toDataURL('image/png', 1.0)
imgRef.width = bitmap.width
imgRef.height = bitmap.height
@@ -80,7 +80,7 @@
(blob) => {
resolve(blob)
},
'image/jpeg',
'image/png',
0.95
)
})
+1
View File
@@ -38,6 +38,7 @@
},
"dependencies": {
"svelte": "^4.2.19",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/core": "^0.6.32",
"@hcengineering/presentation": "^0.6.3",
@@ -4,6 +4,7 @@
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { Doc, type Blob, type Ref } from '@hcengineering/core'
import presentation, { PDFViewer, createQuery, getClient } from '@hcengineering/presentation'
import guest, { PublicLink, createPublicLink } from '@hcengineering/guest'
@@ -55,6 +56,7 @@
isLoading = false
},
(err) => {
Analytics.handleError(err)
console.error(err)
isLoading = false
}
@@ -65,6 +67,7 @@
}
},
(err) => {
Analytics.handleError(err)
console.error(err)
isLoading = false
}
+1 -5
View File
@@ -7,7 +7,6 @@ import { Analytics } from '@hcengineering/analytics'
import { SplitLogger, configureAnalytics } from '@hcengineering/analytics-service'
import contactPlugin from '@hcengineering/contact'
import { MeasureMetricsContext, newMetrics, setOperationLogProfiling } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { setMetadata } from '@hcengineering/platform'
import { serverConfigFromEnv } from '@hcengineering/server'
import serverAiBot from '@hcengineering/server-ai-bot'
@@ -65,10 +64,7 @@ setMetadata(serverCore.metadata.FrontUrl, config.frontUrl)
setMetadata(serverCore.metadata.FilesUrl, config.filesUrl)
setMetadata(serverToken.metadata.Secret, config.serverSecret)
setMetadata(serverNotification.metadata.SesUrl, config.sesUrl ?? '')
setMetadata(notification.metadata.PushPublicKey, config.pushPublicKey)
setMetadata(serverNotification.metadata.PushPrivateKey, config.pushPrivateKey)
setMetadata(serverNotification.metadata.PushSubject, config.pushSubject)
setMetadata(serverCore.metadata.ElasticIndexVersion, 'v1')
setMetadata(serverNotification.metadata.SesAuthToken, config.sesAuthToken)
setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL)
setMetadata(serverAiBot.metadata.SupportWorkspaceId, process.env.SUPPORT_WORKSPACE)
setMetadata(serverAiBot.metadata.EndpointURL, process.env.AI_BOT_URL)
+5
View File
@@ -475,6 +475,11 @@
"projectFolder": "packages/theme",
"shouldPublish": true
},
{
"packageName": "@hcengineering/text-core",
"projectFolder": "packages/text-core",
"shouldPublish": true
},
{
"packageName": "@hcengineering/text",
"projectFolder": "packages/text",
@@ -44,7 +44,7 @@
"@hcengineering/server-activity": "^0.6.0",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/text": "^0.6.5",
"@hcengineering/text-core": "^0.6.0",
"@hcengineering/contact": "^0.6.24"
}
}
@@ -14,7 +14,7 @@
//
import { Class, Doc, Ref } from '@hcengineering/core'
import { EmptyMarkup, MarkupNodeType, jsonToMarkup } from '@hcengineering/text'
import { EmptyMarkup, MarkupNodeType, jsonToMarkup } from '@hcengineering/text-core'
import { getReferencesData } from '../references'
@@ -141,6 +141,7 @@ export async function createReactionNotifications (tx: TxCUD<Reaction>, control:
res = res.concat(
await createCollabDocInfo(
control.ctx,
res,
[user] as Ref<PersonAccount>[],
control,
tx,
@@ -51,7 +51,7 @@ import {
toReceiverInfo,
type NotificationProviderControl
} from '@hcengineering/server-notification-resources'
import { areEqualJson, extractReferences, jsonToMarkup, markupToJSON } from '@hcengineering/text'
import { areEqualJson, extractReferences, jsonToMarkup, markupToJSON } from '@hcengineering/text-core'
export function isDocMentioned (doc: Ref<Doc>, content: string): boolean {
const references = []
+2 -11
View File
@@ -15,7 +15,8 @@ import {
TxCUD,
TxMixin,
TxProcessor,
TxUpdateDoc
TxUpdateDoc,
combineAttributes
} from '@hcengineering/core'
import core from '@hcengineering/core/src/component'
import notification from '@hcengineering/notification'
@@ -76,16 +77,6 @@ function getModifiedAttributes (tx: TxCUD<Doc>, hierarchy: Hierarchy): Record<st
return {}
}
function combineAttributes (attributes: any[], key: string, operator: string, arrayKey: string): any[] {
return Array.from(
new Set(
attributes.flatMap((attr) =>
Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key]
)
)
).filter((v) => v != null)
}
export function getDocUpdateAction (control: ActivityControl, tx: TxCUD<Doc>): DocUpdateAction {
const hierarchy = control.hierarchy
@@ -50,6 +50,6 @@
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/server": "^0.6.4",
"@hcengineering/text": "^0.6.5"
"@hcengineering/text-core": "^0.6.0"
}
}
+3 -12
View File
@@ -34,7 +34,8 @@ import core, {
TxRemoveDoc,
TxUpdateDoc,
UserStatus,
type MeasureContext
type MeasureContext,
combineAttributes
} from '@hcengineering/core'
import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification'
import { getMetadata, IntlString, translate } from '@hcengineering/platform'
@@ -44,7 +45,7 @@ import {
getDocCollaborators,
getMixinTx
} from '@hcengineering/server-notification-resources'
import { markupToHTML, markupToText, stripTags } from '@hcengineering/text'
import { markupToHTML, markupToText, stripTags } from '@hcengineering/text-core'
import { workbenchId } from '@hcengineering/workbench'
import { getPersonAccountById, NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
@@ -359,16 +360,6 @@ async function OnChatMessageRemoved (txes: TxCUD<ChatMessage>[], control: Trigge
return res
}
function combineAttributes (attributes: any[], key: string, operator: string, arrayKey: string): any[] {
return Array.from(
new Set(
attributes.flatMap((attr) =>
Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key]
)
)
).filter((v) => v != null)
}
function getDirectsToHide (directs: DocNotifyContext[], date: Timestamp): DocNotifyContext[] {
const minVisibleDirects = 10
@@ -45,7 +45,6 @@
"@hcengineering/server-token": "^0.6.11",
"@hcengineering/chunter": "^0.6.20",
"@hcengineering/server-chunter-resources": "^0.6.0",
"@hcengineering/text": "^0.6.5",
"@hcengineering/request": "^0.6.14",
"@hcengineering/controlled-documents": "^0.1.0",
"@hcengineering/training": "^0.1.0"
+1 -2
View File
@@ -45,7 +45,6 @@
"@hcengineering/platform": "^0.6.11",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/text": "^0.6.5"
"@hcengineering/server-notification-resources": "^0.6.0"
}
}
+3 -1
View File
@@ -115,10 +115,12 @@ export async function sendEmailNotification (
ctx.error('Please provide email service url to enable email notifications.')
return
}
const sesAuth: string | undefined = getMetadata(serverNotification.metadata.SesAuthToken)
await fetch(concatLink(sesURL, '/send'), {
method: 'post',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
...(sesAuth != null ? { Authorization: `Bearer ${sesAuth}` } : {})
},
body: JSON.stringify({
text,
+2 -11
View File
@@ -26,7 +26,8 @@ import core, {
TxMixin,
TxProcessor,
TxUpdateDoc,
UserStatus
UserStatus,
combineAttributes
} from '@hcengineering/core'
import love, {
Invite,
@@ -431,16 +432,6 @@ async function isRoomEmpty (
return false
}
function combineAttributes (attributes: any[], key: string, operator: string, arrayKey: string): any[] {
return Array.from(
new Set(
attributes.flatMap((attr) =>
Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key]
)
)
).filter((v) => v != null)
}
async function OnRoomInfo (txes: TxCUD<RoomInfo>[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
const personsByRoom = new Map<Ref<RoomInfo>, Ref<Person>[]>()
@@ -34,8 +34,7 @@
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"@types/web-push": "~3.6.3"
"@types/jest": "^29.5.5"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
@@ -48,9 +47,8 @@
"@hcengineering/workbench": "^0.6.16",
"@hcengineering/chunter": "^0.6.20",
"@hcengineering/view": "^0.6.13",
"@hcengineering/text": "^0.6.5",
"@hcengineering/text-core": "^0.6.0",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/server-view": "^0.6.0",
"web-push": "~3.6.7"
"@hcengineering/server-view": "^0.6.0"
}
}
@@ -31,6 +31,7 @@ import core, {
AttachedDoc,
Class,
Collection,
combineAttributes,
concatLink,
Data,
Doc,
@@ -78,10 +79,10 @@ import serverNotification, {
SenderInfo
} from '@hcengineering/server-notification'
import serverView from '@hcengineering/server-view'
import { markupToText, stripTags } from '@hcengineering/text'
import { markupToText, stripTags } from '@hcengineering/text-core'
import { encodeObjectURI } from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
import webpush, { WebPushError } from 'web-push'
import { Analytics } from '@hcengineering/analytics'
import { Content, ContextsCache, ContextsCacheKey, NotifyParams, NotifyResult } from './types'
import {
@@ -102,7 +103,8 @@ import {
replaceAll,
toReceiverInfo,
updateNotifyContextsSpace,
type NotificationProviderControl
type NotificationProviderControl,
getObjectSpace
} from './utils'
export function getPushCollaboratorTx (
@@ -571,10 +573,9 @@ export async function createPushNotification (
senderAvatar?: Data<AvatarInfo>,
path?: string[]
): Promise<void> {
const publicKey = getMetadata(notification.metadata.PushPublicKey)
const privateKey = getMetadata(serverNotification.metadata.PushPrivateKey)
const subject = getMetadata(serverNotification.metadata.PushSubject) ?? 'mailto:hey@huly.io'
if (privateKey === undefined || publicKey === undefined) return
const sesURL: string | undefined = getMetadata(serverNotification.metadata.SesUrl)
const sesAuth: string | undefined = getMetadata(serverNotification.metadata.SesAuthToken)
if (sesURL === undefined || sesURL === '') return
const userSubscriptions = subscriptions.filter((it) => it.user === target)
const data: PushData = {
title,
@@ -601,34 +602,45 @@ export async function createPushNotification (
}
}
webpush.setVapidDetails(subject, publicKey, privateKey)
const limiter = new RateLimiter(5)
for (const subscription of userSubscriptions) {
await limiter.add(async () => {
await sendPushToSubscription(control, target, subscription, data)
await sendPushToSubscription(sesURL, sesAuth, control, target, subscription, data)
})
}
await limiter.waitProcessing()
}
const errorMessages = ['expired', 'Unregistered', 'No such subscription']
async function sendPushToSubscription (
sesURL: string,
sesAuth: string | undefined,
control: TriggerControl,
targetUser: Ref<Account>,
subscription: PushSubscription,
data: PushData
): Promise<void> {
try {
await webpush.sendNotification(subscription, JSON.stringify(data))
const result: 'ok' | 'clear-push' = (
await (
await fetch(concatLink(sesURL, '/web-push'), {
method: 'post',
headers: {
'Content-Type': 'application/json',
...(sesAuth != null ? { Authorization: `Bearer ${sesAuth}` } : {})
},
body: JSON.stringify({
subscription,
data
})
})
).json()
).result
if (result === 'clear-push') {
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
await control.apply(control.ctx, [tx])
}
} catch (err) {
control.ctx.info('Cannot send push notification to', { user: targetUser, err })
if (err instanceof WebPushError) {
if (errorMessages.some((p) => JSON.stringify((err as WebPushError).body).includes(p))) {
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
await control.apply(control.ctx, [tx])
}
}
}
}
@@ -933,16 +945,16 @@ async function removeContexts (
export async function createCollabDocInfo (
ctx: MeasureContext,
res: Tx[],
collaborators: Ref<PersonAccount>[],
control: TriggerControl,
tx: TxCUD<Doc>,
object: Doc,
activityMessages: ActivityMessage[],
params: NotifyParams,
unsubscribe: Ref<PersonAccount>[] = []
unsubscribe: Ref<PersonAccount>[] = [],
cache = new Map<Ref<Doc>, Doc>()
): Promise<Tx[]> {
let res: Tx[] = []
if (tx.space === core.space.DerivedTx) {
return res
}
@@ -973,7 +985,35 @@ export async function createCollabDocInfo (
await removeContexts(ctx, notifyContexts, unsubscribe, control)
}
const targets = new Set(collaborators)
const space = await getObjectSpace(control, object, cache)
if (space === undefined) {
control.ctx.error('Cannot find space for object', object)
Analytics.handleError(
new Error(`Cannot find space ${object.space} for objectId ${object._id}, objectClass ${object._class}`)
)
return res
}
cache.set(space._id, space)
const filteredCollaborators = control.hierarchy.isDerived(object._class, core.class.SystemSpace)
? collaborators
: collaborators.filter(
(it) =>
space.members.includes(it) ||
res.some((tx) => {
if (tx._class === core.class.TxUpdateDoc) {
const updateTx = tx as TxUpdateDoc<Space>
if (updateTx.objectId === space._id) {
const added = combineAttributes([updateTx.operations], 'members', '$push', '$each')
return added.includes(it)
}
}
return false
})
)
const targets = new Set(filteredCollaborators)
// 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)) {
@@ -1106,8 +1146,7 @@ async function getSpaceCollabTxes (
return []
}
const space =
cache.get(doc.space) ?? (await control.findAll(ctx, core.class.Space, { _id: doc.space }, { limit: 1 }))[0]
const space = await getObjectSpace(control, doc, cache)
if (space === undefined) return []
cache.set(space._id, space)
@@ -1121,18 +1160,45 @@ async function getSpaceCollabTxes (
if (collabs.collaborators !== undefined) {
return await createCollabDocInfo(
ctx,
[],
collabs.collaborators as Ref<PersonAccount>[],
control,
tx,
doc,
activityMessages,
{ isSpace: true, isOwn: false, shouldUpdateTimestamp: true }
{ isSpace: true, isOwn: false, shouldUpdateTimestamp: true },
[],
cache
)
}
}
return []
}
async function pushCollaboratorsToPublicSpace (
control: TriggerControl,
doc: Doc,
collaborators: Ref<Account>[],
cache: Map<Ref<Doc>, Doc>
): Promise<Tx[]> {
const space = await getObjectSpace(control, doc, cache)
if (space === undefined) return []
cache.set(space._id, space)
if (control.hierarchy.isDerived(space._class, core.class.SystemSpace)) {
return []
}
if (space.private) {
return []
}
return collaborators
.filter((it) => !space.members.includes(it))
.map((it) => control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { $push: { members: it } }))
}
async function createCollaboratorDoc (
ctx: MeasureContext,
tx: TxCreateDoc<Doc>,
@@ -1152,15 +1218,7 @@ async function createCollaboratorDoc (
const collaborators = await ctx.with('get-collaborators', {}, (ctx) => getDocCollaborators(ctx, doc, mixin, control))
const mixinTx = getMixinTx(tx, control, collaborators)
const notificationTxes = await ctx.with('create-collabdocinfo', {}, (ctx) =>
createCollabDocInfo(ctx, collaborators as Ref<PersonAccount>[], control, tx, doc, activityMessage, {
isOwn: true,
isSpace: false,
shouldUpdateTimestamp: true
})
)
res.push(mixinTx)
res.push(...notificationTxes)
res.push(
...(await ctx.with('get-space-collabtxes', {}, (ctx) =>
@@ -1168,6 +1226,29 @@ async function createCollaboratorDoc (
))
)
res.push(...(await pushCollaboratorsToPublicSpace(control, doc, collaborators, cache)))
const notificationTxes = await ctx.with('create-collabdocinfo', {}, (ctx) =>
createCollabDocInfo(
ctx,
res,
collaborators as Ref<PersonAccount>[],
control,
tx,
doc,
activityMessage,
{
isOwn: true,
isSpace: false,
shouldUpdateTimestamp: true
},
[],
cache
)
)
res.push(...notificationTxes)
return res
}
@@ -1176,7 +1257,8 @@ async function updateCollaboratorsMixin (
tx: TxMixin<Doc, Collaborators>,
control: TriggerControl,
activityMessages: ActivityMessage[],
originTx: TxCUD<Doc>
originTx: TxCUD<Doc>,
cache: Map<Ref<Doc>, Doc>
): Promise<Tx[]> {
const { hierarchy } = control
@@ -1232,6 +1314,13 @@ async function updateCollaboratorsMixin (
}
if (newCollabs.length > 0) {
const object = cache.get(tx.objectId) ?? (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }))[0]
if (object === undefined) return res
const space = await getObjectSpace(control, object, cache)
cache.set(object._id, object)
cache.set(space._id, space)
const docNotifyContexts = await control.findAll(ctx, notification.class.DocNotifyContext, {
user: { $in: newCollabs },
objectId: tx.objectId
@@ -1245,6 +1334,15 @@ async function updateCollaboratorsMixin (
for (const collab of newCollabs) {
const target = toReceiverInfo(hierarchy, infos.get(collab))
if (target === undefined) continue
if (space.private && !space.members.includes(target.account._id)) continue
if (!hierarchy.isDerived(space._class, core.class.SystemSpace) && !space.members.includes(target.account._id)) {
res.push(
control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
$push: { members: target.account._id }
})
)
}
for (const message of activityMessages) {
await pushActivityInboxNotifications(
@@ -1309,11 +1407,22 @@ async function collectionCollabDoc (
res = res.concat(
await ctx.with('create-collab-doc-info', {}, (ctx) =>
createCollabDocInfo(ctx, collaborators as Ref<PersonAccount>[], control, tx, doc, activityMessages, {
isOwn: false,
isSpace: false,
shouldUpdateTimestamp: true
})
createCollabDocInfo(
ctx,
res,
collaborators as Ref<PersonAccount>[],
control,
tx,
doc,
activityMessages,
{
isOwn: false,
isSpace: false,
shouldUpdateTimestamp: true
},
[],
cache
)
)
)
@@ -1446,7 +1555,6 @@ async function updateCollaboratorDoc (
ctx: MeasureContext,
tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>,
control: TriggerControl,
originTx: TxCUD<Doc>,
activityMessages: ActivityMessage[],
cache: Map<Ref<Doc>, Doc>
): Promise<Tx[]> {
@@ -1479,13 +1587,15 @@ async function updateCollaboratorDoc (
await ctx.with('create-collab-docinfo', {}, (ctx) =>
createCollabDocInfo(
ctx,
res,
collabsInfo.result as Ref<PersonAccount>[],
control,
tx,
doc,
activityMessages,
params,
collabsInfo.removed as Ref<PersonAccount>[]
collabsInfo.removed as Ref<PersonAccount>[],
cache
)
)
)
@@ -1495,7 +1605,18 @@ async function updateCollaboratorDoc (
)
res.push(getMixinTx(tx, control, collaborators))
res = res.concat(
await createCollabDocInfo(ctx, collaborators as Ref<PersonAccount>[], control, tx, doc, activityMessages, params)
await createCollabDocInfo(
ctx,
res,
collaborators as Ref<PersonAccount>[],
control,
tx,
doc,
activityMessages,
params,
[],
cache
)
)
}
@@ -1737,11 +1858,18 @@ export async function createCollaboratorNotifications (
case core.class.TxUpdateDoc:
case core.class.TxMixin: {
let res = await ctx.with('updateCollaboratorDoc', {}, (ctx) =>
updateCollaboratorDoc(ctx, tx as TxUpdateDoc<Doc>, control, originTx ?? tx, activityMessages, cache)
updateCollaboratorDoc(ctx, tx as TxUpdateDoc<Doc>, control, activityMessages, cache)
)
res = res.concat(
await ctx.with('updateCollaboratorMixin', {}, (ctx) =>
updateCollaboratorsMixin(ctx, tx as TxMixin<Doc, Collaborators>, control, activityMessages, originTx ?? tx)
updateCollaboratorsMixin(
ctx,
tx as TxMixin<Doc, Collaborators>,
control,
activityMessages,
originTx ?? tx,
cache
)
)
)
return await applyUserTxes(ctx, control, res)
@@ -654,3 +654,10 @@ export async function getNotificationProviderControl (
}
return new NotificationProviderControl(providersSettings, typesSettings)
}
export async function getObjectSpace (control: TriggerControl, doc: Doc, cache: Map<Ref<Doc>, Doc>): Promise<Space> {
return control.hierarchy.isDerived(doc._class, core.class.Space)
? (doc as Space)
: (cache.get(doc.space) as Space) ??
(await control.findAll<Space>(control.ctx, core.class.Space, { _id: doc.space }, { limit: 1 }))[0]
}
+1 -2
View File
@@ -151,8 +151,7 @@ export const PUSH_NOTIFICATION_TITLE_SIZE = 80
export default plugin(serverNotificationId, {
metadata: {
SesUrl: '' as Metadata<string>,
PushPrivateKey: '' as Metadata<string>,
PushSubject: '' as Metadata<string>,
SesAuthToken: '' as Metadata<string>,
InboxOnlyNotifications: '' as Metadata<boolean>
},
class: {
@@ -50,6 +50,6 @@
"@hcengineering/server-token": "^0.6.11",
"@hcengineering/setting": "^0.6.17",
"@hcengineering/telegram": "^0.6.21",
"@hcengineering/text": "^0.6.5"
"@hcengineering/text-core": "^0.6.0"
}
}
@@ -49,7 +49,7 @@ import serverTelegram from '@hcengineering/server-telegram'
import { generateToken } from '@hcengineering/server-token'
import setting, { Integration } from '@hcengineering/setting'
import telegram, { TelegramMessage, TelegramNotificationRequest } from '@hcengineering/telegram'
import { markupToHTML } from '@hcengineering/text'
import { markupToHTML } from '@hcengineering/text-core'
/**
* @public
+1 -1
View File
@@ -47,7 +47,7 @@
"@hcengineering/task": "^0.6.20",
"@hcengineering/tracker": "^0.6.24",
"@hcengineering/server-time": "^0.6.0",
"@hcengineering/text": "^0.6.5",
"@hcengineering/text-core": "^0.6.0",
"@hcengineering/time": "^0.6.0"
}
}
+1 -1
View File
@@ -43,7 +43,7 @@ import {
} from '@hcengineering/server-notification-resources'
import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time'
import task, { makeRank } from '@hcengineering/task'
import { jsonToMarkup, nodeDoc, nodeParagraph, nodeText } from '@hcengineering/text'
import { jsonToMarkup, nodeDoc, nodeParagraph, nodeText } from '@hcengineering/text-core'
import time, { ProjectToDo, ToDo, ToDoPriority, TodoAutomationHelper, WorkSlot } from '@hcengineering/time'
import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker'
@@ -47,7 +47,7 @@
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-task-resources": "^0.6.0",
"@hcengineering/task": "^0.6.20",
"@hcengineering/text": "^0.6.5",
"@hcengineering/text-core": "^0.6.0",
"@hcengineering/tracker": "^0.6.24",
"@hcengineering/view": "^0.6.13",
"@hcengineering/workbench": "^0.6.16"
@@ -35,7 +35,7 @@ import { NotificationContent } from '@hcengineering/notification'
import { getMetadata, IntlString } from '@hcengineering/platform'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
import { stripTags } from '@hcengineering/text'
import { stripTags } from '@hcengineering/text-core'
import tracker, { Component, Issue, IssueParentInfo, TimeSpendReport, trackerId } from '@hcengineering/tracker'
import { workbenchId } from '@hcengineering/workbench'
+4 -2
View File
@@ -35,7 +35,8 @@
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"@types/otp-generator": "^4.0.2"
"@types/otp-generator": "^4.0.2",
"@types/crypto-js": "^4.2.2"
},
"dependencies": {
"@hcengineering/mongo": "^0.6.1",
@@ -55,6 +56,7 @@
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/server-storage": "^0.6.0",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-pipeline": "^0.6.0"
"@hcengineering/server-pipeline": "^0.6.0",
"crypto-js": "^4.2.0"
}
}
+4 -1
View File
@@ -16,7 +16,6 @@
import { Analytics } from '@hcengineering/analytics'
import contact, {
AvatarType,
buildGravatarId,
checkHasGravatar,
combineName,
Employee,
@@ -95,6 +94,10 @@ import {
} from './utils'
import { getWorkspaceDestroyAdapter } from '@hcengineering/server-pipeline'
import MD5 from 'crypto-js/md5'
function buildGravatarId (email: string): string {
return MD5(email.trim().toLowerCase()).toString()
}
/**
* @public
*/
+2 -1
View File
@@ -48,7 +48,8 @@ export async function start (ctx: MeasureContext, config: Config, storageAdapter
const app = express()
app.use(cors())
app.use(bp.json())
app.use(express.json({ limit: '10mb' }))
app.use(bp.json({ limit: '10mb' }))
const extensionsCtx = ctx.newChild('extensions', {})
const transformer = new MarkupTransformer()
+2 -2
View File
@@ -466,8 +466,8 @@ export class DatalakeClient {
const response = await fetchSafe(ctx, url, { method: 'POST', body })
return (await response.json()) as MultipartUploadPart
} catch (err: any) {
ctx.error('failed to abort multipart upload', { workspace, objectName, err })
throw new DatalakeError('Failed to abort multipart upload')
ctx.error('failed to upload multipart part', { workspace, objectName, err })
throw new DatalakeError('Failed to upload multipart part')
}
}
+11 -12
View File
@@ -352,13 +352,12 @@ class ValuesVariables {
)
}
addArrayI (value: any[], type: string = ''): string[] {
addArrayI (value: any[], type: string = ''): string {
const vals = value.filter((it) => it != null)
const vv: string[] = []
vals.forEach((it) => {
vv.push(this.add(it, type))
})
return vv
if (vals.length === 0) {
return "array['NULL']"
}
return this.add(vals, type)
}
injectVars (sql: string): string {
@@ -716,11 +715,11 @@ abstract class PostgresAdapterBase implements DbAdapter {
}
})) as FindResult<T>
} catch (err) {
ctx.error('Error in findAll', { err, sql: vars.injectVars(fquery) })
ctx.error('Error in findAll', { err, sql: fquery, sqlFull: vars.injectVars(fquery) })
throw err
}
},
() => ({ fquery, vars: vars.getValues() })
() => ({ query, psql: fquery, sql: vars.injectVars(fquery) })
)
}
@@ -1303,14 +1302,14 @@ abstract class PostgresAdapterBase implements DbAdapter {
break
case 'array':
{
const vv = vars.addArrayI(val)
res.push(`${tkey} && array[${vv.length > 0 ? vv.join(', ') : 'NULL'}]`)
const vv = vars.addArrayI(val, inferType(val))
res.push(`${tkey} && ${vv}`)
}
break
case 'dataArray':
{
const vv = vars.addArrayI(val)
res.push(`${tkey} ?| array[${vv.length > 0 ? vv.join(', ') : 'NULL'}]`)
const vv = vars.addArrayI(val, inferType(val))
res.push(`${tkey} ?| ${vv}`)
}
break
}
+3 -9
View File
@@ -6,12 +6,10 @@ export interface ServerEnv {
frontUrl: string
filesUrl: string | undefined
sesUrl: string | undefined
sesAuthToken: string | undefined
accountsUrl: string
serverPort: number
enableCompression: boolean
pushPublicKey: string | undefined
pushPrivateKey: string | undefined
pushSubject: string | undefined
brandingPath: string | undefined
}
@@ -47,6 +45,7 @@ export function serverConfigFromEnv (): ServerEnv {
const filesUrl = process.env.FILES_URL
const sesUrl = process.env.SES_URL
const sesAuthToken = process.env.SES_AUTH_TOKEN
const accountsUrl = process.env.ACCOUNTS_URL
if (accountsUrl === undefined) {
@@ -54,9 +53,6 @@ export function serverConfigFromEnv (): ServerEnv {
process.exit(1)
}
const pushPublicKey = process.env.PUSH_PUBLIC_KEY
const pushPrivateKey = process.env.PUSH_PRIVATE_KEY
const pushSubject = process.env.PUSH_SUBJECT
const brandingPath = process.env.BRANDING_PATH
return {
@@ -67,12 +63,10 @@ export function serverConfigFromEnv (): ServerEnv {
frontUrl,
filesUrl,
sesUrl,
sesAuthToken,
accountsUrl,
serverPort,
enableCompression,
pushPublicKey,
pushPrivateKey,
pushSubject,
brandingPath
}
}
+4
View File
@@ -3,4 +3,8 @@ FROM hardcoreeng/print-base:v20250113a
WORKDIR /usr/src/app
COPY bundle/bundle.js ./
# Chromium hangs when usging LD_PRELOAD and MALLOC_CONF
ENV LD_PRELOAD=
ENV MALLOC_CONF=
CMD [ "dumb-init", "node", "bundle.js" ]
+4 -2
View File
@@ -50,7 +50,8 @@
"@tsconfig/node16": "^1.0.4",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"eslint-plugin-node": "^11.1.0"
"eslint-plugin-node": "^11.1.0",
"@types/web-push": "^3.6.4"
},
"dependencies": {
"@hcengineering/client": "^0.6.18",
@@ -62,6 +63,7 @@
"aws-sdk": "^2.1423.0",
"cors": "^2.8.5",
"dotenv": "~16.0.0",
"express": "^4.21.2"
"express": "^4.21.2",
"web-push": "^3.6.7"
}
}
+15 -3
View File
@@ -19,14 +19,22 @@ interface Config {
AccessKey: string
SecretKey: string
Region: string
AuthToken?: string
PushSubject?: string
PushPublicKey?: string
PushPrivateKey?: string
}
const envMap: { [key in keyof Config]: string } = {
const envMap: { [key in keyof Required<Config>]: string } = {
Port: 'PORT',
Source: 'SOURCE',
AccessKey: 'ACCESS_KEY',
SecretKey: 'SECRET_KEY',
Region: 'REGION'
Region: 'REGION',
AuthToken: 'AUTH_TOKEN',
PushPublicKey: 'PUSH_PUBLIC_KEY',
PushPrivateKey: 'PUSH_PRIVATE_KEY',
PushSubject: 'PUSH_SUBJECT'
}
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
@@ -37,7 +45,11 @@ const config: Config = (() => {
Source: process.env[envMap.Source],
AccessKey: process.env[envMap.AccessKey],
SecretKey: process.env[envMap.SecretKey],
Region: process.env[envMap.Region] ?? 'us-east-1'
Region: process.env[envMap.Region] ?? 'us-east-1',
AuthToken: process.env[envMap.AuthToken],
PushPublicKey: process.env[envMap.PushPublicKey],
PushPrivateKey: process.env[envMap.PushPrivateKey],
PushSubject: process.env[envMap.PushSubject]
}
const required: Array<keyof Config> = ['Port', 'Source', 'AccessKey', 'SecretKey', 'Region']
+59
View File
@@ -13,20 +13,57 @@
// limitations under the License.
//
import { PushSubscription, type PushData } from '@hcengineering/notification'
import type { Request, Response } from 'express'
import webpush, { WebPushError } from 'web-push'
import config from './config'
import { createServer, listen } from './server'
import { SES } from './ses'
import { Endpoint } from './types'
const errorMessages = ['expired', 'Unregistered', 'No such subscription']
async function sendPushToSubscription (subscription: PushSubscription, data: PushData): Promise<'ok' | 'clear-push'> {
try {
await webpush.sendNotification(subscription, JSON.stringify(data))
} catch (err: any) {
if (err instanceof WebPushError) {
if (errorMessages.some((p) => JSON.stringify((err as WebPushError).body).includes(p))) {
return 'clear-push'
}
}
}
return 'ok'
}
export const main = async (): Promise<void> => {
const ses = new SES()
console.log('SES service has been started')
if (config.PushPublicKey !== undefined && config.PushPrivateKey !== undefined) {
webpush.setVapidDetails(config.PushSubject ?? 'mailto:hey@huly.io', config.PushPublicKey, config.PushPublicKey)
}
const checkAuth = (req: Request<any>, res: Response<any>): boolean => {
if (config.AuthToken !== undefined) {
// We need to verify authorization
const authorization = req.headers.authorization ?? ''
const token = authorization.replace('Bearer ', '')
if (token !== config.AuthToken) {
res.status(401).send({ err: 'Invalid auth token' })
return false
}
}
return true
}
const endpoints: Endpoint[] = [
{
endpoint: '/send',
type: 'post',
handler: async (req, res) => {
if (!checkAuth(req, res)) {
return
}
const text = req.body?.text
if (text === undefined) {
res.status(400).send({ err: "'text' is missing" })
@@ -54,6 +91,28 @@ export const main = async (): Promise<void> => {
res.send()
}
},
{
endpoint: '/web-push',
type: 'post',
handler: async (req, res) => {
if (!checkAuth(req, res)) {
return
}
const data: PushData | undefined = req.body?.data
if (data === undefined) {
res.status(400).send({ err: "'data' is missing" })
return
}
const subscription: PushSubscription | undefined = req.body?.subscription
if (subscription === undefined) {
res.status(400).send({ err: "'subscription' is missing" })
return
}
const result = await sendPushToSubscription(subscription, data)
res.json({ result })
}
}
]
+3 -1
View File
@@ -17,5 +17,7 @@ done <<< "$files"
# Sort the array by size (numerically, in descending order) and print
printf '%s\n' "${file_info[@]}" | sort -t: -k1,1nr | while IFS=: read -r size path; do
echo "Size: $(($size/1024)) KB - $path"
if [ $(($size/1024)) -ne 0 ]; then
echo "Size: $(($size/1024)) KB - $path"
fi
done
+5
View File
@@ -54,7 +54,12 @@
"@hcengineering/server-client": "^0.6.0",
"@hcengineering/server-pipeline": "^0.6.0",
"@hcengineering/server-token": "^0.6.11",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/storage": "^0.6.0",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/server-ai-bot": "^0.6.0",
"@hcengineering/server-telegram": "^0.6.0",
"itty-router": "^5.0.18",
"snappyjs": "^0.7.0"
}
+21 -1
View File
@@ -16,7 +16,7 @@ import { setMetadata } from '@hcengineering/platform'
import { RPCHandler } from '@hcengineering/rpc'
import { ClientSession, createSessionManager, doSessionOp, type WebsocketData } from '@hcengineering/server'
import serverClient from '@hcengineering/server-client'
import {
import serverCore, {
createDummyStorageAdapter,
initStatisticsContext,
loadBrandingMap,
@@ -52,6 +52,12 @@ import {
} from '@hcengineering/server-pipeline'
import { CloudFlareLogger } from './logger'
import model from './model.json'
// import { configureAnalytics } from '@hcengineering/analytics-service'
// import { Analytics } from '@hcengineering/analytics'
import serverAiBot from '@hcengineering/server-ai-bot'
import serverNotification from '@hcengineering/server-notification'
import serverTelegram from '@hcengineering/server-telegram'
import contactPlugin from '@hcengineering/contact'
export const PREFERRED_SAVE_SIZE = 500
export const PREFERRED_SAVE_INTERVAL = 30 * 1000
@@ -82,6 +88,20 @@ export class Transactor extends DurableObject<Env> {
setExtraOptions({
useCF: true
})
// configureAnalytics(env.SENTRY_DSN, {})
// Analytics.setTag('application', 'transactor')
const lastNameFirst = process.env.LAST_NAME_FIRST === 'true'
setMetadata(contactPlugin.metadata.LastNameFirst, lastNameFirst)
setMetadata(serverCore.metadata.FrontUrl, env.FRONT_URL)
setMetadata(serverCore.metadata.FilesUrl, env.FILES_URL)
setMetadata(serverNotification.metadata.SesUrl, env.SES_URL ?? '')
setMetadata(serverNotification.metadata.SesAuthToken, env.SES_AUTH_TOKEN)
setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL)
setMetadata(serverAiBot.metadata.SupportWorkspaceId, process.env.SUPPORT_WORKSPACE)
setMetadata(serverAiBot.metadata.EndpointURL, process.env.AI_BOT_URL)
registerTxAdapterFactory('postgresql', createPostgresTxAdapter, true)
registerAdapterFactory('postgresql', createPostgresAdapter, true)
registerDestroyFactory('postgresql', createPostgreeDestroyAdapter, true)
+12
View File
@@ -18,4 +18,16 @@ interface Env {
FULLTEXT_URL: string | undefined
DB_MODE: 'hyperdrive' | 'direct' | undefined
FRONT_URL: string
FILES_URL?: string
SES_URL?: string
SES_AUTH_TOKEN?: string
SUPPORT_WORKSPACE?: string
TELEGRAM_BOT_URL: string
AI_BOT_URL?: string
LAST_NAME_FIRST?: string
}
+12
View File
@@ -25,6 +25,18 @@ mode = "smart"
# ACCOUNTS_URL = "http://127.0.0.1:3000"
# SERVER_SECRET = "secret"
DB_MODE='hyperdrive'
# FRONT_URL
ENABLE_COMPRESSION=true
# ACCOUNTS_URL
# FULLTEXT_URL
# STATS_URL
# PUSH_PUBLIC_KEY
# PUSH_PRIVATE_KEY
# SENTRY_DSN
# SUPPORT_WORKSPACE
# TELEGRAM_BOT_URL
# AI_BOT_URL
# LAST_NAME_FIRST
# Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflares global network
# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai