Support Bitrix Lead import (#2445)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-12-16 13:35:51 +07:00 committed by GitHub
parent fac40733f4
commit 27b003f35f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 3696 additions and 169 deletions

View File

@ -17,6 +17,9 @@ specifiers:
'@rush-temp/automation': file:./projects/automation.tgz
'@rush-temp/automation-assets': file:./projects/automation-assets.tgz
'@rush-temp/automation-resources': file:./projects/automation-resources.tgz
'@rush-temp/bitrix': file:./projects/bitrix.tgz
'@rush-temp/bitrix-assets': file:./projects/bitrix-assets.tgz
'@rush-temp/bitrix-resources': file:./projects/bitrix-resources.tgz
'@rush-temp/board': file:./projects/board.tgz
'@rush-temp/board-assets': file:./projects/board-assets.tgz
'@rush-temp/board-resources': file:./projects/board-resources.tgz
@ -70,6 +73,7 @@ specifiers:
'@rush-temp/model-all': file:./projects/model-all.tgz
'@rush-temp/model-attachment': file:./projects/model-attachment.tgz
'@rush-temp/model-automation': file:./projects/model-automation.tgz
'@rush-temp/model-bitrix': file:./projects/model-bitrix.tgz
'@rush-temp/model-board': file:./projects/model-board.tgz
'@rush-temp/model-calendar': file:./projects/model-calendar.tgz
'@rush-temp/model-chunter': file:./projects/model-chunter.tgz
@ -238,6 +242,7 @@ specifiers:
'@types/minio': ~7.0.11
'@types/node': ~16.11.12
'@types/pdfkit': ~0.12.3
'@types/qs': ~6.9.7
'@types/request': ~2.48.8
'@types/sharp': ~0.30.4
'@types/tar-stream': ^2.2.2
@ -296,6 +301,7 @@ specifiers:
mini-css-extract-plugin: ^2.2.0
minio: ^7.0.26
mongodb: ^4.11.0
p-queue: ~7.3.0
pdfkit: ~0.13.0
postcss: ^8.3.4
postcss-load-config: ^3.1.0
@ -308,6 +314,7 @@ specifiers:
prosemirror-state: ~1.4.1
prosemirror-transform: ~1.7.0
prosemirror-view: ~1.29.0
qs: ~6.11.0
rfc6902: ~5.0.1
sass: ^1.53.0
sass-loader: ^12.1.0
@ -354,6 +361,9 @@ dependencies:
'@rush-temp/automation': file:projects/automation.tgz
'@rush-temp/automation-assets': file:projects/automation-assets.tgz_typescript@4.8.4
'@rush-temp/automation-resources': file:projects/automation-resources.tgz_49b4785992daa3b61a639b2b31601e76
'@rush-temp/bitrix': file:projects/bitrix.tgz
'@rush-temp/bitrix-assets': file:projects/bitrix-assets.tgz_typescript@4.8.4
'@rush-temp/bitrix-resources': file:projects/bitrix-resources.tgz_49b4785992daa3b61a639b2b31601e76
'@rush-temp/board': file:projects/board.tgz
'@rush-temp/board-assets': file:projects/board-assets.tgz_typescript@4.8.4
'@rush-temp/board-resources': file:projects/board-resources.tgz_49b4785992daa3b61a639b2b31601e76
@ -407,6 +417,7 @@ dependencies:
'@rush-temp/model-all': file:projects/model-all.tgz_typescript@4.8.4
'@rush-temp/model-attachment': file:projects/model-attachment.tgz_typescript@4.8.4
'@rush-temp/model-automation': file:projects/model-automation.tgz_typescript@4.8.4
'@rush-temp/model-bitrix': file:projects/model-bitrix.tgz_typescript@4.8.4
'@rush-temp/model-board': file:projects/model-board.tgz_typescript@4.8.4
'@rush-temp/model-calendar': file:projects/model-calendar.tgz_typescript@4.8.4
'@rush-temp/model-chunter': file:projects/model-chunter.tgz_typescript@4.8.4
@ -575,6 +586,7 @@ dependencies:
'@types/minio': 7.0.14
'@types/node': 16.11.68
'@types/pdfkit': 0.12.8
'@types/qs': 6.9.7
'@types/request': 2.48.8
'@types/sharp': 0.30.5
'@types/tar-stream': 2.2.2
@ -633,6 +645,7 @@ dependencies:
mini-css-extract-plugin: 2.6.1_webpack@5.75.0
minio: 7.0.32
mongodb: 4.11.0
p-queue: 7.3.0
pdfkit: 0.13.0
postcss: 8.4.19
postcss-load-config: 3.1.4_postcss@8.4.19+ts-node@10.9.1
@ -645,6 +658,7 @@ dependencies:
prosemirror-state: 1.4.2
prosemirror-transform: 1.7.0
prosemirror-view: 1.29.0
qs: 6.11.0
rfc6902: 5.0.1
sass: 1.56.1
sass-loader: 12.6.0_sass@1.56.1+webpack@5.75.0
@ -676,6 +690,21 @@ dependencies:
packages:
/@2bad/bitrix/2.5.0:
resolution: {integrity: sha512-AXlbCsn5NV1x8H982JHk2yyNU/vtTBx34a6DdtqqgHQEYd6nfEVnHEhUF/yAmElV1/rtjubOtKVxwVdFUrwLDA==}
dependencies:
'@types/lodash.chunk': 4.2.7
'@types/lodash.frompairs': 4.0.7
'@types/lodash.range': 3.2.7
'@types/qs': 6.9.7
got: 11.8.2
lodash.chunk: 4.2.0
lodash.frompairs: 4.0.1
lodash.range: 3.2.0
p-queue: 6.6.2
qs: 6.11.0
dev: false
/@_ueberdosis/prosemirror-tables/1.1.3:
resolution: {integrity: sha512-su3pbFi1DT89g6Cuh72TE0MWWKHmWgHcQJ3ODRkm6XfIppWaGpU49t02ur3sgJc7hUhfQXjB93aSkDgOmIii2w==}
dependencies:
@ -3091,6 +3120,28 @@ packages:
'@types/koa': 2.13.5
dev: false
/@types/lodash.chunk/4.2.7:
resolution: {integrity: sha512-//tmaWHiANgToom/YYYKKqiCtlNz11fwYtMUUbaemNSbWTI+2zHtYW5nt1PHNCRWHPAJHHhn4UVFD9LKUFvatA==}
dependencies:
'@types/lodash': 4.14.191
dev: false
/@types/lodash.frompairs/4.0.7:
resolution: {integrity: sha512-7UeH2+GF9yop4AqnPwae5/2TE+eY0WRDy0RRQtNGHjzIgdUhilRskMXvXqUcCSazvbkxasjqydXrIE1OB6bPKA==}
dependencies:
'@types/lodash': 4.14.191
dev: false
/@types/lodash.range/3.2.7:
resolution: {integrity: sha512-sq3eRZWspMKGAE0QLH+tIE/EWeSEVKlFqaUoqmgR/c4keXiTP8ob24TUzclYKihw/ojftz6vBb632NRmxR4qrQ==}
dependencies:
'@types/lodash': 4.14.191
dev: false
/@types/lodash/4.14.191:
resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==}
dev: false
/@types/mime-types/2.1.1:
resolution: {integrity: sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==}
dev: false
@ -6354,6 +6405,23 @@ packages:
get-intrinsic: 1.1.3
dev: false
/got/11.8.2:
resolution: {integrity: sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==}
engines: {node: '>=10.19.0'}
dependencies:
'@sindresorhus/is': 4.6.0
'@szmarczak/http-timer': 4.0.6
'@types/cacheable-request': 6.0.3
'@types/responselike': 1.0.0
cacheable-lookup: 5.0.4
cacheable-request: 7.0.2
decompress-response: 6.0.0
http2-wrapper: 1.0.3
lowercase-keys: 2.0.0
p-cancelable: 2.1.1
responselike: 2.0.1
dev: false
/got/11.8.5:
resolution: {integrity: sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==}
engines: {node: '>=10.19.0'}
@ -7994,10 +8062,18 @@ packages:
p-locate: 5.0.0
dev: false
/lodash.chunk/4.2.0:
resolution: {integrity: sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==}
dev: false
/lodash.debounce/4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
dev: false
/lodash.frompairs/4.0.1:
resolution: {integrity: sha512-dvqe2I+cO5MzXCMhUnfYFa9MD+/760yx2aTAN1lqEcEkf896TxgrX373igVdqSJj6tQd0jnSLE1UMuKufqqxFw==}
dev: false
/lodash.get/4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
dev: false
@ -8010,6 +8086,10 @@ packages:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: false
/lodash.range/3.2.0:
resolution: {integrity: sha512-Fgkb7SinmuzqgIhNhAElo0BL/R1rHCnhwSZf78omqSwvWqD0kD2ssOAutQonDKH/ldS8BxA72ORYI09qAY9CYg==}
dev: false
/lodash/4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: false
@ -8595,6 +8675,11 @@ packages:
engines: {node: '>=8'}
dev: false
/p-finally/1.0.0:
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
engines: {node: '>=4'}
dev: false
/p-limit/2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
@ -8623,6 +8708,22 @@ packages:
p-limit: 3.1.0
dev: false
/p-queue/6.6.2:
resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
engines: {node: '>=8'}
dependencies:
eventemitter3: 4.0.7
p-timeout: 3.2.0
dev: false
/p-queue/7.3.0:
resolution: {integrity: sha512-5fP+yVQ0qp0rEfZoDTlP2c3RYBgxvRsw30qO+VtPPc95lyvSG+x6USSh1TuLB4n96IO6I8/oXQGsTgtna4q2nQ==}
engines: {node: '>=12'}
dependencies:
eventemitter3: 4.0.7
p-timeout: 5.1.0
dev: false
/p-retry/4.6.2:
resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==}
engines: {node: '>=8'}
@ -8631,6 +8732,18 @@ packages:
retry: 0.13.1
dev: false
/p-timeout/3.2.0:
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
engines: {node: '>=8'}
dependencies:
p-finally: 1.0.0
dev: false
/p-timeout/5.1.0:
resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==}
engines: {node: '>=12'}
dev: false
/p-try/2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
@ -11139,7 +11252,7 @@ packages:
dev: false
file:projects/account.tgz:
resolution: {integrity: sha512-QSlfOq1M5zdpWg0tNGWBu3VegEZkkPLQG2gim+GXAC7vEQd7vRsWXTMbKntwOdkovV29NDNsIcUWp6G5DUfFRg==, tarball: file:projects/account.tgz}
resolution: {integrity: sha512-Cu4HQ37cZjY4LU7x7/btaVEfxIjb+rKA9hll64ZloH41F54jembIyQHHufIMatcMOnPFivsRZ4P9oj7WMyvOsA==, tarball: file:projects/account.tgz}
name: '@rush-temp/account'
version: 0.0.0
dependencies:
@ -11422,6 +11535,89 @@ packages:
- supports-color
dev: false
file:projects/bitrix-assets.tgz_typescript@4.8.4:
resolution: {integrity: sha512-Hod/zdxq3gCpr1vfwff7eAusB6KTImLvny1hk3x5Rcn9Rri0SMTJKm3NY0Ds+XTCnnOFZQscD81EfIpaeQPITw==, tarball: file:projects/bitrix-assets.tgz}
id: file:projects/bitrix-assets.tgz
name: '@rush-temp/bitrix-assets'
version: 0.0.0
dependencies:
'@rushstack/heft': 0.47.11
'@types/heft-jest': 1.0.3
'@types/node': 16.11.68
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
eslint: 8.27.0
eslint-config-standard-with-typescript: 23.0.0_c9fe9619f50f4e82337a86c3af25e566
eslint-plugin-import: 2.26.0_eslint@8.27.0
eslint-plugin-n: 15.5.1_eslint@8.27.0
eslint-plugin-promise: 6.1.1_eslint@8.27.0
prettier: 2.7.1
transitivePeerDependencies:
- supports-color
- typescript
dev: false
file:projects/bitrix-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-QQs9ffRu6ZbMcDsIHbbqwEnqlVIFPT5SOemp5PMLCfLcAhJn4arE89dCCuVLZ6p/JMYF3g7SCy/dgUllg4nNxA==, tarball: file:projects/bitrix-resources.tgz}
id: file:projects/bitrix-resources.tgz
name: '@rush-temp/bitrix-resources'
version: 0.0.0
dependencies:
'@2bad/bitrix': 2.5.0
'@types/qs': 6.9.7
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
eslint: 8.27.0
eslint-config-standard-with-typescript: 23.0.0_c9fe9619f50f4e82337a86c3af25e566
eslint-plugin-import: 2.26.0_eslint@8.27.0
eslint-plugin-n: 15.5.1_eslint@8.27.0
eslint-plugin-promise: 6.1.1_eslint@8.27.0
eslint-plugin-svelte3: 4.0.0_eslint@8.27.0+svelte@3.53.1
fast-equals: 2.0.4
p-queue: 7.3.0
prettier: 2.7.1
prettier-plugin-svelte: 2.8.0_prettier@2.7.1+svelte@3.53.1
qs: 6.11.0
sass: 1.56.1
svelte: 3.53.1
svelte-check: 2.9.2_911be9570a99ae2f9e069a41b9f14a00
svelte-loader: 3.1.4_svelte@3.53.1
svelte-preprocess: 4.10.7_1cd24d71cb02643c0a6ca17ff2edd158
typescript: 4.8.4
transitivePeerDependencies:
- '@babel/core'
- coffeescript
- less
- node-sass
- postcss
- postcss-load-config
- pug
- stylus
- sugarss
- supports-color
dev: false
file:projects/bitrix.tgz:
resolution: {integrity: sha512-fLYL3iRRLeMJbRIjfkylukQ/uDZ/OfDuzAvrVHW/dCu2RIEFSpbYDvTlH4Cu0CR6adruVNSP4E3F+yAZSIHmQg==, tarball: file:projects/bitrix.tgz}
name: '@rush-temp/bitrix'
version: 0.0.0
dependencies:
'@2bad/bitrix': 2.5.0
'@rushstack/heft': 0.47.11
'@types/heft-jest': 1.0.3
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
eslint: 8.27.0
eslint-config-standard-with-typescript: 23.0.0_c9fe9619f50f4e82337a86c3af25e566
eslint-plugin-import: 2.26.0_eslint@8.27.0
eslint-plugin-n: 15.5.1_eslint@8.27.0
eslint-plugin-promise: 6.1.1_eslint@8.27.0
prettier: 2.7.1
typescript: 4.8.4
transitivePeerDependencies:
- supports-color
dev: false
file:projects/board-assets.tgz_typescript@4.8.4:
resolution: {integrity: sha512-rjdJ/U7sW/VdIPr31Kf1TCA9NxfStFMcypFbhhP13V00V4KM15ZzjdS9dcVhR0S1mqrXSomEZqzUeHpjkyfGgA==, tarball: file:projects/board-assets.tgz}
id: file:projects/board-assets.tgz
@ -11841,7 +12037,7 @@ packages:
dev: false
file:projects/dev-client-resources.tgz:
resolution: {integrity: sha512-Bf1K9DyTyiKCNSXwfDfnMEQvyTpwTKLxtermBHCAtH/X07jkRTQR78TityPmMY13qEP29GzVbiLdcYwMStLuoA==, tarball: file:projects/dev-client-resources.tgz}
resolution: {integrity: sha512-aBdWjVRiHsvyjb4ZVwV3CcoYF+LnWbnBtZjvQh0JbVTBZOZfOcXUiDjw73dugHlSu0EqemahiIpKl0B8H40f2g==, tarball: file:projects/dev-client-resources.tgz}
name: '@rush-temp/dev-client-resources'
version: 0.0.0
dependencies:
@ -11907,7 +12103,7 @@ packages:
dev: false
file:projects/devmodel-resources.tgz_5536a2d3219f8677582bfc0330dae14a:
resolution: {integrity: sha512-ipNVydhF5HfLSsx/TEb3G3Zo1Cag6f/yyDcWDXZfI73extFwzmdObyiPFPvXeSm5yjdu0EsEJH/TSSyxk3QJtA==, tarball: file:projects/devmodel-resources.tgz}
resolution: {integrity: sha512-4ZrlsE0DJ62seKCcvfHeMqCc/JI7gix7sw7G31SBZ5oJbm/4Uz62BplxJ+Gt57wbAzC6X1wTozIPHQI+ksaaZg==, tarball: file:projects/devmodel-resources.tgz}
id: file:projects/devmodel-resources.tgz
name: '@rush-temp/devmodel-resources'
version: 0.0.0
@ -11942,7 +12138,7 @@ packages:
dev: false
file:projects/devmodel.tgz_typescript@4.8.4:
resolution: {integrity: sha512-PgNRY82kL7qujBpgY/LHg52igSugdIOfpunDxUE7zq7R9TGU5rAyCdnZvTvfnQ4ZztcC0ICzJ9z0YoqpFjctRQ==, tarball: file:projects/devmodel.tgz}
resolution: {integrity: sha512-eQP4GWVA+cCkwjFfcFy0r+/m/u8acg6ts/YBxVDX36+FmiOuHVvHE7aAciR7uZ0Zhz/SS3rEcW19L7ol9/BVnQ==, tarball: file:projects/devmodel.tgz}
id: file:projects/devmodel.tgz
name: '@rush-temp/devmodel'
version: 0.0.0
@ -12107,7 +12303,7 @@ packages:
dev: false
file:projects/generator.tgz:
resolution: {integrity: sha512-9WrSXJKYs18Mv/42PcUJ93qVi9iOCFRVwm6/WMtgeSiZHUwE7EExXtlnMlB7RcpH6H6F8LRLBQ28xTC59duCMg==, tarball: file:projects/generator.tgz}
resolution: {integrity: sha512-MBL1q/ENOolYC7S4zK/F93ToETBseT2NO4sb2uvmw+hVok93zZdGt+L3sX6WZvQSGkIv/GV/Rd5ochzNYvYPcg==, tarball: file:projects/generator.tgz}
name: '@rush-temp/generator'
version: 0.0.0
dependencies:
@ -12683,7 +12879,7 @@ packages:
dev: false
file:projects/model-all.tgz_typescript@4.8.4:
resolution: {integrity: sha512-TZENWGRR6McuiodV3kw5TsRolcDNGyj9/oe7KaS98cElBPKtK5+0xmVMsQjCkxkelSS5XU5acxy0F13IVKS+LQ==, tarball: file:projects/model-all.tgz}
resolution: {integrity: sha512-ypjI0QqQeCCDYXOlHjJqJ4Xpq/KaUIvpYqHoCz3kjRHqR3IuW+nOgxCNYfk5oB5kY2MDhWym0KQ5m05WjVfyGA==, tarball: file:projects/model-all.tgz}
id: file:projects/model-all.tgz
name: '@rush-temp/model-all'
version: 0.0.0
@ -12749,6 +12945,27 @@ packages:
- typescript
dev: false
file:projects/model-bitrix.tgz_typescript@4.8.4:
resolution: {integrity: sha512-8dZO37adBNzvRYUR8UsawVY1GPRN9F4g4IZj2viQyCZBx0z5vtY+Y+xfoDbHhY23D6pFSBMEz7O0vSCrH2jPSQ==, tarball: file:projects/model-bitrix.tgz}
id: file:projects/model-bitrix.tgz
name: '@rush-temp/model-bitrix'
version: 0.0.0
dependencies:
'@rushstack/heft': 0.47.11
'@types/heft-jest': 1.0.3
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
eslint: 8.27.0
eslint-config-standard-with-typescript: 23.0.0_c9fe9619f50f4e82337a86c3af25e566
eslint-plugin-import: 2.26.0_eslint@8.27.0
eslint-plugin-n: 15.5.1_eslint@8.27.0
eslint-plugin-promise: 6.1.1_eslint@8.27.0
prettier: 2.7.1
transitivePeerDependencies:
- supports-color
- typescript
dev: false
file:projects/model-board.tgz_typescript@4.8.4:
resolution: {integrity: sha512-5I0fthDx1r5yySLmftHGUUPgvFiWEtDZO0M7nLwmfT34wPqhE7EgwsGJHSatDYm6P8e/XCbhl5L55MY58tGrJQ==, tarball: file:projects/model-board.tgz}
id: file:projects/model-board.tgz
@ -13869,7 +14086,7 @@ packages:
dev: false
file:projects/pod-backup.tgz:
resolution: {integrity: sha512-aYuP6b79VLRNZxzds3pAkI3pU7nnflvilszNFThWoIUxrMF6UToUJJZsYWH41PIXprpXJqRIsYGBZhMTQobg0w==, tarball: file:projects/pod-backup.tgz}
resolution: {integrity: sha512-HE0Z8soW6YkoYXsUW6c/sx2ZE9I7T7KHfXtxzNuAbyokYYfaH0vGRUE4y8NZt+bDDAfzzzDOivhu2TPeJUQCYw==, tarball: file:projects/pod-backup.tgz}
name: '@rush-temp/pod-backup'
version: 0.0.0
dependencies:
@ -14050,7 +14267,7 @@ packages:
dev: false
file:projects/presentation.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-Nry3q2egydTo0b3dfhd/ejmLBKBROZ6rpyn7943+PJe5vcriJMOazi/OQur0J3LmFl8ozg2IVqeW+A2STDOt+Q==, tarball: file:projects/presentation.tgz}
resolution: {integrity: sha512-s/CjB7oBP/pwiNFtVjC7R1Rs0Ejth0XA+1QgbgYZ3qDqnybD0jbwbXFwnZK8xyCWAK+HSvnzrxI9+SbqGE0p3g==, tarball: file:projects/presentation.tgz}
id: file:projects/presentation.tgz
name: '@rush-temp/presentation'
version: 0.0.0
@ -14086,7 +14303,7 @@ packages:
dev: false
file:projects/prod.tgz_b3a81ceaadec606c0eb174aef12a6049:
resolution: {integrity: sha512-xW0K2fEEkHycO18ZB6wSk6u5Qm5X8VLVss47tsS2KHIEwWgLHAIasxhL/SA1DJClWW+kS0o42U2fj76DjLhWJw==, tarball: file:projects/prod.tgz}
resolution: {integrity: sha512-HF6aMWOyjHsQ6STxaZz0/dFY3zb6o4J+tz/NZYcSTdspxgHb+yuyqvfm+/RPItDiTTDO6GHVogJ8i/WYIkSyhQ==, tarball: file:projects/prod.tgz}
id: file:projects/prod.tgz
name: '@rush-temp/prod'
version: 0.0.0
@ -14297,7 +14514,7 @@ packages:
dev: false
file:projects/server-backup.tgz:
resolution: {integrity: sha512-+Sxk2DeLDmhbuG/OMQMwfEJRl/atZE9ZXzdiZotTpd+Et1q9w4WH3h34RWdiCOTiInZyJZXjXF5xDF375VAzpQ==, tarball: file:projects/server-backup.tgz}
resolution: {integrity: sha512-mOaBUJaifcxXZ6AqSpdIiI19QTCADyT/565qkkfja5HI02gLLNdrmyRKoc65YN3UAZbM2C1l+JXMBU8r58St1A==, tarball: file:projects/server-backup.tgz}
name: '@rush-temp/server-backup'
version: 0.0.0
dependencies:
@ -14923,7 +15140,7 @@ packages:
dev: false
file:projects/server-tool.tgz:
resolution: {integrity: sha512-sdJKsx2tdPWOmolw3blM7QpXlkN+WUikx/uRmofKYuS/Y1HZkY8UHCYLYzWoDmf2/VplseccWe6HatjkLG6c8Q==, tarball: file:projects/server-tool.tgz}
resolution: {integrity: sha512-5WEO5W7xOR+BMCgmj9G6+cfEtU+FSGcz7Mfb+apZ24IXhwCrn2eVh/YgDGwSRzQV4W3yeIcO0sUbvowKwLhIDQ==, tarball: file:projects/server-tool.tgz}
name: '@rush-temp/server-tool'
version: 0.0.0
dependencies:
@ -15556,7 +15773,7 @@ packages:
dev: false
file:projects/tool.tgz:
resolution: {integrity: sha512-otLw170db0pwKQJWhEYqLlU29LqXHyjuS6V/oYcOPxYCvWXe7Nu1NT82TtVD4DgVE4ecgsS1iR1tVfyPwWjeYQ==, tarball: file:projects/tool.tgz}
resolution: {integrity: sha512-eV5BJ5aZRslw3lgvX7imSH6m5EBU8wCOPmZUl4tuKxZAnpjSJA9pHyUivn6EFaJ3r+pdu+ZVSjZ3D/DEbwzAKw==, tarball: file:projects/tool.tgz}
name: '@rush-temp/tool'
version: 0.0.0
dependencies:
@ -15624,7 +15841,7 @@ packages:
dev: false
file:projects/tracker-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-IcoVWcO8DdlZyXbAml4GE9UlmPBzihwEzKWq9+Xs8H1U7cDcHhNwGgF+qv8N+gH5mgBHSziEBQrKfnvF+iz5vw==, tarball: file:projects/tracker-resources.tgz}
resolution: {integrity: sha512-z547GvQvHWfDkFJs1VvW4xJKZEzr4O7cICzIfB/lbxZt8uVrbXMVWHxWqfLHrlHxGEalqDeSRdkZ59EQ64cveA==, tarball: file:projects/tracker-resources.tgz}
id: file:projects/tracker-resources.tgz
name: '@rush-temp/tracker-resources'
version: 0.0.0
@ -15817,7 +16034,7 @@ packages:
dev: false
file:projects/workbench-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-aZ25sMEnPTO2r84j+DL2Ej0KWk71wnvPdmJSntkdrWZH3HhLYVEd+xY0kYoDB+zm4yxLPvr94hrkoo+IG+LCFA==, tarball: file:projects/workbench-resources.tgz}
resolution: {integrity: sha512-hyxyGITfGafwcakPTdjJ7iQkwz25QiBIWF/QK0/sdPYTqSlxDHxkzvwBh2bKNXGuAPuqKS7aykvhrhHXqZsFXQ==, tarball: file:projects/workbench-resources.tgz}
id: file:projects/workbench-resources.tgz
name: '@rush-temp/workbench-resources'
version: 0.0.0

View File

@ -40,6 +40,7 @@ const fs = __importStar(require("fs"));
const install_run_1 = require("./install-run");
const PACKAGE_NAME = '@microsoft/rush';
const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION';
const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH';
function _getRushVersion(logger) {
const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION];
if (rushPreviewVersion !== undefined) {
@ -104,7 +105,11 @@ function _run() {
(0, install_run_1.runWithErrorAndStatusCode)(logger, () => {
const version = _getRushVersion(logger);
logger.info(`The rush.json configuration requests Rush version ${version}`);
return (0, install_run_1.installAndRun)(logger, PACKAGE_NAME, version, bin, packageBinArgs);
const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE];
if (lockFilePath) {
logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`);
}
return (0, install_run_1.installAndRun)(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath);
});
}
_run();

View File

@ -42,6 +42,7 @@ const os = __importStar(require("os"));
const path = __importStar(require("path"));
exports.RUSH_JSON_FILENAME = 'rush.json';
const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER';
const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH';
const INSTALLED_FLAG_FILENAME = 'installed.flag';
const NODE_MODULES_FOLDER_NAME = 'node_modules';
const PACKAGE_JSON_FILENAME = 'package.json';
@ -82,6 +83,9 @@ function _parsePackageSpecifier(rawPackageSpecifier) {
* home directory.
*
* IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities.copyAndTrimNpmrcFile()
*
* @returns
* The text of the the .npmrc.
*/
function _copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath) {
logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose
@ -121,20 +125,25 @@ function _copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath) {
resultLines.push(line);
}
}
fs.writeFileSync(targetNpmrcPath, resultLines.join(os.EOL));
const combinedNpmrc = resultLines.join(os.EOL);
fs.writeFileSync(targetNpmrcPath, combinedNpmrc);
return combinedNpmrc;
}
/**
* syncNpmrc() copies the .npmrc file to the target folder, and also trims unusable lines from the .npmrc file.
* If the source .npmrc file not exist, then syncNpmrc() will delete an .npmrc that is found in the target folder.
*
* IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities._syncNpmrc()
*
* @returns
* The text of the the synced .npmrc, if one exists. If one does not exist, then undefined is returned.
*/
function _syncNpmrc(logger, sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish) {
const sourceNpmrcPath = path.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish');
const targetNpmrcPath = path.join(targetNpmrcFolder, '.npmrc');
try {
if (fs.existsSync(sourceNpmrcPath)) {
_copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath);
return _copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath);
}
else if (fs.existsSync(targetNpmrcPath)) {
// If the source .npmrc doesn't exist and there is one in the target, delete the one in the target
@ -306,28 +315,43 @@ function _isPackageAlreadyInstalled(packageInstallFolder) {
return false;
}
}
/**
* Delete a file. Fail silently if it does not exist.
*/
function _deleteFile(file) {
try {
fs.unlinkSync(file);
}
catch (err) {
if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {
throw err;
}
}
}
/**
* Removes the following files and directories under the specified folder path:
* - installed.flag
* -
* - node_modules
*/
function _cleanInstallFolder(rushTempFolder, packageInstallFolder) {
function _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) {
try {
const flagFile = path.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME);
if (fs.existsSync(flagFile)) {
fs.unlinkSync(flagFile);
}
_deleteFile(flagFile);
const packageLockFile = path.resolve(packageInstallFolder, 'package-lock.json');
if (fs.existsSync(packageLockFile)) {
fs.unlinkSync(packageLockFile);
if (lockFilePath) {
fs.copyFileSync(lockFilePath, packageLockFile);
}
else {
// Not running `npm ci`, so need to cleanup
_deleteFile(packageLockFile);
const nodeModulesFolder = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME);
if (fs.existsSync(nodeModulesFolder)) {
const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler');
fs.renameSync(nodeModulesFolder, path.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`));
}
}
}
catch (e) {
throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`);
}
@ -354,17 +378,17 @@ function _createPackageJson(packageInstallFolder, name, version) {
/**
* Run "npm install" in the package install folder.
*/
function _installPackage(logger, packageInstallFolder, name, version) {
function _installPackage(logger, packageInstallFolder, name, version, command) {
try {
logger.info(`Installing ${name}...`);
const npmPath = getNpmPath();
const result = childProcess.spawnSync(npmPath, ['install'], {
const result = childProcess.spawnSync(npmPath, [command], {
stdio: 'inherit',
cwd: packageInstallFolder,
env: process.env
});
if (result.status !== 0) {
throw new Error('"npm install" encountered an error');
throw new Error(`"npm ${command}" encountered an error`);
}
logger.info(`Successfully installed ${name}@${version}`);
}
@ -392,18 +416,19 @@ function _writeFlagFile(packageInstallFolder) {
throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`);
}
}
function installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs) {
function installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs, lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE]) {
const rushJsonFolder = findRushJsonFolder();
const rushCommonFolder = path.join(rushJsonFolder, 'common');
const rushTempFolder = _getRushTempFolder(rushCommonFolder);
const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`);
if (!_isPackageAlreadyInstalled(packageInstallFolder)) {
// The package isn't already installed
_cleanInstallFolder(rushTempFolder, packageInstallFolder);
_cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath);
const sourceNpmrcFolder = path.join(rushCommonFolder, 'config', 'rush');
_syncNpmrc(logger, sourceNpmrcFolder, packageInstallFolder);
_createPackageJson(packageInstallFolder, packageName, packageVersion);
_installPackage(logger, packageInstallFolder, packageName, packageVersion);
const command = lockFilePath ? 'ci' : 'install';
_installPackage(logger, packageInstallFolder, packageName, packageVersion, command);
_writeFlagFile(packageInstallFolder);
}
const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`;

View File

@ -154,6 +154,10 @@
"@hcengineering/server-hr-resources": "~0.6.0",
"@hcengineering/document": "^0.6.0",
"@hcengineering/document-assets": "^0.6.0",
"@hcengineering/document-resources": "^0.6.0"
"@hcengineering/document-resources": "^0.6.0",
"@hcengineering/bitrix": "^0.6.0",
"@hcengineering/bitrix-assets": "^0.6.0",
"@hcengineering/bitrix-resources": "^0.6.0"
}
}

View File

@ -44,6 +44,8 @@ import { hrId } from '@hcengineering/hr'
import rekoni from '@hcengineering/rekoni'
import document, { documentId } from '@hcengineering/document'
import bitrix, { bitrixId } from '@hcengineering/bitrix'
import '@hcengineering/login-assets'
import '@hcengineering/task-assets'
import '@hcengineering/view-assets'
@ -68,6 +70,7 @@ import '@hcengineering/board-assets'
import '@hcengineering/preference-assets'
import '@hcengineering/hr-assets'
import '@hcengineering/document-assets'
import '@hcengineering/bitrix-assets'
import presentation, { presentationId } from '@hcengineering/presentation'
import { coreId } from '@hcengineering/core'
@ -134,6 +137,7 @@ export async function configurePlatform() {
addLocation(automationId, () => import(/* webpackChunkName: "automation" */ '@hcengineering/automation-resources'))
addLocation(hrId, () => import(/* webpackChunkName: "hr" */ '@hcengineering/hr-resources'))
addLocation(documentId, () => import(/* webpackChunkName: "hr" */ '@hcengineering/document-resources'))
addLocation(bitrixId, () => import(/* webpackChunkName: "bitrix" */ '@hcengineering/bitrix-resources'))
setMetadata(workbench.metadata.PlatformTitle, 'Platform')
}

View File

@ -74,6 +74,7 @@
"@hcengineering/model-preference": "~0.6.0",
"@hcengineering/model-hr": "~0.6.0",
"@hcengineering/model-server-hr": "~0.6.0",
"@hcengineering/model-document": "~0.6.0"
"@hcengineering/model-document": "~0.6.0",
"@hcengineering/model-bitrix": "~0.6.0"
}
}

View File

@ -59,6 +59,7 @@ import { createModel as preferenceModel } from '@hcengineering/model-preference'
import { createModel as hrModel } from '@hcengineering/model-hr'
import { createModel as serverHrModel } from '@hcengineering/model-server-hr'
import { createModel as documentModel } from '@hcengineering/model-document'
import { createModel as bitrixModel } from '@hcengineering/model-bitrix'
export const version: Data<Version> = jsonVersion as Data<Version>
@ -90,6 +91,7 @@ const builders: [(b: Builder) => void, string][] = [
[trackerModel, 'tracker'],
[boardModel, 'board'],
[calendarModel, 'calendar'],
[bitrixModel, 'bitrix'],
[serverCoreModel, 'server-core'],
[serverAttachmentModel, 'server-attachment'],

View File

@ -35,6 +35,7 @@ import { boardOperation } from '@hcengineering/model-board'
import { demoOperation } from '@hcengineering/model-demo'
import { hrOperation } from '@hcengineering/model-hr'
import { documentOperation } from '@hcengineering/model-document'
import { bitrixOperation } from '@hcengineering/model-bitrix'
export const migrateOperations: MigrateOperation[] = [
coreOperation,
@ -56,5 +57,6 @@ export const migrateOperations: MigrateOperation[] = [
trackerOperation,
boardOperation,
hrOperation,
documentOperation
documentOperation,
bitrixOperation
]

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/model-rig/profiles/default/config/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

4
models/bitrix/.npmignore Normal file
View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,18 @@
// The "rig.json" file directs tools to look for their config files in an external package.
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
/**
* (Required) The name of the rig package to inherit from.
* It should be an NPM package name with the "-rig" suffix.
*/
"rigPackageName": "@hcengineering/model-rig"
/**
* (Optional) Selects a config profile from the rig package. The name must consist of
* lowercase alphanumeric words separated by hyphens, for example "sample-profile".
* If omitted, then the "default" profile will be used."
*/
// "rigProfile": "your-profile-name"
}

View File

@ -0,0 +1,45 @@
{
"name": "@hcengineering/model-bitrix",
"version": "0.6.0",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "heft build",
"build:watch": "tsc",
"lint:fix": "eslint --fix src",
"lint": "eslint src",
"format": "prettier --write src && eslint --fix src"
},
"devDependencies": {
"@hcengineering/model-rig": "~0.6.0",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.26.0",
"@types/heft-jest": "^1.0.3",
"@typescript-eslint/parser": "^5.41.0",
"eslint-config-standard-with-typescript": "^23.0.0",
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9"
},
"dependencies": {
"@hcengineering/activity": "~0.6.0",
"@hcengineering/model": "~0.6.0",
"@hcengineering/core": "^0.6.19",
"@hcengineering/platform": "^0.6.8",
"@hcengineering/model-core": "~0.6.0",
"@hcengineering/model-attachment": "~0.6.0",
"@hcengineering/model-contact": "~0.6.1",
"@hcengineering/view": "^0.6.1",
"@hcengineering/model-view": "~0.6.0",
"@hcengineering/contact": "~0.6.8",
"@hcengineering/bitrix": "^0.6.0",
"@hcengineering/bitrix-resources": "^0.6.0",
"@hcengineering/preference": "^0.6.1",
"@hcengineering/model-preference": "~0.6.0",
"@hcengineering/setting": "~0.6.1",
"@hcengineering/ui": "^0.6.2"
}
}

View File

@ -0,0 +1,99 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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 { Builder, Collection, Mixin, Model, Prop, TypeRef, TypeString } from '@hcengineering/model'
import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core'
import bitrix from './plugin'
import { BitrixEntityMapping, BitrixFieldMapping, BitrixSyncDoc, Fields } from '@hcengineering/bitrix'
import { AnyAttribute, Class, Doc, Domain, Ref } from '@hcengineering/core'
import view, { createAction } from '@hcengineering/model-view'
import setting from '@hcengineering/setting'
const DOMAIN_BITRIX = 'bitrix' as Domain
@Mixin(bitrix.mixin.BitrixSyncDoc, core.class.Doc)
export class TBitrixSyncDoc extends TDoc implements BitrixSyncDoc {
type!: string
bitrixId!: string
}
@Model(bitrix.class.EntityMapping, core.class.Doc, DOMAIN_BITRIX)
export class TBitrixEntityMapping extends TDoc implements BitrixEntityMapping {
@Prop(TypeRef(core.class.Attribute), core.string.ClassPropertyLabel)
ofClass!: Ref<Class<Doc>>
type!: string
@Prop(Collection(bitrix.class.FieldMapping), bitrix.string.FieldMapping)
fields!: number
bitrixFields!: Fields
comments!: boolean
activity!: boolean
attachments!: boolean
}
@Model(bitrix.class.FieldMapping, core.class.Doc, DOMAIN_BITRIX)
export class TBitrixFieldMapping extends TAttachedDoc implements BitrixFieldMapping {
@Prop(TypeRef(core.class.Attribute), core.string.ClassPropertyLabel)
ofClass!: Ref<Class<Doc>>
@Prop(TypeString(), core.string.ClassPropertyLabel)
attributeName!: Ref<AnyAttribute>
bitrixTitle!: string
bitrixType!: string
operation!: any
}
export function createModel (builder: Builder): void {
builder.createModel(TBitrixEntityMapping, TBitrixFieldMapping, TBitrixSyncDoc)
builder.createDoc(
setting.class.IntegrationType,
core.space.Model,
{
label: bitrix.string.Bitrix,
description: bitrix.string.BitrixDesc,
icon: bitrix.component.BitrixIcon,
createComponent: bitrix.component.BitrixConnect,
configureComponent: bitrix.component.BitrixConfigure
},
bitrix.integrationType.Bitrix
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: bitrix.component.BitrixImport
},
label: bitrix.string.BitrixImport,
icon: bitrix.icon.Bitrix,
input: 'any',
category: view.category.General,
target: core.class.Doc,
context: { mode: ['workbench', 'browser', 'editor', 'panel', 'popup'], group: 'create' }
},
bitrix.action.BitrixImport
)
}
export { bitrixOperation } from './migration'

View File

@ -0,0 +1,21 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
export const bitrixOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {}
}

View File

@ -0,0 +1,37 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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 { bitrixId } from '@hcengineering/bitrix'
import bitrix from '@hcengineering/bitrix-resources/src/plugin'
import { IntlString, mergeIds } from '@hcengineering/platform'
import { Action } from '@hcengineering/view'
import { Ref } from '@hcengineering/core'
import type { AnyComponent } from '@hcengineering/ui'
export default mergeIds(bitrixId, bitrix, {
component: {
BitrixConnect: '' as AnyComponent,
BitrixConfigure: '' as AnyComponent,
BitrixImport: '' as AnyComponent
},
string: {
BitrixImport: '' as IntlString
},
action: {
BitrixImport: '' as Ref<Action>
}
})

View File

@ -0,0 +1,8 @@
{
"extends": "./node_modules/@hcengineering/model-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
}
}

View File

@ -1,5 +1,5 @@
import { DocumentUpdate, Hierarchy, MixinData, MixinUpdate, ModelDb } from '.'
import type { Account, AttachedData, AttachedDoc, Class, Data, Doc, Mixin, Ref, Space } from './classes'
import type { Account, AttachedData, AttachedDoc, Class, Data, Doc, Mixin, Ref, Space, Timestamp } from './classes'
import { Client } from './client'
import core from './component'
import type { DocumentQuery, FindOptions, FindResult, TxResult, WithLookup } from './storage'
@ -55,9 +55,11 @@ export class TxOperations implements Omit<Client, 'notify'> {
_class: Ref<Class<T>>,
space: Ref<Space>,
attributes: Data<T>,
id?: Ref<T>
id?: Ref<T>,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): Promise<Ref<T>> {
const tx = this.txFactory.createTxCreateDoc(_class, space, attributes, id)
const tx = this.txFactory.createTxCreateDoc(_class, space, attributes, id, modifiedOn, modifiedBy)
await this.client.tx(tx)
return tx.objectId
}
@ -69,14 +71,16 @@ export class TxOperations implements Omit<Client, 'notify'> {
attachedToClass: Ref<Class<T>>,
collection: string,
attributes: AttachedData<P>,
id?: Ref<P>
id?: Ref<P>,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): Promise<Ref<P>> {
const tx = this.txFactory.createTxCollectionCUD<T, P>(
attachedToClass,
attachedTo,
space,
collection,
this.txFactory.createTxCreateDoc<P>(_class, space, attributes as unknown as Data<P>, id)
this.txFactory.createTxCreateDoc<P>(_class, space, attributes as unknown as Data<P>, id, modifiedOn, modifiedBy)
)
await this.client.tx(tx)
return tx.tx.objectId as unknown as Ref<P>
@ -90,14 +94,16 @@ export class TxOperations implements Omit<Client, 'notify'> {
attachedToClass: Ref<Class<T>>,
collection: string,
operations: DocumentUpdate<P>,
retrieve?: boolean
retrieve?: boolean,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): Promise<Ref<T>> {
const tx = this.txFactory.createTxCollectionCUD(
attachedToClass,
attachedTo,
space,
collection,
this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve)
this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve, modifiedOn, modifiedBy)
)
await this.client.tx(tx)
return tx.objectId
@ -109,14 +115,16 @@ export class TxOperations implements Omit<Client, 'notify'> {
objectId: Ref<P>,
attachedTo: Ref<T>,
attachedToClass: Ref<Class<T>>,
collection: string
collection: string,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): Promise<Ref<T>> {
const tx = this.txFactory.createTxCollectionCUD(
attachedToClass,
attachedTo,
space,
collection,
this.txFactory.createTxRemoveDoc(_class, space, objectId)
this.txFactory.createTxRemoveDoc(_class, space, objectId, modifiedOn, modifiedBy)
)
await this.client.tx(tx)
return tx.objectId
@ -127,14 +135,22 @@ export class TxOperations implements Omit<Client, 'notify'> {
space: Ref<Space>,
objectId: Ref<T>,
operations: DocumentUpdate<T>,
retrieve?: boolean
retrieve?: boolean,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): Promise<TxResult> {
const tx = this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve)
const tx = this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve, modifiedOn, modifiedBy)
return this.client.tx(tx)
}
removeDoc<T extends Doc>(_class: Ref<Class<T>>, space: Ref<Space>, objectId: Ref<T>): Promise<TxResult> {
const tx = this.txFactory.createTxRemoveDoc(_class, space, objectId)
removeDoc<T extends Doc>(
_class: Ref<Class<T>>,
space: Ref<Space>,
objectId: Ref<T>,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): Promise<TxResult> {
const tx = this.txFactory.createTxRemoveDoc(_class, space, objectId, modifiedOn, modifiedBy)
return this.client.tx(tx)
}
@ -143,9 +159,19 @@ export class TxOperations implements Omit<Client, 'notify'> {
objectClass: Ref<Class<D>>,
objectSpace: Ref<Space>,
mixin: Ref<Mixin<M>>,
attributes: MixinData<D, M>
attributes: MixinData<D, M>,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): Promise<TxResult> {
const tx = this.txFactory.createTxMixin(objectId, objectClass, objectSpace, mixin, attributes)
const tx = this.txFactory.createTxMixin(
objectId,
objectClass,
objectSpace,
mixin,
attributes,
modifiedOn,
modifiedBy
)
return this.client.tx(tx)
}
@ -154,17 +180,33 @@ export class TxOperations implements Omit<Client, 'notify'> {
objectClass: Ref<Class<D>>,
objectSpace: Ref<Space>,
mixin: Ref<Mixin<M>>,
attributes: MixinUpdate<D, M>
attributes: MixinUpdate<D, M>,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): Promise<TxResult> {
const tx = this.txFactory.createTxMixin(objectId, objectClass, objectSpace, mixin, attributes)
const tx = this.txFactory.createTxMixin(
objectId,
objectClass,
objectSpace,
mixin,
attributes,
modifiedOn,
modifiedBy
)
return this.client.tx(tx)
}
update<T extends Doc>(doc: T, update: DocumentUpdate<T>, retrieve?: boolean): Promise<TxResult> {
update<T extends Doc>(
doc: T,
update: DocumentUpdate<T>,
retrieve?: boolean,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): Promise<TxResult> {
const hierarchy = this.client.getHierarchy()
if (hierarchy.isMixin(doc._class)) {
const baseClass = hierarchy.getBaseClass(doc._class)
return this.updateMixin(doc._id, baseClass, doc.space, doc._class, update)
return this.updateMixin(doc._id, baseClass, doc.space, doc._class, update, modifiedOn, modifiedBy)
}
if (hierarchy.isDerived(doc._class, core.class.AttachedDoc)) {
const adoc = doc as unknown as AttachedDoc
@ -176,13 +218,15 @@ export class TxOperations implements Omit<Client, 'notify'> {
adoc.attachedToClass,
adoc.collection,
update,
retrieve
retrieve,
modifiedOn,
modifiedBy
)
}
return this.updateDoc(doc._class, doc.space, doc._id, update, retrieve)
return this.updateDoc(doc._class, doc.space, doc._id, update, retrieve, modifiedOn, modifiedBy)
}
remove<T extends Doc>(doc: T): Promise<TxResult> {
remove<T extends Doc>(doc: T, modifiedOn?: Timestamp, modifiedBy?: Ref<Account>): Promise<TxResult> {
if (this.client.getHierarchy().isDerived(doc._class, core.class.AttachedDoc)) {
const adoc = doc as unknown as AttachedDoc
return this.removeCollection(
@ -191,7 +235,9 @@ export class TxOperations implements Omit<Client, 'notify'> {
adoc._id,
adoc.attachedTo,
adoc.attachedToClass,
adoc.collection
adoc.collection,
modifiedOn,
modifiedBy
)
}
return this.removeDoc(doc._class, doc.space, doc._id)

View File

@ -14,7 +14,20 @@
//
import type { KeysByType } from 'simplytyped'
import type { Account, Arr, AttachedDoc, Class, Data, Doc, Domain, Mixin, PropertyType, Ref, Space } from './classes'
import type {
Account,
Arr,
AttachedDoc,
Class,
Data,
Doc,
Domain,
Mixin,
PropertyType,
Ref,
Space,
Timestamp
} from './classes'
import core from './component'
import { setObjectValue } from './objvalue'
import { _getOperator } from './operator'
@ -398,7 +411,9 @@ export class TxFactory {
_class: Ref<Class<T>>,
space: Ref<Space>,
attributes: Data<T>,
objectId?: Ref<T>
objectId?: Ref<T>,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): TxCreateDoc<T> {
return {
_id: generateId(),
@ -407,8 +422,8 @@ export class TxFactory {
objectId: objectId ?? generateId(),
objectClass: _class,
objectSpace: space,
modifiedOn: Date.now(),
modifiedBy: this.account,
modifiedOn: modifiedOn ?? Date.now(),
modifiedBy: modifiedBy ?? this.account,
attributes
}
}
@ -418,7 +433,9 @@ export class TxFactory {
objectId: Ref<T>,
space: Ref<Space>,
collection: string,
tx: TxCUD<P>
tx: TxCUD<P>,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): TxCollectionCUD<T, P> {
return {
_id: generateId(),
@ -427,8 +444,8 @@ export class TxFactory {
objectId,
objectClass: _class,
objectSpace: space,
modifiedOn: Date.now(),
modifiedBy: this.account,
modifiedOn: modifiedOn ?? Date.now(),
modifiedBy: modifiedBy ?? this.account,
collection,
tx
}
@ -439,14 +456,16 @@ export class TxFactory {
space: Ref<Space>,
objectId: Ref<T>,
operations: DocumentUpdate<T>,
retrieve?: boolean
retrieve?: boolean,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): TxUpdateDoc<T> {
return {
_id: generateId(),
_class: core.class.TxUpdateDoc,
space: core.space.Tx,
modifiedBy: this.account,
modifiedOn: Date.now(),
modifiedBy: modifiedBy ?? this.account,
modifiedOn: modifiedOn ?? Date.now(),
objectId,
objectClass: _class,
objectSpace: space,
@ -455,13 +474,19 @@ export class TxFactory {
}
}
createTxRemoveDoc<T extends Doc>(_class: Ref<Class<T>>, space: Ref<Space>, objectId: Ref<T>): TxRemoveDoc<T> {
createTxRemoveDoc<T extends Doc>(
_class: Ref<Class<T>>,
space: Ref<Space>,
objectId: Ref<T>,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): TxRemoveDoc<T> {
return {
_id: generateId(),
_class: core.class.TxRemoveDoc,
space: core.space.Tx,
modifiedBy: this.account,
modifiedOn: Date.now(),
modifiedBy: modifiedBy ?? this.account,
modifiedOn: modifiedOn ?? Date.now(),
objectId,
objectClass: _class,
objectSpace: space
@ -473,14 +498,16 @@ export class TxFactory {
objectClass: Ref<Class<D>>,
objectSpace: Ref<Space>,
mixin: Ref<Mixin<M>>,
attributes: MixinUpdate<D, M>
attributes: MixinUpdate<D, M>,
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): TxMixin<D, M> {
return {
_id: generateId(),
_class: core.class.TxMixin,
space: core.space.Tx,
modifiedBy: this.account,
modifiedOn: Date.now(),
modifiedBy: modifiedBy ?? this.account,
modifiedOn: modifiedOn ?? Date.now(),
objectId,
objectClass,
objectSpace,
@ -489,13 +516,20 @@ export class TxFactory {
}
}
createTxApplyIf (space: Ref<Space>, scope: string, match: DocumentClassQuery<Doc>[], txes: TxCUD<Doc>[]): TxApplyIf {
createTxApplyIf (
space: Ref<Space>,
scope: string,
match: DocumentClassQuery<Doc>[],
txes: TxCUD<Doc>[],
modifiedOn?: Timestamp,
modifiedBy?: Ref<Account>
): TxApplyIf {
return {
_id: generateId(),
_class: core.class.TxApplyIf,
space: core.space.Tx,
modifiedBy: this.account,
modifiedOn: Date.now(),
modifiedBy: modifiedBy ?? this.account,
modifiedOn: modifiedOn ?? Date.now(),
objectSpace: space,
scope,
match,

View File

@ -27,6 +27,7 @@
export let createMore: boolean | undefined = undefined
export let okLabel: IntlString = presentation.string.Create
export let onCancel: Function | undefined = undefined
export let fullSize = false
const dispatch = createEventDispatcher()
@ -36,6 +37,7 @@
<form
id={label}
class="antiCard {$deviceInfo.isMobile ? 'mobile' : 'dialog'}"
class:full={fullSize}
on:submit|preventDefault={() => {}}
use:resizeObserver={() => {
dispatch('changeContent')

View File

@ -64,12 +64,14 @@
const dispatch = createEventDispatcher()
const query = createQuery()
$: _idExtra = typeof docQuery?._id === 'object' ? docQuery?._id : {}
$: query.query<Doc>(
_class,
{
...(docQuery ?? {}),
[searchField]: { $like: '%' + search + '%' },
_id: { $nin: ignoreObjects }
_id: { $nin: ignoreObjects, ..._idExtra }
},
(result) => {
result.sort((a, b) => {

View File

@ -214,13 +214,19 @@
margin: 2.5rem;
}
}
&.vScroll {
overflow-y: auto;
&::-webkit-scrollbar-track {
margin: 2.5rem;
}
}
.ac-column {
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 1.75rem 2.5rem;
min-width: 25rem;
padding: 0.75rem 0.5rem;
min-width: 15rem;
max-width: 25rem;
height: 100%;
border-right: 1px solid var(--theme-menu-divider);

View File

@ -139,6 +139,10 @@
height: auto;
max-width: 60rem;
max-height: inherit;
&.full {
width: max-content;
// max-width: 100%;
}
}
&.mobile {
width: 90vw;

View File

@ -37,6 +37,7 @@
export let focusIndex = -1
export let autoSelect: boolean = true
export let useFlexGrow = false
export let minW0 = true
let container: HTMLElement
let opened: boolean = false
@ -53,7 +54,7 @@
const mgr = getFocusManager()
</script>
<div bind:this={container} class="min-w-0" class:flex-grow={useFlexGrow}>
<div bind:this={container} class:min-w-0={minW0} class:flex-grow={useFlexGrow}>
<Button
{focusIndex}
{icon}

View File

@ -35,6 +35,7 @@
export let width: string | undefined = undefined
export let labelDirection: TooltipAlignment | undefined = undefined
export let shouldUpdateUndefined: boolean = true
export let minW0 = true
let container: HTMLElement
let opened: boolean = false
@ -72,7 +73,7 @@
}
</script>
<div bind:this={container} class="min-w-0">
<div bind:this={container} class:min-w-0={minW0}>
<Button
{icon}
width={width ?? 'min-content'}

View File

@ -36,6 +36,7 @@
export let focus: boolean = false
export let focusable: boolean = false
export let disabled: boolean = false
export let fullSize = false
const dispatch = createEventDispatcher()
@ -74,7 +75,7 @@
} else if (kind === 'underline') {
target.style.width = `calc(${text.clientWidth}px + 1.125rem)`
} else {
target.style.width = text.clientWidth + 'px'
target.style.width = Math.max(text.clientWidth, 50) + 'px'
}
dispatch('input')
}
@ -118,7 +119,8 @@
<div
class="editbox-container"
class:w-full={focusable}
class:flex-grow={fullSize}
class:w-full={focusable || fullSize}
on:click={() => {
input.focus()
}}

View File

@ -0,0 +1,66 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Asset, IntlString } from '@hcengineering/platform'
import ExpandCollapse from './ExpandCollapse.svelte'
import Icon from './Icon.svelte'
import Label from './Label.svelte'
export let icon: Asset | undefined = undefined
export let label: IntlString | undefined = undefined
export let expanded: boolean = false
export let expandable = true
</script>
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-grow flex" class:expanded class:expandable>
<div
class="fs-title flex-row-center mr-4"
on:click|stopPropagation={() => {
expanded = !expanded
}}
>
<div class="chevron" class:expanded></div>
<div class="an-element__icon">
{#if icon}
<Icon {icon} size={'small'} />
{/if}
</div>
<span class="an-element__label title">
{#if label}<Label {label} />{/if}
</span>
</div>
<slot name="tools" />
</div>
<ExpandCollapse isExpanded={expanded}>
<div class="antiComponent p-2">
<slot />
</div>
</ExpandCollapse>
<style lang="scss">
.expandable {
.chevron {
content: '▶';
margin-right: 0.5rem;
font-size: 0.75rem;
color: var(--dark-color);
&.expanded {
transform: rotateZ(90deg);
}
}
}
</style>

View File

@ -21,6 +21,7 @@
<slot name="popup-header" />
{/if}
{#each $modal as popup, i}
{#key popup.id}
<PopupInstance
is={popup.is}
props={popup.props}
@ -32,4 +33,5 @@
close={popup.close}
overlay={popup.options.overlay}
/>
{/key}
{/each}

View File

@ -150,6 +150,7 @@ export { default as FocusHandler } from './components/FocusHandler.svelte'
export { default as ListView } from './components/ListView.svelte'
export { default as ToggleButton } from './components/ToggleButton.svelte'
export { default as ExpandCollapse } from './components/ExpandCollapse.svelte'
export { default as Expandable } from './components/Expandable.svelte'
export { default as BarDashboard } from './components/BarDashboard.svelte'
export { default as Notifications } from './components/notifications/Notifications.svelte'
export { default as notificationsStore } from './components/notifications/store'

View File

@ -229,6 +229,15 @@ async function getAllRealValues (client: TxOperations, values: any[], _class: Re
if (values.some((value) => typeof value !== 'string')) {
return values
}
if (
_class === core.class.TypeString ||
_class === core.class.EnumOf ||
_class === core.class.TypeNumber ||
_class === core.class.TypeDate
) {
return values
}
const realValues = await client.findAll(_class, { _id: { $in: values } })
const realValuesIds = realValues.map(({ _id }) => _id)
return [

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/config/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,16 @@
<!-- ALL HASHTAGs -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="bitrix" viewBox="0 0 200.9 201.2">
<style>
.st0 {
fill: #c60c30;
}
</style>
<g id="layer1" transform="translate(-76.221 -152.77)">
<path
id="path821"
class="st0"
d="M157.3 352.8c-17.6-3.6-34-11.8-46.8-23.2-33-29.3-43.3-75.4-25.8-116.5C89 203 96 192.8 104.6 183.9l7.6-7.8.5 45.8c.5 43.9.6 46.1 2.7 51.6 4.2 11.1 8.6 18.1 16.3 25.7 19.8 19.7 48.8 24.6 73.5 12.5 23.2-11.4 37.8-36.3 36.1-61.5-1.1-16.9-6.8-29.3-19.1-41.6-9.3-9.3-20.2-15.4-32-17.9-12.1-2.5-11.5-2.9-11.5 7.3v8.8l6.6 1.2c25.6 4.7 42.6 29.9 37.3 55.3-1.7 8.1-5.5 15.3-11.6 21.9-9.3 10-22.4 14.8-33.8 14.8-18.9 0-34.6-10.8-42.3-26.6 0 0-2-4.5-2.6-7.3-3.8-16.5-1.4-51.1-1.4-51.1l-.3-50.7 3-1.5c1.6-.9 3.2-1.5 3.5-1.5s.6 23.3.8 51.8c0 0-3.6 37.3.9 51.6.6 2.1 1.9 5.6 1.9 5.6 3.9 8 10.2 15.1 18.4 19 6.2 2.9 9.4 4.3 18.6 4.3 6 0 11.3-1.9 17.5-4.7 7.2-3.3 15.5-12.1 18.6-20.2 8.4-22.2-3.3-46-26-52.7-9.3-2.7-9.1-2.9-9.1 7.2v8.8l4.4 1.2c16 4.3 20.9 25.8 8.3 36.8-4.4 3.9-8.4 5.6-15.4 5.6-7.5 0-14.7-4.5-18.4-11.5-2.2-4.2-2.4-8.1-2.4-9.9 0-16.7-.4-46.9-.4-46.9l-.3-51.7 2.3-.6c1.3-.3 6.4-1.1 11.4-1.7 16.9-2.2 41.9 3.4 58.7 13 22.9 13.1 40.2 35.6 47.4 61.6 2.5 8.9 2.8 11.6 2.8 25.9 0 14.3-.3 17-2.7 25.8-9.8 35.4-34.8 60.6-70.5 71.4-7.5 2.3-11 2.7-24.3 3-10.7.1-17.6-.2-22.3-1.2z"
/></g>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "assets"
}

View File

@ -0,0 +1,15 @@
{
"string": {
"BitrixTokenUrl": "Bitrix24 Token Url",
"Bitrix": "",
"BitrixDesc": "Bitrix 24 integration",
"Settings": "Configure",
"NotAllowed": "Not allowed without administrative rights",
"BitrixEntityType": "Entity mappping",
"FieldMapping": "Field mapping",
"AddField": "Add mapping",
"Attribute": "Attribute",
"MapField": "Map...",
"BitrixImport": "Synchronize with Bitrix"
}
}

View File

@ -0,0 +1,15 @@
{
"string": {
"BitrixTokenUrl": "Bitrix24 Token Url",
"Bitrix": "",
"BitrixDesc": "Bitrix 24 integration",
"Settings": "Configure",
"NotAllowed": "Not allowed without administrative rights",
"BitrixEntityType": "Entity mappping",
"FieldMapping": "Field mapping",
"AddField": "Add mapping",
"Attribute": "Attribute",
"MapField": "Map...",
"BitrixImport": "Synchronize with Bitrix"
}
}

View File

@ -0,0 +1,35 @@
{
"name": "@hcengineering/bitrix-assets",
"version": "0.6.0",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"template": "@hcengineering/assets-package",
"license": "EPL-2.0",
"scripts": {
"build": "heft build",
"test": "heft test",
"build:docs": "",
"lint": "eslint src",
"lint:fix": "eslint --fix src",
"format": "prettier --write src && eslint --fix src",
"build:watch": "tsc"
},
"devDependencies": {
"@hcengineering/platform-rig": "~0.6.0",
"@types/heft-jest": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"@typescript-eslint/parser": "^5.41.0",
"eslint-config-standard-with-typescript": "^23.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"eslint": "^8.26.0",
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9",
"@types/node": "~16.11.12"
},
"dependencies": {
"@hcengineering/platform": "^0.6.8",
"@hcengineering/bitrix": "^0.6.0"
}
}

View File

@ -0,0 +1,6 @@
import { makeLocalesTest } from '@hcengineering/platform'
it(
'Locales are equale',
makeLocalesTest((lang) => import(`../../lang/${lang}.json`))
)

View File

@ -0,0 +1,24 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { addStringsLoader, loadMetadata } from '@hcengineering/platform'
import bitrix, { bitrixId } from '@hcengineering/bitrix'
const icons = require('../assets/icons.svg') as string // eslint-disable-line
loadMetadata(bitrix.icon, {
Bitrix: `${icons}#bitrix`
})
addStringsLoader(bitrixId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "esnext",
"module": "commonjs",
"sourceMap": true,
"types": ["node", "heft-jest"],
"declaration": true,
"outDir": "./lib",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"lib": [
"esnext",
"dom"
]
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/config/eslint.config.json'],
parserOptions: { tsconfigRootDir: __dirname },
settings: {
'svelte3/ignore-styles': () => true
}
}

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "ui"
}

View File

@ -0,0 +1,56 @@
{
"name": "@hcengineering/bitrix-resources",
"version": "0.6.0",
"main": "src/index.ts",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "tsc --incremental --noEmit --outDir ./dist_cache && echo build",
"build:docs": "api-extractor run --local",
"lint": "svelte-check && eslint",
"lint:fix": "eslint --fix src",
"format": "prettier --write --plugin-search-dir=. src && eslint --fix src"
},
"devDependencies": {
"@hcengineering/platform-rig": "~0.6.0",
"svelte-loader": "^3.1.3",
"sass": "^1.53.0",
"svelte-preprocess": "^4.10.7",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.26.0",
"@typescript-eslint/parser": "^5.41.0",
"eslint-config-standard-with-typescript": "^23.0.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier-plugin-svelte": "^2.8.0",
"prettier": "^2.7.1",
"svelte-check": "^2.8.0",
"typescript": "^4.3.5"
},
"dependencies": {
"@hcengineering/platform": "^0.6.8",
"svelte": "^3.47",
"@hcengineering/bitrix": "^0.6.0",
"@hcengineering/ui": "^0.6.2",
"@hcengineering/presentation": "~0.6.2",
"@hcengineering/text-editor": "~0.6.0",
"@hcengineering/contact": "~0.6.8",
"@hcengineering/lead": "~0.6.0",
"@hcengineering/setting": "~0.6.1",
"@hcengineering/core": "^0.6.19",
"@hcengineering/attachment": "~0.6.1",
"@hcengineering/attachment-resources": "~0.6.0",
"@hcengineering/view": "^0.6.1",
"@hcengineering/view-resources": "~0.6.0",
"@hcengineering/setting-resources": "~0.6.0",
"@hcengineering/chunter": "~0.6.1",
"p-queue": "~7.3.0",
"qs": "~6.11.0",
"@types/qs": "~6.9.7",
"@hcengineering/tags": "~0.6.2",
"@hcengineering/tags-resources": "~0.6.0",
"fast-equals": "^2.0.3"
}
}

View File

@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}

View File

@ -0,0 +1,28 @@
import PQueue from 'p-queue'
import { stringify as toQuery } from 'qs'
import { BitrixResult } from './types'
const queue = new PQueue({
intervalCap: 1,
interval: 1000
})
export class BitrixClient {
constructor (readonly url: string) {}
async call (method: string, params: any): Promise<BitrixResult> {
return await queue.add(async () => {
let query: string = toQuery(params)
if (query.length > 0) {
query = `?${query}`
}
return await (
await fetch(`${this.url}/${method}${query}`, {
method: 'get',
headers: {
'user-agent': 'anticrm'
}
})
).json()
})
}
}

View File

@ -0,0 +1,102 @@
<script lang="ts">
import { BitrixEntityMapping, BitrixFieldMapping, MappingOperation, Fields } from '@hcengineering/bitrix'
import core, { AnyAttribute } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Button, eventToHTMLElement, IconDelete, Menu, showPopup, Action, IconEdit } from '@hcengineering/ui'
import bitrix from '../plugin'
import CreateMappingAttribute from './CreateMappingAttribute.svelte'
export let mapping: BitrixEntityMapping
export let fieldMapping: BitrixFieldMapping[]
export let fields: Fields
export let attribute: AnyAttribute
$: mappedField = fieldMapping.find((it) => it.attributeName === attribute.name)
const client = getClient()
const allowed = new Set([
core.class.TypeString,
core.class.TypeMarkup,
core.class.TypeBoolean,
core.class.TypeDate,
core.class.TypeNumber,
core.class.EnumOf,
core.class.Collection
])
function addMapping (evt: MouseEvent, kind: MappingOperation): void {
showPopup(
CreateMappingAttribute,
{
mapping,
attribute,
fields,
kind
},
'middle'
)
}
$: actions = [
{
label: getEmbeddedLabel('Add Copy mapping'),
action: (_: any, evt: MouseEvent) => {
addMapping(evt, MappingOperation.CopyValue)
}
},
{
label: getEmbeddedLabel('Add Tag mapping'),
action: (_: any, evt: MouseEvent) => {
addMapping(evt, MappingOperation.CreateTag)
}
},
{
label: getEmbeddedLabel('Add Channel mapping'),
action: (_: any, evt: MouseEvent) => {
addMapping(evt, MappingOperation.CreateChannel)
}
}
] as Action[]
</script>
<div class="flex-row-center">
{#if mappedField !== undefined}
<Button
icon={IconEdit}
on:click={(evt) => {
showPopup(
CreateMappingAttribute,
{
mapping,
attribute,
fields,
field: mappedField
},
'middle'
)
}}
/>
<Button
icon={IconDelete}
on:click={() => {
if (mappedField) {
client.remove(mappedField)
}
}}
/>
{:else if allowed.has(attribute.type._class)}
<Button
label={bitrix.string.MapField}
on:click={(evt) => {
showPopup(
Menu,
{
actions
},
eventToHTMLElement(evt)
)
}}
/>
{/if}
</div>

View File

@ -0,0 +1,96 @@
<!--
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
-->
<script lang="ts">
import presentation, { Card, createQuery } from '@hcengineering/presentation'
import { Integration } from '@hcengineering/setting'
import { createEventDispatcher, onMount } from 'svelte'
import bitrix from '../plugin'
import { BitrixEntityMapping } from '@hcengineering/bitrix'
import { Button, eventToHTMLElement, IconAdd, Label, showPopup } from '@hcengineering/ui'
import { BitrixClient } from '../client'
import { BitrixProfile, StatusValue } from '../types'
import CreateMapping from './CreateMapping.svelte'
import EntiryMapping from './EntityMapping.svelte'
export let integration: Integration
const dispatch = createEventDispatcher()
const bitrixClient = new BitrixClient(integration.value)
let profile: BitrixProfile | undefined
let statusList: StatusValue[] = []
onMount(() => {
bitrixClient.call('profile', {}).then((res: any) => {
profile = res.result
})
bitrixClient.call('crm.status.list', {}).then((res) => {
statusList = res.result
})
})
const mQuery = createQuery()
let mappings: BitrixEntityMapping[] = []
$: mQuery.query(bitrix.class.EntityMapping, {}, (res) => {
mappings = res
})
function addMapping (evt: MouseEvent): void {
showPopup(CreateMapping, { integration, mappings, bitrixClient }, eventToHTMLElement(evt))
}
</script>
<Card
label={bitrix.string.BitrixDesc}
okAction={() => {
dispatch('close')
}}
canSave={true}
okLabel={presentation.string.Ok}
on:close={() => dispatch('close')}
fullSize={true}
>
<svelte:fragment slot="header">
{#if profile}
<div class="lines-limit-2">
{profile.LAST_NAME}
{profile.NAME}
</div>
{/if}
</svelte:fragment>
{#if profile}
{#if !profile.ADMIN}
<Label label={bitrix.string.NotAllowed} />
{:else}
<div class="flex flex-reverse flex-grab">
<Button icon={IconAdd} label={presentation.string.Add} on:click={addMapping} />
</div>
<div class="flex-row">
{#each mappings as mapping}
<EntiryMapping {mapping} {bitrixClient} {statusList} />
{/each}
</div>
{/if}
{/if}
<!-- <EditBox label={bitrix.string.BitrixTokenUrl} bind:value={url} /> -->
<svelte:fragment slot="pool" />
</Card>

View File

@ -0,0 +1,44 @@
<!--
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
-->
<script lang="ts">
import presentation, { Card } from '@hcengineering/presentation'
import { Integration } from '@hcengineering/setting'
import { EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import bitrix from '../plugin'
export let integration: Integration
let url: string = integration.value
const dispatch = createEventDispatcher()
function save (): void {
dispatch('close', { value: url })
}
</script>
<Card
label={bitrix.string.BitrixDesc}
okAction={save}
canSave={true}
okLabel={presentation.string.Ok}
on:close={() => dispatch('close')}
>
<EditBox label={bitrix.string.BitrixTokenUrl} bind:value={url} />
<svelte:fragment slot="pool" />
</Card>

View File

@ -0,0 +1,142 @@
<script lang="ts">
import { BitrixEntityMapping, Fields, FieldValue } from '@hcengineering/bitrix'
import core, { Enum, Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting-resources/src/plugin'
import {
Button,
DatePresenter,
EditBox,
eventToHTMLElement,
getEventPositionElement,
IconAdd,
IconEdit,
Menu,
showPopup
} from '@hcengineering/ui'
import Grid from '@hcengineering/ui/src/components/Grid.svelte'
import { BitrixClient } from '../client'
import EnumPopup from './EnumPopup.svelte'
export let mapping: BitrixEntityMapping
export let bitrixClient: BitrixClient
export let fields: Fields = {}
const client = getClient()
let items: any[] = []
function loadItems (order: boolean): void {
bitrixClient
.call(mapping.type + '.list', { select: ['*', 'UF_*'], order: { ID: order ? 'ASC' : 'DSC' } })
.then((res) => {
items = res.result
})
}
const enumQuery = createQuery()
let enums: Enum[] = []
enumQuery.query(core.class.Enum, {}, (res) => {
enums = res
})
async function updateEnum (evt: MouseEvent, fieldId: string, field: FieldValue): Promise<void> {
const enumId = ('bitrix_' + fieldId) as Ref<Enum>
const existingEnum = await client.findOne(core.class.Enum, { _id: enumId })
if (existingEnum !== undefined) {
existingEnum.enumValues = [...existingEnum.enumValues, ...(field.items?.map((it) => it.VALUE) ?? [])]
existingEnum.enumValues = existingEnum.enumValues.filter((it, idx) => existingEnum.enumValues.indexOf(it) === idx)
showPopup(setting.component.EditEnum, { value: existingEnum }, 'top')
} else {
showPopup(
Menu,
{
actions: [
{
label: getEmbeddedLabel('New'),
action: (_: any, evt: MouseEvent) => {
showPopup(
setting.component.EditEnum,
{
name: 'Bitrix:' + field.formLabel ?? field.title,
values: field.items?.map((it) => it.VALUE) ?? [],
value: existingEnum
},
getEventPositionElement(evt)
)
}
},
{
label: 'Modify existing',
component: EnumPopup,
props: {
_class: core.class.Enum,
action: async (existingEnum: Enum) => {
if (existingEnum !== undefined) {
existingEnum.enumValues = [
...existingEnum.enumValues,
...(field.items?.map((it) => it.VALUE) ?? [])
]
existingEnum.enumValues = existingEnum.enumValues.filter(
(it, idx) => existingEnum.enumValues.indexOf(it) === idx
)
showPopup(setting.component.EditEnum, { value: existingEnum }, 'top')
}
}
}
}
]
},
eventToHTMLElement(evt)
)
}
}
let pattern = ''
</script>
<div class="top-divider">
<div class="flex gap-2">
<Button label={getEmbeddedLabel('Load First N values')} on:click={() => loadItems(true)} />
<Button label={getEmbeddedLabel('Load Last N values')} on:click={() => loadItems(false)} />
</div>
<EditBox kind={'search-style'} bind:value={pattern} />
</div>
<div class="p-2 top-divider" style:overflow={'auto'} style:max-height={'40rem'}>
<Grid column={2} rowGap={1}>
{#each Object.entries(fields) as kv}
{@const field = kv[1]}
{@const title = field.formLabel ?? field.title}
{#if title.includes(pattern) || field.type.includes(pattern) || pattern.length === 0}
<span class="fs-title select-text flex-row-center">
{title} : {field.type}
<span class="select-text ml-2 mr-2">
"{kv[0]}"
</span>
{#if field.type === 'enumeration' || field.type === 'crm_status'}
<Button
icon={enums.find((it) => it._id === 'bitrix_' + kv[0]) ? IconEdit : IconAdd}
on:click={(evt) => updateEnum(evt, kv[0], field)}
/>
{/if}
</span>
<div class="whitespace-nowrap select-text flex-row-center gap-2">
{#each items.map((it) => it[kv[0]]).filter((it) => it != null && it !== '') as value}
<div class="ml-2">
{#if value !== null && value !== undefined && value !== ''}
{#if (field.type === 'datetime' || field.type === 'date') && value != null && value !== ''}
<DatePresenter value={new Date(value).getTime()} withTime={field.type === 'datetime'} />
{:else if field.type === 'enumeration'}
{field.items?.find((it) => it.ID === value)?.VALUE}
{:else}
{JSON.stringify(value)}
{/if}
{/if}
</div>
{/each}
</div>
{/if}
{/each}
</Grid>
</div>

View File

@ -0,0 +1,61 @@
<script lang="ts">
import { BitrixEntityMapping } from '@hcengineering/bitrix'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Card, createQuery } from '@hcengineering/presentation'
import setting, { Integration } from '@hcengineering/setting'
import { Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { BitrixClient } from '../client'
import bitrix from '../plugin'
import FieldMappingSynchronizer from './FieldMappingSynchronizer.svelte'
const mappingQuery = createQuery()
let mappings: BitrixEntityMapping[] = []
mappingQuery.query(
bitrix.class.EntityMapping,
{},
(res) => {
mappings = res
},
{
lookup: {
_id: {
fields: bitrix.class.FieldMapping
}
}
}
)
const dispatch = createEventDispatcher()
const q = createQuery()
let integration: Integration | undefined
q.query(setting.class.Integration, { type: bitrix.integrationType.Bitrix }, (res) => {
integration = res.shift()
})
$: bitrixClient = integration !== undefined ? new BitrixClient(integration.value) : undefined
let loading = false
</script>
<Card
label={getEmbeddedLabel('Bitrix Synchronization...')}
canSave={!loading}
fullSize={false}
okAction={() => {
dispatch('close')
}}
okLabel={getEmbeddedLabel('Close')}
on:close
>
{#if integration && bitrixClient}
{#each mappings as mapping}
<FieldMappingSynchronizer {mapping} {bitrixClient} bind:loading />
{/each}
{:else}
<Label label={getEmbeddedLabel('No integration configured')} />
{/if}
</Card>

View File

@ -0,0 +1,67 @@
<script lang="ts">
import { BitrixEntityMapping, BitrixEntityType, mappingTypes } from '@hcengineering/bitrix'
import core, { Class, ClassifierKind, Doc, Obj, Ref } from '@hcengineering/core'
import { Card, createQuery, getClient } from '@hcengineering/presentation'
import setting, { Integration } from '@hcengineering/setting'
import DropdownLabels from '@hcengineering/ui/src/components/DropdownLabels.svelte'
import { ObjectBox } from '@hcengineering/view-resources'
import bitrix from '../plugin'
export let integration: Integration
export let mappings: BitrixEntityMapping[]
const client = getClient()
async function save (): Promise<void> {
client.createDoc(bitrix.class.EntityMapping, bitrix.space.Mappings, {
ofClass,
type
})
}
$: existingTypes = new Set(mappings.map((it) => it.type))
$: items = existingTypes !== undefined ? mappingTypes.filter((it) => !existingTypes.has(it.id)) : []
let type: string = BitrixEntityType.Lead
let ofClass: Ref<Class<Doc>>
$: if (items.find((it) => it.id === type) === undefined) {
type = items[0]?.id ?? ''
}
const classQuery = createQuery()
let _classes: Ref<Class<Doc>>[] = []
$: classQuery.query(
core.class.Class,
{
label: { $exists: true },
kind: ClassifierKind.CLASS,
[setting.mixin.Editable + '.value']: true
},
(res) => {
const withDerived: Ref<Class<Obj>>[] = []
for (const r of res) {
withDerived.push(...client.getHierarchy().getDescendants(r._id))
}
_classes = withDerived
}
)
</script>
<Card label={bitrix.string.AddMapping} canSave={type !== ''} okAction={save} on:close>
<div class="flex">
<DropdownLabels label={bitrix.string.BitrixEntityType} {items} bind:selected={type} />
<ObjectBox
_class={core.class.Class}
label={core.string.Class}
showNavigate={false}
bind:value={ofClass}
docQuery={{
_id: { $in: _classes },
kind: ClassifierKind.CLASS
}}
/>
</div>
</Card>

View File

@ -0,0 +1,43 @@
<script lang="ts">
import { BitrixEntityMapping, BitrixFieldMapping, Fields, MappingOperation } from '@hcengineering/bitrix'
import { AnyAttribute } from '@hcengineering/core'
import presentation, { Card } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import bitrix from '../plugin'
import CopyMapping from './mappings/CopyMapping.svelte'
import CreateChannelMapping from './mappings/CreateChannelMapping.svelte'
import CreateTagMapping from './mappings/CreateTagMapping.svelte'
export let mapping: BitrixEntityMapping
export let fields: Fields = {}
export let attribute: AnyAttribute
export let kind: MappingOperation | undefined
export let field: BitrixFieldMapping | undefined
$: _kind = kind ?? field?.operation.kind
let op: CopyMapping | CreateTagMapping | CreateChannelMapping
async function save (): Promise<void> {
op.save()
}
</script>
<Card
label={bitrix.string.AddField}
canSave={attribute !== undefined}
okAction={save}
okLabel={presentation.string.Save}
on:close
>
<svelte:fragment slot="header">
<Label label={attribute.label} />
</svelte:fragment>
{#if _kind === MappingOperation.CopyValue}
<CopyMapping {mapping} {fields} {attribute} {field} bind:this={op} />
{:else if _kind === MappingOperation.CreateTag}
<CreateTagMapping {mapping} {fields} {attribute} {field} bind:this={op} />
{:else if _kind === MappingOperation.CreateChannel}
<CreateChannelMapping {mapping} {fields} {attribute} {field} bind:this={op} />
{/if}
</Card>

View File

@ -0,0 +1,167 @@
<script lang="ts">
import { BitrixEntityMapping, BitrixFieldMapping, Fields, mappingTypes } from '@hcengineering/bitrix'
import { Class, Doc, Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { ClassSetting } from '@hcengineering/setting-resources'
import { Button, Expandable, Icon, IconDelete, IconEdit, Label, showPopup } from '@hcengineering/ui'
import { BitrixClient } from '../client'
import bitrix from '../plugin'
import AttributeMapper from './AttributeMapper.svelte'
import FieldMappingPresenter from './FieldMappingPresenter.svelte'
import CheckBox from '@hcengineering/ui/src/components/CheckBox.svelte'
import { deepEqual } from 'fast-equals'
import { StatusValue } from '../types'
import { toClassRef } from '../utils'
import BitrixFieldLookup from './BitrixFieldLookup.svelte'
import CreateMappingAttribute from './CreateMappingAttribute.svelte'
export let mapping: BitrixEntityMapping
export let bitrixClient: BitrixClient
export let statusList: StatusValue[] = []
const client = getClient()
$: ofClass = client.getHierarchy().getClass(mapping.ofClass)
$: typeTitle = mappingTypes.find((it) => it.id === mapping.type)
let fields: Fields = {}
bitrixClient.call(mapping.type + '.fields', {}).then((res) => {
fields = res.result
})
async function updateMappingFields (
mapping: BitrixEntityMapping,
fields: Fields,
statusList: StatusValue[]
): Promise<void> {
if (deepEqual(fields, {})) {
return // no value resieved yet.
}
// Update fields with status valies if missing.
for (const f of Object.values(fields)) {
if (f.type === 'crm_status') {
f.items = statusList
.filter((it) => it.ENTITY_ID === f.statusType)
.map((it) => ({ ID: `${it.STATUS_ID}`, VALUE: it.NAME }))
}
}
// Store if changed.
// TODO: We need to inform about mapping changes and remove old ones.
if (!deepEqual(mapping.bitrixFields, fields)) {
await client.update(mapping, { bitrixFields: fields })
}
}
$: updateMappingFields(mapping, fields, statusList)
const attrs = createQuery()
let fieldMapping: BitrixFieldMapping[] = []
$: attrs.query(bitrix.class.FieldMapping, { attachedTo: mapping._id }, (res) => {
fieldMapping = res
})
$: fieldsByClass = fieldMapping.reduce((p, c) => {
p[c.ofClass] = [...(p[c.ofClass] ?? []), c]
return p
}, {} as Record<Ref<Class<Doc>>, BitrixFieldMapping[]>)
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="antiComponent max-w-240 flex-grow p-1">
<Expandable icon={ofClass?.icon} label={getEmbeddedLabel(typeTitle?.label ?? '')}>
<svelte:fragment slot="tools">
<div class="flex flex-reverse flex-grow">
<Button icon={IconDelete} on:click={() => client.remove(mapping)} size={'small'} />
</div>
</svelte:fragment>
<Expandable label={getEmbeddedLabel('Options')}>
<div class="flex-col">
<div class="flex-row-center">
<CheckBox
bind:checked={mapping.comments}
on:value={(evt) => client.update(mapping, { comments: evt.detail })}
/>
<div class="ml-2">
<Label label={getEmbeddedLabel('Comments')} />
</div>
</div>
<div class="flex-row-center">
<CheckBox
bind:checked={mapping.attachments}
on:value={(evt) => client.update(mapping, { attachments: evt.detail })}
/>
<div class="ml-2">
<Label label={getEmbeddedLabel('Attachments')} />
</div>
</div>
<div class="flex-row-center">
<CheckBox
bind:checked={mapping.activity}
readonly
on:value={(evt) => client.update(mapping, { activity: evt.detail })}
/>
<div class="ml-2">
<Label label={getEmbeddedLabel('Activity')} />
</div>
</div>
</div>
</Expandable>
<Expandable label={getEmbeddedLabel('Mappings')} expanded>
<div class="flex-row flex-grow bottom-divider p-2">
{#each Object.entries(fieldsByClass) as field, i}
{@const cl = client.getHierarchy().getClass(toClassRef(field[0]))}
<div class="fs-title flex-row-center">
{#if cl.icon}
<div class="mr-1">
<Icon icon={cl.icon} size={'large'} />
</div>
{/if}
<Label label={cl.label} />
</div>
<div class="flex-row">
{#each field[1] as cfield, i}
<div class="fs-title flex-row-center ml-4">
{i + 1}.
<FieldMappingPresenter {mapping} value={cfield} />
<Button
icon={IconEdit}
on:click={(evt) => {
showPopup(
CreateMappingAttribute,
{
mapping,
attribute: client.getHierarchy().getAttribute(cfield.ofClass, cfield.attributeName),
fields,
field: cfield
},
'middle'
)
}}
/>
</div>
{/each}
</div>
{/each}
</div>
</Expandable>
<Expandable label={getEmbeddedLabel('Class settings')}>
<ClassSetting
ofClass={mapping.ofClass}
withoutHeader
attributeMapper={{
component: AttributeMapper,
props: { mapping, fields, fieldMapping },
label: bitrix.string.FieldMapping
}}
/>
</Expandable>
<Expandable label={getEmbeddedLabel('Bitrix field lookup')}>
<BitrixFieldLookup {mapping} {bitrixClient} {fields} />
</Expandable>
</Expandable>
</div>

View File

@ -0,0 +1,46 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Class, DocumentQuery, Enum, Ref } from '@hcengineering/core'
import { ObjectPopup } from '@hcengineering/presentation'
import { createEventDispatcher } from 'svelte'
export let _class: Ref<Class<Enum>>
export let selected: Ref<Enum> | undefined
export let query: DocumentQuery<Enum> | undefined
export let action: (evt: Enum) => void | Promise<void>
const dispatch = createEventDispatcher()
</script>
<ObjectPopup
{_class}
{selected}
bind:docQuery={query}
multiSelect={false}
allowDeselect={false}
shadows={true}
on:update
on:close={(evt) => {
if (evt.detail != null) {
action(evt.detail)
}
dispatch('close')
}}
>
<svelte:fragment slot="item" let:item>
{item.name}
</svelte:fragment>
</ObjectPopup>

View File

@ -0,0 +1,34 @@
<script lang="ts">
import { BitrixEntityMapping, BitrixFieldMapping, MappingOperation } from '@hcengineering/bitrix'
import { AnyAttribute } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Icon, IconArrowLeft, Label } from '@hcengineering/ui'
import CopyMappingPresenter from './mappings/CopyMappingPresenter.svelte'
import CreateChannelMappingPresenter from './mappings/CreateChannelMappingPresenter.svelte'
import CreateTagMappingPresenter from './mappings/CreateTagMappingPresenter.svelte'
export let mapping: BitrixEntityMapping
export let value: BitrixFieldMapping
$: kind = value.operation.kind
const attr: AnyAttribute | undefined = getClient().getHierarchy().getAttribute(value.ofClass, value.attributeName)
</script>
<div class="flex-row-center top-divider">
{#if attr}
<div class="ml-4 fs-title mr-2 flex-row-center">
<Label label={attr.label} />
<Icon icon={IconArrowLeft} size={'small'} />
</div>
{/if}
{#if mapping && mapping.bitrixFields}
{#if kind === MappingOperation.CopyValue}
<CopyMappingPresenter {mapping} {value} />
{:else if kind === MappingOperation.CreateTag}
<CreateTagMappingPresenter {mapping} {value} />
{:else if kind === MappingOperation.CreateChannel}
<CreateChannelMappingPresenter {mapping} {value} />
{/if}
{/if}
</div>

View File

@ -0,0 +1,389 @@
<script lang="ts">
import bitrix, {
BitrixEntityMapping,
BitrixEntityType,
BitrixFieldMapping,
BitrixSyncDoc
} from '@hcengineering/bitrix'
import chunter, { Comment } from '@hcengineering/chunter'
import contact, { combineName, EmployeeAccount } from '@hcengineering/contact'
import core, {
AccountRole,
ApplyOperations,
AttachedDoc,
Class,
Data,
Doc,
DocumentUpdate,
generateId,
Mixin,
Ref,
Space,
WithLookup
} from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { getClient, SpaceSelect } from '@hcengineering/presentation'
import { TagElement } from '@hcengineering/tags'
import { Button, Expandable, Icon, Label } from '@hcengineering/ui'
import DropdownLabels from '@hcengineering/ui/src/components/DropdownLabels.svelte'
import { NumberEditor } from '@hcengineering/view-resources'
import { deepEqual } from 'fast-equals'
import { BitrixClient } from '../client'
import { convert, ConvertResult, toClassRef } from '../utils'
import FieldMappingPresenter from './FieldMappingPresenter.svelte'
export let mapping: WithLookup<BitrixEntityMapping>
export let bitrixClient: BitrixClient
const client = getClient()
$: fieldMapping = (mapping.$lookup?.fields as BitrixFieldMapping[]) ?? []
$: fieldsByClass = fieldMapping.reduce((p, c) => {
p[c.ofClass] = [...(p[c.ofClass] ?? []), c]
return p
}, {} as Record<Ref<Class<Doc>>, BitrixFieldMapping[]>)
let direction: 'ASC' | 'DSC' = 'DSC'
let limit = 200
let space: Ref<Space> | undefined
export let loading = false
let state = ''
async function updateDoc (client: ApplyOperations, doc: Doc, raw: Doc | Data<Doc>): Promise<void> {
// We need to update fields if they are different.
const documentUpdate: DocumentUpdate<Doc> = {}
for (const [k, v] of Object.entries(raw)) {
if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space'].includes(k)) {
continue
}
if (!deepEqual((doc as any)[k], v)) {
;(documentUpdate as any)[k] = v
}
}
if (Object.keys(documentUpdate).length > 0) {
await client.update(doc, documentUpdate)
}
}
let docsProcessed = 0
let total = 0
async function syncPlatform (documents: ConvertResult[]): Promise<void> {
const existingDocuments = await client.findAll(mapping.ofClass, {
[bitrix.mixin.BitrixSyncDoc + '.bitrixId']: { $in: documents.map((it) => it.document.bitrixId) }
})
const hierarchy = client.getHierarchy()
for (const d of documents) {
const existing = existingDocuments.find(
(it) => hierarchy.as(it, bitrix.mixin.BitrixSyncDoc).bitrixId === d.document.bitrixId
)
const applyOp = client.apply('bitrix')
if (existing !== undefined) {
// We need to update fields if they are different.
await updateDoc(applyOp, existing, d.document)
// Check and update mixins
for (const [m, mv] of Object.entries(d.mixins)) {
const mRef = m as Ref<Mixin<Doc>>
if (hierarchy.hasMixin(existing, mRef)) {
await applyOp.createMixin(
d.document._id,
d.document._class,
d.document.space,
m as Ref<Mixin<Doc>>,
mv,
d.document.modifiedOn,
d.document.modifiedBy
)
} else {
const existingM = hierarchy.as(existing, mRef)
await updateDoc(applyOp, existingM, mv)
}
}
} else {
await applyOp.createDoc(
d.document._class,
d.document.space,
d.document,
d.document._id,
d.document.modifiedOn,
d.document.modifiedBy
)
await applyOp.createMixin<Doc, BitrixSyncDoc>(
d.document._id,
d.document._class,
d.document.space,
bitrix.mixin.BitrixSyncDoc,
{
type: d.document.type,
bitrixId: d.document.bitrixId
},
d.document.modifiedOn,
d.document.modifiedBy
)
for (const [m, mv] of Object.entries(d.mixins)) {
await applyOp.createMixin(
d.document._id,
d.document._class,
d.document.space,
m as Ref<Mixin<Doc>>,
mv,
d.document.modifiedOn,
d.document.modifiedBy
)
}
for (const ed of d.extraDocs) {
if (applyOp.getHierarchy().isDerived(ed._class, core.class.AttachedDoc)) {
const adoc = ed as AttachedDoc
await applyOp.addCollection(
adoc._class,
adoc.space,
adoc.attachedTo,
adoc.attachedToClass,
adoc.collection,
adoc,
adoc._id,
d.document.modifiedOn,
d.document.modifiedBy
)
} else {
await applyOp.createDoc(ed._class, ed.space, ed, ed._id, d.document.modifiedOn, d.document.modifiedBy)
}
}
if (d.comments !== undefined) {
const comments = await d.comments
if (comments !== undefined && comments.length > 0) {
const existingComments = await client.findAll(chunter.class.Comment, {
attachedTo: d.document._id,
[bitrix.mixin.BitrixSyncDoc + '.bitrixId']: { $in: comments.map((it) => it.bitrixId) }
})
for (const comment of comments) {
const existing = existingComments.find(
(it) => hierarchy.as<Doc, BitrixSyncDoc>(it, bitrix.mixin.BitrixSyncDoc).bitrixId === comment.bitrixId
)
if (existing !== undefined) {
// We need to update fields if they are different.
await updateDoc(applyOp, existing, comment)
} else {
await applyOp.addCollection(
comment._class,
comment.space,
comment.attachedTo,
comment.attachedToClass,
comment.collection,
comment,
comment._id,
comment.modifiedOn,
comment.modifiedBy
)
await applyOp.createMixin<Doc, BitrixSyncDoc>(
comment._id,
comment._class,
comment.space,
bitrix.mixin.BitrixSyncDoc,
{
type: d.document.type,
bitrixId: d.document.bitrixId
},
comment.modifiedOn,
comment.modifiedBy
)
}
}
}
}
}
await applyOp.commit()
docsProcessed++
state = `processed: ${docsProcessed}/${total}`
}
}
async function doSync (): Promise<void> {
loading = true
const commentFields = await bitrixClient.call(BitrixEntityType.Comment + '.fields', {})
const commentFieldKeys = Object.keys(commentFields.result)
const allEmployee = await client.findAll(contact.class.EmployeeAccount, {})
const userList = new Map<string, Ref<EmployeeAccount>>()
// Fill all users and create new ones, if required.
let totalUsers = 1
let next = 0
while (userList.size < totalUsers) {
const users = await bitrixClient.call('user.search', { start: next })
next = users.next
totalUsers = users.total
for (const u of users.result) {
let accountId = allEmployee.find((it) => it.email === u.EMAIL)?._id
if (accountId === undefined) {
const employeeId = await client.createDoc(contact.class.Employee, contact.space.Contacts, {
name: combineName(u.NAME, u.LAST_NAME),
avatar: u.PERSONAL_PHOTO,
active: u.ACTIVE,
city: u.PERSONAL_CITY
})
accountId = await client.createDoc(contact.class.EmployeeAccount, core.space.Model, {
email: u.EMAIL,
name: combineName(u.NAME, u.LAST_NAME),
employee: employeeId,
role: AccountRole.User
})
}
userList.set(u.ID, accountId)
}
}
try {
if (space === undefined || mapping.$lookup?.fields === undefined) {
return
}
let processed = 0
const tagElements: Map<Ref<Class<Doc>>, TagElement[]> = new Map()
let added = 0
while (added <= limit) {
const result = await bitrixClient.call(mapping.type + '.list', {
select: ['*', 'UF_*'],
order: { ID: direction },
start: processed
})
const extraDocs: Doc[] = []
const convertResults: ConvertResult[] = []
for (const r of result.result) {
// Convert documents.
const res = await convert(
client,
mapping,
space,
mapping.$lookup?.fields as BitrixFieldMapping[],
r,
extraDocs,
tagElements,
userList
)
if (mapping.comments) {
res.comments = bitrixClient
.call(BitrixEntityType.Comment + '.list', {
filter: {
ENTITY_ID: res.document.bitrixId,
ENTITY_TYPE: mapping.type.replace('crm.', '')
},
select: commentFieldKeys,
order: { ID: direction }
})
.then((comments) => {
return comments.result.map(
(it: any) =>
({
_id: generateId(),
_class: chunter.class.Comment,
message: it.COMMENT,
bitrixId: it.ID,
type: it.ENTITY_TYPE,
attachedTo: res.document._id,
attachedToClass: res.document._class,
collection: 'comments',
space: res.document.space,
modifiedBy: userList.get(it.AUTHOR_ID) ?? core.account.System,
modifiedOn: new Date(userList.get(it.CREATED) ?? new Date().toString()).getTime()
} as Comment)
)
})
}
convertResults.push(res)
extraDocs.push(...res.extraDocs)
added++
if (added > limit) {
break
}
}
total = result.total
await syncPlatform(convertResults)
processed = result.next
}
} catch (err: any) {
state = err.message
console.error(err)
} finally {
loading = false
}
}
</script>
<Expandable label={getEmbeddedLabel(mapping.type)}>
<svelte:fragment slot="tools">
<div class="flex-row-center">
<SpaceSelect
_class={core.class.Space}
label={core.string.Space}
bind:value={space}
on:change={(evt) => {
space = evt.detail
}}
autoSelect
spaceQuery={{ _id: { $in: [contact.space.Contacts] } }}
/>
<DropdownLabels
label={getEmbeddedLabel('Direction')}
items={[
{ id: 'ASC', label: 'Ascending' },
{ id: 'DSC', label: 'Descending' }
]}
bind:selected={direction}
/>
<div class="fs-title flex-row-center">
<NumberEditor
kind={'button'}
value={limit}
focus={false}
placeholder={getEmbeddedLabel('Limit')}
onChange={(val) => {
if (val) {
limit = val
}
}}
/>
</div>
<div class="ml-2 flex-row-center">
<div class="p-1">
{state}
</div>
<Button size={'large'} label={getEmbeddedLabel('Synchronize')} {loading} on:click={doSync} />
</div>
</div>
</svelte:fragment>
<div class="flex-row flex-grow bottom-divider p-2">
{#each Object.entries(fieldsByClass) as field, i}
{@const cl = client.getHierarchy().getClass(toClassRef(field[0]))}
<div class="fs-title flex-row-center">
{#if cl.icon}
<div class="mr-1">
<Icon icon={cl.icon} size={'large'} />
</div>
{/if}
<Label label={cl.label} />
</div>
<div class="flex-row">
{#each field[1] as cfield, i}
<div class="fs-title flex-row-center ml-4">
{i + 1}.
<FieldMappingPresenter {mapping} value={cfield} />
</div>
{/each}
</div>
{/each}
</div>
</Expandable>

View File

@ -0,0 +1,38 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<div class="flex-grow">
<svg class="svg-{size}" {fill} viewBox="0 0 174 33" width="174" height="33" xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path
d="M106.8 32.4h18.7v-4.1h-12.4c1.7-6.8 12.1-8.3 12.1-15.9 0-4.1-2.8-7.1-8.6-7.1-3.7 0-6.8 1.1-9 2.2l1.3 3.8c2-.9 4.2-1.8 6.9-1.8 2.2 0 4.2.9 4.2 3.4.1 5.6-12.1 6-13.2 19.5zm56.1-5.7c-5.7 0-10.4-4.7-10.4-10.4s4.7-10.4 10.4-10.4 10.4 4.7 10.4 10.4-4.7 10.4-10.4 10.4zm0-18.9c-4.7 0-8.5 3.8-8.5 8.5s3.8 8.5 8.5 8.5 8.5-3.8 8.5-8.5-3.8-8.5-8.5-8.5z"
fill="#00629A"
/>
<path d="M168.8 16.3h-5.2v-5.2h-1.4v6.6h6.6v-1.4z" fill="#00629A" /><path
d="M0 1.6h9c6.6 0 9.6 3.8 9.6 7.8 0 2.7-1.3 5.1-3.7 6.4v.1c3.6.9 5.8 3.8 5.8 7.4 0 4.8-3.6 9.1-10.8 9.1H0V1.6zm8.3 12.9c3.1 0 4.8-1.7 4.8-4.1 0-2.3-1.5-4.1-4.8-4.1H5.7v8.2h2.6zm.9 13.3c3.7 0 5.8-1.4 5.8-4.5 0-2.6-2-4.2-5.1-4.2H5.7v8.7h3.5zM24.9 3.4c0-1.9 1.5-3.4 3.4-3.4s3.5 1.4 3.5 3.4c0 1.8-1.5 3.3-3.5 3.3s-3.4-1.4-3.4-3.3zm.6 6.9h5.6v22.1h-5.6V10.3zM38.6 27V14.7h-4v-4.4h4V5.2l5.6-1.6v6.7h6.7l-1.4 4.4h-5.3v10.9c0 2.1.7 2.8 2.2 2.8 1.3 0 2.5-.5 3.4-1.1l1.7 3.8c-1.6 1.1-4.3 1.7-6.5 1.7-4 .1-6.4-2.1-6.4-5.8zm15.9-16.7h4.7l.6 2.5c2-2 3.8-3 6.1-3 1 0 2.2.3 3.1.9l-2 4.7c-1-.6-1.9-.7-2.5-.7-1.5 0-2.7.6-4.5 2.2v15.6h-5.6V10.3h.1zm16.7-6.9c0-1.9 1.5-3.4 3.4-3.4S78 1.5 78 3.4c0 1.8-1.5 3.3-3.5 3.3s-3.3-1.4-3.3-3.3zm.6 6.9h5.6v22.1h-5.6V10.3zm16.9 11-8-11h5.8l5.2 7.2 5.3-7.2h5.8l-8.1 11 8.2 11.1h-5.8L91.8 25l-5.4 7.4h-5.8l8.1-11.1z"
fill="#3FC0F0"
/>
<path
d="M142.9 21.9V5.3h-3.5l-13.3 17.3v3.3h12v6.6h4.8v-6.6h4v-4h-4zm-4.8-3.9v3.8h-3.2c-1 0-2.8.1-3.4.1l6.8-9.2c0 .8-.2 3.2-.2 5.3z"
fill="#00629A"
/></g
>
</svg>
</div>

View File

@ -0,0 +1,134 @@
<script lang="ts">
import {
BitrixEntityMapping,
BitrixFieldMapping,
CopyPattern,
CopyValueOperation,
Fields,
MappingOperation
} from '@hcengineering/bitrix'
import core, { AnyAttribute } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Button, IconActivity, IconAdd, IconClose, IconDelete } from '@hcengineering/ui'
import DropdownLabels from '@hcengineering/ui/src/components/DropdownLabels.svelte'
import EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
import bitrix from '../../plugin'
export let mapping: BitrixEntityMapping
export let fields: Fields = {}
export let attribute: AnyAttribute
export let field: BitrixFieldMapping | undefined
let patterns: CopyPattern[] = [
...((field?.operation as CopyValueOperation)?.patterns ?? [
{
text: ''
}
])
]
const client = getClient()
export async function save (): Promise<void> {
if (field) {
await client.update(field, {
operation: {
kind: MappingOperation.CopyValue,
patterns
}
})
} else {
await client.addCollection(bitrix.class.FieldMapping, mapping.space, mapping._id, mapping._class, 'fields', {
ofClass: attribute.attributeOf,
attributeName: attribute.name,
operation: {
kind: MappingOperation.CopyValue,
patterns
}
})
}
}
$: items = Object.entries(fields)
.filter((it) => {
if (attribute.type._class === core.class.EnumOf) {
return it[1].type === 'enumeration' || it[1].type === 'crm_status'
}
return true
})
.map((it) => ({
id: it[0],
label: `${it[1].formLabel ?? it[1].title}${it[0].startsWith('UF_') ? ' *' : ''} - ${it[0]}`
}))
</script>
<div class="flex-row-center flex-wrap">
{#each patterns as p, i}
<div class="pattern flex-row-center">
{#if attribute.type._class !== core.class.EnumOf}
<EditBox kind={'editbox'} bind:value={p.text} maxWidth={'5rem'} fullSize />
{/if}
<DropdownLabels minW0={false} label={bitrix.string.FieldMapping} {items} bind:selected={p.field} />
{#if p.alternatives}
{#each p.alternatives as alt, i}
<DropdownLabels minW0={false} label={bitrix.string.FieldMapping} {items} bind:selected={p.alternatives[i]} />
<Button
icon={IconClose}
size={'small'}
on:click={() => {
p.alternatives?.splice(i, 1)
patterns = patterns
}}
/>
{/each}
{/if}
<div class="ml-1">
<Button
icon={IconActivity}
size={'small'}
on:click={() => {
p.alternatives = [...(p.alternatives ?? []), items[0].id]
}}
/>
<Button
icon={IconDelete}
size={'small'}
on:click={() => {
patterns.splice(i, 1)
patterns = patterns
}}
/>
</div>
</div>
{/each}
{#if attribute.type._class !== core.class.EnumOf || patterns.length === 0}
<div class="ml-2">
<Button
icon={IconAdd}
size={'small'}
on:click={() => {
patterns = [...patterns, { text: '', field: undefined }]
}}
/>
</div>
{/if}
</div>
<style lang="scss">
.pattern {
margin: 0.5rem;
padding: 0.5rem;
flex-shrink: 0;
border: 1px dashed var(--accent-color);
border-radius: 0.25rem;
font-weight: 500;
font-size: 0.75rem;
// text-transform: uppercase;
color: var(--accent-color);
&:hover {
color: var(--caption-color);
}
}
</style>

View File

@ -0,0 +1,48 @@
<script lang="ts">
import { BitrixEntityMapping, BitrixFieldMapping, CopyValueOperation } from '@hcengineering/bitrix'
export let mapping: BitrixEntityMapping
export let value: BitrixFieldMapping
$: op = value.operation as CopyValueOperation
const fieldOf = (field?: string) =>
field ? mapping.bitrixFields[field]?.formLabel ?? mapping.bitrixFields[field]?.title : field ?? ''
</script>
<div class="flex-row-center flex-wrap">
{#each op.patterns as p, i}
<div class="pattern flex-row-center">
<span>
{p.text}
</span>
{#if mapping.bitrixFields}
=> {fieldOf(p.field)}
{/if}
{#if p.alternatives}
{#each p.alternatives as alt, i}
|{fieldOf(alt)}
{/each}
{/if}
</div>
{/each}
</div>
<style lang="scss">
.pattern {
margin: 0.1rem;
padding: 0.3rem;
flex-shrink: 0;
border: 1px dashed var(--accent-color);
border-radius: 0.25rem;
font-weight: 500;
font-size: 0.75rem;
// text-transform: uppercase;
color: var(--accent-color);
&:hover {
color: var(--caption-color);
}
}
</style>

View File

@ -0,0 +1,116 @@
<script lang="ts">
import {
BitrixEntityMapping,
BitrixFieldMapping,
ChannelFieldMapping,
Fields,
MappingOperation,
CreateChannelOperation
} from '@hcengineering/bitrix'
import { AnyAttribute } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import contact, { ChannelProvider } from '@hcengineering/contact'
import { Button, DropdownLabelsIntl, IconAdd, IconDelete } from '@hcengineering/ui'
import DropdownLabels from '@hcengineering/ui/src/components/DropdownLabels.svelte'
import bitrix from '../../plugin'
export let mapping: BitrixEntityMapping
export let fields: Fields = {}
export let attribute: AnyAttribute
export let field: BitrixFieldMapping | undefined
let channelFields: ChannelFieldMapping[] = [...((field?.operation as CreateChannelOperation)?.fields ?? [])]
const client = getClient()
export async function save (): Promise<void> {
if (field !== undefined) {
await client.update(field, {
operation: {
kind: MappingOperation.CreateChannel,
fields: channelFields
}
})
} else {
await client.addCollection(bitrix.class.FieldMapping, mapping.space, mapping._id, mapping._class, 'fields', {
ofClass: attribute.attributeOf,
attributeName: attribute.name,
operation: {
kind: MappingOperation.CreateChannel,
fields: channelFields
}
})
}
}
$: items = Object.entries(fields)
.filter((it) => {
return it[1].type !== 'enumeration'
})
.map((it) => ({
id: it[0],
label: `${it[1].formLabel ?? it[1].title}${it[0].startsWith('UF_') ? ' *' : ''} - ${it[0]}`
}))
let providers: ChannelProvider[] = []
client.findAll(contact.class.ChannelProvider, {}).then((res) => {
providers = res
})
$: providerItems = providers.map((it) => ({ id: it._id, label: it.label }))
</script>
<div class="flex-col flex-wrap">
{#each channelFields as p, i}
<div class="pattern flex-row-center gap-2">
<DropdownLabelsIntl
minW0={false}
label={getEmbeddedLabel('Channel')}
items={providerItems}
bind:selected={p.provider}
/>
<DropdownLabels minW0={false} label={bitrix.string.FieldMapping} {items} bind:selected={p.field} />
<div class="ml-1">
<Button
icon={IconDelete}
size={'small'}
on:click={() => {
channelFields.splice(i, 1)
channelFields = channelFields
}}
/>
</div>
</div>
{/each}
<div class="ml-2">
<Button
icon={IconAdd}
size={'small'}
on:click={() => {
channelFields = [...channelFields, { field: items[0].id, provider: providers[0]._id }]
}}
/>
</div>
</div>
<style lang="scss">
.pattern {
margin: 0.5rem;
padding: 0.5rem;
flex-shrink: 0;
border: 1px dashed var(--accent-color);
border-radius: 0.25rem;
font-weight: 500;
font-size: 0.75rem;
// text-transform: uppercase;
color: var(--accent-color);
&:hover {
color: var(--caption-color);
}
}
</style>

View File

@ -0,0 +1,46 @@
<script lang="ts">
import { BitrixEntityMapping, BitrixFieldMapping, CreateChannelOperation } from '@hcengineering/bitrix'
import contact from '@hcengineering/contact'
import { Component } from '@hcengineering/ui'
import view from '@hcengineering/view'
export let mapping: BitrixEntityMapping
export let value: BitrixFieldMapping
$: op = value.operation as CreateChannelOperation
</script>
<div class="flex flex-wrap">
{#each op.fields as p, i}
<div class="pattern flex-row-center gap-2">
<Component
is={view.component.ObjectPresenter}
props={{ _class: contact.class.ChannelProvider, objectId: p.provider }}
/>
->
{#if mapping.bitrixFields}
{p.field ? mapping.bitrixFields[p.field]?.formLabel ?? mapping.bitrixFields[p.field]?.title : p.field ?? ''}
{/if}
</div>
{/each}
</div>
<style lang="scss">
.pattern {
margin: 0.1rem;
padding: 0.3rem;
flex-shrink: 0;
border: 1px dashed var(--accent-color);
border-radius: 0.25rem;
font-weight: 500;
font-size: 0.75rem;
// text-transform: uppercase;
color: var(--accent-color);
&:hover {
color: var(--caption-color);
}
}
</style>

View File

@ -0,0 +1,137 @@
<script lang="ts">
import {
BitrixEntityMapping,
BitrixFieldMapping,
CreateTagOperation,
Fields,
MappingOperation,
TagField
} from '@hcengineering/bitrix'
import { AnyAttribute } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags'
import { WeightPopup } from '@hcengineering/tags-resources'
import {
Button,
DropdownTextItem,
getEventPopupPositionElement,
IconAdd,
IconDelete,
showPopup
} from '@hcengineering/ui'
import DropdownLabels from '@hcengineering/ui/src/components/DropdownLabels.svelte'
import EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
import bitrix from '../../plugin'
export let mapping: BitrixEntityMapping
export let fields: Fields = {}
export let attribute: AnyAttribute
export let field: BitrixFieldMapping | undefined
let tagFields: TagField[] = [...((field?.operation as CreateTagOperation)?.fields ?? [])]
const client = getClient()
export async function save (): Promise<void> {
if (field !== undefined) {
await client.update(field, {
operation: {
kind: MappingOperation.CreateTag,
fields: tagFields
}
})
} else {
await client.addCollection(bitrix.class.FieldMapping, mapping.space, mapping._id, mapping._class, 'fields', {
ofClass: attribute.attributeOf,
attributeName: attribute.name,
operation: {
kind: MappingOperation.CreateTag,
fields: tagFields
}
})
}
}
function getItems (fields: Fields): DropdownTextItem[] {
return Object.entries(fields).map((it) => ({
id: it[0],
label: `${it[1].formLabel ?? it[1].title}${it[0].startsWith('UF_') ? ' *' : ''} - ${it[0]}`
}))
}
$: items = getItems(fields)
const tagLevel = [tags.icon.Level1, tags.icon.Level2, tags.icon.Level3]
const labels = [getEmbeddedLabel('Initial'), getEmbeddedLabel('Meaningfull'), getEmbeddedLabel('Expert')]
</script>
<div class="flex-col flex-wrap">
{#each tagFields as p, i}
{@const tagIcon = tagLevel[p.weight % 3]}
{@const tagLabel = labels[Math.floor(p.weight / 3)]}
<div class="pattern flex-row-center gap-2">
<DropdownLabels minW0={false} label={bitrix.string.FieldMapping} {items} bind:selected={p.field} />
<Button
label={tagLabel}
icon={tagIcon}
on:click={(evt) => {
showPopup(WeightPopup, { value: p.weight }, getEventPopupPositionElement(evt), (res) => {
if (Number.isFinite(res) && res >= 0 && res <= 8) {
if (res != null) {
p.weight = res
}
}
})
}}
/>
<EditBox
kind={'editbox'}
bind:value={p.split}
maxWidth={'5rem'}
placeholder={getEmbeddedLabel('Separator...')}
fullSize
/>
<div class="ml-1">
<Button
icon={IconDelete}
size={'small'}
on:click={() => {
tagFields.splice(i, 1)
tagFields = tagFields
}}
/>
</div>
</div>
{/each}
<div class="ml-2">
<Button
icon={IconAdd}
size={'small'}
on:click={() => {
tagFields = [...tagFields, { weight: 0, field: items[0].id, split: '' }]
}}
/>
</div>
</div>
<style lang="scss">
.pattern {
margin: 0.5rem;
padding: 0.5rem;
flex-shrink: 0;
border: 1px dashed var(--accent-color);
border-radius: 0.25rem;
font-weight: 500;
font-size: 0.75rem;
// text-transform: uppercase;
color: var(--accent-color);
&:hover {
color: var(--caption-color);
}
}
</style>

View File

@ -0,0 +1,49 @@
<script lang="ts">
import { BitrixEntityMapping, BitrixFieldMapping, CreateTagOperation } from '@hcengineering/bitrix'
import { getEmbeddedLabel } from '@hcengineering/platform'
import tags from '@hcengineering/tags'
import { Button } from '@hcengineering/ui'
export let mapping: BitrixEntityMapping
export let value: BitrixFieldMapping
$: op = value.operation as CreateTagOperation
const tagLevel = [tags.icon.Level1, tags.icon.Level2, tags.icon.Level3]
const labels = [getEmbeddedLabel('Initial'), getEmbeddedLabel('Meaningfull'), getEmbeddedLabel('Expert')]
</script>
<div class="flex flex-wrap">
{#each op.fields as p, i}
{@const tagIcon = tagLevel[p.weight % 3]}
{@const tagLabel = labels[Math.floor(p.weight / 3)]}
<div class="pattern flex-row-center gap-2">
{#if mapping.bitrixFields}
{p.field ? mapping.bitrixFields[p.field]?.formLabel ?? mapping.bitrixFields[p.field]?.title : p.field ?? ''}
{/if}
<Button label={tagLabel} icon={tagIcon} size={'small'} disabled={true} />
{p.split}
</div>
{/each}
</div>
<style lang="scss">
.pattern {
margin: 0.1rem;
padding: 0.3rem;
flex-shrink: 0;
border: 1px dashed var(--accent-color);
border-radius: 0.25rem;
font-weight: 500;
font-size: 0.75rem;
// text-transform: uppercase;
color: var(--accent-color);
&:hover {
color: var(--caption-color);
}
}
</style>

View File

@ -0,0 +1,33 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Resources } from '@hcengineering/platform'
import BitrixConnect from './components/BitrixConnect.svelte'
import BitrixConfigure from './components/BitrixConfigure.svelte'
import BitrixIcon from './components/icons/Bitrix.svelte'
import BitrixImport from './components/BitrixImport.svelte'
export default async (): Promise<Resources> => ({
component: {
BitrixIcon,
BitrixConnect,
BitrixConfigure,
BitrixImport
},
handler: {
DisconnectHandler: async () => {}
}
})

View File

@ -0,0 +1,47 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { IntlString, mergeIds } from '@hcengineering/platform'
import bitrix, { bitrixId } from '@hcengineering/bitrix'
import { Ref } from '@hcengineering/core'
import { Handler, IntegrationType } from '@hcengineering/setting'
import { AnyComponent } from '@hcengineering/ui'
export default mergeIds(bitrixId, bitrix, {
string: {
BitrixTokenUrl: '' as IntlString,
Bitrix: '' as IntlString,
BitrixDesc: '' as IntlString,
Settings: '' as IntlString,
EntityMapping: '' as IntlString,
NotAllowed: '' as IntlString,
AddMapping: '' as IntlString,
BitrixEntityType: '' as IntlString,
FieldMapping: '' as IntlString,
AddField: '' as IntlString,
Attribute: '' as IntlString,
MapField: '' as IntlString
},
component: {
BitrixIcon: '' as AnyComponent
},
handler: {
DisconnectHandler: '' as Handler
},
integrationType: {
Bitrix: '' as Ref<IntegrationType>
}
})

View File

@ -0,0 +1,45 @@
/**
* @public
*/
export interface BitrixProfile {
ID: string
ADMIN: boolean
NAME: string
LAST_NAME: string
PERSONAL_GENDER: string
PERSONAL_PHOTO: string
TIME_ZONE: string
TIME_ZONE_OFFSET: number
}
export type NumberString = string
export type ISODate = string
export type BoolString = 'Y' | 'N'
export type GenderString = 'M' | 'F' | ''
export interface MultiField {
readonly ID: NumberString
readonly VALUE_TYPE: string
readonly VALUE: string
readonly TYPE_ID: string
}
export type MultiFieldArray = ReadonlyArray<Pick<MultiField, 'VALUE' | 'VALUE_TYPE'>>
export interface StatusValue {
CATEGORY_ID: string | null
COLOR: string | null
ENTITY_ID: string | null
ID: number
NAME: string
NAME_INIT: string | null
SEMANTICS: string | null
SORT: string | null
STATUS_ID: string | null
SYSTEM: 'Y' | 'N'
}
export interface BitrixResult {
result: any
next: number
total: number
}

View File

@ -0,0 +1,277 @@
import {
BitrixEntityMapping,
BitrixFieldMapping,
BitrixSyncDoc,
CopyValueOperation,
CreateChannelOperation,
CreateTagOperation,
MappingOperation
} from '@hcengineering/bitrix'
import { Comment } from '@hcengineering/chunter'
import contact, { Channel, EmployeeAccount } from '@hcengineering/contact'
import core, { AnyAttribute, Class, Client, Data, Doc, generateId, Mixin, Ref, Space } from '@hcengineering/core'
import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { getColorNumberByText } from '@hcengineering/ui'
export function collectFields (fieldMapping: BitrixFieldMapping[]): string[] {
const fields: string[] = ['ID']
for (const f of fieldMapping) {
switch (f.operation.kind) {
case MappingOperation.CopyValue:
fields.push(
...Array.from(f.operation.patterns.map((it) => it.field).filter((it) => it !== undefined) as string[])
)
break
case MappingOperation.CreateChannel:
fields.push(...Array.from(f.operation.fields.map((it) => it.field).filter((it) => it !== undefined)))
break
case MappingOperation.CreateTag:
fields.push(...Array.from(f.operation.fields.map((it) => it.field).filter((it) => it !== undefined)))
break
}
}
return fields
}
export interface ConvertResult {
document: BitrixSyncDoc // Document we should achive
mixins: Record<Ref<Mixin<Doc>>, Data<Doc>> // Mixins of document we will achive
extraDocs: Doc[] // Extra documents we will achive, comments etc.
blobs: File[] //
comments?: Promise<Array<BitrixSyncDoc & Comment>>
}
export async function convert (
client: Client,
entity: BitrixEntityMapping,
space: Ref<Space>,
fields: BitrixFieldMapping[],
rawDocument: any,
prevExtra: Doc[], // <<-- a list of previous extra documents, so for example TagElement will be reused, if present for more what one item and required to be created
tagElements: Map<Ref<Class<Doc>>, TagElement[]>, // TagElement cache.
userList: Map<string, Ref<EmployeeAccount>>
): Promise<ConvertResult> {
const hierarchy = client.getHierarchy()
const document: BitrixSyncDoc = {
_id: generateId(),
type: entity.type,
bitrixId: `${rawDocument.ID as string}`,
_class: entity.ofClass,
space,
modifiedOn: new Date(rawDocument.DATE_CREATE).getTime(),
modifiedBy: userList.get(rawDocument.CREATED_BY_ID) ?? core.account.System
}
// Obtain a proper modified by for document
const newExtraDocs: Doc[] = []
const blobs: File[] = []
const mixins: Record<Ref<Mixin<Doc>>, Data<Doc>> = {}
const extractValue = (field?: string, alternatives?: string[]): any | undefined => {
if (field !== undefined) {
let lval = rawDocument[field]
if ((lval == null || lval === '') && alternatives !== undefined) {
for (const alt of alternatives) {
lval = rawDocument[alt]
if (lval != null) {
break
}
}
}
const bfield = entity.bitrixFields[field]
if (bfield === undefined) {
console.trace('Bitrix field not found', field)
} else if (bfield.type === 'integer' || bfield.type === 'double') {
if (bfield.isMultiple && Array.isArray(lval)) {
return lval[0] ?? 0
}
return lval
} else if (bfield.type === 'crm_multifield') {
if (Array.isArray(lval)) {
return lval.map((it) => it.VALUE)
}
} else if (bfield.type === 'string' || bfield.type === 'url') {
if (bfield.isMultiple && Array.isArray(lval)) {
return lval.join(', ')
}
return lval
} else if (bfield.type === 'date') {
if (lval !== '' && lval != null) {
return new Date(lval)
}
} else if (bfield.type === 'char') {
return lval === 'Y'
} else if (bfield.type === 'enumeration' || bfield.type === 'crm_status') {
if (lval != null && lval !== '') {
if (bfield.isMultiple && Array.isArray(lval)) {
lval = lval[0] ?? ''
}
const eValue = bfield.items?.find((it) => it.ID === lval)?.VALUE
if (eValue !== undefined) {
return eValue
}
}
}
}
}
const getCopyValue = async (attr: AnyAttribute, operation: CopyValueOperation): Promise<any> => {
const r: Array<string | number | boolean | Date> = []
for (const o of operation.patterns) {
if (o.text.length > 0) {
r.push(o.text)
}
const lval = extractValue(o.field, o.alternatives)
if (lval != null) {
r.push(lval)
}
}
if (r.length === 1) {
return r[0]
}
if (r.length === 0) {
return
}
return r.join('').trim()
}
const getChannelValue = async (attr: AnyAttribute, operation: CreateChannelOperation): Promise<any> => {
for (const f of operation.fields) {
const lval = extractValue(f.field)
if (lval != null && lval !== '') {
const vals = Array.isArray(lval) ? lval : [lval]
for (const llVal of vals) {
const c: Channel = {
_id: generateId(),
_class: contact.class.Channel,
attachedTo: document._id,
attachedToClass: attr.attributeOf,
collection: attr.name,
modifiedBy: document.modifiedBy,
value: llVal,
provider: f.provider,
space: document.space,
modifiedOn: document.modifiedOn
}
newExtraDocs.push(c)
}
}
}
return undefined
}
const getTagValue = async (attr: AnyAttribute, operation: CreateTagOperation): Promise<any> => {
const elements =
tagElements.get(attr.attributeOf) ??
(await client.findAll(tags.class.TagElement, {
targetClass: attr.attributeOf
}))
const references = await client.findAll(tags.class.TagReference, {
attachedTo: document._id
})
// Add tags creation requests from previous conversions.
elements.push(...prevExtra.filter((it) => it._class === tags.class.TagElement).map((it) => it as TagElement))
tagElements.set(attr.attributeOf, elements)
const defaultCategory = await client.findOne(tags.class.TagCategory, {
targetClass: attr.attributeOf,
default: true
})
if (defaultCategory === undefined) {
console.error('could not proceed tags without default category')
return
}
for (const o of operation.fields) {
const lval = extractValue(o.field)
let vals: string[] = []
if (lval == null) {
continue
}
if (o.split !== '' && o.split != null) {
vals = `${lval as string}`.split(o.split)
} else {
vals = [lval as string]
}
for (let vv of vals) {
vv = vv.trim()
if (vv === '') {
continue
}
// Find existing element and create reference based on it.
let tag: TagElement | undefined = elements.find((it) => it.title === vv)
if (tag === undefined) {
tag = {
_id: generateId(),
_class: tags.class.TagElement,
category: defaultCategory._id,
color: getColorNumberByText(vv),
description: '',
title: vv,
targetClass: attr.attributeOf,
space: tags.space.Tags,
modifiedBy: document.modifiedBy,
modifiedOn: document.modifiedOn
}
newExtraDocs.push(tag)
}
const ref: TagReference = {
_id: generateId(),
attachedTo: document._id,
attachedToClass: attr.attributeOf,
collection: attr.name,
_class: tags.class.TagReference,
tag: tag._id,
color: getColorNumberByText(vv),
title: vv,
weight: o.weight,
modifiedBy: document.modifiedBy,
modifiedOn: document.modifiedOn,
space: tags.space.Tags
}
if (references.find((it) => it.title === vv) === undefined) {
// Add only if not already added
newExtraDocs.push(ref)
}
}
}
return undefined
}
for (const f of fields) {
const attr = hierarchy.getAttribute(f.ofClass, f.attributeName)
if (attr === undefined) {
console.trace('Attribue not found', f)
continue
}
let value: any
switch (f.operation.kind) {
case MappingOperation.CopyValue:
value = await getCopyValue(attr, f.operation)
break
case MappingOperation.CreateChannel:
value = await getChannelValue(attr, f.operation)
break
case MappingOperation.CreateTag:
value = await getTagValue(attr, f.operation)
break
}
if (value !== undefined) {
if (hierarchy.isMixin(attr.attributeOf)) {
mixins[attr.attributeOf] = { ...mixins[attr.attributeOf], [attr.name]: value }
} else {
;(document as any)[attr.name] = value
}
}
}
return { document, mixins, extraDocs: newExtraDocs, blobs }
}
/**
* @public
*/
export function toClassRef (val: any): Ref<Class<Doc>> {
return val as Ref<Class<Doc>>
}

View File

@ -0,0 +1,5 @@
const sveltePreprocess = require('svelte-preprocess')
module.exports = {
preprocess: sveltePreprocess()
};

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "esnext",
"module": "esnext",
"declaration": true,
"outDir": "./lib",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"lib": [
"esnext",
"dom"
]
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/config/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,18 @@
// The "rig.json" file directs tools to look for their config files in an external package.
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
/**
* (Required) The name of the rig package to inherit from.
* It should be an NPM package name with the "-rig" suffix.
*/
"rigPackageName": "@hcengineering/platform-rig"
/**
* (Optional) Selects a config profile from the rig package. The name must consist of
* lowercase alphanumeric words separated by hyphens, for example "sample-profile".
* If omitted, then the "default" profile will be used."
*/
// "rigProfile": "your-profile-name"
}

View File

@ -0,0 +1,36 @@
{
"name": "@hcengineering/bitrix",
"version": "0.6.0",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "heft build",
"build:watch": "tsc",
"lint:fix": "eslint --fix src",
"lint": "eslint src",
"format": "prettier --write src && eslint --fix src"
},
"devDependencies": {
"@hcengineering/platform-rig": "~0.6.0",
"@types/heft-jest": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.26.0",
"@typescript-eslint/parser": "^5.41.0",
"eslint-config-standard-with-typescript": "^23.0.0",
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5"
},
"dependencies": {
"@hcengineering/platform": "^0.6.8",
"@hcengineering/core": "^0.6.19",
"@hcengineering/ui": "^0.6.2",
"@hcengineering/preference": "^0.6.1",
"@hcengineering/tags": "~0.6.2",
"@hcengineering/contact": "~0.6.8"
}
}

184
plugins/bitrix/src/index.ts Normal file
View File

@ -0,0 +1,184 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { ChannelProvider } from '@hcengineering/contact'
import type { AttachedDoc, Class, Doc, Mixin, Ref, Space } from '@hcengineering/core'
import type { Plugin } from '@hcengineering/platform'
import { Asset, plugin } from '@hcengineering/platform'
import { ExpertKnowledge, InitialKnowledge, MeaningfullKnowledge } from '@hcengineering/tags'
import { AnyComponent } from '@hcengineering/ui'
/**
* @public
*/
export interface BitrixSyncDoc extends Doc {
type: string
bitrixId: string
}
/**
* @public
*/
export enum BitrixEntityType {
Comment = 'crm.timeline.comment',
Binding = 'crm.timeline.bindings',
Lead = 'crm.lead',
Activity = 'crm.activity',
Company = 'crm.company'
}
/**
* @public
*/
export const mappingTypes = [
{ label: 'Leads', id: BitrixEntityType.Lead },
// { label: 'Comments', id: BitrixEntityType.Comment },
{ label: 'Company', id: BitrixEntityType.Company }
// { label: 'Activity', id: BitrixEntityType.Activity }
]
/**
* @public
*/
export interface FieldValue {
type: string
statusType?: string
isRequired: boolean
isReadOnly: boolean
isImmutable: boolean
isMultiple: boolean
isDynamic: boolean
title: string
formLabel?: string
filterLabel?: string
items?: Array<{
ID: string
VALUE: string
}>
}
/**
* @public
*/
export interface Fields {
[key: string]: FieldValue
}
/**
* @public
*/
export interface BitrixEntityMapping extends Doc {
ofClass: Ref<Class<Doc>>
type: string
bitrixFields: Fields
fields: number
comments: boolean
activity: boolean
attachments: boolean
}
/**
* @public
*/
export enum MappingOperation {
CopyValue,
CreateTag, // Create tag
CreateChannel // Create channel
}
/**
* @public
*/
export interface CopyPattern {
text: string
field?: string
alternatives?: string[]
}
/**
* @public
*/
export interface CopyValueOperation {
kind: MappingOperation.CopyValue
patterns: CopyPattern[]
}
/**
* @public
*/
export interface TagField {
weight: InitialKnowledge | MeaningfullKnowledge | ExpertKnowledge
field: string
split: string // If defined values from field will be split to check for multiple values.
}
/**
* @public
*/
export interface CreateTagOperation {
kind: MappingOperation.CreateTag
fields: TagField[]
}
/**
* @public
*/
export interface ChannelFieldMapping {
provider: Ref<ChannelProvider>
field: string
}
/**
* @public
*/
export interface CreateChannelOperation {
kind: MappingOperation.CreateChannel
fields: ChannelFieldMapping[]
}
/**
* @public
*/
export interface BitrixFieldMapping extends AttachedDoc {
ofClass: Ref<Class<Doc>> // Specify mixin if applicable
attributeName: string
operation: CopyValueOperation | CreateTagOperation | CreateChannelOperation
}
/**
* @public
*/
export const bitrixId = 'bitrix' as Plugin
export default plugin(bitrixId, {
mixin: {
BitrixSyncDoc: '' as Ref<Mixin<BitrixSyncDoc>>
},
class: {
EntityMapping: '' as Ref<Class<BitrixEntityMapping>>,
FieldMapping: '' as Ref<Class<BitrixFieldMapping>>
},
component: {
BitrixIntegration: '' as AnyComponent
},
icon: {
Bitrix: '' as Asset
},
space: {
Mappings: '' as Ref<Space>
}
})

View File

@ -0,0 +1,9 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"lib": ["esnext", "dom"]
}
}

View File

@ -66,6 +66,7 @@
"ShowAttribute": "Show attribute",
"HideAttribute": "Hide attribute",
"Visibility": "Visibility",
"Hidden": "Hidden"
"Hidden": "Hidden",
"Configure": "Configure"
}
}

View File

@ -67,6 +67,7 @@
"ShowAttribute": "Hide",
"HideAttribute": "Show",
"Visibility": "Видимость",
"Hidden": "Спрятанный"
"Hidden": "Спрятанный",
"Configure": "Настроить..."
}
}

View File

@ -31,6 +31,7 @@
import {
Action,
ActionIcon,
AnySvelteComponent,
CircleButton,
Component,
getEventPositionElement,
@ -51,6 +52,15 @@
import EditClassLabel from './EditClassLabel.svelte'
export let _class: Ref<Class<Doc>>
export let ofClass: Ref<Class<Doc>> | undefined
export let attributeMapper:
| {
component: AnySvelteComponent
label: IntlString
props: Record<string, any>
}
| undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -66,7 +76,9 @@
function getCustomAttributes (_class: Ref<Class<Doc>>): AnyAttribute[] {
const cl = hierarchy.getClass(_class)
const attributes = Array.from(hierarchy.getAllAttributes(_class, cl.extends).values())
const attributes = Array.from(
hierarchy.getAllAttributes(_class, _class === ofClass ? core.class.Doc : cl.extends).values()
)
// const filtred = attributes.filter((p) => !p.hidden)
return attributes
}
@ -210,6 +222,11 @@
<Label label={settings.string.Custom} />
</div>
</th>
{#if attributeMapper}
<th>
<Label label={attributeMapper.label} />
</th>
{/if}
</tr>
</thead>
<tbody>
@ -223,7 +240,7 @@
}}
>
<td>
<div class="antiTable-cells__firstCell">
<div class="antiTable-cells__firstCell whitespace-nowrap">
<Label label={attr.label} />
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div id="context-menu" class="antiTable-cells__firstCell-menuRow" on:click={(ev) => showMenu(ev, attr)}>
@ -231,7 +248,7 @@
</div>
</div>
</td>
<td class="select-text">
<td class="select-text whitespace-nowrap">
<Label label={attr.type.label} />
{#if attrType !== undefined}
: <Label label={attrType} />
@ -252,6 +269,11 @@
<td>
<Component is={view.component.BooleanTruePresenter} props={{ value: attr.isCustom ?? false }} />
</td>
{#if attributeMapper}
<td>
<svelte:component this={attributeMapper.component} {...attributeMapper.props} attribute={attr} />
</td>
{/if}
</tr>
{/each}
</tbody>

View File

@ -22,6 +22,7 @@
export let classes: Ref<Class<Doc>>[] = ['contact:class:Contact' as Ref<Class<Doc>>]
export let _class: Ref<Class<Doc>> | undefined
export let ofClass: Ref<Class<Doc>> | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -30,12 +31,16 @@
function getDescendants (_class: Ref<Class<Doc>>): Ref<Class<Doc>>[] {
const result: Ref<Class<Doc>>[] = []
const desc = hierarchy.getDescendants(_class)
const vars = [ClassifierKind.MIXIN]
if (ofClass === undefined) {
vars.push(ClassifierKind.CLASS)
}
for (const clazz of desc) {
const cls = hierarchy.getClass(clazz)
if (
cls.extends === _class &&
!cls.hidden &&
[ClassifierKind.CLASS, ClassifierKind.MIXIN].includes(cls.kind) &&
vars.includes(cls.kind) &&
cls.label !== undefined &&
(!hierarchy.hasMixin(cls, settings.mixin.Editable) || hierarchy.as(cls, settings.mixin.Editable).value)
) {

View File

@ -13,20 +13,32 @@
// limitations under the License.
-->
<script lang="ts">
import core, { Class, Doc, Ref } from '@hcengineering/core'
import core, { Class, Doc, Obj, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getCurrentLocation, Icon, Label, navigate } from '@hcengineering/ui'
import { AnySvelteComponent, getCurrentLocation, Icon, Label, navigate } from '@hcengineering/ui'
import setting from '../plugin'
import { filterDescendants } from '../utils'
import ClassAttributes from './ClassAttributes.svelte'
import ClassHierarchy from './ClassHierarchy.svelte'
export let ofClass: Ref<Class<Obj>> | undefined
export let attributeMapper:
| {
component: AnySvelteComponent
label: IntlString
props: Record<string, any>
}
| undefined
export let withoutHeader = false
const loc = getCurrentLocation()
const client = getClient()
const hierarchy = client.getHierarchy()
let _class: Ref<Class<Doc>> | undefined = loc.query?._class as Ref<Class<Doc>> | undefined
let _class: Ref<Class<Doc>> | undefined = ofClass ?? (loc.query?._class as Ref<Class<Doc>> | undefined)
$: if (_class !== undefined) {
$: if (_class !== undefined && ofClass === undefined) {
const loc = getCurrentLocation()
loc.query = undefined
navigate(loc)
@ -36,28 +48,24 @@
let classes: Ref<Class<Doc>>[] = []
clQuery.query(core.class.Class, {}, (res) => {
classes = res
.filter(
(p) =>
hierarchy.hasMixin(p, setting.mixin.Editable) &&
hierarchy.as(p, setting.mixin.Editable).value &&
!hierarchy.hasMixin(p, setting.mixin.UserMixin)
)
.map((p) => p._id)
classes = filterDescendants(hierarchy, ofClass, res)
})
</script>
<div class="antiComponent">
{#if !withoutHeader}
<div class="ac-header short divide">
<div class="ac-header__icon"><Icon icon={setting.icon.Clazz} size={'medium'} /></div>
<div class="ac-header__title"><Label label={setting.string.ClassSetting} /></div>
</div>
{/if}
<div class="ac-body columns hScroll">
<div class="ac-column">
<div class="overflow-y-auto">
<ClassHierarchy
{classes}
{_class}
{ofClass}
on:select={(e) => {
_class = e.detail
}}
@ -66,7 +74,7 @@
</div>
<div class="ac-column max">
{#if _class !== undefined}
<ClassAttributes {_class} />
<ClassAttributes {_class} {ofClass} {attributeMapper} />
{/if}
</div>
</div>

View File

@ -31,8 +31,8 @@
import view from '@hcengineering/view-resources/src/plugin'
export let value: Enum | undefined
let name: string = value?.name ?? ''
let values: string[] = value?.enumValues ?? []
export let name: string = value?.name ?? ''
export let values: string[] = value?.enumValues ?? []
const client = getClient()
const dispatch = createEventDispatcher()

View File

@ -13,25 +13,30 @@
// limitations under the License.
-->
<script lang="ts">
import { Button, Component, eventToHTMLElement, Label, Link } from '@hcengineering/ui'
import { getResource } from '@hcengineering/platform'
import { showPopup } from '@hcengineering/ui'
import type { Integration, IntegrationType } from '@hcengineering/setting'
import setting from '../plugin'
import { getClient } from '@hcengineering/presentation'
import { getCurrentAccount, Ref, Space } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import type { Integration, IntegrationType } from '@hcengineering/setting'
import {
AnyComponent,
Button,
Component,
eventToHTMLElement,
Label,
PopupPosAlignment,
showPopup
} from '@hcengineering/ui'
import setting from '../plugin'
export let integrationType: IntegrationType
export let integration: Integration | undefined
const accountId = getCurrentAccount()._id
const client = getClient()
const onDisconnectP = getResource(integrationType.onDisconnect)
const space = accountId as string as Ref<Space>
async function close (res: any): Promise<void> {
if (res?.value) {
await client.createDoc(setting.class.Integration, space, {
type: integrationType._id,
if (res?.value && integration !== undefined) {
await client.update(integration, {
value: res.value,
disabled: false
})
@ -52,18 +57,28 @@
}
async function disconnect (): Promise<void> {
if (integration !== undefined) {
await (
await onDisconnectP
)()
if (integration !== undefined && integrationType.onDisconnect !== undefined) {
const disconnect = await getResource(integrationType.onDisconnect)
await disconnect()
}
}
const handleAdd = (e: any) => {
showPopup(integrationType.createComponent, {}, eventToHTMLElement(e), close)
const handleConfigure = async (e: any, component?: AnyComponent, pos?: PopupPosAlignment): Promise<void> => {
if (component === undefined) {
return
}
if (integration === undefined) {
const id = await client.createDoc(setting.class.Integration, space, {
type: integrationType._id,
value: '',
disabled: false
})
integration = await client.findOne(setting.class.Integration, { _id: id })
}
showPopup(component, { integration }, pos ?? eventToHTMLElement(e), close)
}
const handleReconnect = (e: any) => {
if (integrationType.reconnectComponent) {
showPopup(integrationType.reconnectComponent, {}, eventToHTMLElement(e), reconnect)
showPopup(integrationType.reconnectComponent, { integration }, eventToHTMLElement(e), reconnect)
}
}
</script>
@ -72,23 +87,34 @@
<div class="flex-row-center header">
<div class="icon mr-4"><Component is={integrationType.icon} /></div>
<div class="flex-grow flex-col">
<div class="fs-title max-label overflow-label"><Label label={integrationType.label} /></div>
<div class="text-sm content-dark-color max-label overflow-label">
<div class="fs-title overflow-label"><Label label={integrationType.label} /></div>
</div>
</div>
<div class="content">
<Label label={integrationType.description} />
</div>
</div>
</div>
<div class="content">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temp</div>
<div class="footer">
{#if integration}
{#if integration.disabled && integrationType.reconnectComponent}
{#if (integration?.value ?? '') === ''}
{#if integrationType.createComponent}
<Button
label={setting.string.Add}
kind={'primary'}
on:click={(ev) => handleConfigure(ev, integrationType.createComponent)}
/>
{/if}
{:else if (integration?.disabled ?? false) && integrationType.reconnectComponent}
<Button label={setting.string.Reconnect} kind={'primary'} on:click={handleReconnect} />
{:else}
{:else if integration?.value !== ''}
{#if integrationType.onDisconnect}
<Button label={setting.string.Disconnect} on:click={disconnect} />
{/if}
{:else}
<Button label={setting.string.Add} kind={'primary'} on:click={handleAdd} />
<Link label={'Learn more'} />
{#if integrationType.configureComponent !== undefined}
<Button
label={setting.string.Configure}
kind={'primary'}
on:click={(ev) => handleConfigure(ev, integrationType.configureComponent, 'top')}
/>
{/if}
{/if}
</div>
</div>
@ -107,8 +133,8 @@
}
.icon {
flex-shrink: 0;
width: 2.25rem;
height: 2.25rem;
min-width: 2.25rem;
min-height: 2.25rem;
}
.content {
margin: 0 1.5rem 0.25rem;

View File

@ -23,14 +23,13 @@
export let type: EnumOf | undefined
export let editable: boolean = true
export let value: Enum | undefined
const client = getClient()
const dispatch = createEventDispatcher()
let value: Enum | undefined
$: value && dispatch('change', { type: TypeEnum(value._id) })
$: ref = type?.of
$: ref = value?._id ?? type?.of
const create = {
label: setting.string.CreateEnum,

View File

@ -36,13 +36,15 @@ import Terms from './components/Terms.svelte'
import BooleanTypeEditor from './components/typeEditors/BooleanTypeEditor.svelte'
import DateTypeEditor from './components/typeEditors/DateTypeEditor.svelte'
import EnumTypeEditor from './components/typeEditors/EnumTypeEditor.svelte'
import HyperlinkTypeEditor from './components/typeEditors/HyperlinkTypeEditor.svelte'
import NumberTypeEditor from './components/typeEditors/NumberTypeEditor.svelte'
import RefEditor from './components/typeEditors/RefEditor.svelte'
import StringTypeEditor from './components/typeEditors/StringTypeEditor.svelte'
import HyperlinkTypeEditor from './components/typeEditors/HyperlinkTypeEditor.svelte'
import WorkspaceSettings from './components/WorkspaceSettings.svelte'
import setting from './plugin'
export { ClassSetting }
async function DeleteMixin (object: Mixin<Class<Doc>>): Promise<void> {
const docs = await getClient().findAll(object._id, {}, { limit: 1 })

View File

@ -0,0 +1,47 @@
import { Class, Doc, Hierarchy, Ref } from '@hcengineering/core'
import setting from '@hcengineering/setting'
function isEditable (hierarchy: Hierarchy, p: Class<Doc>): boolean {
let ancestors = [p._id]
try {
ancestors = [...hierarchy.getAncestors(p._id), p._id]
} catch (err: any) {
console.error(err)
}
for (const ao of ancestors) {
try {
const cl = hierarchy.getClass(ao)
if (hierarchy.hasMixin(cl, setting.mixin.Editable) && hierarchy.as(cl, setting.mixin.Editable).value) {
return true
}
} catch (err: any) {
return false
}
}
return false
}
export function filterDescendants (
hierarchy: Hierarchy,
ofClass: Ref<Class<Doc>> | undefined,
res: Array<Class<Doc>>
): Array<Ref<Class<Doc>>> {
let _classes = res
.filter((it) => {
try {
return ofClass != null ? hierarchy.isDerived(it._id, ofClass) : true
} catch (err: any) {
return false
}
})
.filter((it) => !(it.hidden ?? false))
.filter((p) => isEditable(hierarchy, p))
let len: number
const _set = new Set(_classes.map((it) => it._id))
do {
len = _classes.length
_classes = _classes.filter((it) => (it.extends != null ? !_set.has(it.extends) : false))
} while (len !== _classes.length)
return _classes.map((p) => p._id)
}

View File

@ -13,10 +13,10 @@
// limitations under the License.
//
import { Asset, IntlString, plugin, Resource } from '@hcengineering/platform'
import type { Plugin } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
import type { Class, Doc, Mixin, Ref } from '@hcengineering/core'
import type { Plugin } from '@hcengineering/platform'
import { Asset, IntlString, plugin, Resource } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
/**
* @public
@ -30,9 +30,12 @@ export interface IntegrationType extends Doc {
label: IntlString
description: IntlString
icon: AnyComponent
createComponent: AnyComponent
onDisconnect: Handler
createComponent?: AnyComponent
onDisconnect?: Handler
reconnectComponent?: AnyComponent
configureComponent?: AnyComponent
}
/**
@ -143,7 +146,8 @@ export default plugin(settingId, {
SelectWorkspace: '' as IntlString,
Reconnect: '' as IntlString,
ClassSetting: '' as IntlString,
Owners: '' as IntlString
Owners: '' as IntlString,
Configure: '' as IntlString
},
icon: {
EditProfile: '' as Asset,

View File

@ -34,7 +34,9 @@ import CreateTagElement from './components/CreateTagElement.svelte'
import { ObjQueryType } from '@hcengineering/core'
import { getRefs } from './utils'
import { Filter } from '@hcengineering/view'
import WeightPopup from './components/WeightPopup.svelte'
export { WeightPopup }
export async function tagsInResult (filter: Filter, onUpdate: () => void): Promise<ObjQueryType<any>> {
const result = await getRefs(filter, onUpdate)
return { $in: result }

View File

@ -26,6 +26,7 @@
import { ListView, resizeObserver } from '@hcengineering/ui'
import ObjectPresenter from './ObjectPresenter.svelte'
import { tick } from 'svelte'
import { createEventDispatcher } from 'svelte'
export let viewContext: ViewContext
@ -171,6 +172,7 @@
}
return result
}
const dispatch = createEventDispatcher()
</script>
<ActionContext

View File

@ -15,7 +15,7 @@
-->
<script lang="ts">
import type { IntlString } from '@hcengineering/platform'
import { EditBox, Label, showPopup, eventToHTMLElement } from '@hcengineering/ui'
import { EditBox, Label, showPopup, eventToHTMLElement, Button } from '@hcengineering/ui'
import EditBoxPopup from './EditBoxPopup.svelte'
// export let label: IntlString
@ -24,7 +24,7 @@
export let focus: boolean
// export let maxWidth: string = '10rem'
export let onChange: (value: number | undefined) => void
export let kind: 'no-border' | 'link' = 'no-border'
export let kind: 'no-border' | 'link' | 'button' = 'no-border'
export let readonly = false
let shown: boolean = false
@ -38,6 +38,7 @@
</script>
{#if kind === 'link'}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="link-container"
on:click={(ev) => {
@ -58,6 +59,30 @@
<span class="dark-color"><Label label={placeholder} /></span>
{/if}
</div>
{:else if kind === 'button'}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Button
size={'small'}
on:click={(ev) => {
if (!shown && !readonly) {
showPopup(EditBoxPopup, { value, format: 'number' }, eventToHTMLElement(ev), (res) => {
if (Number.isFinite(res)) {
value = res
onChange(value)
}
shown = false
})
}
}}
>
<svelte:fragment slot="content">
{#if value !== undefined}
<span class="overflow-label">{value}</span>
{:else}
<span class="dark-color"><Label label={placeholder} /></span>
{/if}
</svelte:fragment>
</Button>
{:else if readonly}
{#if value !== undefined}
<span class="overflow-label">{value}</span>

View File

@ -16,7 +16,7 @@
* path segment in the "$schema" field for all your Rush config files. This will ensure
* correct error-underlining and tab-completion for editors such as VS Code.
*/
"rushVersion": "5.78.0",
"rushVersion": "5.86.0",
/**
* The next field selects which package manager should be installed and determines its version.
@ -176,7 +176,7 @@
* and set projectFolderMaxDepth to a large number.
*/
// "projectFolderMinDepth": 2,
// "projectFolderMaxDepth": 2,
"projectFolderMaxDepth": 3,
/**
* Today the npmjs.com registry enforces fairly strict naming rules for packages, but in the early
@ -1413,6 +1413,27 @@
"packageName": "@hcengineering/minio",
"projectFolder": "server/minio",
"shouldPublish": true
}
},
{
"packageName": "@hcengineering/bitrix",
"projectFolder": "plugins/bitrix",
"shouldPublish": true
},
{
"packageName": "@hcengineering/bitrix-assets",
"projectFolder": "plugins/bitrix-assets",
"shouldPublish": true
},
{
"packageName": "@hcengineering/bitrix-resources",
"projectFolder": "plugins/bitrix-resources",
"shouldPublish": true
},
{
"packageName": "@hcengineering/model-bitrix",
"projectFolder": "models/bitrix",
"shouldPublish": true
},
]
}

View File

@ -517,8 +517,13 @@ class TServerStorage implements ServerStorage {
;({ passed, onEnd } = await this.verifyApplyIf(ctx, applyIf))
result = passed
if (passed) {
// Store apply if transaction
await ctx.with('domain-tx', { _class, objClass }, async () => await this.getAdapter(DOMAIN_TX).tx(tx))
// Store apply if transaction's
await ctx.with('domain-tx', { _class, objClass }, async () => {
const atx = await this.getAdapter(DOMAIN_TX)
for (const ctx of applyIf.txes) {
await atx.tx(ctx)
}
})
derived = await this.processDerivedTxes(applyIf.txes, ctx, triggerFx)
}
} else {