mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Vendored
+15
-4
@@ -1,14 +1,25 @@
|
||||
{
|
||||
"eslint.format.enable": true,
|
||||
"eslint.enable": true,
|
||||
"eslint.probe": ["javascript", "typescript", "html", "markdown", "svelte"],
|
||||
"eslint.validate": ["javascript", "typescript", "markdown", "svelte"],
|
||||
"eslint.probe": [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"html",
|
||||
"markdown",
|
||||
"svelte"
|
||||
],
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"markdown",
|
||||
"svelte"
|
||||
],
|
||||
"eslint.lintTask.enable": true,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
@@ -16,4 +27,4 @@
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+37
-47
@@ -683,8 +683,8 @@ importers:
|
||||
specifier: ^10.4.14
|
||||
version: 10.4.21(postcss@8.5.6)
|
||||
browserslist:
|
||||
specifier: ^4.28.1
|
||||
version: 4.28.1
|
||||
specifier: ^4.28.2
|
||||
version: 4.28.2
|
||||
compression-webpack-plugin:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(webpack@5.102.1)
|
||||
@@ -791,8 +791,8 @@ importers:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
update-browserslist-db:
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.4(browserslist@4.28.1)
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3(browserslist@4.28.2)
|
||||
webpack:
|
||||
specifier: ^5.97.1
|
||||
version: 5.102.1(esbuild@0.25.12)(webpack-cli@5.1.4)
|
||||
@@ -1803,8 +1803,8 @@ importers:
|
||||
specifier: ^10.4.14
|
||||
version: 10.4.21(postcss@8.5.6)
|
||||
browserslist:
|
||||
specifier: ^4.28.1
|
||||
version: 4.28.1
|
||||
specifier: ^4.28.2
|
||||
version: 4.28.2
|
||||
compression-webpack-plugin:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(webpack@5.102.1)
|
||||
@@ -1869,8 +1869,8 @@ importers:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
update-browserslist-db:
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.4(browserslist@4.28.1)
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3(browserslist@4.28.2)
|
||||
webpack:
|
||||
specifier: ^5.97.1
|
||||
version: 5.102.1(esbuild@0.25.12)(webpack-cli@5.1.4)
|
||||
@@ -45706,8 +45706,9 @@ packages:
|
||||
resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
baseline-browser-mapping@2.9.11:
|
||||
resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==}
|
||||
baseline-browser-mapping@2.10.33:
|
||||
resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
basic-auth@2.0.1:
|
||||
@@ -45801,8 +45802,8 @@ packages:
|
||||
browser-or-node@2.1.1:
|
||||
resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==}
|
||||
|
||||
browserslist@4.28.1:
|
||||
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
|
||||
browserslist@4.28.2:
|
||||
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
@@ -45947,8 +45948,8 @@ packages:
|
||||
caniuse-lite@1.0.30001754:
|
||||
resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==}
|
||||
|
||||
caniuse-lite@1.0.30001762:
|
||||
resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==}
|
||||
caniuse-lite@1.0.30001793:
|
||||
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
|
||||
|
||||
centra@2.7.0:
|
||||
resolution: {integrity: sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==}
|
||||
@@ -46966,8 +46967,8 @@ packages:
|
||||
electron-store@8.2.0:
|
||||
resolution: {integrity: sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw==}
|
||||
|
||||
electron-to-chromium@1.5.267:
|
||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||
electron-to-chromium@1.5.366:
|
||||
resolution: {integrity: sha512-OlRuhb688YTCzzU3gXPLn6nGyd+F+53INE1qaKKlu6kETErE8FYsyDh0XqXEU+uBRn0MpCzz2vfNwORhkap8qg==}
|
||||
|
||||
electron-updater@6.6.2:
|
||||
resolution: {integrity: sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==}
|
||||
@@ -49500,8 +49501,9 @@ packages:
|
||||
resolution: {integrity: sha512-8bp200rELOX72mHv0cfnuDs7LWmd3jr3Y9pxgaysjeSxnUiZxhTA0Pg+SaXkTplIK+r4CfIVAfrla4omNq6UtA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
node-releases@2.0.27:
|
||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||
node-releases@2.0.47:
|
||||
resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
nodemailer@6.10.1:
|
||||
resolution: {integrity: sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==}
|
||||
@@ -51613,12 +51615,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZX6U1J04K1FoSUeoX1OicAhw4d0aro2qo+L8RhJkiGTNtBNkd/Fi1Wxoc9HzcVu6HfOzm0si/N15JjxFmD1z6A==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
update-browserslist-db@1.1.4:
|
||||
resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
update-browserslist-db@1.2.3:
|
||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||
hasBin: true
|
||||
@@ -52728,7 +52724,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.28.5
|
||||
'@babel/helper-validator-option': 7.27.1
|
||||
browserslist: 4.28.1
|
||||
browserslist: 4.28.2
|
||||
lru-cache: 5.1.1
|
||||
semver: 6.3.1
|
||||
|
||||
@@ -56930,7 +56926,7 @@ snapshots:
|
||||
|
||||
autoprefixer@10.4.21(postcss@8.5.6):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
browserslist: 4.28.2
|
||||
caniuse-lite: 1.0.30001754
|
||||
fraction.js: 4.3.7
|
||||
normalize-range: 0.1.2
|
||||
@@ -57044,7 +57040,7 @@ snapshots:
|
||||
|
||||
base64url@3.0.1: {}
|
||||
|
||||
baseline-browser-mapping@2.9.11: {}
|
||||
baseline-browser-mapping@2.10.33: {}
|
||||
|
||||
basic-auth@2.0.1:
|
||||
dependencies:
|
||||
@@ -57138,13 +57134,13 @@ snapshots:
|
||||
|
||||
browser-or-node@2.1.1: {}
|
||||
|
||||
browserslist@4.28.1:
|
||||
browserslist@4.28.2:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.9.11
|
||||
caniuse-lite: 1.0.30001762
|
||||
electron-to-chromium: 1.5.267
|
||||
node-releases: 2.0.27
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
baseline-browser-mapping: 2.10.33
|
||||
caniuse-lite: 1.0.30001793
|
||||
electron-to-chromium: 1.5.366
|
||||
node-releases: 2.0.47
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
||||
|
||||
bs-logger@0.2.6:
|
||||
dependencies:
|
||||
@@ -57324,7 +57320,7 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001754: {}
|
||||
|
||||
caniuse-lite@1.0.30001762: {}
|
||||
caniuse-lite@1.0.30001793: {}
|
||||
|
||||
centra@2.7.0:
|
||||
dependencies:
|
||||
@@ -58455,7 +58451,7 @@ snapshots:
|
||||
conf: 10.2.0
|
||||
type-fest: 2.19.0
|
||||
|
||||
electron-to-chromium@1.5.267: {}
|
||||
electron-to-chromium@1.5.366: {}
|
||||
|
||||
electron-updater@6.6.2:
|
||||
dependencies:
|
||||
@@ -61610,7 +61606,7 @@ snapshots:
|
||||
camelcase: 6.3.0
|
||||
semver: 7.7.3
|
||||
|
||||
node-releases@2.0.27: {}
|
||||
node-releases@2.0.47: {}
|
||||
|
||||
nodemailer@6.10.1: {}
|
||||
|
||||
@@ -64045,15 +64041,9 @@ snapshots:
|
||||
escape-string-regexp: 5.0.0
|
||||
path-exists: 5.0.0
|
||||
|
||||
update-browserslist-db@1.1.4(browserslist@4.28.1):
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.1):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
browserslist: 4.28.2
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
@@ -64309,7 +64299,7 @@ snapshots:
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.15.0
|
||||
acorn-import-phases: 1.0.4(acorn@8.15.0)
|
||||
browserslist: 4.28.1
|
||||
browserslist: 4.28.2
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.18.3
|
||||
es-module-lexer: 1.7.0
|
||||
@@ -64341,7 +64331,7 @@ snapshots:
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.15.0
|
||||
acorn-import-phases: 1.0.4(acorn@8.15.0)
|
||||
browserslist: 4.28.1
|
||||
browserslist: 4.28.2
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.18.3
|
||||
es-module-lexer: 1.7.0
|
||||
@@ -64373,7 +64363,7 @@ snapshots:
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.15.0
|
||||
acorn-import-phases: 1.0.4(acorn@8.15.0)
|
||||
browserslist: 4.28.1
|
||||
browserslist: 4.28.2
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.18.3
|
||||
es-module-lexer: 1.7.0
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"update-browserslist-db": "^1.1.3",
|
||||
"browserslist": "^4.28.1",
|
||||
"update-browserslist-db": "^1.2.3",
|
||||
"browserslist": "^4.28.2",
|
||||
"typescript": "^5.9.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
@@ -295,4 +295,4 @@
|
||||
"typescript",
|
||||
"svelte"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
"@hcengineering/platform-rig": "workspace:^0.7.21",
|
||||
"@types/node": "^22.18.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"browserslist": "^4.28.1",
|
||||
"browserslist": "^4.28.2",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"cross-env": "~7.0.3",
|
||||
"css-loader": "^5.2.1",
|
||||
@@ -48,7 +48,7 @@
|
||||
"svgo-loader": "^3.0.0",
|
||||
"ts-loader": "^9.2.5",
|
||||
"typescript": "^5.9.3",
|
||||
"update-browserslist-db": "^1.1.3",
|
||||
"update-browserslist-db": "^1.2.3",
|
||||
"webpack": "^5.97.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-cli": "^5.1.4",
|
||||
@@ -297,4 +297,4 @@
|
||||
"readable-stream": "^4.7.0",
|
||||
"svelte": "^4.2.20"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,8 @@ import {
|
||||
TypeAny,
|
||||
TypeBoolean,
|
||||
TypeIntlString,
|
||||
TypeRecord,
|
||||
TypeRank,
|
||||
TypeRecord,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
UX
|
||||
@@ -46,7 +46,7 @@ import { TToDo } from '@hcengineering/model-time'
|
||||
import view, { createAction } from '@hcengineering/model-view'
|
||||
import workbench from '@hcengineering/model-workbench'
|
||||
import notification, { type NotificationGroup } from '@hcengineering/notification'
|
||||
import { type Asset, getEmbeddedLabel, type IntlString, type Resource } from '@hcengineering/platform'
|
||||
import { type Asset, type IntlString, type Resource, getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import {
|
||||
type ApproveRequest,
|
||||
type CheckFunc,
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
type ProcessCustomEvent,
|
||||
type ProcessFunction,
|
||||
type ProcessToDo,
|
||||
type SlotModel,
|
||||
type State,
|
||||
type Step,
|
||||
type Transition,
|
||||
@@ -108,6 +109,12 @@ export class TProcess extends TDoc implements Process {
|
||||
@Prop(TypeBoolean(), process.string.AutomationOnly)
|
||||
automationOnly: boolean | undefined
|
||||
|
||||
@Prop(TypeRecord(), process.string.RequiredSlots)
|
||||
requiredSlots?: Record<string, SlotModel>
|
||||
|
||||
@Prop(TypeRecord(), process.string.Bindings)
|
||||
bindings?: Record<string, string>
|
||||
|
||||
context!: Record<ContextId, ProcessContext>
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,11 @@
|
||||
"TextFromSelect": "Text z výběru",
|
||||
"SelectFromText": "Výběr z textu",
|
||||
"AskSubclass": "Zeptat se na podtřídu",
|
||||
"TextFromIdentifier": "Text z identifikátoru"
|
||||
"TextFromIdentifier": "Text z identifikátoru",
|
||||
"ExportWithSlots": "Exportovat se sloty",
|
||||
"ExportWithoutSlots": "Exportovat bez slotů",
|
||||
"RequiredSlots": "Požadované sloty",
|
||||
"Bindings": "Vazby"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Metoda nenalezena: {methodId}",
|
||||
@@ -191,4 +195,4 @@
|
||||
"TooDeepTransitionRecursion": "Příliš hluboká rekurze přechodů",
|
||||
"ToDoAlreadyCompleted": "Akční položka již byla dokončena"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,11 @@
|
||||
"TextFromSelect": "Text aus Auswahl",
|
||||
"SelectFromText": "Auswahl aus Text",
|
||||
"AskSubclass": "Nach Unterklasse fragen",
|
||||
"TextFromIdentifier": "Text aus Identifikator"
|
||||
"TextFromIdentifier": "Text aus Identifikator",
|
||||
"ExportWithSlots": "Slot-Export",
|
||||
"ExportWithoutSlots": "Slot-freier Export",
|
||||
"RequiredSlots": "Erforderliche Slots",
|
||||
"Bindings": "Bindungen"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Methode nicht gefunden: {methodId}",
|
||||
|
||||
@@ -151,6 +151,10 @@
|
||||
"UnlockSection": "Unlock section",
|
||||
"CancelToDo": "Cancel Action item",
|
||||
"Export": "Export",
|
||||
"ExportWithSlots": "Export with slots",
|
||||
"ExportWithoutSlots": "Export without slots",
|
||||
"RequiredSlots": "Required slots",
|
||||
"Bindings": "Bindings",
|
||||
"Import": "Import",
|
||||
"CancelProcess": "Cancel process",
|
||||
"SyncWithField": "Sync with field",
|
||||
@@ -196,4 +200,4 @@
|
||||
"TooDeepTransitionRecursion": "Too deep transition recursion",
|
||||
"ToDoAlreadyCompleted": "Action item already completed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,11 @@
|
||||
"TextFromSelect": "Texto desde selección",
|
||||
"SelectFromText": "Selección desde texto",
|
||||
"AskSubclass": "Preguntar por subclase",
|
||||
"TextFromIdentifier": "Texto desde identificador"
|
||||
"TextFromIdentifier": "Texto desde identificador",
|
||||
"ExportWithSlots": "Exportar con slots",
|
||||
"ExportWithoutSlots": "Exportar sin slots",
|
||||
"RequiredSlots": "Slots requeridos",
|
||||
"Bindings": "Bindings"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Método no encontrado: {methodId}",
|
||||
|
||||
@@ -177,7 +177,11 @@
|
||||
"TextFromSelect": "Texte depuis sélection",
|
||||
"SelectFromText": "Sélection depuis texte",
|
||||
"AskSubclass": "Demander la sous-classe",
|
||||
"TextFromIdentifier": "Texte depuis identifiant"
|
||||
"TextFromIdentifier": "Texte depuis identifiant",
|
||||
"ExportWithSlots": "Exporter avec slots",
|
||||
"ExportWithoutSlots": "Exporter sans slots",
|
||||
"RequiredSlots": "Slots requis",
|
||||
"Bindings": "Bindings"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Méthode introuvable : {methodId}",
|
||||
|
||||
@@ -177,7 +177,11 @@
|
||||
"TextFromSelect": "Testo da selezione",
|
||||
"SelectFromText": "Selezione da testo",
|
||||
"AskSubclass": "Chiedi sottoclasse",
|
||||
"TextFromIdentifier": "Testo da identificatore"
|
||||
"TextFromIdentifier": "Testo da identificatore",
|
||||
"ExportWithSlots": "Esporta con slot",
|
||||
"ExportWithoutSlots": "Esporta senza slot",
|
||||
"RequiredSlots": "Slot richiesti",
|
||||
"Bindings": "Binding"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Metodo non trovato: {methodId}",
|
||||
|
||||
@@ -176,7 +176,11 @@
|
||||
"TextFromSelect": "テキストから選択",
|
||||
"SelectFromText": "選択からテキスト",
|
||||
"AskSubclass": "サブクラスを尋ねる",
|
||||
"TextFromIdentifier": "テキストから識別子"
|
||||
"TextFromIdentifier": "テキストから識別子",
|
||||
"ExportWithSlots": "スロット付きでエクスポート",
|
||||
"ExportWithoutSlots": "スロットなしでエクスポート",
|
||||
"RequiredSlots": "必須スロット",
|
||||
"Bindings": "バインディング"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "メソッドが見つかりません: {methodId}",
|
||||
|
||||
@@ -177,7 +177,11 @@
|
||||
"TextFromSelect": "선택에서 텍스트로",
|
||||
"SelectFromText": "텍스트에서 선택으로",
|
||||
"AskSubclass": "하위 클래스 선택",
|
||||
"TextFromIdentifier": "식별자에서 텍스트로"
|
||||
"TextFromIdentifier": "식별자에서 텍스트로",
|
||||
"ExportWithSlots": "슬롯 포함 내보내기",
|
||||
"ExportWithoutSlots": "슬롯 없이 내보내기",
|
||||
"RequiredSlots": "필수 슬롯",
|
||||
"Bindings": "바인딩"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "메서드를 찾을 수 없습니다: {methodId}",
|
||||
|
||||
@@ -165,7 +165,11 @@
|
||||
"TextFromSelect": "Texto de seleção",
|
||||
"SelectFromText": "Seleção de texto",
|
||||
"AskSubclass": "Perguntar subclasse",
|
||||
"TextFromIdentifier": "Texto de identificador"
|
||||
"TextFromIdentifier": "Texto de identificador",
|
||||
"ExportWithSlots": "Exportar com slots",
|
||||
"ExportWithoutSlots": "Exportar sem slots",
|
||||
"RequiredSlots": "Slots necessários",
|
||||
"Bindings": "Vinculações"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Método não encontrado: {methodId}",
|
||||
|
||||
@@ -177,7 +177,11 @@
|
||||
"TextFromSelect": "Texto de seleção",
|
||||
"SelectFromText": "Seleção de texto",
|
||||
"AskSubclass": "Perguntar subclasse",
|
||||
"TextFromIdentifier": "Texto de identificador"
|
||||
"TextFromIdentifier": "Texto de identificador",
|
||||
"ExportWithSlots": "Exportar com slots",
|
||||
"ExportWithoutSlots": "Exportar sem slots",
|
||||
"RequiredSlots": "Slots necessários",
|
||||
"Bindings": "Vinculações"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Método não encontrado: {methodId}",
|
||||
|
||||
@@ -151,6 +151,10 @@
|
||||
"UnlockSection": "Разблокировать секцию",
|
||||
"CancelToDo": "Отменить задачу",
|
||||
"Export": "Экспорт",
|
||||
"ExportWithSlots": "Экспортировать со слотами",
|
||||
"ExportWithoutSlots": "Экспортировать без слотов",
|
||||
"RequiredSlots": "Обязательные слоты",
|
||||
"Bindings": "Связки",
|
||||
"Import": "Импорт",
|
||||
"CancelProcess": "Отменить процесс",
|
||||
"SyncWithField": "Синхронизировать с полем",
|
||||
|
||||
@@ -172,7 +172,11 @@
|
||||
"TextFromSelect": "Seçimden metin",
|
||||
"SelectFromText": "Metinden seçim",
|
||||
"AskSubclass": "Alt sınıfı sor",
|
||||
"TextFromIdentifier": "Tanımlayıcıdan metne"
|
||||
"TextFromIdentifier": "Tanımlayıcıdan metne",
|
||||
"ExportWithSlots": "Slotlu dışa aktar",
|
||||
"ExportWithoutSlots": "Slotsuz dışa aktar",
|
||||
"RequiredSlots": "Gerekli slotlar",
|
||||
"Bindings": "Bağlamalar"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Metod bulunamadı: {methodId}",
|
||||
|
||||
@@ -177,7 +177,11 @@
|
||||
"TextFromSelect": "选择到文本",
|
||||
"SelectFromText": "文本到选择",
|
||||
"AskSubclass": "询问子类",
|
||||
"TextFromIdentifier": "文本从标识符"
|
||||
"TextFromIdentifier": "文本从标识符",
|
||||
"ExportWithSlots": "导出包含槽",
|
||||
"ExportWithoutSlots": "导出不含槽",
|
||||
"RequiredSlots": "必需槽",
|
||||
"Bindings": "绑定"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "找不到方法:{methodId}",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
/* eslint-disable no-template-curly-in-string, @typescript-eslint/no-non-null-assertion */
|
||||
import core, { generateId, Hierarchy, ModelDb } from '@hcengineering/core'
|
||||
import { exportProcess, importProcess } from '../exporter'
|
||||
import { createDoc, genMinModel } from './minmodel'
|
||||
import cardPlugin from '@hcengineering/card'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import process, { type Process, type State } from '@hcengineering/process'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
|
||||
jest.mock('@hcengineering/presentation', () => ({
|
||||
getClient: jest.fn()
|
||||
}))
|
||||
|
||||
describe('Process Export/Import Integration', () => {
|
||||
let h: Hierarchy
|
||||
let m: ModelDb
|
||||
const processId = generateId<Process>()
|
||||
const stateId = generateId<State>()
|
||||
const masterTag = cardPlugin.class.Card
|
||||
|
||||
beforeEach(async () => {
|
||||
h = new Hierarchy()
|
||||
m = new ModelDb(h)
|
||||
const txes = genMinModel()
|
||||
|
||||
// Mock getClient to return our test model/hierarchy
|
||||
;(getClient as jest.Mock).mockReturnValue({
|
||||
getModel: () => m,
|
||||
getHierarchy: () => h,
|
||||
apply: () => ({
|
||||
createDoc: jest.fn().mockResolvedValue(undefined),
|
||||
commit: jest.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
for (const tx of txes) {
|
||||
h.tx(tx)
|
||||
}
|
||||
for (const tx of txes) {
|
||||
await m.tx(tx)
|
||||
}
|
||||
|
||||
const _processTx = createDoc(
|
||||
process.class.Process,
|
||||
{
|
||||
masterTag,
|
||||
name: 'Integration Process',
|
||||
context: {
|
||||
__CONTEXT_0__: {
|
||||
_class: process.class.ProcessToDo,
|
||||
_id: '__CONTEXT_0__',
|
||||
name: 'Task 1'
|
||||
}
|
||||
} as any,
|
||||
description: ''
|
||||
},
|
||||
processId
|
||||
)
|
||||
const stateTx = createDoc(
|
||||
process.class.State,
|
||||
{
|
||||
process: processId,
|
||||
title: 'State 1',
|
||||
rank: '0'
|
||||
},
|
||||
stateId
|
||||
)
|
||||
|
||||
h.tx(_processTx)
|
||||
h.tx(stateTx)
|
||||
await m.tx(_processTx)
|
||||
await m.tx(stateTx)
|
||||
})
|
||||
|
||||
async function addAttribute (id: string, name: string, label: string): Promise<void> {
|
||||
const attrTx = createDoc(
|
||||
core.class.Attribute,
|
||||
{
|
||||
name,
|
||||
attributeOf: masterTag,
|
||||
type: { _class: core.class.TypeString, label: core.string.String },
|
||||
label: getEmbeddedLabel(label)
|
||||
},
|
||||
id as any
|
||||
)
|
||||
h.tx(attrTx)
|
||||
await m.tx(attrTx)
|
||||
}
|
||||
|
||||
test('DSL string with slot and context should survive export/import cycle', async () => {
|
||||
const attrId = '69c1c342fe49e50bc1ca7fcf'
|
||||
await addAttribute(attrId, 'sourceAttr', 'Source Attribute')
|
||||
|
||||
const proc = m.findObject(processId) as Process
|
||||
const transition: any = {
|
||||
_id: generateId(),
|
||||
_class: process.class.Transition,
|
||||
process: processId,
|
||||
from: null,
|
||||
to: stateId,
|
||||
trigger: process.trigger.OnExecutionStart,
|
||||
triggerParams: {},
|
||||
actions: [
|
||||
{
|
||||
_id: generateId(),
|
||||
_class: 'process:Action',
|
||||
methodId: process.method.CreateCard,
|
||||
params: {
|
||||
title: `\${$userRequest(${attrId},title,card:class:Card)}`,
|
||||
contextRef: '${$context(__CONTEXT_0__)}'
|
||||
}
|
||||
}
|
||||
],
|
||||
rank: '0'
|
||||
}
|
||||
|
||||
m.findAllSync = jest.fn().mockImplementation((_class, filter) => {
|
||||
if (_class === process.class.Process) return [proc]
|
||||
if (_class === process.class.State) return [m.findObject(stateId)]
|
||||
if (_class === process.class.Transition) return [transition]
|
||||
return []
|
||||
})
|
||||
|
||||
// 1. Export
|
||||
const { docs } = exportProcess(proc)
|
||||
const json = JSON.stringify(docs, null, 2)
|
||||
|
||||
// Verify normalization
|
||||
expect(json).toContain('__SLOT_')
|
||||
expect(json).toContain('__CONTEXT_')
|
||||
expect(json).not.toContain(attrId) // Should be replaced by slot placeholder
|
||||
|
||||
// 2. Import
|
||||
const newMasterTag = generateId()
|
||||
const bindings = { sourceAttr: attrId } // Correct binding for the slot name
|
||||
|
||||
const mockApply = {
|
||||
createDoc: jest.fn().mockResolvedValue(undefined),
|
||||
commit: jest.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
;(getClient as jest.Mock).mockReturnValue({
|
||||
getModel: () => m,
|
||||
getHierarchy: () => h,
|
||||
apply: () => mockApply
|
||||
})
|
||||
|
||||
await importProcess(newMasterTag as any, json, bindings)
|
||||
|
||||
// Check if importProcess called createDoc with the correctly restored DSL
|
||||
const createDocCalls = mockApply.createDoc.mock.calls
|
||||
const transitionCall = createDocCalls.find((call) => call[0] === process.class.Transition)
|
||||
|
||||
expect(transitionCall).toBeDefined()
|
||||
const importedParams = transitionCall![2].actions[0].params
|
||||
|
||||
// CRITICAL: Check if DSL restored the REAL attribute ID
|
||||
expect(importedParams.title).toContain(attrId)
|
||||
expect(importedParams.title).not.toContain('__SLOT_')
|
||||
|
||||
// Check if context was restored (it gets a NEW ID, not the placeholder)
|
||||
expect(importedParams.contextRef).toContain('${$context(')
|
||||
expect(importedParams.contextRef).not.toContain('__CONTEXT_')
|
||||
const restoredCtxId = importedParams.contextRef.match(/\$\{?\$context\(([^)]+)\)\}/)![1]
|
||||
expect(restoredCtxId).toHaveLength(24) // Should be a real generated ID
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,229 @@
|
||||
//
|
||||
// Copyright © 2026 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, {
|
||||
ClassifierKind,
|
||||
DOMAIN_MODEL,
|
||||
DOMAIN_TX,
|
||||
TxFactory,
|
||||
type Class,
|
||||
type Data,
|
||||
type Doc,
|
||||
type Obj,
|
||||
type Ref,
|
||||
type TxCUD,
|
||||
type TxCreateDoc
|
||||
} from '@hcengineering/core'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import processPlugin from '../plugin'
|
||||
import card from '@hcengineering/card'
|
||||
|
||||
export const txFactory = new TxFactory(core.account.System)
|
||||
|
||||
export function createClass (_class: Ref<Class<Obj>>, attributes: Data<Class<Obj>>): TxCreateDoc<Doc> {
|
||||
return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class)
|
||||
}
|
||||
|
||||
export function createDoc<T extends Doc> (_class: Ref<Class<T>>, attributes: Data<T>, id?: Ref<T>): TxCreateDoc<Doc> {
|
||||
return txFactory.createTxCreateDoc(_class, core.space.Model, attributes, id)
|
||||
}
|
||||
|
||||
export function genMinModel (): Array<TxCUD<Doc>> {
|
||||
const txes = []
|
||||
// Fill Tx'es with basic model classes.
|
||||
txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS }))
|
||||
txes.push(
|
||||
createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS })
|
||||
)
|
||||
txes.push(
|
||||
createClass(core.class.AttachedDoc, {
|
||||
label: 'AttachedDoc' as IntlString,
|
||||
extends: core.class.Doc,
|
||||
kind: ClassifierKind.MIXIN
|
||||
})
|
||||
)
|
||||
txes.push(
|
||||
createClass(core.class.Class, {
|
||||
label: 'Class' as IntlString,
|
||||
extends: core.class.Doc,
|
||||
kind: ClassifierKind.CLASS,
|
||||
domain: DOMAIN_MODEL
|
||||
})
|
||||
)
|
||||
txes.push(
|
||||
createClass(core.class.Space, {
|
||||
label: 'Space' as IntlString,
|
||||
extends: core.class.Doc,
|
||||
kind: ClassifierKind.CLASS,
|
||||
domain: DOMAIN_MODEL
|
||||
})
|
||||
)
|
||||
|
||||
txes.push(
|
||||
createClass(core.class.Tx, {
|
||||
label: 'Tx' as IntlString,
|
||||
extends: core.class.Doc,
|
||||
kind: ClassifierKind.CLASS,
|
||||
domain: DOMAIN_TX
|
||||
})
|
||||
)
|
||||
txes.push(
|
||||
createClass(core.class.TxCUD, {
|
||||
label: 'TxCUD' as IntlString,
|
||||
extends: core.class.Tx,
|
||||
kind: ClassifierKind.CLASS,
|
||||
domain: DOMAIN_TX
|
||||
})
|
||||
)
|
||||
txes.push(
|
||||
createClass(core.class.TxCreateDoc, {
|
||||
label: 'TxCreateDoc' as IntlString,
|
||||
extends: core.class.TxCUD,
|
||||
kind: ClassifierKind.CLASS
|
||||
})
|
||||
)
|
||||
|
||||
// Add Card and Attribute related classes
|
||||
txes.push(
|
||||
createClass(card.class.Card, { label: 'Card' as IntlString, extends: core.class.Doc, kind: ClassifierKind.CLASS })
|
||||
)
|
||||
txes.push(
|
||||
createClass(core.class.Attribute, {
|
||||
label: 'Attribute' as IntlString,
|
||||
extends: core.class.Doc,
|
||||
kind: ClassifierKind.CLASS
|
||||
})
|
||||
)
|
||||
|
||||
// Add Process related classes
|
||||
txes.push(
|
||||
createClass(processPlugin.class.Process, {
|
||||
label: 'Process' as IntlString,
|
||||
extends: core.class.Doc,
|
||||
kind: ClassifierKind.CLASS
|
||||
})
|
||||
)
|
||||
txes.push(
|
||||
createClass(processPlugin.class.State, {
|
||||
label: 'State' as IntlString,
|
||||
extends: core.class.Doc,
|
||||
kind: ClassifierKind.CLASS
|
||||
})
|
||||
)
|
||||
txes.push(
|
||||
createClass(processPlugin.class.Transition, {
|
||||
label: 'Transition' as IntlString,
|
||||
extends: core.class.Doc,
|
||||
kind: ClassifierKind.CLASS
|
||||
})
|
||||
)
|
||||
txes.push(
|
||||
createClass(processPlugin.class.Method, {
|
||||
label: 'Method' as IntlString,
|
||||
extends: core.class.Doc,
|
||||
kind: ClassifierKind.CLASS
|
||||
})
|
||||
)
|
||||
txes.push(
|
||||
createClass(processPlugin.class.Trigger, {
|
||||
label: 'Trigger' as IntlString,
|
||||
extends: core.class.Doc,
|
||||
kind: ClassifierKind.CLASS
|
||||
})
|
||||
)
|
||||
|
||||
// Add real methods from actions.ts
|
||||
txes.push(
|
||||
createDoc(processPlugin.class.Method, { requiredParams: ['_id'] } as any, processPlugin.method.RunSubProcess)
|
||||
)
|
||||
txes.push(
|
||||
createDoc(processPlugin.class.Method, { requiredParams: ['user'] } as any, processPlugin.method.RequestApproval)
|
||||
)
|
||||
txes.push(
|
||||
createDoc(processPlugin.class.Method, { requiredParams: ['title', 'user'] } as any, processPlugin.method.CreateToDo)
|
||||
)
|
||||
txes.push(createDoc(processPlugin.class.Method, { requiredParams: [] } as any, processPlugin.method.UpdateCard))
|
||||
txes.push(
|
||||
createDoc(
|
||||
processPlugin.class.Method,
|
||||
{ requiredParams: ['title', '_class'] } as any,
|
||||
processPlugin.method.CreateCard
|
||||
)
|
||||
)
|
||||
txes.push(
|
||||
createDoc(
|
||||
processPlugin.class.Method,
|
||||
{ requiredParams: ['association', 'direction', '_id'] } as any,
|
||||
processPlugin.method.AddRelation
|
||||
)
|
||||
)
|
||||
txes.push(createDoc(processPlugin.class.Method, { requiredParams: ['_id'] } as any, processPlugin.method.AddTag))
|
||||
txes.push(createDoc(processPlugin.class.Method, { requiredParams: ['_id'] } as any, processPlugin.method.CancelToDo))
|
||||
txes.push(
|
||||
createDoc(processPlugin.class.Method, { requiredParams: ['_id'] } as any, processPlugin.method.CancelSubProcess)
|
||||
)
|
||||
txes.push(createDoc(processPlugin.class.Method, { requiredParams: [] } as any, processPlugin.method.LockCard))
|
||||
txes.push(createDoc(processPlugin.class.Method, { requiredParams: ['_id'] } as any, processPlugin.method.LockSection))
|
||||
txes.push(createDoc(processPlugin.class.Method, { requiredParams: [] } as any, processPlugin.method.UnlockCard))
|
||||
txes.push(
|
||||
createDoc(processPlugin.class.Method, { requiredParams: ['_id'] } as any, processPlugin.method.UnlockSection)
|
||||
)
|
||||
txes.push(createDoc(processPlugin.class.Method, { requiredParams: ['value'] } as any, processPlugin.method.LockField))
|
||||
txes.push(
|
||||
createDoc(processPlugin.class.Method, { requiredParams: ['value'] } as any, processPlugin.method.UnlockField)
|
||||
)
|
||||
|
||||
// Add real triggers from triggers.ts
|
||||
txes.push(
|
||||
createDoc(
|
||||
processPlugin.class.Trigger,
|
||||
{ requiredParams: ['_id'] } as any,
|
||||
processPlugin.trigger.OnApproveRequestApproved
|
||||
)
|
||||
)
|
||||
txes.push(
|
||||
createDoc(
|
||||
processPlugin.class.Trigger,
|
||||
{ requiredParams: ['_id'] } as any,
|
||||
processPlugin.trigger.OnApproveRequestRejected
|
||||
)
|
||||
)
|
||||
txes.push(
|
||||
createDoc(processPlugin.class.Trigger, { requiredParams: ['_id'] } as any, processPlugin.trigger.OnToDoClose)
|
||||
)
|
||||
txes.push(
|
||||
createDoc(processPlugin.class.Trigger, { requiredParams: [] } as any, processPlugin.trigger.OnExecutionStart)
|
||||
)
|
||||
txes.push(
|
||||
createDoc(processPlugin.class.Trigger, { requiredParams: ['_id'] } as any, processPlugin.trigger.OnToDoRemove)
|
||||
)
|
||||
txes.push(createDoc(processPlugin.class.Trigger, { requiredParams: [] } as any, processPlugin.trigger.OnCardUpdate))
|
||||
txes.push(
|
||||
createDoc(processPlugin.class.Trigger, { requiredParams: [] } as any, processPlugin.trigger.WhenFieldChanges)
|
||||
)
|
||||
txes.push(
|
||||
createDoc(processPlugin.class.Trigger, { requiredParams: [] } as any, processPlugin.trigger.OnSubProcessesDone)
|
||||
)
|
||||
txes.push(
|
||||
createDoc(
|
||||
processPlugin.class.Trigger,
|
||||
{ requiredParams: ['process'] } as any,
|
||||
processPlugin.trigger.OnSubProcessMatch
|
||||
)
|
||||
)
|
||||
txes.push(createDoc(processPlugin.class.Trigger, { requiredParams: [] } as any, processPlugin.trigger.OnTime))
|
||||
|
||||
return txes
|
||||
}
|
||||
@@ -18,10 +18,20 @@
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Process, State } from '@hcengineering/process'
|
||||
import { ButtonIcon, getCurrentLocation, Icon, IconAdd, IconFile, IconOpen, Label, navigate } from '@hcengineering/ui'
|
||||
import process from '../plugin'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import { importProcess } from '../exporter'
|
||||
import {
|
||||
ButtonIcon,
|
||||
getCurrentLocation,
|
||||
Icon,
|
||||
IconAdd,
|
||||
IconFile,
|
||||
Label,
|
||||
navigate,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { getRequiredSlots, importProcess } from '../exporter'
|
||||
import process from '../plugin'
|
||||
import ImportSlotsPopup from './settings/ImportSlotsPopup.svelte'
|
||||
|
||||
export let masterTag: MasterTag
|
||||
|
||||
@@ -86,7 +96,21 @@
|
||||
const file = (evt.target as HTMLInputElement).files?.[0]
|
||||
if (file != null) {
|
||||
const text = await file.text()
|
||||
await importProcess(masterTag._id, text)
|
||||
const slots = getRequiredSlots(text)
|
||||
if (slots && Object.keys(slots).length > 0) {
|
||||
showPopup(
|
||||
ImportSlotsPopup,
|
||||
{ requiredSlots: slots, masterTag: masterTag._id },
|
||||
undefined,
|
||||
async (bindings) => {
|
||||
if (bindings) {
|
||||
await importProcess(masterTag._id, text, bindings)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
await importProcess(masterTag._id, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<!--
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
||||
import presentation, { Card, getClient } from '@hcengineering/presentation'
|
||||
import { Process } from '@hcengineering/process'
|
||||
import { Button, eventToHTMLElement, Label, SelectPopup, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import processPlugin from '../../plugin'
|
||||
import { isTypeEqual } from '../../utils'
|
||||
|
||||
export let process: Process
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const model = client.getModel()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: allAttrs = hierarchy.getAllAttributes(process.masterTag as any, core.class.Doc)
|
||||
$: allAssociations = model.findAllSync(core.class.Association, {})
|
||||
$: allProcesses = model.findAllSync(processPlugin.class.Process, {})
|
||||
|
||||
function isClassLike (obj: Doc | undefined): boolean {
|
||||
if (!obj || !obj._class) return false
|
||||
return hierarchy.isDerived(obj._class, core.class.Class)
|
||||
}
|
||||
|
||||
function resolveMemberOf (memberOf: string | undefined): Ref<Class<Doc>> | undefined {
|
||||
if (memberOf === undefined) return process.masterTag as any
|
||||
let targetId: string | undefined
|
||||
if (memberOf.startsWith('__SLOT_')) {
|
||||
const parentSlotId = memberOf.replace(/^__SLOT_(.+)__$/, '$1')
|
||||
targetId = process.bindings?.[parentSlotId]
|
||||
if (!targetId) {
|
||||
const parentObj = model.findObject(parentSlotId as any)
|
||||
if (isClassLike(parentObj)) targetId = parentSlotId
|
||||
}
|
||||
} else {
|
||||
targetId = memberOf
|
||||
}
|
||||
const obj = targetId ? model.findObject(targetId as any) : undefined
|
||||
if (isClassLike(obj)) return targetId as Ref<Class<Doc>>
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function setBinding (slotId: string, e: MouseEvent): Promise<void> {
|
||||
const slot = process.requiredSlots?.[slotId]
|
||||
if (slot === undefined) return
|
||||
|
||||
const memberOfTag = resolveMemberOf(slot.memberOf)
|
||||
if (memberOfTag === undefined) return
|
||||
|
||||
let possible: Array<{ id: string, label?: IntlString, name?: string }> = []
|
||||
|
||||
if (slot.slotKind === 'association') {
|
||||
const aAssociations = allAssociations
|
||||
.filter((assoc) => {
|
||||
return (
|
||||
hierarchy.isDerived(process.masterTag as any, assoc.classA) &&
|
||||
hierarchy.isDerived(memberOfTag, assoc.classB)
|
||||
)
|
||||
})
|
||||
.map((a) => ({ id: a._id, name: a.nameA }))
|
||||
const bAssociations = allAssociations
|
||||
.filter((assoc) => {
|
||||
return (
|
||||
hierarchy.isDerived(process.masterTag as any, assoc.classB) &&
|
||||
hierarchy.isDerived(memberOfTag, assoc.classA)
|
||||
)
|
||||
})
|
||||
.map((a) => ({ id: a._id, name: a.nameB }))
|
||||
possible = aAssociations.concat(bAssociations)
|
||||
} else if (slot.slotKind === 'process') {
|
||||
possible = allProcesses
|
||||
.filter((proc) => hierarchy.isDerived(memberOfTag, proc.masterTag))
|
||||
.map((p) => ({ id: p._id, name: p.name }))
|
||||
} else if (slot.slotKind === 'class') {
|
||||
let descendants: string[] = []
|
||||
try {
|
||||
descendants = hierarchy.getDescendants(core.class.Obj)
|
||||
} catch (e) {}
|
||||
possible = descendants
|
||||
.map((id) => model.findObject(id as any))
|
||||
.filter((c) => c && !(c as any).hidden && isClassLike(c))
|
||||
.map((c: any) => ({ id: c._id, label: c.label, name: c.name }))
|
||||
} else {
|
||||
// Default: attribute matching
|
||||
const attributes = hierarchy.getAllAttributes(memberOfTag, core.class.Doc)
|
||||
possible = Array.from(attributes.values())
|
||||
.filter((attr) => !(attr.hidden ?? false) && isTypeEqual(slot as any, attr.type))
|
||||
.map((a) => ({ id: a.name, label: a.label, name: a.name }))
|
||||
}
|
||||
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: possible.map((p) => ({ id: p.id, label: p.label, text: p.name }))
|
||||
},
|
||||
eventToHTMLElement(e),
|
||||
async (res) => {
|
||||
if (res != null) {
|
||||
const bindings = { ...(process.bindings ?? {}) }
|
||||
bindings[slotId] = res as string
|
||||
await client.update(process, { bindings })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function getBindingLabel (
|
||||
allAttrs: Map<string, AnyAttribute>,
|
||||
bindings: Record<string, string> | undefined,
|
||||
id: string
|
||||
): any {
|
||||
if (bindings?.[id] === undefined) return presentation.string.NotSelected
|
||||
const value = bindings[id]
|
||||
|
||||
// Search in resolved memberOf tag's attributes
|
||||
const slot = process.requiredSlots?.[id]
|
||||
const memberOfTag = slot?.memberOf !== undefined ? resolveMemberOf(slot.memberOf) : (process.masterTag as any)
|
||||
const searchTag = memberOfTag ?? process.masterTag
|
||||
|
||||
try {
|
||||
const attrs = hierarchy.getAllAttributes(searchTag, core.class.Doc)
|
||||
const attr = Array.from(attrs.values()).find((a) => a.name === value)
|
||||
if (attr !== undefined) return attr.label
|
||||
} catch (e) {}
|
||||
|
||||
// Also check masterTag if different
|
||||
if (searchTag !== process.masterTag) {
|
||||
const attr = Array.from(allAttrs.values()).find((a) => a.name === value)
|
||||
if (attr !== undefined) return attr.label
|
||||
}
|
||||
|
||||
const assoc = allAssociations.find((a) => a._id === value)
|
||||
if (assoc !== undefined) return getEmbeddedLabel(assoc.nameA)
|
||||
|
||||
const proc = allProcesses.find((p) => p._id === value)
|
||||
if (proc !== undefined) return getEmbeddedLabel(proc.name)
|
||||
|
||||
const cls = model.findObject(value as any)
|
||||
if (isClassLike(cls)) return (cls as Class<Doc>).label
|
||||
|
||||
return value
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={processPlugin.string.Bindings}
|
||||
canSave={true}
|
||||
width="small"
|
||||
okLabel={presentation.string.Save}
|
||||
okAction={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
on:close
|
||||
>
|
||||
<div class="flex-column flex-gap-4">
|
||||
{#each Object.entries(process.requiredSlots ?? {}) as [id, slot]}
|
||||
<div class="flex-column flex-gap-1">
|
||||
<Label label={slot.label ?? getEmbeddedLabel(slot.name ?? '')} />
|
||||
<Button
|
||||
label={getBindingLabel(allAttrs, process.bindings, id)}
|
||||
disabled={slot.memberOf !== undefined && resolveMemberOf(slot.memberOf) === undefined}
|
||||
kind="secondary"
|
||||
width="100%"
|
||||
on:click={(e) => setBinding(id, e)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{#if Object.keys(process.requiredSlots ?? {}).length === 0}
|
||||
<div class="opacity-40 text-center py-4">No slots defined for this process</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
@@ -0,0 +1,241 @@
|
||||
<!--
|
||||
// Copyright © 2026 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 core, { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import presentation, { Card, getClient } from '@hcengineering/presentation'
|
||||
import { Button, eventToHTMLElement, Label, SelectPopup, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { SlotModel } from '@hcengineering/process'
|
||||
import processPlugin from '../../plugin'
|
||||
import { isTypeEqual } from '../../utils'
|
||||
|
||||
export let requiredSlots: Record<string, SlotModel>
|
||||
export let masterTag: Ref<Class<any>>
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const model = client.getModel()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let bindings: Record<string, string> = {}
|
||||
|
||||
$: allAttrs = hierarchy.getAllAttributes(masterTag, core.class.Doc)
|
||||
$: allAssociations = model.findAllSync(core.class.Association, {})
|
||||
$: allProcesses = model.findAllSync(processPlugin.class.Process, {})
|
||||
|
||||
function isClassLike (obj: Doc | undefined): boolean {
|
||||
if (!obj || !obj._class) return false
|
||||
return obj._class === core.class.Class || hierarchy.isDerived(obj._class, core.class.Class)
|
||||
}
|
||||
|
||||
function resolveMemberOf (
|
||||
memberOf: string | undefined,
|
||||
currentBindings: Record<string, string>
|
||||
): Ref<Class<Doc>> | undefined {
|
||||
if (memberOf === undefined) return masterTag
|
||||
let targetId: string | undefined
|
||||
if (memberOf.startsWith('__SLOT_')) {
|
||||
const parentSlotId = memberOf.replace(/^__SLOT_(.+)__$/, '$1')
|
||||
targetId = currentBindings[parentSlotId]
|
||||
if (!targetId) {
|
||||
const parentObj = model.findObject(parentSlotId as any)
|
||||
if (isClassLike(parentObj)) targetId = parentSlotId
|
||||
}
|
||||
} else {
|
||||
targetId = memberOf
|
||||
}
|
||||
|
||||
const obj = targetId ? model.findObject(targetId as any) : undefined
|
||||
if (isClassLike(obj)) return targetId as Ref<Class<Doc>>
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Reactive map: recalculated every time `bindings` changes.
|
||||
// Each slot gets its resolved memberOf tag and whether it's currently enabled.
|
||||
$: resolvedSlots = (() => {
|
||||
const result: Record<string, { resolved: Ref<Class<Doc>> | undefined, enabled: boolean }> = {}
|
||||
for (const [id, slot] of Object.entries(requiredSlots)) {
|
||||
const resolved = resolveMemberOf(slot.memberOf, bindings)
|
||||
const enabled = slot.memberOf === undefined || resolved !== undefined
|
||||
result[id] = { resolved, enabled }
|
||||
}
|
||||
return result
|
||||
})()
|
||||
|
||||
function setBinding (slotId: string, e: MouseEvent): void {
|
||||
const slot = requiredSlots[slotId] as any
|
||||
const memberOfTag = resolvedSlots[slotId]?.resolved
|
||||
if (!memberOfTag) return
|
||||
|
||||
let possible: Array<{ id: string, label?: any, text?: string }> = []
|
||||
|
||||
if (slot.slotKind === 'association' || slot._class === core.class.Association) {
|
||||
possible = allAssociations
|
||||
.filter((assoc) => {
|
||||
const isA =
|
||||
hierarchy.isDerived(masterTag, assoc.classA) &&
|
||||
(memberOfTag ? hierarchy.isDerived(memberOfTag, assoc.classB) : false)
|
||||
const isB =
|
||||
hierarchy.isDerived(masterTag, assoc.classB) &&
|
||||
(memberOfTag ? hierarchy.isDerived(memberOfTag, assoc.classA) : false)
|
||||
return isA || isB
|
||||
})
|
||||
.map((a) => ({
|
||||
id: a._id,
|
||||
text: hierarchy.isDerived(a.classA, masterTag) ? a.nameA + ' -> ' + a.nameB : a.nameB + ' -> ' + a.nameA
|
||||
}))
|
||||
} else if (slot.slotKind === 'process' || slot._class === processPlugin.class.Process) {
|
||||
possible = allProcesses
|
||||
.filter((proc) => !memberOfTag || hierarchy.isDerived(memberOfTag, proc.masterTag))
|
||||
.map((p) => ({
|
||||
id: p._id,
|
||||
label: (p as any).label,
|
||||
text: p.name ?? p._id
|
||||
}))
|
||||
} else if (slot.slotKind === 'class' || isClassLike({ _class: slot._class } as any)) {
|
||||
let descendants: string[] = []
|
||||
try {
|
||||
descendants = hierarchy.getDescendants(core.class.Obj)
|
||||
} catch (e) {}
|
||||
|
||||
const expectedClass = slot._class || core.class.Class
|
||||
possible = descendants
|
||||
.map((id) => model.findObject(id as any))
|
||||
.filter((c) => {
|
||||
if (!c || (c as any).hidden) return false
|
||||
const cClass = c._class
|
||||
return cClass === expectedClass || hierarchy.isDerived(cClass, expectedClass)
|
||||
})
|
||||
.map((c: any) => ({
|
||||
id: c._id,
|
||||
label: c.label,
|
||||
text: c.name ?? c._id
|
||||
}))
|
||||
} else {
|
||||
const attributes = memberOfTag ? hierarchy.getAllAttributes(memberOfTag, core.class.Doc) : []
|
||||
possible = Array.from(attributes.values())
|
||||
.filter((attr) => !attr.hidden && isTypeEqual(slot, attr.type))
|
||||
.map((a) => ({
|
||||
id: a.name,
|
||||
label: a.label,
|
||||
text: a.name
|
||||
}))
|
||||
}
|
||||
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: possible.map((p) => ({ id: p.id, label: p.label, text: p.label ? undefined : p.text }))
|
||||
},
|
||||
eventToHTMLElement(e),
|
||||
(res) => {
|
||||
if (res != null) {
|
||||
// Clear dependent bindings when a parent slot changes
|
||||
const nextBindings: Record<string, string | undefined> = { ...bindings, [slotId]: res as string }
|
||||
for (const [depId, depSlot] of Object.entries(requiredSlots)) {
|
||||
if (depSlot.memberOf === `__SLOT_${slotId}__`) {
|
||||
nextBindings[depId] = undefined
|
||||
}
|
||||
}
|
||||
const filteredBindings: Record<string, string> = {}
|
||||
for (const [k, v] of Object.entries(nextBindings)) {
|
||||
if (v !== undefined) {
|
||||
filteredBindings[k] = v
|
||||
}
|
||||
}
|
||||
bindings = filteredBindings
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function onSave (): void {
|
||||
dispatch('close', bindings)
|
||||
}
|
||||
|
||||
$: allBound = Object.keys(requiredSlots).every((id) => bindings[id] !== undefined)
|
||||
|
||||
function getBindingLabel (currentBindings: Record<string, string>, id: string): any {
|
||||
const value = currentBindings[id]
|
||||
if (value === undefined) return presentation.string.NotSelected
|
||||
|
||||
// Search in the resolved memberOf tag's attributes first, then fallback to masterTag
|
||||
const slot = requiredSlots[id]
|
||||
const memberOfTag = slot?.memberOf !== undefined ? resolveMemberOf(slot.memberOf, currentBindings) : masterTag
|
||||
const searchTag = memberOfTag ?? masterTag
|
||||
|
||||
try {
|
||||
const attrs = hierarchy.getAllAttributes(searchTag, core.class.Doc)
|
||||
const attr = Array.from(attrs.values()).find((a) => a.name === value)
|
||||
if (attr) return attr.label ?? attr.name
|
||||
} catch (e) {
|
||||
// Tag may not be in hierarchy yet
|
||||
}
|
||||
|
||||
// Also search masterTag if different
|
||||
if (searchTag !== masterTag) {
|
||||
try {
|
||||
const masterAttrs = hierarchy.getAllAttributes(masterTag, core.class.Doc)
|
||||
const attr = Array.from(masterAttrs.values()).find((a) => a.name === value)
|
||||
if (attr !== undefined) return attr.label ?? attr.name
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const assoc = allAssociations.find((a) => a._id === value)
|
||||
if (assoc !== undefined) {
|
||||
return hierarchy.isDerived(assoc.classA, masterTag)
|
||||
? assoc.nameA + ' -> ' + assoc.nameB
|
||||
: assoc.nameB + ' -> ' + assoc.nameA
|
||||
}
|
||||
|
||||
const proc = allProcesses.find((p) => p._id === value)
|
||||
if (proc !== undefined) return proc.name
|
||||
|
||||
const cls = model.findObject(value as any)
|
||||
if (isClassLike(cls)) return (cls as any).label ?? (cls as any).name
|
||||
|
||||
return value
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={processPlugin.string.RequiredSlots}
|
||||
canSave={allBound}
|
||||
width="small"
|
||||
okLabel={processPlugin.string.Import}
|
||||
okAction={onSave}
|
||||
on:close
|
||||
>
|
||||
<div class="flex-column flex-gap-4">
|
||||
{#each Object.entries(requiredSlots) as [id, slot]}
|
||||
<div class="flex-column flex-gap-1">
|
||||
{#if slot.label}
|
||||
<Label label={slot.label} />
|
||||
{:else}
|
||||
{slot.name}
|
||||
{/if}
|
||||
<Button
|
||||
label={getBindingLabel(bindings, id)}
|
||||
disabled={!resolvedSlots[id]?.enabled}
|
||||
kind="secondary"
|
||||
width="100%"
|
||||
on:click={(e) => {
|
||||
setBinding(id, e)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Card>
|
||||
@@ -46,7 +46,10 @@
|
||||
|
||||
$: value = object[objectKey ?? key]
|
||||
|
||||
$: attribute = hierarchy.getAttribute(_class, key)
|
||||
$: slot = process.requiredSlots?.[key] as any
|
||||
$: attribute = slot
|
||||
? ({ name: key, label: slot.label, _id: key, type: slot } as any)
|
||||
: hierarchy.getAttribute(_class, key)
|
||||
$: presenterClass = getAttributePresenterClass(hierarchy, attribute.type)
|
||||
$: context = getContext(client, process, presenterClass.attrClass, presenterClass.category, attribute._id, true)
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
import { clearSettingsStore, settingsStore } from '@hcengineering/setting-resources'
|
||||
import {
|
||||
ButtonIcon,
|
||||
ButtonMenu,
|
||||
defineSeparators,
|
||||
DropdownIntlItem,
|
||||
EditBox,
|
||||
getCurrentLocation,
|
||||
IconDelete,
|
||||
@@ -28,13 +30,15 @@
|
||||
navigate,
|
||||
Scroller,
|
||||
secondNavSeparators,
|
||||
showPopup
|
||||
showPopup,
|
||||
IconLink
|
||||
} from '@hcengineering/ui'
|
||||
import { exportProcess } from '../../exporter'
|
||||
import view from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import process from '../../plugin'
|
||||
import ContextEditor from './ContextEditor.svelte'
|
||||
import BindingsEditor from './BindingsEditor.svelte'
|
||||
import Navigator from './Navigator.svelte'
|
||||
import ProcesssSetting from './ProcesssSetting.svelte'
|
||||
import StatesInlineEditor from './StatesInlineEditor.svelte'
|
||||
@@ -137,10 +141,37 @@
|
||||
showPopup(ProcesssSetting, { value })
|
||||
}
|
||||
|
||||
function handleExport (): void {
|
||||
function handleBindings (): void {
|
||||
showPopup(BindingsEditor, { process: value })
|
||||
}
|
||||
|
||||
const EXPORT_WITH_SLOTS = 'with-slots'
|
||||
const EXPORT_WITHOUT_SLOTS = 'without-slots'
|
||||
|
||||
let exportItems: DropdownIntlItem[]
|
||||
$: exportItems = [
|
||||
{
|
||||
id: EXPORT_WITH_SLOTS,
|
||||
label: process.string.ExportWithSlots
|
||||
},
|
||||
{
|
||||
id: EXPORT_WITHOUT_SLOTS,
|
||||
label: process.string.ExportWithoutSlots
|
||||
}
|
||||
]
|
||||
|
||||
function onExportSelected (event: CustomEvent<string | number>): void {
|
||||
if (event.detail === EXPORT_WITH_SLOTS) {
|
||||
handleExport(true)
|
||||
} else if (event.detail === EXPORT_WITHOUT_SLOTS) {
|
||||
handleExport(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleExport (withSlots: boolean): void {
|
||||
if (value === undefined) return
|
||||
const str = JSON.stringify(
|
||||
exportProcess(value).docs.map((doc) => {
|
||||
exportProcess(value, withSlots).docs.map((doc) => {
|
||||
const { modifiedBy, modifiedOn, createdBy, createdOn, ...rest } = doc
|
||||
return rest
|
||||
})
|
||||
@@ -171,13 +202,24 @@
|
||||
placeholder={process.string.Untitled}
|
||||
/>
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
{#if value.requiredSlots && Object.keys(value.requiredSlots).length > 0}
|
||||
<ButtonIcon
|
||||
icon={IconLink}
|
||||
tooltip={{ label: process.string.Bindings, direction: 'bottom' }}
|
||||
size="small"
|
||||
kind="secondary"
|
||||
on:click={handleBindings}
|
||||
/>
|
||||
{/if}
|
||||
<ButtonIcon icon={IconSettings} size="small" kind="secondary" on:click={handleSettings} />
|
||||
<ButtonIcon
|
||||
<ButtonMenu
|
||||
icon={IconDownload}
|
||||
tooltip={{ label: process.string.Export, direction: 'bottom' }}
|
||||
size="small"
|
||||
kind="secondary"
|
||||
on:click={handleExport}
|
||||
items={exportItems}
|
||||
noSelection
|
||||
on:selected={onExportSelected}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon={IconDetails}
|
||||
|
||||
@@ -54,7 +54,12 @@
|
||||
let keys = Object.keys(params)
|
||||
|
||||
$: allAttrs = getKeys(process.masterTag)
|
||||
$: possibleAttrs = allAttrs.filter((attr) => !keys.includes(attr.name))
|
||||
$: slots = Object.entries(process.requiredSlots ?? {}).map(([id, s]) => ({
|
||||
name: id,
|
||||
label: (s as any).label,
|
||||
isSlot: true
|
||||
}))
|
||||
$: possibleAttrs = [...allAttrs, ...slots].filter((attr) => !keys.includes(attr.name))
|
||||
|
||||
function addKey (key: string): void {
|
||||
keys = [...keys, key]
|
||||
|
||||
@@ -1,84 +1,110 @@
|
||||
import { type MasterTag } from '@hcengineering/card'
|
||||
import card from '@hcengineering/card'
|
||||
import card, { type MasterTag } from '@hcengineering/card'
|
||||
import core, {
|
||||
type Doc,
|
||||
type Ref,
|
||||
type Class,
|
||||
type EnumOf,
|
||||
type RefTo,
|
||||
type AnyAttribute,
|
||||
type ArrOf,
|
||||
type ModelDb,
|
||||
type Association,
|
||||
type Attribute,
|
||||
type Class,
|
||||
type Doc,
|
||||
type EnumOf,
|
||||
generateId,
|
||||
type Hierarchy,
|
||||
type ModelDb,
|
||||
type Ref,
|
||||
type RefTo,
|
||||
type Type
|
||||
} from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { type AttributeSlotModel, type Process, type SlotModel, type Transition } from '@hcengineering/process'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { type Process, type Transition } from '@hcengineering/process'
|
||||
import processPlugin from './plugin'
|
||||
|
||||
export function exportProcesses (_id: Ref<MasterTag>): {
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
interface ExportResult {
|
||||
docs: Doc[]
|
||||
required: Array<Ref<Class<Doc>>>
|
||||
} {
|
||||
const docs: Doc[] = []
|
||||
const required: Array<Ref<Class<Doc>>> = []
|
||||
const client = getClient()
|
||||
const m = client.getModel()
|
||||
const processes = m.findAllSync(processPlugin.class.Process, { masterTag: _id })
|
||||
for (const proc of processes) {
|
||||
const res = exportProcess(proc)
|
||||
docs.push(...res.docs)
|
||||
for (const req of res.required) {
|
||||
if (!required.includes(req)) required.push(req)
|
||||
}
|
||||
}
|
||||
return { docs, required }
|
||||
}
|
||||
|
||||
export function exportProcess (proc: Doc): {
|
||||
docs: Doc[]
|
||||
required: Array<Ref<Class<Doc>>>
|
||||
} {
|
||||
const docs: Doc[] = [proc]
|
||||
const required: Array<Ref<Class<Doc>>> = []
|
||||
const client = getClient()
|
||||
const m = client.getModel()
|
||||
const h = client.getHierarchy()
|
||||
export type { AttributeSlotModel, SlotModel }
|
||||
|
||||
docs.push(...m.findAllSync(processPlugin.class.State, { process: proc._id as any }))
|
||||
const transitions = m.findAllSync(processPlugin.class.Transition, { process: proc._id as any }) as Transition[]
|
||||
docs.push(...transitions)
|
||||
type DetailedSlotModel = AttributeSlotModel | SlotModel
|
||||
|
||||
const processDoc = proc as Process
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
for (const tr of transitions) {
|
||||
processParams(tr.triggerParams, processDoc.masterTag, docs, required, m, h)
|
||||
for (const action of tr.actions) {
|
||||
processParams(action.params, processDoc.masterTag, docs, required, m, h)
|
||||
if (action.results !== undefined) {
|
||||
for (const res of action.results) {
|
||||
processType(res.type, docs, required, m, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Normalizes a dot-separated platform ID to colon-separated form. */
|
||||
const normalizeId = (id: string | undefined): string | undefined => id?.replace(/\./g, ':')
|
||||
|
||||
if (processDoc.context !== undefined) {
|
||||
for (const key in processDoc.context) {
|
||||
const ctx = processDoc.context[key as any]
|
||||
if (ctx.type !== undefined) {
|
||||
processType(ctx.type, docs, required, m, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (processDoc.resultType !== undefined) {
|
||||
processType(processDoc.resultType, docs, required, m, h)
|
||||
}
|
||||
|
||||
return { docs, required }
|
||||
/** Checks if a normalized ID belongs to a system namespace (core or process). */
|
||||
function isSystemId (id: string): boolean {
|
||||
return id.startsWith('core:class:') || id.startsWith('process:class:')
|
||||
}
|
||||
|
||||
function stripData<T extends Doc> (
|
||||
doc: T
|
||||
): Omit<T, '_id' | '_class' | 'space' | 'modifiedBy' | 'modifiedOn' | 'createdBy' | 'createdOn'> {
|
||||
const { _id, _class, space, modifiedBy, modifiedOn, createdBy, createdOn, ...rest } = doc
|
||||
return rest
|
||||
}
|
||||
|
||||
// ─── Slot Factories ──────────────────────────────────────────────────────────
|
||||
|
||||
function attributeToSlot (attr: AnyAttribute, memberOf?: string): AttributeSlotModel {
|
||||
return {
|
||||
slotKind: 'attribute',
|
||||
_class: attr._class,
|
||||
label: attr.label,
|
||||
name: attr.name,
|
||||
memberOf,
|
||||
type: attr.type
|
||||
}
|
||||
}
|
||||
|
||||
function classToSlot (cls: Class<any>, memberOf?: string): SlotModel {
|
||||
return {
|
||||
slotKind: 'class',
|
||||
_class: cls._class,
|
||||
label: cls.label,
|
||||
memberOf
|
||||
}
|
||||
}
|
||||
|
||||
function associationToSlot (assoc: Association, direction: string | undefined, memberOf?: string): SlotModel {
|
||||
let name = (assoc as any).name ?? assoc.nameA
|
||||
if (direction === 'B') name = assoc.nameB
|
||||
else if (direction === 'A') name = assoc.nameA
|
||||
return {
|
||||
slotKind: 'association',
|
||||
_class: assoc._class,
|
||||
label: (assoc as any).label,
|
||||
name,
|
||||
memberOf
|
||||
}
|
||||
}
|
||||
|
||||
function processToSlot (proc: Process, memberOf?: string): SlotModel {
|
||||
return {
|
||||
slotKind: 'process',
|
||||
_class: proc._class,
|
||||
label: (proc as any).label,
|
||||
name: proc.name,
|
||||
memberOf
|
||||
}
|
||||
}
|
||||
|
||||
function unknownToSlot (id: string, memberOf?: string): SlotModel {
|
||||
return {
|
||||
slotKind: 'unknown',
|
||||
_class: core.class.Obj as any,
|
||||
label: id as any,
|
||||
name: id,
|
||||
memberOf
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Type & Param Processing ─────────────────────────────────────────────────
|
||||
|
||||
/** Collects type-level dependencies: enum docs and required Card-derived classes. */
|
||||
function processType (type: Type<any>, docs: Doc[], required: Array<Ref<Class<Doc>>>, m: ModelDb, h: Hierarchy): void {
|
||||
if (type._class === core.class.EnumOf) {
|
||||
const enumRef = (type as EnumOf).of
|
||||
@@ -94,11 +120,11 @@ function processType (type: Type<any>, docs: Doc[], required: Array<Ref<Class<Do
|
||||
}
|
||||
}
|
||||
if (type._class === core.class.ArrOf) {
|
||||
const of = (type as ArrOf<Doc>).of
|
||||
processType(of, docs, required, m, h)
|
||||
processType((type as ArrOf<Doc>).of, docs, required, m, h)
|
||||
}
|
||||
}
|
||||
|
||||
/** Recursively scans action/trigger params for type dependencies. */
|
||||
function processParams (
|
||||
params: Record<string, any> | undefined,
|
||||
masterTag: Ref<MasterTag>,
|
||||
@@ -122,27 +148,180 @@ function processParams (
|
||||
}
|
||||
continue
|
||||
}
|
||||
const attr = h.findAttribute(masterTag, key)
|
||||
const attr = h.findAttribute(masterTag as any, key)
|
||||
if (attr !== undefined) {
|
||||
processType(attr.type, docs, required, m, h)
|
||||
}
|
||||
// Also scan values for DSL
|
||||
const val = params[key]
|
||||
if (typeof val === 'string') {
|
||||
scanDSL(val, masterTag, docs, required, m, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function importProcess (masterTag: Ref<MasterTag>, json: string): Promise<void> {
|
||||
/** Scans DSL expressions for attribute and object references. */
|
||||
function scanDSL (
|
||||
dsl: string,
|
||||
masterTag: Ref<MasterTag>,
|
||||
docs: Doc[],
|
||||
required: Array<Ref<Class<Doc>>>,
|
||||
model: ModelDb,
|
||||
h: Hierarchy
|
||||
): void {
|
||||
// Detect @attr references
|
||||
for (const m of dsl.matchAll(/\$\{@([a-zA-Z0-9_]+)/g)) {
|
||||
const attr = h.findAttribute(masterTag as any, m[1])
|
||||
if (attr !== undefined) processType(attr.type, docs, required, model, h)
|
||||
}
|
||||
// Detect inline 24-char hex IDs
|
||||
for (const match of dsl.matchAll(/[0-9a-fA-F]{24}/g)) {
|
||||
const obj = h.findAttribute(masterTag as any, match[0]) ?? model.findObject(match[0] as any)
|
||||
if ((obj as any)?.type !== undefined) {
|
||||
processType((obj as any).type, docs, required, model, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Export API ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Exports all processes associated with a MasterTag. */
|
||||
export function exportProcesses (_id: Ref<MasterTag>): ExportResult {
|
||||
const docs: Doc[] = []
|
||||
const required: Array<Ref<Class<Doc>>> = []
|
||||
const client = getClient()
|
||||
const m = client.getModel()
|
||||
const processes = m.findAllSync(processPlugin.class.Process, { masterTag: _id })
|
||||
for (const proc of processes) {
|
||||
const res = exportProcess(proc, false)
|
||||
docs.push(...res.docs)
|
||||
for (const req of res.required) {
|
||||
if (!required.includes(req)) required.push(req)
|
||||
}
|
||||
}
|
||||
return { docs, required }
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a single process into a portable template.
|
||||
*
|
||||
* Collects the process doc, its states, transitions, and all type dependencies.
|
||||
* Detects environment-specific references and replaces them with named slots.
|
||||
* Returns normalized docs where all IDs are replaced with placeholders.
|
||||
*/
|
||||
export function exportProcess (proc: Process, withSlots: boolean = true): ExportResult {
|
||||
const docs: Doc[] = [proc]
|
||||
const required: Array<Ref<Class<Doc>>> = []
|
||||
const client = getClient()
|
||||
const m = client.getModel()
|
||||
const h = client.getHierarchy()
|
||||
|
||||
// Collect states and transitions
|
||||
docs.push(...m.findAllSync(processPlugin.class.State, { process: proc._id }))
|
||||
const transitions = m.findAllSync(processPlugin.class.Transition, { process: proc._id })
|
||||
docs.push(...transitions)
|
||||
|
||||
// Process type dependencies from transitions
|
||||
for (const tr of transitions) {
|
||||
processParams(tr.triggerParams, proc.masterTag, docs, required, m, h)
|
||||
for (const action of tr.actions) {
|
||||
processParams(action.params, proc.masterTag, docs, required, m, h)
|
||||
if (action.results !== undefined) {
|
||||
for (const res of action.results) {
|
||||
processType(res.type, docs, required, m, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process type dependencies from context
|
||||
if (proc.context !== undefined) {
|
||||
for (const key in proc.context) {
|
||||
const ctx = proc.context[key as any]
|
||||
if (ctx.type !== undefined) {
|
||||
processType(ctx.type, docs, required, m, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (proc.resultType !== undefined) {
|
||||
processType(proc.resultType, docs, required, m, h)
|
||||
}
|
||||
|
||||
if (withSlots) {
|
||||
// Detect slots and bindings
|
||||
const requiredSlots: Record<string, SlotModel> = {}
|
||||
const bindings: Record<string, string> = {}
|
||||
detectSlots(proc, transitions, requiredSlots, bindings, m, h)
|
||||
|
||||
// Attach slots/bindings to the process doc for serialization
|
||||
const clonedProc = { ...proc, requiredSlots, bindings }
|
||||
const docIndex = docs.findIndex((d) => d._id === proc._id)
|
||||
if (docIndex !== -1) docs[docIndex] = clonedProc
|
||||
|
||||
// Process type dependencies from detected slots
|
||||
for (const slotId in requiredSlots) {
|
||||
const slot = requiredSlots[slotId]
|
||||
if (slot.slotKind === 'attribute' && (slot as AttributeSlotModel).type != null) {
|
||||
processType((slot as AttributeSlotModel).type, docs, required, m, h)
|
||||
} else if (slot.slotKind === 'class' || slot.slotKind === 'process') {
|
||||
if (h.isDerived(slot._class, card.class.Card) && !required.includes(slot._class)) {
|
||||
required.push(slot._class)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { requiredSlots, bindings, ...restProc } = proc as any
|
||||
const docIndex = docs.findIndex((d) => d._id === proc._id)
|
||||
if (docIndex !== -1) docs[docIndex] = restProc
|
||||
}
|
||||
|
||||
return { docs: normalizeIds(docs), required }
|
||||
}
|
||||
|
||||
// ─── Import API ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Extracts slot requirements from an exported process JSON.
|
||||
* Returns the slot definitions if the process has any, for use in binding UI.
|
||||
*/
|
||||
export function getRequiredSlots (json: string): Record<string, SlotModel> | undefined {
|
||||
try {
|
||||
const rawData = JSON.parse(json) as Doc[]
|
||||
const data = Array.from(new Map(rawData.map((d) => [d._id, d])).values())
|
||||
const data = JSON.parse(json) as Doc[]
|
||||
const proc = data.find((d: any) => d._class === processPlugin.class.Process)
|
||||
return (proc as any)?.requiredSlots
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a process template into the current environment.
|
||||
*
|
||||
* Denormalizes placeholder IDs back to real IDs, applies slot bindings,
|
||||
* and creates all documents (process, states, transitions) in the model.
|
||||
*/
|
||||
export async function importProcess (
|
||||
masterTag: Ref<MasterTag>,
|
||||
json: string,
|
||||
bindings?: Record<string, string>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data = JSON.parse(json) as Doc[]
|
||||
if (data.length === 0) return
|
||||
const denormalizedData = denormalizeIds(data, masterTag, bindings ?? {})
|
||||
const client = getClient()
|
||||
const m = client.getModel()
|
||||
const apply = client.apply('Import process')
|
||||
for (const elem of data) {
|
||||
for (const elem of denormalizedData) {
|
||||
if (elem._class === processPlugin.class.Process) {
|
||||
;(elem as any).masterTag = masterTag
|
||||
if (bindings !== undefined) {
|
||||
;(elem as any).bindings = bindings
|
||||
}
|
||||
// Strip template metadata — slots will be re-detected for the new environment
|
||||
delete (elem as any).requiredSlots
|
||||
}
|
||||
}
|
||||
for (const elem of data) {
|
||||
const existing = m.findObject(elem._id)
|
||||
if (existing !== undefined) {
|
||||
const newData = stripData(elem)
|
||||
@@ -160,9 +339,543 @@ export async function importProcess (masterTag: Ref<MasterTag>, json: string): P
|
||||
}
|
||||
}
|
||||
|
||||
function stripData<T extends Doc> (
|
||||
doc: T
|
||||
): Omit<T, '_id' | '_class' | 'space' | 'modifiedBy' | 'modifiedOn' | 'createdBy' | 'createdOn'> {
|
||||
const { _id, _class, space, modifiedBy, modifiedOn, createdBy, createdOn, ...rest } = doc
|
||||
return rest
|
||||
// ─── ID Normalization ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Replaces all environment-specific IDs in exported docs with deterministic placeholders.
|
||||
*
|
||||
* Uses `__PROCESS__`, `__MASTER_TAG__`, `__STATE_N__`, `__TRANSITION_N__`,
|
||||
* `__ACTION_N__`, `__CONTEXT_N__`, and `__SLOT_name__` patterns.
|
||||
*/
|
||||
export function normalizeIds (docs: Doc[]): Doc[] {
|
||||
const idMap = buildNormalizationMap(docs)
|
||||
|
||||
return docs.map((doc) => {
|
||||
let json = JSON.stringify(doc)
|
||||
// Normalize dot-separated class/method IDs to colon-separated
|
||||
json = json.replace(/"([a-z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+)"/g, (_, p1) => `"${p1.replace(/\./g, ':')}"`)
|
||||
|
||||
// Replace IDs sorted by length (longest first to avoid partial matches)
|
||||
const sortedIds = Object.keys(idMap).sort((a, b) => b.length - a.length)
|
||||
for (const id of sortedIds) {
|
||||
json = replaceIdInJson(json, id, idMap[id])
|
||||
}
|
||||
return JSON.parse(json)
|
||||
})
|
||||
}
|
||||
|
||||
/** Builds the mapping from real IDs to normalized placeholders. */
|
||||
function buildNormalizationMap (docs: Doc[]): Record<string, string> {
|
||||
const idMap: Record<string, string> = {}
|
||||
let stateCount = 0
|
||||
let transitionCount = 0
|
||||
let contextCount = 0
|
||||
let actionCount = 0
|
||||
|
||||
for (const doc of docs) {
|
||||
if (doc._class === processPlugin.class.Process) {
|
||||
idMap[doc._id] = '__PROCESS__'
|
||||
const proc = doc as any
|
||||
if (proc.masterTag !== undefined) idMap[proc.masterTag] = '__MASTER_TAG__'
|
||||
if (proc.context !== undefined) {
|
||||
for (const contextId in proc.context) {
|
||||
idMap[contextId] = `__CONTEXT_${contextCount++}__`
|
||||
}
|
||||
}
|
||||
if (proc.bindings !== undefined) {
|
||||
for (const [slotId, attrId] of Object.entries(proc.bindings)) {
|
||||
if (typeof attrId === 'string' && attrId.length > 0) {
|
||||
idMap[attrId] = `__SLOT_${slotId}__`
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (doc._class === processPlugin.class.State) {
|
||||
idMap[doc._id] = `__STATE_${stateCount++}__`
|
||||
} else if (doc._class === processPlugin.class.Transition) {
|
||||
idMap[doc._id] = `__TRANSITION_${transitionCount++}__`
|
||||
const tr = doc as Transition
|
||||
for (const action of tr.actions) {
|
||||
idMap[action._id] = `__ACTION_${actionCount++}__`
|
||||
if (action.context !== undefined && action.context !== null) {
|
||||
if (idMap[action.context._id] === undefined) {
|
||||
idMap[action.context._id] = `__CONTEXT_${contextCount++}__`
|
||||
}
|
||||
}
|
||||
if (action.results !== undefined) {
|
||||
for (const res of action.results) {
|
||||
idMap[res._id] = `__CONTEXT_${contextCount++}__`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return idMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an ID within a JSON string using an appropriate strategy:
|
||||
* - 24/30-char IDs: global split/join (safe for hex/custom IDs)
|
||||
* - Shorter IDs: surgical regex to avoid replacing JSON keys
|
||||
*/
|
||||
function replaceIdInJson (json: string, id: string, replacement: string): string {
|
||||
if (id.length === 24 || id.length === 30) {
|
||||
return json.split(id).join(replacement)
|
||||
}
|
||||
const escapedId = id.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||
const regex = new RegExp(
|
||||
`(?<=[@(\\s,])\\b${escapedId}\\b|\\b${escapedId}\\b(?=[)\\s,])|(?<=:\\s?")${escapedId}(?=")`,
|
||||
'g'
|
||||
)
|
||||
return json.replace(regex, replacement)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces placeholder IDs with real IDs for the target environment.
|
||||
* Generates fresh IDs for states, transitions, actions, and contexts.
|
||||
* Bindings map slot placeholders to their actual bound entity IDs.
|
||||
*/
|
||||
export function denormalizeIds (docs: Doc[], masterTag: Ref<MasterTag>, bindings?: Record<string, string>): Doc[] {
|
||||
const idMap: Record<string, string> = {
|
||||
__PROCESS__: generateId(),
|
||||
__MASTER_TAG__: masterTag as string
|
||||
}
|
||||
|
||||
// Map slot placeholders to bound IDs
|
||||
if (bindings !== undefined) {
|
||||
for (const [slotId, attrId] of Object.entries(bindings)) {
|
||||
idMap[`__SLOT_${slotId}__`] = attrId
|
||||
}
|
||||
}
|
||||
|
||||
// Discover all remaining placeholders and assign fresh IDs
|
||||
const findPlaceholders = (val: any): void => {
|
||||
if (typeof val === 'string') {
|
||||
for (const m of val.matchAll(/(__[a-zA-Z0-9_]+__)/g)) {
|
||||
if (idMap[m[1]] === undefined) {
|
||||
idMap[m[1]] = generateId()
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(val)) {
|
||||
val.forEach(findPlaceholders)
|
||||
} else if (typeof val === 'object' && val !== null) {
|
||||
for (const k of Object.keys(val)) {
|
||||
findPlaceholders(k)
|
||||
findPlaceholders(val[k])
|
||||
}
|
||||
}
|
||||
}
|
||||
docs.forEach(findPlaceholders)
|
||||
|
||||
// Replace all placeholders (longest first)
|
||||
const sortedPlaceholders = Object.keys(idMap).sort((a, b) => b.length - a.length)
|
||||
return docs.map((doc) => {
|
||||
let json = JSON.stringify(doc)
|
||||
for (const placeholder of sortedPlaceholders) {
|
||||
json = json.split(placeholder).join(idMap[placeholder])
|
||||
}
|
||||
const denormalized = JSON.parse(json)
|
||||
if (denormalized._class === processPlugin.class.Process) {
|
||||
denormalized.masterTag = masterTag
|
||||
}
|
||||
return denormalized
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Slot Detection ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* System parameters for each method that should NOT be treated as user-bound slots.
|
||||
* Keyed by normalized method ID.
|
||||
*/
|
||||
function getMethodSystemParams (methodId: string): string[] {
|
||||
const normalized = normalizeId(methodId)
|
||||
if (normalized === normalizeId(processPlugin.method.RunSubProcess)) return ['_id', 'card', 'context']
|
||||
if (normalized === normalizeId(processPlugin.method.AddRelation)) return ['_id', 'card', 'direction']
|
||||
if (normalized === normalizeId(processPlugin.method.AddTag)) return ['_id', 'card']
|
||||
if (normalized === normalizeId(processPlugin.method.CreateToDo)) return ['state', 'title', 'user', 'withRollback']
|
||||
if (normalized === normalizeId(processPlugin.method.CreateCard)) return ['_class', 'title']
|
||||
return []
|
||||
}
|
||||
|
||||
/** Extracts all potential entity IDs from a DSL string value. */
|
||||
function extractPotentialIds (val: string): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
// DSL attribute references: ${@attrName ...}
|
||||
for (const m of val.matchAll(/\$\{@([a-zA-Z0-9_]+)/g)) ids.add(m[1])
|
||||
// Document IDs: 24-char hex or 30-char custom
|
||||
for (const m of val.matchAll(/\b(?:custom)?[0-9a-zA-Z]{24,30}\b/g)) ids.add(m[0])
|
||||
// Already-normalized slot references
|
||||
if (val.startsWith('__SLOT_')) ids.add(val)
|
||||
// Fallback: entire value if it looks like a simple identifier
|
||||
if (val.length > 0 && !val.includes(' ') && !val.includes('$')) ids.add(val)
|
||||
return ids
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the active MasterTag context for an action step.
|
||||
* Uses explicit context, method-specific logic, or falls back to the process MasterTag.
|
||||
*/
|
||||
function resolveActionContext (
|
||||
step: any,
|
||||
contextClasses: Record<string, Ref<MasterTag>>,
|
||||
masterTag: Ref<MasterTag>
|
||||
): Ref<MasterTag> {
|
||||
const methodId = normalizeId(step.methodId)
|
||||
|
||||
// Explicit context
|
||||
if (step.context?._class !== undefined) return step.context._class
|
||||
|
||||
// CreateCard provides the target class directly
|
||||
if (methodId === normalizeId(processPlugin.method.CreateCard) && step.params?._class !== undefined) {
|
||||
return step.params._class
|
||||
}
|
||||
|
||||
// AddRelation/RunSubProcess derive context from $context() references
|
||||
if (methodId === normalizeId(processPlugin.method.AddRelation) && step.params?._id !== undefined) {
|
||||
const ctxId = step.params._id.match(/\$context\("?([a-zA-Z0-9_]+)/)?.[1]
|
||||
if (ctxId !== undefined && contextClasses[ctxId] !== undefined) return contextClasses[ctxId]
|
||||
}
|
||||
|
||||
if (methodId === normalizeId(processPlugin.method.RunSubProcess) && step.params?.card !== undefined) {
|
||||
const ctxId = step.params.card.match(/\$context\("?([a-zA-Z0-9_]+)/)?.[1]
|
||||
if (ctxId !== undefined && contextClasses[ctxId] !== undefined) return contextClasses[ctxId]
|
||||
}
|
||||
|
||||
return masterTag
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the memberOf slot reference for a process or association whose
|
||||
* target class differs from the process's masterTag.
|
||||
*
|
||||
* If the target class is foreign, creates a parent class slot and returns
|
||||
* its `__SLOT_name__` reference for use as `memberOf`.
|
||||
*/
|
||||
function resolveParentSlot (
|
||||
targetTag: Ref<MasterTag>,
|
||||
masterTag: Ref<MasterTag>,
|
||||
memberOfRef: string | undefined,
|
||||
getOrAddSlot: (id: string, model: DetailedSlotModel) => string,
|
||||
m: ModelDb
|
||||
): string | undefined {
|
||||
if (targetTag === undefined || targetTag === '' || normalizeId(targetTag) === normalizeId(masterTag)) {
|
||||
return memberOfRef
|
||||
}
|
||||
const targetDoc = m.findObject(targetTag) as Class<any> | undefined
|
||||
const parentSlotId = getOrAddSlot(
|
||||
targetTag,
|
||||
targetDoc !== undefined ? classToSlot(targetDoc) : unknownToSlot(targetTag)
|
||||
)
|
||||
return `__SLOT_${parentSlotId}__`
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans all transitions to detect environment-specific references
|
||||
* (attributes, associations, sub-processes) and registers them as named slots.
|
||||
*
|
||||
* Populates `slots` with slot metadata and `bindings` with the mapping
|
||||
* from slot names to actual entity IDs.
|
||||
*/
|
||||
export function detectSlots (
|
||||
proc: Process,
|
||||
transitions: Transition[],
|
||||
slots: Record<string, SlotModel>,
|
||||
bindings: Record<string, string>,
|
||||
m: ModelDb,
|
||||
h: Hierarchy
|
||||
): void {
|
||||
const masterTag = proc.masterTag
|
||||
const attrToSlot: Record<string, string> = {}
|
||||
const contextClasses: Record<string, Ref<MasterTag>> = {}
|
||||
|
||||
// Initialize context classes from process context
|
||||
if (proc.context !== undefined) {
|
||||
for (const [id, ctx] of Object.entries(proc.context)) {
|
||||
if (ctx.type?._class === core.class.RefTo) {
|
||||
contextClasses[id] = (ctx.type as any).to
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize reverse mapping from existing bindings
|
||||
for (const [slotId, attrId] of Object.entries(bindings)) {
|
||||
attrToSlot[attrId] = slotId
|
||||
}
|
||||
|
||||
// ── Slot Registration ────────────────────────
|
||||
|
||||
/** Registers or updates a slot. Returns the slot name. */
|
||||
const getOrAddSlot = (id: string, model: DetailedSlotModel): string => {
|
||||
let slotName = attrToSlot[id]
|
||||
|
||||
// Generate a new slot name if this ID hasn't been seen
|
||||
if (slotName === undefined) {
|
||||
slotName =
|
||||
model.name ?? (model.label !== undefined && typeof model.label === 'string' ? model.label : undefined) ?? id
|
||||
if (slotName.startsWith('custom') || Object.values(attrToSlot).includes(slotName)) {
|
||||
slotName = `slot${Object.keys(attrToSlot).length + 1}`
|
||||
}
|
||||
attrToSlot[id] = slotName
|
||||
|
||||
const normalizedId = normalizeId(id) ?? ''
|
||||
if (!id.startsWith('__SLOT_') && !isSystemId(normalizedId)) {
|
||||
bindings[slotName] = id
|
||||
}
|
||||
}
|
||||
|
||||
// Don't overwrite concrete metadata with unknown
|
||||
const existingSlot = slots[slotName]
|
||||
if (existingSlot !== undefined && model.slotKind === 'unknown') return slotName
|
||||
|
||||
const meta: SlotModel = existingSlot ?? {
|
||||
slotKind: model.slotKind,
|
||||
_class: model._class
|
||||
}
|
||||
|
||||
// Upgrade unknown → concrete
|
||||
if (meta.slotKind === 'unknown' || meta.slotKind === undefined) {
|
||||
meta.slotKind = model.slotKind
|
||||
}
|
||||
|
||||
// Merge type-specific metadata
|
||||
if (model.slotKind === 'attribute') {
|
||||
mergeAttributeSlotMeta(meta, model as AttributeSlotModel)
|
||||
} else {
|
||||
mergeNonAttributeSlotMeta(meta, model)
|
||||
}
|
||||
|
||||
// Set memberOf unless it would be self-referential or undefined
|
||||
const selfRef = `__SLOT_${slotName}__`
|
||||
if (model.memberOf !== undefined && model.memberOf !== selfRef) {
|
||||
meta.memberOf = model.memberOf
|
||||
}
|
||||
|
||||
slots[slotName] = meta
|
||||
return slotName
|
||||
}
|
||||
|
||||
// ── Param Scanning ───────────────────────────
|
||||
|
||||
/** Scans action/trigger params to find slottable references. */
|
||||
const scanParams = (
|
||||
params: Record<string, any> | undefined,
|
||||
activeTag: Ref<MasterTag>,
|
||||
requiredParams: string[] = []
|
||||
): void => {
|
||||
if (params === undefined) return
|
||||
|
||||
// Ensure the active tag itself is registered as a slot if non-system & non-master
|
||||
const nTag = normalizeId(activeTag) ?? ''
|
||||
if (activeTag !== masterTag && attrToSlot[activeTag] === undefined && !isSystemId(nTag)) {
|
||||
const tagDoc = m.findObject(activeTag) as any
|
||||
if (tagDoc !== undefined) {
|
||||
if (normalizeId(tagDoc._class) === normalizeId(core.class.Attribute)) {
|
||||
getOrAddSlot(activeTag, attributeToSlot(tagDoc))
|
||||
} else {
|
||||
getOrAddSlot(activeTag, classToSlot(tagDoc))
|
||||
}
|
||||
} else {
|
||||
getOrAddSlot(activeTag, unknownToSlot(activeTag))
|
||||
}
|
||||
}
|
||||
|
||||
const memberOf = activeTag !== masterTag ? attrToSlot[activeTag] : undefined
|
||||
const memberOfRef = memberOf !== undefined && memberOf !== '' ? `__SLOT_${memberOf}__` : undefined
|
||||
|
||||
for (const key in params) {
|
||||
const val = params[key]
|
||||
// Scan keys as potential attribute references
|
||||
scanParamKey(key, val, activeTag, masterTag, memberOfRef, requiredParams, h, getOrAddSlot, m)
|
||||
|
||||
// Scan values for entity references
|
||||
if (typeof val === 'string') {
|
||||
scanParamValue(val, activeTag, masterTag, memberOfRef, params.direction, slots, getOrAddSlot, m, h)
|
||||
} else if (typeof val === 'object' && val !== null) {
|
||||
scanParams(val, activeTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main Loop ────────────────────────────────
|
||||
|
||||
for (const tr of transitions) {
|
||||
const triggerDoc = m.findObject(tr.trigger) as any
|
||||
const triggerRequired = triggerDoc?.requiredParams ?? []
|
||||
scanParams(tr.triggerParams, masterTag, triggerRequired)
|
||||
|
||||
for (const step of tr.actions) {
|
||||
const methodDoc = m.findObject(step.methodId)
|
||||
const methodRequired = methodDoc?.requiredParams ?? []
|
||||
|
||||
// Register result contexts for subsequent action resolution
|
||||
if (step.results !== undefined) {
|
||||
for (const res of step.results) {
|
||||
if (res.type?._class === core.class.RefTo) {
|
||||
contextClasses[res._id] = (res.type as RefTo<Doc>).to
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const activeTag = resolveActionContext(step, contextClasses, masterTag)
|
||||
if (step.context?._id != null) {
|
||||
contextClasses[step.context._id] = activeTag
|
||||
}
|
||||
|
||||
const sysParams = getMethodSystemParams(step.methodId)
|
||||
scanParams(step.params, activeTag, [...methodRequired, ...sysParams])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Slot Detection Helpers ──────────────────────────────────────────────────
|
||||
|
||||
/** Merges attribute-specific metadata into a slot. */
|
||||
function mergeAttributeSlotMeta (meta: SlotModel, model: AttributeSlotModel): void {
|
||||
if (typeof model.type === 'object' && model.type !== null) {
|
||||
;(meta as any).type = model.type
|
||||
meta._class = model.type._class
|
||||
} else if (typeof model.type === 'string') {
|
||||
meta._class = model.type as any
|
||||
}
|
||||
if (model.label !== undefined) meta.label = model.label
|
||||
}
|
||||
|
||||
/** Merges non-attribute (process/association/class) metadata into a slot. */
|
||||
function mergeNonAttributeSlotMeta (meta: SlotModel, model: DetailedSlotModel): void {
|
||||
const currentClass = normalizeId(meta._class)
|
||||
const isStrongType =
|
||||
currentClass === normalizeId(processPlugin.class.Process) || currentClass === normalizeId(core.class.Association)
|
||||
|
||||
if (!isStrongType || model.slotKind === 'process' || model.slotKind === 'association') {
|
||||
meta._class = model._class
|
||||
if (model.label !== undefined) meta.label = model.label
|
||||
}
|
||||
if (
|
||||
model.name !== undefined &&
|
||||
model.name !== '' &&
|
||||
(model.slotKind === 'process' || model.slotKind === 'association')
|
||||
) {
|
||||
meta.name = model.name
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks if a value is contextual/dynamic (e.g. contains DSL, is an ID, or nested object). */
|
||||
function isContextualValue (
|
||||
val: any,
|
||||
activeTag: Ref<MasterTag>,
|
||||
masterTag: Ref<MasterTag>,
|
||||
h: Hierarchy,
|
||||
m: ModelDb
|
||||
): boolean {
|
||||
if (typeof val === 'string') {
|
||||
if (val.includes('${')) return true
|
||||
if (val.startsWith('__SLOT_')) return true
|
||||
const potentialIds = extractPotentialIds(val)
|
||||
for (const id of potentialIds) {
|
||||
const obj = h.findAttribute(activeTag, id) ?? h.findAttribute(masterTag, id) ?? m.findObject(id as any)
|
||||
if (obj !== undefined) return true
|
||||
}
|
||||
}
|
||||
if (typeof val === 'object' && val !== null) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Checks if a param key represents a slottable attribute. */
|
||||
function scanParamKey (
|
||||
key: string,
|
||||
val: any,
|
||||
activeTag: Ref<MasterTag>,
|
||||
masterTag: Ref<MasterTag>,
|
||||
memberOfRef: string | undefined,
|
||||
requiredParams: string[],
|
||||
h: Hierarchy,
|
||||
getOrAddSlot: (id: string, model: DetailedSlotModel) => string,
|
||||
m: ModelDb
|
||||
): void {
|
||||
if (key.startsWith('$') || key === '_id' || key === '_class' || requiredParams.includes(key)) return
|
||||
|
||||
if (typeof val === 'string' && val.includes('$userRequest')) {
|
||||
return
|
||||
}
|
||||
|
||||
const keyAttr = (h.findAttribute(activeTag, key) ?? h.findAttribute(masterTag, key)) as Attribute<any> | undefined
|
||||
if (keyAttr !== undefined) {
|
||||
// For system attributes, only slotify if the value is contextual/dynamic
|
||||
const parentClassId = normalizeId(keyAttr.attributeOf) ?? ''
|
||||
if (isSystemId(parentClassId) && !isContextualValue(val, activeTag, masterTag, h, m)) {
|
||||
return
|
||||
}
|
||||
getOrAddSlot(key, attributeToSlot(keyAttr, memberOfRef))
|
||||
} else if (key.length >= 24) {
|
||||
getOrAddSlot(key, unknownToSlot(key, memberOfRef))
|
||||
}
|
||||
}
|
||||
|
||||
/** Scans a string param value for entity references and registers them as slots. */
|
||||
function scanParamValue (
|
||||
val: string,
|
||||
activeTag: Ref<MasterTag>,
|
||||
masterTag: Ref<MasterTag>,
|
||||
memberOfRef: string | undefined,
|
||||
direction: string | undefined,
|
||||
slots: Record<string, SlotModel>,
|
||||
getOrAddSlot: (id: string, model: DetailedSlotModel) => string,
|
||||
m: ModelDb,
|
||||
h: Hierarchy
|
||||
): void {
|
||||
const potentialIds = extractPotentialIds(val)
|
||||
|
||||
for (const id of potentialIds) {
|
||||
const obj = h.findAttribute(activeTag, id) ?? h.findAttribute(masterTag, id) ?? m.findObject(id as any)
|
||||
|
||||
if (obj === undefined) {
|
||||
// Handle already-normalized slot references
|
||||
if (id.startsWith('__SLOT_')) {
|
||||
const slotName = id.replace(/^__SLOT_(.+)__$/, '$1')
|
||||
const existing = slots[slotName]
|
||||
if (existing !== undefined) {
|
||||
const slotModel: SlotModel = { ...existing, name: slotName, memberOf: memberOfRef }
|
||||
getOrAddSlot(id, slotModel)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
classifyAndRegisterSlot(id, obj, direction, memberOfRef, masterTag, getOrAddSlot, m, h)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies a resolved object by its type and registers it as the appropriate slot kind.
|
||||
*/
|
||||
function classifyAndRegisterSlot (
|
||||
id: string,
|
||||
obj: Doc,
|
||||
direction: string | undefined,
|
||||
memberOfRef: string | undefined,
|
||||
masterTag: Ref<MasterTag>,
|
||||
getOrAddSlot: (id: string, model: DetailedSlotModel) => string,
|
||||
m: ModelDb,
|
||||
h: Hierarchy
|
||||
): void {
|
||||
const classId = normalizeId(obj._class)
|
||||
|
||||
if (classId === normalizeId(processPlugin.class.Process)) {
|
||||
const subProc = obj as any as Process
|
||||
const procMemberOf = resolveParentSlot(subProc.masterTag, masterTag, memberOfRef, getOrAddSlot, m)
|
||||
getOrAddSlot(id, processToSlot(subProc, procMemberOf))
|
||||
} else if (classId === normalizeId(core.class.Association)) {
|
||||
const assoc = obj as any as Association
|
||||
const targetClass = direction === 'A' ? assoc.classA : assoc.classB
|
||||
const assocMemberOf = resolveParentSlot(targetClass, masterTag, memberOfRef, getOrAddSlot, m)
|
||||
getOrAddSlot(id, associationToSlot(assoc, direction, assocMemberOf))
|
||||
} else if (classId === normalizeId(core.class.Attribute)) {
|
||||
getOrAddSlot(id, attributeToSlot(obj as Attribute<any>, memberOfRef))
|
||||
} else if (classId === normalizeId(core.class.Class) || h.isDerived(obj._class, core.class.Class)) {
|
||||
getOrAddSlot(id, classToSlot(obj as any as Class<any>, memberOfRef))
|
||||
} else {
|
||||
getOrAddSlot(id, unknownToSlot(id, memberOfRef))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +266,8 @@ export default mergeIds(processId, process, {
|
||||
LockField: '' as IntlString,
|
||||
UnlockField: '' as IntlString,
|
||||
Export: '' as IntlString,
|
||||
ExportWithSlots: '' as IntlString,
|
||||
ExportWithoutSlots: '' as IntlString,
|
||||
Import: '' as IntlString,
|
||||
TextFromIdentifier: '' as IntlString,
|
||||
TextFromNumber: '' as IntlString,
|
||||
@@ -283,7 +285,9 @@ export default mergeIds(processId, process, {
|
||||
DateDifference: '' as IntlString,
|
||||
TextFromSelect: '' as IntlString,
|
||||
SelectFromText: '' as IntlString,
|
||||
AskSubclass: '' as IntlString
|
||||
AskSubclass: '' as IntlString,
|
||||
RequiredSlots: '' as IntlString,
|
||||
Bindings: '' as IntlString
|
||||
},
|
||||
permission: {
|
||||
RunProcess: '' as Ref<Permission>,
|
||||
|
||||
@@ -63,13 +63,18 @@ import { showPopup } from '@hcengineering/ui'
|
||||
import { type AttributeCategory } from '@hcengineering/view'
|
||||
import process from './plugin'
|
||||
|
||||
export function isTypeEqual (toCheck: Type<any> | undefined, attr: Type<any>): boolean {
|
||||
const skip = ['label', 'icon', 'hidden', 'readonly']
|
||||
export function isTypeEqual (toCheck: any | undefined, attr: Type<any>): boolean {
|
||||
if (toCheck === undefined) return true
|
||||
if (Object.keys(attr).length !== Object.keys(toCheck).length) return true
|
||||
for (const key of Object.keys(attr)) {
|
||||
if (skip.includes(key)) continue
|
||||
if (toCheck[key as keyof Type<any>] !== attr[key as keyof Type<any>]) return false
|
||||
const check = toCheck.type !== undefined ? toCheck.type : toCheck
|
||||
if (check._class !== attr._class) return false
|
||||
if (check._class === core.class.RefTo) {
|
||||
return (check as RefTo<Doc>).to === (attr as RefTo<Doc>).to
|
||||
}
|
||||
if (check._class === core.class.ArrOf) {
|
||||
return isTypeEqual((check as ArrOf<Doc>).of, (attr as ArrOf<Doc>).of)
|
||||
}
|
||||
if (check._class === core.class.EnumOf) {
|
||||
return check.of === (attr as any).of
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -24,6 +24,19 @@ import { SelectedExecutionContext } from './types'
|
||||
*/
|
||||
export const processId = 'process' as Plugin
|
||||
|
||||
export interface SlotModel {
|
||||
slotKind: 'attribute' | 'class' | 'association' | 'process' | 'unknown'
|
||||
_class: Ref<Class<Doc>>
|
||||
label?: IntlString
|
||||
name?: string
|
||||
memberOf?: string
|
||||
}
|
||||
|
||||
export interface AttributeSlotModel extends SlotModel {
|
||||
slotKind: 'attribute'
|
||||
type: Type<any>
|
||||
}
|
||||
|
||||
// Process model dscription
|
||||
export interface Process extends Doc {
|
||||
masterTag: Ref<MasterTag | Tag>
|
||||
@@ -34,6 +47,8 @@ export interface Process extends Doc {
|
||||
automationOnly?: boolean
|
||||
context: Record<ContextId, ProcessContext>
|
||||
resultType?: Type<any>
|
||||
requiredSlots?: Record<string, SlotModel>
|
||||
bindings?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface ProcessContext {
|
||||
|
||||
@@ -50,6 +50,7 @@ import process, {
|
||||
import { ExecuteResult, ProcessControl, SuccessExecutionContext } from '@hcengineering/server-process'
|
||||
import { isEmptyMarkup } from '@hcengineering/text-core'
|
||||
import time, { ToDoPriority } from '@hcengineering/time'
|
||||
import { resolveAttributeId } from './utils'
|
||||
|
||||
function checkResult (execution: Execution, results: Record<string, any> | undefined): boolean {
|
||||
if (results === undefined) return true
|
||||
@@ -148,16 +149,23 @@ export function MatchCardCheck (
|
||||
if (card === undefined) return false
|
||||
const process = control.client.getModel().findObject(execution.process)
|
||||
if (process === undefined) return false
|
||||
const markup = getMarkupParams(process, params, control)
|
||||
|
||||
const h = control.client.getHierarchy()
|
||||
if (h.isMixin(process.masterTag)) {
|
||||
card = h.as(card, process.masterTag)
|
||||
}
|
||||
|
||||
const resolvedParams: Record<string, any> = {}
|
||||
for (const key in params) {
|
||||
resolvedParams[resolveAttributeId(process, key)] = params[key]
|
||||
}
|
||||
const markup = getMarkupParams(process, resolvedParams, control)
|
||||
|
||||
for (const key of Object.keys(markup)) {
|
||||
if (isEmptyMarkup(card[key])) return false
|
||||
}
|
||||
|
||||
const res = matchQuery([card], params, process.masterTag, control.client.getHierarchy(), true)
|
||||
const res = matchQuery([card], resolvedParams, process.masterTag, control.client.getHierarchy(), true)
|
||||
return res.length > 0
|
||||
}
|
||||
|
||||
@@ -189,23 +197,25 @@ export function FieldChangedCheck (
|
||||
context: Record<string, any>
|
||||
): boolean {
|
||||
if (context.card === undefined) return false
|
||||
const process = control.client.getModel().findObject(execution.process)
|
||||
if (process === undefined) return false
|
||||
const _process = control.client.getModel().findObject(execution.process)
|
||||
if (_process === undefined) return false
|
||||
if (context.operations === undefined) return false
|
||||
const operations = context.operations as DocumentUpdate<Doc>
|
||||
const target = Object.keys(params)[0]
|
||||
if (!TxProcessor.hasUpdate(operations, target)) return false
|
||||
const markup = getMarkupParams(process, params, control)
|
||||
const h = control.client.getHierarchy()
|
||||
let card = context.card
|
||||
if (h.isMixin(process.masterTag)) {
|
||||
card = h.as(card, process.masterTag)
|
||||
if (h.isMixin(_process.masterTag)) {
|
||||
card = h.as(card, _process.masterTag)
|
||||
}
|
||||
const realTarget = resolveAttributeId(_process, target)
|
||||
if (!TxProcessor.hasUpdate(operations, realTarget)) return false
|
||||
const resolvedParams = { [realTarget]: params[target] }
|
||||
const markup = getMarkupParams(_process, resolvedParams, control)
|
||||
for (const key of Object.keys(markup)) {
|
||||
if (isEmptyMarkup(card[key])) return false
|
||||
}
|
||||
|
||||
const res = matchQuery([card], params, process.masterTag, control.client.getHierarchy(), true)
|
||||
const res = matchQuery([card], resolvedParams, _process.masterTag, control.client.getHierarchy(), true)
|
||||
return res.length > 0
|
||||
}
|
||||
|
||||
@@ -292,13 +302,14 @@ export async function UpdateCard (
|
||||
const update: Record<string, any> = {}
|
||||
const prevValue: Record<string, any> = {}
|
||||
for (const key in params) {
|
||||
const prevKey = checkMixinKey(key, _process.masterTag, hierarchy)
|
||||
prevValue[key] = getObjectValue(prevKey, target)
|
||||
const attr = hierarchy.findAttribute(_process.masterTag, key)
|
||||
const realKey = resolveAttributeId(_process, key)
|
||||
const prevKey = checkMixinKey(realKey, _process.masterTag, hierarchy)
|
||||
prevValue[realKey] = getObjectValue(prevKey, target)
|
||||
const attr = hierarchy.findAttribute(_process.masterTag, realKey)
|
||||
if (attr === undefined) {
|
||||
update[key] = (params as any)[key]
|
||||
update[realKey] = (params as any)[key]
|
||||
} else {
|
||||
update[key] = respectAttributeType(attr.type, (params as any)[key])
|
||||
update[realKey] = respectAttributeType(attr.type, (params as any)[key])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,6 +503,10 @@ export async function RequestApproval (
|
||||
const group = generateId()
|
||||
const res: TxCreateDoc<ApproveRequest>[] = []
|
||||
const rollback: Tx[] = []
|
||||
const _process = control.client.getModel().findObject(execution.process)
|
||||
if (_process === undefined) {
|
||||
throw processError(process.error.RequiredParamsNotProvided, { params: 'user' })
|
||||
}
|
||||
for (const user of Array.isArray(params.user) ? params.user : [params.user]) {
|
||||
const id = generateId<ApproveRequest>()
|
||||
const tx = control.client.txFactory.createTxCreateDoc(
|
||||
@@ -516,7 +531,7 @@ export async function RequestApproval (
|
||||
results,
|
||||
group,
|
||||
actionType: params.actionType,
|
||||
field: (params as any).field
|
||||
field: resolveAttributeId(_process, (params as any).field)
|
||||
},
|
||||
id
|
||||
)
|
||||
@@ -728,6 +743,9 @@ export async function CreateToDo (
|
||||
const res: Tx[] = []
|
||||
const rollback: Tx[] = []
|
||||
const id = generateId<ProcessToDo>()
|
||||
const _process = control.client.getModel().findObject(execution.process)
|
||||
if (_process === undefined) return { txes: [], rollback: [], context: null }
|
||||
const field = resolveAttributeId(_process, (params as any).field)
|
||||
const tx = control.client.txFactory.createTxCreateDoc(
|
||||
process.class.ProcessToDo,
|
||||
time.space.ToDos,
|
||||
@@ -747,7 +765,7 @@ export async function CreateToDo (
|
||||
rank: '',
|
||||
withRollback: params.withRollback ?? false,
|
||||
results,
|
||||
field: (params as any).field
|
||||
field
|
||||
},
|
||||
id
|
||||
)
|
||||
@@ -832,13 +850,21 @@ export async function CreateCard (
|
||||
throw processError(process.error.RequiredParamsNotProvided, { params: key })
|
||||
}
|
||||
}
|
||||
const _process = control.client.getModel().findObject(execution.process)
|
||||
if (_process === undefined) {
|
||||
throw processError(process.error.RequiredParamsNotProvided, {})
|
||||
}
|
||||
const resolvedAttrs: Record<string, any> = {}
|
||||
for (const key in attrs) {
|
||||
resolvedAttrs[resolveAttributeId(_process, key)] = (attrs as any)[key]
|
||||
}
|
||||
const masterTag = _class as Ref<MasterTag>
|
||||
const _id = generateId<Card>()
|
||||
const newContent =
|
||||
content !== undefined && !isEmpty(content) ? await getContent(control, content, _id, masterTag) : content
|
||||
const data = {
|
||||
title,
|
||||
...attrs
|
||||
...resolvedAttrs
|
||||
} as any
|
||||
if (newContent !== undefined) {
|
||||
data.content = newContent
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
|
||||
import process, {
|
||||
Execution,
|
||||
parseContext,
|
||||
Process,
|
||||
ProcessError,
|
||||
processError,
|
||||
SelectedConst,
|
||||
@@ -67,11 +68,19 @@ export async function getContextValue (value: any, control: ProcessControl, exec
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveAttributeId<T extends string> (_process: Process, key: T): T {
|
||||
if (_process.bindings?.[key] !== undefined) {
|
||||
return _process.bindings[key] as T
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
function getValue (control: ProcessControl, execution: Execution, rawKey: string, card: Doc): any {
|
||||
const hierarchy = control.client.getHierarchy()
|
||||
const _process = control.client.getModel().findObject(execution.process)
|
||||
if (_process === undefined) throw processError(process.error.ObjectNotFound, { _id: execution.process })
|
||||
const key = checkMixinKey(rawKey, _process.masterTag, hierarchy)
|
||||
const realKey = resolveAttributeId(_process, rawKey)
|
||||
const key = checkMixinKey(realKey, _process.masterTag, hierarchy)
|
||||
return getObjectValue(key, card)
|
||||
}
|
||||
|
||||
@@ -119,8 +128,11 @@ async function fillValue (
|
||||
async function getNestedValue (control: ProcessControl, execution: Execution, context: SelectedNested): Promise<any> {
|
||||
const card = control.cache.get(execution.card)
|
||||
if (card === undefined) throw processError(process.error.ObjectNotFound, { _id: execution.card }, {}, true)
|
||||
const attr = control.client.getHierarchy().findAttribute(card._class, context.path)
|
||||
if (attr === undefined) throw processError(process.error.AttributeNotExists, { key: context.path })
|
||||
const _process = control.client.getModel().findObject(execution.process)
|
||||
if (_process === undefined) throw processError(process.error.ObjectNotFound, { _id: execution.process })
|
||||
const realPath = resolveAttributeId(_process, context.path)
|
||||
const attr = control.client.getHierarchy().findAttribute(card._class, realPath)
|
||||
if (attr === undefined) throw processError(process.error.AttributeNotExists, { key: realPath })
|
||||
const nestedValue = getValue(control, execution, context.path, card)
|
||||
if (nestedValue === undefined) throw processError(process.error.EmptyAttributeContextValue, {}, { attr: attr.label })
|
||||
const parentType = attr.type._class === core.class.ArrOf ? (attr.type as ArrOf<Doc>).of : attr.type
|
||||
@@ -130,7 +142,8 @@ async function getNestedValue (control: ProcessControl, execution: Execution, co
|
||||
_id: { $in: refs }
|
||||
})
|
||||
if (target.length === 0) throw processError(process.error.RelatedObjectNotFound, {}, { attr: attr.label })
|
||||
const nested = control.client.getHierarchy().findAttribute(targetClass, context.key)
|
||||
const realKey = resolveAttributeId(_process, context.key)
|
||||
const nested = control.client.getHierarchy().findAttribute(targetClass, realKey)
|
||||
if (context.sourceFunction !== undefined) {
|
||||
const transform = control.client.getModel().findObject(context.sourceFunction.func)
|
||||
if (transform === undefined) {
|
||||
@@ -172,7 +185,10 @@ async function getRelationValue (
|
||||
execution: Execution,
|
||||
context: SelectedRelation
|
||||
): Promise<any> {
|
||||
const assoc = control.client.getModel().findObject(context.association)
|
||||
const _process = control.client.getModel().findObject(execution.process)
|
||||
if (_process === undefined) throw processError(process.error.ObjectNotFound, { _id: execution.process })
|
||||
const realAssociation = resolveAttributeId(_process, context.association)
|
||||
const assoc = control.client.getModel().findObject(realAssociation)
|
||||
if (assoc === undefined) throw processError(process.error.RelationNotExists, {})
|
||||
const targetClass = context.direction === 'A' ? assoc.classA : assoc.classB
|
||||
const q = context.direction === 'A' ? { docB: execution.card } : { docA: execution.card }
|
||||
@@ -188,7 +204,8 @@ async function getRelationValue (
|
||||
})
|
||||
const target = await control.client.findAll(targetClass, { _id: { $in: ids } })
|
||||
if (target.length === 0) throw processError(process.error.RelatedObjectNotFound, { attr: context.name })
|
||||
const attr = context.key !== '' ? control.client.getHierarchy().findAttribute(targetClass, context.key) : undefined
|
||||
const realKey = resolveAttributeId(_process, context.key)
|
||||
const attr = realKey !== '' ? control.client.getHierarchy().findAttribute(targetClass, realKey) : undefined
|
||||
if (context.sourceFunction !== undefined) {
|
||||
const transform = control.client.getModel().findObject(context.sourceFunction.func)
|
||||
if (transform === undefined) {
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"update-browserslist-db": "^1.1.3",
|
||||
"browserslist": "^4.28.1",
|
||||
"update-browserslist-db": "^1.2.3",
|
||||
"browserslist": "^4.28.2",
|
||||
"esbuild": "^0.25.10",
|
||||
"esbuild-loader": "^4.3.0",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -41,4 +41,4 @@
|
||||
"webpack": "*"
|
||||
},
|
||||
"#replaces": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user