feat(ui): new workflow designer

This commit is contained in:
Fu Diwei
2025-08-04 16:59:23 +08:00
parent 344e9c719e
commit cdf39a0f39
44 changed files with 2351 additions and 484 deletions
+282 -265
View File
@@ -1,25 +1,25 @@
{
"name": "ui",
"name": "@certimate/webui",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ui",
"version": "0.0.0",
"name": "@certimate/webui",
"dependencies": {
"@ant-design/pro-components": "^2.8.10",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.2",
"@codemirror/legacy-modes": "^6.5.1",
"@flowgram.ai/fixed-layout-editor": "^0.2.28",
"@flowgram.ai/document": "^0.3.2",
"@flowgram.ai/fixed-layout-editor": "^0.3.2",
"@tabler/icons-react": "^3.34.1",
"@uiw/codemirror-extensions-basic-setup": "^4.24.1",
"@uiw/codemirror-theme-vscode": "^4.24.1",
"@uiw/react-codemirror": "^4.24.1",
"@uiw/codemirror-extensions-basic-setup": "^4.24.2",
"@uiw/codemirror-theme-vscode": "^4.24.2",
"@uiw/react-codemirror": "^4.24.2",
"ahooks": "^3.9.0",
"antd": "^5.26.6",
"antd": "^5.26.7",
"antd-zod": "^7.0.0",
"clsx": "^2.1.1",
"cron-parser": "^5.3.0",
@@ -36,8 +36,8 @@
"react-i18next": "^15.6.1",
"react-router-dom": "^7.7.1",
"tailwind-merge": "^3.3.1",
"zod": "^4.0.10",
"zustand": "^5.0.6"
"zod": "^4.0.14",
"zustand": "^5.0.7"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
@@ -54,7 +54,7 @@
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-better-tailwindcss": "^3.7.2",
"eslint-plugin-better-tailwindcss": "^3.7.3",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-react-hooks": "^5.2.0",
@@ -2784,12 +2784,12 @@
}
},
"node_modules/@flowgram.ai/background-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/background-plugin/-/background-plugin-0.2.28.tgz",
"integrity": "sha512-GEH0e1TioFQuKTZli7UO0yz171xs2T2XAnoWdxGhuWTcZ3PVyJSwJSF6BPDxfWGgN3kL9JeghbCPgElhwEOKGg==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/background-plugin/-/background-plugin-0.3.2.tgz",
"integrity": "sha512-3BeVeZ3bvhqIfsSVuUpxMBdOON1eY9E+7SlAvs1iWbEgzn8LVg7i6lXdl0BIVlUEIthl7aLH7uxrbb/MAj9Fxg==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2"
},
@@ -2799,11 +2799,11 @@
}
},
"node_modules/@flowgram.ai/command": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/command/-/command-0.2.28.tgz",
"integrity": "sha512-/OMswJnXKm6aHIjwOQfWzCeYCr19x6gA8YNY8z9BpWJa3mPpPMUKjigldvOZVq/LZ3UPLj4EtbW7u4yyfsNCgA==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/command/-/command-0.3.2.tgz",
"integrity": "sha512-P6tuALZrSzjGBAB3BttJWumtmN0NcBJ88dHGsWStMMDW81LYCFKCsybB2wU0YoGVV7viRjCe/szSoQAbp8B2NQ==",
"dependencies": {
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2"
},
@@ -2813,12 +2813,12 @@
}
},
"node_modules/@flowgram.ai/core": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/core/-/core-0.2.28.tgz",
"integrity": "sha512-iVHIGHZTjoI0OJCf2m2ch70jkopWu6P+G1wy4cYMWqWI89lz2grtAEd7Qg8vRHQameLAdoQnAHuSkAXpIIKcaA==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/core/-/core-0.3.2.tgz",
"integrity": "sha512-pb+YP2DKIrTP2woQEbt7cKWnqwXObsMtGhS0DuRgbCIN1+6fLXFjvN2M2QhMRsOPiqY60dWnZWLKtagwPVUDqA==",
"dependencies": {
"@flowgram.ai/command": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/command": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"@phosphor/messaging": "^1.3.0",
"@tweenjs/tween.js": "^18",
"clsx": "^1.1.1",
@@ -2858,12 +2858,12 @@
}
},
"node_modules/@flowgram.ai/document": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/document/-/document-0.2.28.tgz",
"integrity": "sha512-HzEG5EZNdtooAaGjCCd4k9cjxoUaMs0j2dowVA2/z8CO42ylqLnk+GJqr8jE+gPxS4muwjxdzEa/OXcgsrlqhw==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/document/-/document-0.3.2.tgz",
"integrity": "sha512-N+VAQzZcNt8PUfB/32cGzMFtUUiKGTzCFMFMElB9ffSWG2stHgueRO3kBWCD8yweArfUofhCTk3eERJ345dj3A==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"lodash": "^4.17.21",
"nanoid": "^4.0.2",
@@ -2888,29 +2888,29 @@
}
},
"node_modules/@flowgram.ai/editor": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/editor/-/editor-0.2.28.tgz",
"integrity": "sha512-QMSuOfFXZHHtnjuuX/pDqoGn/iCKjD6I05wcKUn0LqqbFgyYKp49Ju77IS9rGBs6R1Cj0jqFPl3OLW50sFqLEg==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/editor/-/editor-0.3.2.tgz",
"integrity": "sha512-Vdq53nKUoHQKWtVOmMRcI3DgS8U8pW2jFQRklYfAVhmLxyw4636ZplF8F3/AiAbt5tJ7P9OOs5xI456U1U2dqQ==",
"dependencies": {
"@flowgram.ai/background-plugin": "0.2.28",
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/form": "0.2.28",
"@flowgram.ai/form-core": "0.2.28",
"@flowgram.ai/group-plugin": "0.2.28",
"@flowgram.ai/history": "0.2.28",
"@flowgram.ai/history-node-plugin": "0.2.28",
"@flowgram.ai/i18n-plugin": "0.2.28",
"@flowgram.ai/materials-plugin": "0.2.28",
"@flowgram.ai/node": "0.2.28",
"@flowgram.ai/node-core-plugin": "0.2.28",
"@flowgram.ai/node-variable-plugin": "0.2.28",
"@flowgram.ai/playground-react": "0.2.28",
"@flowgram.ai/redux-devtool-plugin": "0.2.28",
"@flowgram.ai/renderer": "0.2.28",
"@flowgram.ai/shortcuts-plugin": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/variable-plugin": "0.2.28",
"@flowgram.ai/background-plugin": "0.3.2",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/form": "0.3.2",
"@flowgram.ai/form-core": "0.3.2",
"@flowgram.ai/group-plugin": "0.3.2",
"@flowgram.ai/history": "0.3.2",
"@flowgram.ai/history-node-plugin": "0.3.2",
"@flowgram.ai/i18n-plugin": "0.3.2",
"@flowgram.ai/materials-plugin": "0.3.2",
"@flowgram.ai/node": "0.3.2",
"@flowgram.ai/node-core-plugin": "0.3.2",
"@flowgram.ai/node-variable-plugin": "0.3.2",
"@flowgram.ai/playground-react": "0.3.2",
"@flowgram.ai/redux-devtool-plugin": "0.3.2",
"@flowgram.ai/renderer": "0.3.2",
"@flowgram.ai/shortcuts-plugin": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"@flowgram.ai/variable-plugin": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2"
},
@@ -2920,13 +2920,13 @@
}
},
"node_modules/@flowgram.ai/fixed-drag-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-drag-plugin/-/fixed-drag-plugin-0.2.28.tgz",
"integrity": "sha512-dNrFBd6G1uFgkhf0LpAC9/7vV8kIQCBuBAmbeUi6EQZkmWsp9rVd4SAdKGqhNLrs6klt8n5oMltv6WJsMCRS6Q==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-drag-plugin/-/fixed-drag-plugin-0.3.2.tgz",
"integrity": "sha512-XK09rklrkU1EzhmqFBsm/zZGcnnb4LXhNMkol2fDviiq2Aq0UumXL2jRjRaWcFQJVjBawlOd3YciTamSY6xNgg==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/renderer": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/renderer": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2"
},
@@ -2936,29 +2936,29 @@
}
},
"node_modules/@flowgram.ai/fixed-history-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-history-plugin/-/fixed-history-plugin-0.2.28.tgz",
"integrity": "sha512-s9ckjS6NS3ds+FfmvTIvO/02N8rSh+EcSDyKwLaGcrYk6X6Fs1T5h/PyBgGTryJ7EzseqlLTFVBqYXzgbymDtg==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-history-plugin/-/fixed-history-plugin-0.3.2.tgz",
"integrity": "sha512-27+z5s+9LeRBG+sk5e6mPuqu2dw4nBFV49ouNe/l11r+A+w5j6haZdX2kLR0wrWdJWDu7Dn0y9MQyxwPSyVEmg==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/form-core": "0.2.28",
"@flowgram.ai/history": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/form-core": "0.3.2",
"@flowgram.ai/history": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"lodash": "^4.17.21",
"reflect-metadata": "~0.2.2"
}
},
"node_modules/@flowgram.ai/fixed-layout-core": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-layout-core/-/fixed-layout-core-0.2.28.tgz",
"integrity": "sha512-pIa7vaksAmBZm3aaDMkpxB8RsS+fHGntDjGBWuP5tKJkjMKSXyPDzA7vGOwcVLyX3OrYiMD8ypuCDOGPruAr2A==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-layout-core/-/fixed-layout-core-0.3.2.tgz",
"integrity": "sha512-e+J/oxFs4HD8FOdMHh79bXLEp+7gzoCk+42fMTAJ4Or4HEIkep8gRTpFxB0fINnWxpw3CpZZFnhMLIuyR5R46g==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/renderer": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/renderer": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"lodash": "^4.17.21",
"reflect-metadata": "~0.2.2"
@@ -2969,19 +2969,19 @@
}
},
"node_modules/@flowgram.ai/fixed-layout-editor": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-layout-editor/-/fixed-layout-editor-0.2.28.tgz",
"integrity": "sha512-PpLQXWjC2L7eqVzNdt7Z4DxEh6GcIJyTrOQE0FjUB3XuS2IW7ciHogs3gEfAVpoFbZGj8tzIcs6IMUu1GxjeqQ==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/fixed-layout-editor/-/fixed-layout-editor-0.3.2.tgz",
"integrity": "sha512-wZxyYOt4f9CFX+iUiq5J4tyn4K9YmfcprYs26i7CB2D542vvpREVWXzl/FdTiylcW6qTTGcFAsQkdVdmd33xEg==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/editor": "0.2.28",
"@flowgram.ai/fixed-drag-plugin": "0.2.28",
"@flowgram.ai/fixed-history-plugin": "0.2.28",
"@flowgram.ai/fixed-layout-core": "0.2.28",
"@flowgram.ai/history": "0.2.28",
"@flowgram.ai/reactive": "0.2.28",
"@flowgram.ai/select-box-plugin": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/editor": "0.3.2",
"@flowgram.ai/fixed-drag-plugin": "0.3.2",
"@flowgram.ai/fixed-history-plugin": "0.3.2",
"@flowgram.ai/fixed-layout-core": "0.3.2",
"@flowgram.ai/history": "0.3.2",
"@flowgram.ai/reactive": "0.3.2",
"@flowgram.ai/select-box-plugin": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2"
},
@@ -2991,12 +2991,12 @@
}
},
"node_modules/@flowgram.ai/form": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/form/-/form-0.2.28.tgz",
"integrity": "sha512-OgMDayBmHDUEjb6xWZeDmyn8BKZZazEsas3WJKlm2r6zaobygxE5K5NzJ/oy3OyLTgMtpYq3JOWZPgGu16ZRHg==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/form/-/form-0.3.2.tgz",
"integrity": "sha512-OHp6IhOQUcW3Hw/BWrxi4Kn/UTB96BkQgjl2mQ0o+HyqTwzGa9wNtLkNWqIK/CogD/l31kRp39L6rFH71S2AgQ==",
"dependencies": {
"@flowgram.ai/reactive": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/reactive": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"fast-equals": "^2.0.0",
"lodash": "^4.17.21",
"nanoid": "^4.0.2"
@@ -3007,13 +3007,13 @@
}
},
"node_modules/@flowgram.ai/form-core": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/form-core/-/form-core-0.2.28.tgz",
"integrity": "sha512-JpapezHkKEl4hgNnhBo/5mBUcgAv7aUQpp3ypZ0g/0L9qLd9vB3PjBw7mL+K4t+954ckJMLW0AHJyqeWKXpO4g==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/form-core/-/form-core-0.3.2.tgz",
"integrity": "sha512-RjipVRN/pKqDF27vL4GhtRpRtlcDlZZgv2chEehd1tnedJRmgH30REN3ET9ya+cF2Lte9z9ALdAD6KMfCqdySw==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"lodash": "^4.17.21",
"reflect-metadata": "~0.2.2"
@@ -3041,16 +3041,16 @@
}
},
"node_modules/@flowgram.ai/free-layout-core": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/free-layout-core/-/free-layout-core-0.2.28.tgz",
"integrity": "sha512-oGLr4dTggl1e7h/usm0Hs4uYjiAKOU8ZY6W9xpEgvXJkRngiGDgh1+S3Sh7WfxvDQeHkaQhGRgcKNqECGqL1LQ==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/free-layout-core/-/free-layout-core-0.3.2.tgz",
"integrity": "sha512-xvoneqfmNqsfzLotqdikBOfMDXC173G0Gqs5XxDZv3fpCuUMyy3eI0KrnoJZMIp6BD5buDmBrm6vk9D8vE1WnQ==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/form-core": "0.2.28",
"@flowgram.ai/node": "0.2.28",
"@flowgram.ai/reactive": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/form-core": "0.3.2",
"@flowgram.ai/node": "0.3.2",
"@flowgram.ai/reactive": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2",
@@ -3079,14 +3079,14 @@
}
},
"node_modules/@flowgram.ai/group-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/group-plugin/-/group-plugin-0.2.28.tgz",
"integrity": "sha512-Gk4A5oCOxF45Htf68OIR2yZyQR2416HMDsDISWCuAZiV/LafGDSNwRWiUv/6tpyUpwT1uHSipkTFSRwAWApO8g==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/group-plugin/-/group-plugin-0.3.2.tgz",
"integrity": "sha512-9fn4Sdu6uDqUPz00VBJu8F4CEdkQECg2ZIDETZLU8iG49eeX1qCGPOai7MRMGY83LcEDIR0Sk5w0Z7i4wM5mXQ==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/renderer": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/renderer": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2"
},
@@ -3096,12 +3096,12 @@
}
},
"node_modules/@flowgram.ai/history": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/history/-/history-0.2.28.tgz",
"integrity": "sha512-Em5yq2ZmP5lRglrdIfHe54zt3PSMM3BUj95jge5rC1uxNes7vhu5jaKhVIK6aehu9m4pOY8ZJ7/Dudr3w04aJQ==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/history/-/history-0.3.2.tgz",
"integrity": "sha512-dL4v3tbUkZ4q4ph3LsCHQV5gV+kyYoPfjcR+ok4SyMoLzOg46YsAri7KVhNDR2El77EzGDPiCZ+9ZSQJqOX/eQ==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"lodash": "^4.17.21",
"nanoid": "^4.0.2",
@@ -3109,17 +3109,17 @@
}
},
"node_modules/@flowgram.ai/history-node-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/history-node-plugin/-/history-node-plugin-0.2.28.tgz",
"integrity": "sha512-NnFt6ocTvGwWjgluUuvJL+Xivj75LrPEFPqhC+GcmciiNmD0HkhW/ojO0aBKv8/Ppemfjo840dhSyxAxyInPsw==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/history-node-plugin/-/history-node-plugin-0.3.2.tgz",
"integrity": "sha512-Q4kMsq11Z7tpHD9mIwPN3jQMrFter7xzbMznr36G4TLRlPV9bfsWNBhLbBto1WEhbiphQ/TTEiBjWQrxlpAkXQ==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/form": "0.2.28",
"@flowgram.ai/form-core": "0.2.28",
"@flowgram.ai/history": "0.2.28",
"@flowgram.ai/node": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/form": "0.3.2",
"@flowgram.ai/form-core": "0.3.2",
"@flowgram.ai/history": "0.3.2",
"@flowgram.ai/node": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"lodash": "^4.17.21",
"reflect-metadata": "~0.2.2"
@@ -3148,32 +3148,32 @@
}
},
"node_modules/@flowgram.ai/i18n": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/i18n/-/i18n-0.2.28.tgz",
"integrity": "sha512-FsFPlLa834zTBhLKgw6PWm+SoLvsPp2xL9XiphXdjD3kDgLvAsotiO/mdGGuwC+nNMByo2MsUgf7gS7BIVMuTw==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/i18n/-/i18n-0.3.2.tgz",
"integrity": "sha512-k+9f6gwDh4YTL91BfliRL8IEgzGZ4ALBAuFmGVucpx+01NEYAoqwF1+g4u9udlXX9Ncny5DDMNNka1C3B5m46Q==",
"dependencies": {
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/utils": "0.3.2",
"i18n-js": "^4.5.1"
}
},
"node_modules/@flowgram.ai/i18n-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/i18n-plugin/-/i18n-plugin-0.2.28.tgz",
"integrity": "sha512-U0AjDZekzx7Z8ttxSI5JZWE6A2rzQncILYQ4ALTPpaS6nFqna7ju2qEedcUAIf/xo4gZiYugcQc511lvqtnhLg==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/i18n-plugin/-/i18n-plugin-0.3.2.tgz",
"integrity": "sha512-1Zyk6Sh+W62FAF7oP/p+fJ1JxF7iqpoKpOCC65VEHKVQ1TAbHg6fyOoSxNTE7wjnATvt6c3fDw7AYcevIlcRSA==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/i18n": "0.2.28"
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/i18n": "0.3.2"
}
},
"node_modules/@flowgram.ai/materials-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/materials-plugin/-/materials-plugin-0.2.28.tgz",
"integrity": "sha512-2s50E0MZwHnE4QVS564d2abd29iMv70r7cnRWeLtc6w91Y+mKHQWM5noeoi/diggIWUmj6w4pFYDnSOcN4DeGA==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/materials-plugin/-/materials-plugin-0.3.2.tgz",
"integrity": "sha512-svG1M1uMIeavXYj8UV5Mv2glcumQ7AYg4/U6xVGhMJFJmnAWNlNVQCqz5PFRkjyIH6FwW/xJVh03TSqpR2Svkw==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/form-core": "0.2.28",
"@flowgram.ai/renderer": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/form-core": "0.3.2",
"@flowgram.ai/renderer": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2"
},
@@ -3184,15 +3184,15 @@
}
},
"node_modules/@flowgram.ai/node": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/node/-/node-0.2.28.tgz",
"integrity": "sha512-XZ+kKjZdOFYdC8XL2L+BCYAa/wVLnb2dD/lfedMd9Fnf0QaJaQupwhwPnLtmL4HcdB8ro8uBICSIgVGXED66Uw==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/node/-/node-0.3.2.tgz",
"integrity": "sha512-jVr5zlL/vab7g1JSv69fIivKZAb+kPDQJugeNm9Ux9vjaF3nfssFRPeeRCj8JWMw4hqdRUnTms9Mx1udrCLXTg==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/form": "0.2.28",
"@flowgram.ai/form-core": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/form": "0.3.2",
"@flowgram.ai/form-core": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"lodash": "^4.17.21",
"nanoid": "^4.0.2",
@@ -3204,16 +3204,16 @@
}
},
"node_modules/@flowgram.ai/node-core-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/node-core-plugin/-/node-core-plugin-0.2.28.tgz",
"integrity": "sha512-72R6APzlW2Yy1od2QibIhSW3TW+xz44s0+k4rzJ+EZW0gHEwtmo+OCQLo19WM0CrBFjAomTyK/2J5L6a1mp9OA==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/node-core-plugin/-/node-core-plugin-0.3.2.tgz",
"integrity": "sha512-pSOKiVXSSL4u+7REMfK57n29Kpx7iuH///FAU9/1VdpjtnwLH25F/RBF8fyR+VLqdtwTfhqz2A7OjGMqIardmg==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/form-core": "0.2.28",
"@flowgram.ai/node": "0.2.28",
"@flowgram.ai/renderer": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/form-core": "0.3.2",
"@flowgram.ai/node": "0.3.2",
"@flowgram.ai/renderer": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2"
},
@@ -3224,16 +3224,16 @@
}
},
"node_modules/@flowgram.ai/node-variable-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/node-variable-plugin/-/node-variable-plugin-0.2.28.tgz",
"integrity": "sha512-3GpLS6LG1w6Y24TJrXnxSa8BnMhs5sgjqv9VgBdx5DFQRynNtRV52bd3X+eNarNFMMcpEqTGiybrvRqmf84K/w==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/node-variable-plugin/-/node-variable-plugin-0.3.2.tgz",
"integrity": "sha512-GjSqMXv3IqZs6mbiSbHTk/anLQFHl6ozdqM1iCZoOn4V15HW7gJMvXHe3VGwsGxAunsgrbCkkciiZmayWSN9Xg==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/form-core": "0.2.28",
"@flowgram.ai/node": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/variable-plugin": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/form-core": "0.3.2",
"@flowgram.ai/node": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"@flowgram.ai/variable-plugin": "0.3.2",
"inversify": "^6.0.1",
"lodash": "^4.17.21",
"reflect-metadata": "~0.2.2"
@@ -3262,14 +3262,14 @@
}
},
"node_modules/@flowgram.ai/playground-react": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/playground-react/-/playground-react-0.2.28.tgz",
"integrity": "sha512-VvUewy+Av1XHeUa5CBPumkSZR3J/M0m4ME9na6hTfzs7bigKjU8rk+X7397JOWswUBBxGod3Lrw+ujttxxvNag==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/playground-react/-/playground-react-0.3.2.tgz",
"integrity": "sha512-dbuuLF+UKCaIgvlVLd1TbM9t13bePKc+BrFRcYvriXZTAbKI6XvWUD7kQ1hIydU5GgTrhpZEWR6WnrxAvEBlrA==",
"dependencies": {
"@flowgram.ai/background-plugin": "0.2.28",
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/shortcuts-plugin": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/background-plugin": "0.3.2",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/shortcuts-plugin": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2"
},
@@ -3279,11 +3279,11 @@
}
},
"node_modules/@flowgram.ai/reactive": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/reactive/-/reactive-0.2.28.tgz",
"integrity": "sha512-WC8FWI4iBhrrip8If1ghFUhqBxGcHExE4rc6Erx1QQqN7YNFII5/cKF1wAaEgQ99rSVGFxIxjkwrUOjNXnnS6g==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/reactive/-/reactive-0.3.2.tgz",
"integrity": "sha512-U39SOidMenXvcKRgWZ01L7NGCefDwE90S7nil1/1yJ2bqAT9YLpmIY07Sw474sepeZgx2R9HDStuVpLpZOgq0Q==",
"dependencies": {
"@flowgram.ai/utils": "0.2.28"
"@flowgram.ai/utils": "0.3.2"
},
"peerDependencies": {
"react": ">=16.8",
@@ -3291,12 +3291,12 @@
}
},
"node_modules/@flowgram.ai/redux-devtool-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/redux-devtool-plugin/-/redux-devtool-plugin-0.2.28.tgz",
"integrity": "sha512-D8C0sWzGAFpFElacEHwONRr2vezcLa3jT6SRtYEbQ0poSA6HLPPYSigRjug8A19jd9NayCVulPm1M59GpuZF3Q==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/redux-devtool-plugin/-/redux-devtool-plugin-0.3.2.tgz",
"integrity": "sha512-yPeQYWK3juW+TeSWYDxjcpjSH1MlbXGpumKaoX4dFfoeNatVVzH77z3R345jLeJv2SwB0bHC7djfp6Dlb2vIsQ==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/variable-core": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/variable-core": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2",
"styled-components": "^5"
@@ -3351,14 +3351,14 @@
}
},
"node_modules/@flowgram.ai/renderer": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/renderer/-/renderer-0.2.28.tgz",
"integrity": "sha512-7AVVZqsUFeNvq339UfUSohk0ByVDq9ocllODScbeIAVWGZyNjG0tyJyaIEoCYrSqh2l+78C+AsaaBJvOPZG5bg==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/renderer/-/renderer-0.3.2.tgz",
"integrity": "sha512-Pzc43vzAxwpXjJrUkuMopgMMxkplarSlWPw3KdBd9wSIroAslBI6i7z7x3WljfGTJPHGt50EI24tLKTZzGECeQ==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/i18n": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/i18n": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"lodash": "^4.17.21",
"reflect-metadata": "~0.2.2"
@@ -3369,12 +3369,12 @@
}
},
"node_modules/@flowgram.ai/select-box-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/select-box-plugin/-/select-box-plugin-0.2.28.tgz",
"integrity": "sha512-xIPLKpT3hUtlHzvSOqeu8yqhVJDL7En8cRawKWwJjBs0nrFXEGryPnMCrsmbF1JwTFZrvKrznJacocqmiLwI3A==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/select-box-plugin/-/select-box-plugin-0.3.2.tgz",
"integrity": "sha512-mM58wToixwB/lfGtojwYPgip/sh6Go3+caEH4Eo447dlf88R6bSjWxThZG689QFsY6ZSQEfchg4GO1eYERvOwQ==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/renderer": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/renderer": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2"
},
@@ -3384,13 +3384,13 @@
}
},
"node_modules/@flowgram.ai/shortcuts-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/shortcuts-plugin/-/shortcuts-plugin-0.2.28.tgz",
"integrity": "sha512-S3XkBmZnQ4FZ+A7sD56v6L2UFOhMSwqTmA4S1G8XqMvmbfHjUjR/OxojfQw/MSd5QNygD4ikBesPoj6FMP45FQ==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/shortcuts-plugin/-/shortcuts-plugin-0.3.2.tgz",
"integrity": "sha512-D5Z3CAd61c2e0bQ9n8ULFAK1yFqV9q7UpMQ89IAu+armcT6w0XORC0CP/c7ojoBvGKYaWMH1gZf0NlQHiT6CZQ==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2",
"styled-components": "^5"
@@ -3445,9 +3445,9 @@
}
},
"node_modules/@flowgram.ai/utils": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/utils/-/utils-0.2.28.tgz",
"integrity": "sha512-HG4LpVo3gUcTp/tJCe72vZ9nByRXgkvp2wYyFFD3ZnGvEL8V3PIRwHGvBMP0UqgYBPHOZyqOZhOenP73msvkwg==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/utils/-/utils-0.3.2.tgz",
"integrity": "sha512-XVmiYft8JfrZsV5M2M8i2AMjhz6Fe4Euv0gOuLiSsZL1ZnR95IHzEHuvSd98of4srcUL+b/RcwB2BMXH+/EJ9w==",
"dependencies": {
"clsx": "^1.1.1",
"inversify": "^6.0.1",
@@ -3485,18 +3485,18 @@
}
},
"node_modules/@flowgram.ai/variable-core": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-core/-/variable-core-0.2.28.tgz",
"integrity": "sha512-Au6UCupM1d044w++GMv2HC4YGc2nsDNBdem71gjxcDLowGo9FDgKF1Fxh2E+UfCv9QfAcShJzCg0R4EtCpEsuQ==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-core/-/variable-core-0.3.2.tgz",
"integrity": "sha512-95E7akQXzPItx7H/tKT4g5CZ0gg6OCsBou13OfotivZS1Yn5GxtqOeWGz7EHUZwurzB9w6o30VR6Y8vuk049Bg==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"fast-equals": "^2.0.0",
"inversify": "^6.0.1",
"lodash": "^4.17.21",
"nanoid": "^4.0.2",
"reflect-metadata": "~0.2.2",
"rxjs": "^7.8.1"
"rxjs": "^7.8.2"
},
"peerDependencies": {
"react": ">=16.8",
@@ -3521,15 +3521,15 @@
}
},
"node_modules/@flowgram.ai/variable-layout": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-layout/-/variable-layout-0.2.28.tgz",
"integrity": "sha512-31mxxnLJFanmdMkMvl6WzGV+CO15gPT0jnMsf0Mqp+s7qadtwuHcwzsDEUU9RKR+d/oXuGBpNGrdtQJzBh2JMQ==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-layout/-/variable-layout-0.3.2.tgz",
"integrity": "sha512-vX9lGLl5yY0VAYv5Bua3jqkh2M80+YB/4Qea0crkPAWzg1eVQcG4zbnR+Wc6c3wNz9cTpT+vq+2kiTsP5+95eQ==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/free-layout-core": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/variable-core": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/free-layout-core": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"@flowgram.ai/variable-core": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2",
"styled-components": "^5"
@@ -3584,16 +3584,16 @@
}
},
"node_modules/@flowgram.ai/variable-plugin": {
"version": "0.2.28",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-plugin/-/variable-plugin-0.2.28.tgz",
"integrity": "sha512-lPxcLNClR865dQMGAGy+NSbC+80mWbVXRolbN1ScIj7on9ypTtDs86VTdFGlKMzP8BSWIXM1Rjb6oSEShCxuJA==",
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@flowgram.ai/variable-plugin/-/variable-plugin-0.3.2.tgz",
"integrity": "sha512-Fm4DS/DrfcyKtvwUyoB4y5dBr8D/JWaSZjhu6kWee3HIV46uJLruufynMBQprWEgSMj9RyYFoFVQWH4hYisNWg==",
"dependencies": {
"@flowgram.ai/core": "0.2.28",
"@flowgram.ai/document": "0.2.28",
"@flowgram.ai/free-layout-core": "0.2.28",
"@flowgram.ai/utils": "0.2.28",
"@flowgram.ai/variable-core": "0.2.28",
"@flowgram.ai/variable-layout": "0.2.28",
"@flowgram.ai/core": "0.3.2",
"@flowgram.ai/document": "0.3.2",
"@flowgram.ai/free-layout-core": "0.3.2",
"@flowgram.ai/utils": "0.3.2",
"@flowgram.ai/variable-core": "0.3.2",
"@flowgram.ai/variable-layout": "0.3.2",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2",
"styled-components": "^5"
@@ -4028,9 +4028,9 @@
}
},
"node_modules/@rc-component/trigger": {
"version": "2.2.7",
"resolved": "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-2.2.7.tgz",
"integrity": "sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==",
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-2.3.0.tgz",
"integrity": "sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==",
"dependencies": {
"@babel/runtime": "^7.23.2",
"@rc-component/portal": "^1.1.0",
@@ -5000,9 +5000,9 @@
}
},
"node_modules/@uiw/codemirror-extensions-basic-setup": {
"version": "4.24.1",
"resolved": "https://registry.npmmirror.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.24.1.tgz",
"integrity": "sha512-o1m1a8eUS3fWERMbDFvN8t8sZUFPgDKNemmlQ5Ot2vKm+Ax84lKP1dhEFgkiOaZ1bDHk4T5h6SjHuTghrJHKww==",
"version": "4.24.2",
"resolved": "https://registry.npmmirror.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.24.2.tgz",
"integrity": "sha512-wW/gjLRvVUeYyhdh2TApn25cvdcR+Rhg6R/j3eTOvXQzU1HNzNYCVH4YKVIfgtfdM/Xs+N8fkk+rbr1YvBppCg==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
@@ -5026,20 +5026,20 @@
}
},
"node_modules/@uiw/codemirror-theme-vscode": {
"version": "4.24.1",
"resolved": "https://registry.npmmirror.com/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.24.1.tgz",
"integrity": "sha512-w2/eaPcYsHi03aWoKzVxIWv5dBDKW4YPnjwyYLgFY3uNdZmSfoNkFhqW7GJgNNF77ATixPq2wm9XqYeOTtvuww==",
"version": "4.24.2",
"resolved": "https://registry.npmmirror.com/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.24.2.tgz",
"integrity": "sha512-5rLvAMKNjqtHSy3BZnI1Q4ZLqUDDJC5fKOp8Mv5VjPqEmAxEAIGrIXBnhAFlRg5VjHbHEMtM1O5ugL3IeHoBdQ==",
"dependencies": {
"@uiw/codemirror-themes": "4.24.1"
"@uiw/codemirror-themes": "4.24.2"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
}
},
"node_modules/@uiw/codemirror-themes": {
"version": "4.24.1",
"resolved": "https://registry.npmmirror.com/@uiw/codemirror-themes/-/codemirror-themes-4.24.1.tgz",
"integrity": "sha512-hduBbFNiWNW6nYa2/giKQ9YpzhWNw87BGpCjC+cXYMZ7bCD6q5DC6Hw+7z7ZwSzEaOQvV91lmirOjJ8hn9+pkg==",
"version": "4.24.2",
"resolved": "https://registry.npmmirror.com/@uiw/codemirror-themes/-/codemirror-themes-4.24.2.tgz",
"integrity": "sha512-0fQusJE08APL+1WM0xYqvampVM2RpABHjvZbcJWHdRc+teZbrP8907VW1ZX1tO/7/xgRRt4Fc3RHPCcTs7b0NQ==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@@ -5055,15 +5055,15 @@
}
},
"node_modules/@uiw/react-codemirror": {
"version": "4.24.1",
"resolved": "https://registry.npmmirror.com/@uiw/react-codemirror/-/react-codemirror-4.24.1.tgz",
"integrity": "sha512-BivF4NLqbuBQK5gPVhSkOARi9nPXw8X5r25EnInPeY+I9l1dfEX8O9V6+0xHTlGHyUo0cNfGEF9t1KHEicUfJw==",
"version": "4.24.2",
"resolved": "https://registry.npmmirror.com/@uiw/react-codemirror/-/react-codemirror-4.24.2.tgz",
"integrity": "sha512-kp7DhTq4RR+M2zJBQBrHn1dIkBrtOskcwJX4vVsKGByReOvfMrhqRkGTxYMRDTX6x75EG2mvBJPDKYcUQcHWBw==",
"dependencies": {
"@babel/runtime": "^7.18.6",
"@codemirror/commands": "^6.1.0",
"@codemirror/state": "^6.1.1",
"@codemirror/theme-one-dark": "^6.0.0",
"@uiw/codemirror-extensions-basic-setup": "4.24.1",
"@uiw/codemirror-extensions-basic-setup": "4.24.2",
"codemirror": "^6.0.0"
},
"funding": {
@@ -5075,8 +5075,8 @@
"@codemirror/theme-one-dark": ">=6.0.0",
"@codemirror/view": ">=6.0.0",
"codemirror": ">=6.0.0",
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@umijs/route-utils": {
@@ -5472,9 +5472,9 @@
}
},
"node_modules/antd": {
"version": "5.26.6",
"resolved": "https://registry.npmmirror.com/antd/-/antd-5.26.6.tgz",
"integrity": "sha512-k8ipeT+UL2tP/x4jHTXElScAxsD94JgrIEeGHj80nNO4dL9hqcmaOUBpHo3ieCf6MFjhS7gLUthysQeP6e7DUg==",
"version": "5.26.7",
"resolved": "https://registry.npmmirror.com/antd/-/antd-5.26.7.tgz",
"integrity": "sha512-iCyXN6+i2CUVEOSzzJKfbKeg115qoJhGvSkCh5uzAf9hANwHUOJQhsMn+KtN+Lx/2NQ6wfM7nGZ+7NPNO5Pn1w==",
"dependencies": {
"@ant-design/colors": "^7.2.1",
"@ant-design/cssinjs": "^1.23.0",
@@ -5487,7 +5487,7 @@
"@rc-component/mutate-observer": "^1.1.0",
"@rc-component/qrcode": "~1.0.0",
"@rc-component/tour": "~1.15.1",
"@rc-component/trigger": "^2.2.7",
"@rc-component/trigger": "^2.3.0",
"classnames": "^2.5.1",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.11",
@@ -5517,7 +5517,7 @@
"rc-switch": "~4.1.0",
"rc-table": "~7.51.1",
"rc-tabs": "~15.6.1",
"rc-textarea": "~1.10.0",
"rc-textarea": "~1.10.1",
"rc-tooltip": "~6.4.0",
"rc-tree": "~5.13.1",
"rc-tree-select": "~5.27.0",
@@ -6151,6 +6151,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"optional": true,
"peer": true,
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
@@ -6631,9 +6648,9 @@
}
},
"node_modules/eslint-plugin-better-tailwindcss": {
"version": "3.7.2",
"resolved": "https://registry.npmmirror.com/eslint-plugin-better-tailwindcss/-/eslint-plugin-better-tailwindcss-3.7.2.tgz",
"integrity": "sha512-uJnoDGmUPtxHHcIW5njEoh1CYlq39E4J2pWq+D7REDGhW41arSCeeaiiwAT4Ok04kXNvmFBiYmC6Jdd6Bz7o2Q==",
"version": "3.7.3",
"resolved": "https://registry.npmmirror.com/eslint-plugin-better-tailwindcss/-/eslint-plugin-better-tailwindcss-3.7.3.tgz",
"integrity": "sha512-7I2MM1/OetBamiNWGkAetzQtvph6N7B6Iy5nIU9srFkHXnqR5G6lFX7ZT+JZmLrirrYSn8E0THeJg9QTQaI/kQ==",
"dev": true,
"dependencies": {
"@eslint/css-tree": "^3.6.2",
@@ -9326,9 +9343,9 @@
}
},
"node_modules/rc-textarea": {
"version": "1.10.0",
"resolved": "https://registry.npmmirror.com/rc-textarea/-/rc-textarea-1.10.0.tgz",
"integrity": "sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==",
"version": "1.10.2",
"resolved": "https://registry.npmmirror.com/rc-textarea/-/rc-textarea-1.10.2.tgz",
"integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.1",
@@ -11146,17 +11163,17 @@
}
},
"node_modules/zod": {
"version": "4.0.10",
"resolved": "https://registry.npmmirror.com/zod/-/zod-4.0.10.tgz",
"integrity": "sha512-3vB+UU3/VmLL2lvwcY/4RV2i9z/YU0DTV/tDuYjrwmx5WeJ7hwy+rGEEx8glHp6Yxw7ibRbKSaIFBgReRPe5KA==",
"version": "4.0.14",
"resolved": "https://registry.npmmirror.com/zod/-/zod-4.0.14.tgz",
"integrity": "sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zustand": {
"version": "5.0.6",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.6.tgz",
"integrity": "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==",
"version": "5.0.7",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.7.tgz",
"integrity": "sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==",
"engines": {
"node": ">=12.20.0"
},
+10 -10
View File
@@ -1,7 +1,6 @@
{
"name": "ui",
"name": "@certimate/webui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
@@ -15,13 +14,14 @@
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.2",
"@codemirror/legacy-modes": "^6.5.1",
"@flowgram.ai/fixed-layout-editor": "^0.2.28",
"@flowgram.ai/fixed-layout-editor": "^0.3.2",
"@flowgram.ai/document": "^0.3.2",
"@tabler/icons-react": "^3.34.1",
"@uiw/codemirror-extensions-basic-setup": "^4.24.1",
"@uiw/codemirror-theme-vscode": "^4.24.1",
"@uiw/react-codemirror": "^4.24.1",
"@uiw/codemirror-extensions-basic-setup": "^4.24.2",
"@uiw/codemirror-theme-vscode": "^4.24.2",
"@uiw/react-codemirror": "^4.24.2",
"ahooks": "^3.9.0",
"antd": "^5.26.6",
"antd": "^5.26.7",
"antd-zod": "^7.0.0",
"clsx": "^2.1.1",
"cron-parser": "^5.3.0",
@@ -38,8 +38,8 @@
"react-i18next": "^15.6.1",
"react-router-dom": "^7.7.1",
"tailwind-merge": "^3.3.1",
"zod": "^4.0.10",
"zustand": "^5.0.6"
"zod": "^4.0.14",
"zustand": "^5.0.7"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
@@ -56,7 +56,7 @@
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-better-tailwindcss": "^3.7.2",
"eslint-plugin-better-tailwindcss": "^3.7.3",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-react-hooks": "^5.2.0",
@@ -2,7 +2,7 @@
import { IconList } from "@tabler/icons-react";
import { useControllableValue } from "ahooks";
import { Button, Form, Input, type InputProps, Space } from "antd";
import { nanoid } from "nanoid";
import { nanoid } from "nanoid/non-secure";
import { useAntdForm } from "@/hooks";
import ModalForm from "./ModalForm";
@@ -30,15 +30,19 @@ const WorkflowElementsContainer = ({ className, style, disabled }: WorkflowEleme
</div>
</div>
<div className="absolute right-6 bottom-4 z-2">
<Card className="rounded-lg p-2 shadow-lg" styles={{ body: { padding: 0 } }}>
<div className="flex items-center gap-2">
<Button icon={<IconMinus size="1.25em" />} disabled={scale <= MIN_SCALE} onClick={() => setScale((s) => Math.max(MIN_SCALE, s - STEP_SCALE))} />
<Typography.Text className="min-w-12 text-center">{Math.round(scale * 100)}%</Typography.Text>
<Button icon={<IconPlus size="1.25em" />} disabled={scale >= MAX_SCALE} onClick={() => setScale((s) => Math.min(MAX_SCALE, s + STEP_SCALE))} />
<Button icon={<IconMaximize size="1.25em" />} onClick={() => setScale(1)} />
<div className="absolute bottom-4 z-2 w-full px-4">
<div className="container">
<div className="flex justify-end">
<Card className="rounded-lg p-2 shadow-lg" styles={{ body: { padding: 0 } }}>
<div className="flex items-center gap-2">
<Button icon={<IconMinus size="1.25em" />} disabled={scale <= MIN_SCALE} onClick={() => setScale((s) => Math.max(MIN_SCALE, s - STEP_SCALE))} />
<Typography.Text className="min-w-12 text-center">{Math.round(scale * 100)}%</Typography.Text>
<Button icon={<IconPlus size="1.25em" />} disabled={scale >= MAX_SCALE} onClick={() => setScale((s) => Math.min(MAX_SCALE, s + STEP_SCALE))} />
<Button icon={<IconMaximize size="1.25em" />} onClick={() => setScale(1)} />
</div>
</Card>
</div>
</Card>
</div>
</div>
</div>
);
@@ -0,0 +1,119 @@
import { useMemo, useRef } from "react";
import {
ConstantKeys,
EditorRenderer,
FixedLayoutEditorProvider,
type FixedLayoutPluginContext,
type FixedLayoutProps,
type FlowDocumentJSON,
FlowTextKey,
} from "@flowgram.ai/fixed-layout-editor";
import "@flowgram.ai/fixed-layout-editor/index.css";
import { useDeepCompareEffect } from "ahooks";
import { theme } from "antd";
import { getFlowComponents } from "./components";
import NodeRender from "./NodeRender";
import { getFlowNodeRegistries } from "./nodes";
import { BlockNode } from "./nodes/_shared";
import "./flowgram.css";
export interface EditorProps {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
initialData?: FlowDocumentJSON;
readonly?: boolean;
}
const Editor = ({ className, style, children, initialData, readonly }: EditorProps) => {
const { token: themeToken } = theme.useToken();
const flowgramEditorRef = useRef<FixedLayoutPluginContext>(null);
const flowgramEditorProps = useMemo<FixedLayoutProps>(
() => ({
initialData: initialData,
constants: {
[ConstantKeys.BASE_COLOR]: themeToken.colorBorder,
[ConstantKeys.BASE_ACTIVATED_COLOR]: themeToken.colorPrimary,
[ConstantKeys.NODE_SPACING]: 64,
[ConstantKeys.BRANCH_SPACING]: 64,
// [ConstantKeys.INLINE_BLOCKS_PADDING_TOP]: 48,
// [ConstantKeys.INLINE_BLOCKS_PADDING_BOTTOM]: 48,
},
background: {
backgroundColor: themeToken.colorBgContainer,
dotSize: 0,
},
playground: {
autoFocus: true,
autoResize: true,
preventGlobalGesture: true,
},
scroll: {
enableScrollLimit: true,
},
readonly: readonly,
nodeEngine: {
enable: true,
},
variableEngine: {
enable: true,
},
materials: {
components: getFlowComponents(),
renderTexts: {
[FlowTextKey.TRY_START_TEXT]: "Try",
[FlowTextKey.TRY_END_TEXT]: "Finally",
[FlowTextKey.CATCH_TEXT]: "Catch",
},
renderDefaultNode: NodeRender,
},
nodeRegistries: getFlowNodeRegistries(),
getNodeDefaultRegistry(type) {
return {
type,
meta: {
defaultExpanded: true,
},
formMeta: {
render: () => <BlockNode>{type}</BlockNode>,
},
};
},
onAllLayersRendered: (ctx) => {
// 画布初始化后向下滚动一点,露出可能被 Alert 遮挡的部分
setTimeout(() => {
ctx.playground.config.scroll({ scrollY: -80 });
}, 1);
},
}),
[themeToken, initialData, readonly]
);
useDeepCompareEffect(() => {
flowgramEditorRef.current?.document?.fromJSON?.(initialData ?? {});
}, [initialData]);
return (
<div className={className} style={style}>
<FixedLayoutEditorProvider ref={flowgramEditorRef} {...flowgramEditorProps}>
<EditorRenderer />
{children}
</FixedLayoutEditorProvider>
</div>
);
};
export default Editor;
@@ -0,0 +1,27 @@
import { type NodeRenderProps, useNodeRender } from "@flowgram.ai/fixed-layout-editor";
export interface NodeProps extends NodeRenderProps {}
const Node = (_: NodeProps) => {
const nodeRender = useNodeRender();
return (
<div
style={{
opacity: nodeRender.dragging ? 0.3 : 1,
outline: nodeRender.form?.state?.invalid ? "1px solid var(--color-error)" : "none",
...nodeRender.node.getNodeRegistry().meta.style,
}}
onMouseEnter={nodeRender.onMouseEnter}
onMouseLeave={nodeRender.onMouseLeave}
onMouseDown={(e) => {
nodeRender.startDrag(e);
e.stopPropagation();
}}
>
{nodeRender.form?.render()}
</div>
);
};
export default Node;
@@ -0,0 +1,78 @@
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { EditorState, FlowLayoutDefault, useClientContext, usePlaygroundTools, useRefresh } from "@flowgram.ai/fixed-layout-editor";
import { IconHandStop, IconLayoutCards, IconMaximize, IconMinus, IconPlus, IconPointer } from "@tabler/icons-react";
import { Button, Tooltip } from "antd";
import { mergeCls } from "@/utils/css";
export interface ToolbarProps {
className?: string;
style?: React.CSSProperties;
}
const Toolbar = ({ className, style }: ToolbarProps) => {
const { t } = useTranslation();
const ctx = useClientContext();
const { playground } = ctx;
const tools = usePlaygroundTools({ minZoom: 0.1, maxZoom: 3, padding: 48 });
const refresh = useRefresh();
useEffect(() => {
const disposable = playground.config.onReadonlyOrDisabledChange(() => refresh());
return () => disposable.dispose();
}, [playground]);
const [isMouseFriendly, setIsMouseFriendly] = useState(playground.editorState.is(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id));
const handleToggleLayout = useCallback(() => {
if (tools.isVertical) {
tools.changeLayout(FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT);
} else {
tools.changeLayout(FlowLayoutDefault.VERTICAL_FIXED_LAYOUT);
}
}, [tools.isVertical]);
const handleToggleMouseFriendly = useCallback(() => {
if (isMouseFriendly) {
playground.editorState.changeState(EditorState.STATE_SELECT.id);
setIsMouseFriendly(false);
} else {
playground.editorState.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id);
setIsMouseFriendly(true);
}
}, [isMouseFriendly]);
return (
<div className={className} style={style}>
<div className="flex items-center gap-2">
<Tooltip title={t("workflow.detail.design.toolbar.zoomout")}>
<Button icon={<IconMinus size="1.25em" />} onClick={() => tools.zoomout()} />
</Tooltip>
<Tooltip title={t("workflow.detail.design.toolbar.zoom")}>
<Button className="w-16 text-center" onClick={() => tools.updateZoom(1)}>
{Math.round(tools.zoom * 100)}%
</Button>
</Tooltip>
<Tooltip title={t("workflow.detail.design.toolbar.zoomin")}>
<Button icon={<IconPlus size="1.25em" />} onClick={() => tools.zoomin()} />
</Tooltip>
<Tooltip title={t("workflow.detail.design.toolbar.auto_fit")}>
<Button icon={<IconMaximize size="1.25em" />} onClick={() => tools.fitView()} />
</Tooltip>
<Tooltip title={tools.isVertical ? t("workflow.detail.design.toolbar.vertical_layout") : t("workflow.detail.design.toolbar.horizontal_layout")}>
<Button icon={<IconLayoutCards className={mergeCls({ ["rotate-90"]: tools.isVertical })} size="1.25em" />} onClick={handleToggleLayout} />
</Tooltip>
<Tooltip title={isMouseFriendly ? t("workflow.detail.design.toolbar.hand_mode") : t("workflow.detail.design.toolbar.pointer_mode")}>
<Button icon={isMouseFriendly ? <IconHandStop size="1.25em" /> : <IconPointer size="1.25em" />} onClick={handleToggleMouseFriendly} />
</Tooltip>
</div>
</div>
);
};
export default Toolbar;
@@ -0,0 +1,65 @@
import { type AdderProps as FlowgramAdderProps, useClientContext } from "@flowgram.ai/fixed-layout-editor";
import { IconPlus } from "@tabler/icons-react";
import { Button, Dropdown } from "antd";
import { getFlowNodeRegistries } from "../nodes";
export interface AdderProps extends FlowgramAdderProps {}
const Adder = ({ from, hoverActivated }: AdderProps) => {
const ctx = useClientContext();
const { operation, playground } = ctx;
const menuItems = getFlowNodeRegistries()
.filter((registry) => {
if (registry.canAdd != null) {
return registry.canAdd(ctx, from);
}
if (registry.meta?.addDisable != null) {
return registry.meta.addDisable;
}
return true;
})
.map((registry) => {
const Icon = registry.meta?.icon;
return {
key: registry.type,
label: registry.type,
icon: (
<span className="anticon scale-125">
<Icon size="1em" />
</span>
),
onClick: () => {
const props = registry.onAdd!(ctx, from);
const block = operation.addFromNode(from, {
...props,
blocks: props?.blocks ?? [],
});
setTimeout(() => {
playground.scrollToView({
bounds: block.bounds,
scrollToCenter: true,
});
}, 1);
},
};
});
return playground.config.readonlyOrDisabled ? null : (
<div className="relative">
<Dropdown menu={{ items: menuItems }} placement="bottomRight" trigger={["click"]}>
{hoverActivated ? (
<Button icon={<IconPlus size="1em" stroke={3} />} shape="circle" size="small" type="primary" />
) : (
<div className="size-2 rounded-full bg-primary opacity-75"></div>
)}
</Dropdown>
</div>
);
};
export default Adder;
@@ -0,0 +1,62 @@
import { useTranslation } from "react-i18next";
import { FlowNodeBaseType, type FlowNodeEntity, type AdderProps as FlowgramAdderProps, useClientContext } from "@flowgram.ai/fixed-layout-editor";
import { Button } from "antd";
import { nanoid } from "nanoid";
import { BranchBlockNodeRegistry } from "../nodes/ConditionNode";
import { CatchBlockNodeRegistry } from "../nodes/TryCatchNode";
import { NodeType } from "../nodes/typings";
export interface BranchAdderProps extends FlowgramAdderProps {}
const BranchAdder = ({ node }: BranchAdderProps) => {
const { t } = useTranslation();
const ctx = useClientContext();
const { operation, playground } = ctx;
const handleAddBranch = () => {
let block: FlowNodeEntity;
switch (node.flowNodeType) {
case NodeType.Condition:
block = operation.addBlock(node, BranchBlockNodeRegistry.onAdd!(ctx, node));
break;
case NodeType.TryCatch:
block = operation.addBlock(node, CatchBlockNodeRegistry.onAdd!(ctx, node));
break;
default:
console.warn(`[certimate] unsupported node type for adding branch: '${node.flowNodeType}'`);
block = operation.addBlock(node, {
id: nanoid(),
type: FlowNodeBaseType.BLOCK,
data: {
name: "Branch",
},
});
break;
}
setTimeout(() => {
playground.scrollToView({
bounds: block.bounds,
scrollToCenter: true,
});
}, 1);
};
return playground.config.readonlyOrDisabled ? null : (
<div
className="relative"
onMouseEnter={() => node.firstChild?.renderData?.toggleMouseEnter()}
onMouseLeave={() => node.firstChild?.renderData?.toggleMouseLeave()}
>
<Button shape="round" size="small" onClick={handleAddBranch}>
<span className="text-xs">{t("workflow_node.action.add_branch")}</span>
</Button>
</div>
);
};
export default BranchAdder;
@@ -0,0 +1,9 @@
import { type CollapseProps as FlowgramCollapseProps } from "@flowgram.ai/fixed-layout-editor";
export interface CollapseProps extends FlowgramCollapseProps {}
const Collapse = (_: CollapseProps) => {
return null;
};
export default Collapse;
@@ -0,0 +1,14 @@
import { type DragNodeProps as FlowgramDragNodeProps } from "@flowgram.ai/fixed-layout-editor";
import { IconGradienter } from "@tabler/icons-react";
const DragHighlightAdder = (_: FlowgramDragNodeProps) => {
return (
<div className="size-4 animate-ping rounded-full bg-primary text-white shadow">
<div className="flex size-full items-center justify-center">
<IconGradienter size="1em" />
</div>
</div>
);
};
export default DragHighlightAdder;
@@ -0,0 +1,26 @@
import { type FlowNodeEntity, type DragNodeProps as FlowgramDragNodeProps, getNodeForm } from "@flowgram.ai/fixed-layout-editor";
import { Badge, Card } from "antd";
export interface DragNodeProps extends FlowgramDragNodeProps {
dragStart: FlowNodeEntity;
dragNodes: FlowNodeEntity[];
}
const DragNode = ({ dragStart, dragNodes }: DragNodeProps) => {
const count = (dragNodes || [])
.map((n) => (n.allCollapsedChildren.length ? n.allCollapsedChildren.filter((_n) => !_n.hidden).length : 1))
.reduce((acc, cur) => acc + cur, 0);
return (
<Badge count={count > 1 ? count : 0} size="small">
<div className="relative w-[160px]">
<Card className="bg-transparent shadow" styles={{ body: { padding: 0 } }}>
<div className="overflow-hidden px-4 py-2 text-primary">
<div className="truncate">{dragStart ? getNodeForm(dragStart)?.getValueIn("name") || `#${dragStart?.id}` : "\u00A0"}</div>
</div>
</Card>
</div>
</Badge>
);
};
export default DragNode;
@@ -0,0 +1,31 @@
import { FlowDragLayer, type AdderProps as FlowgramAdderProps, usePlayground } from "@flowgram.ai/fixed-layout-editor";
import { IconChevronsDown } from "@tabler/icons-react";
export interface DraggingAdderProps extends FlowgramAdderProps {}
const DraggingAdder = ({ from }: DraggingAdderProps) => {
const playground = usePlayground();
const layer = playground.getLayer(FlowDragLayer);
if (!layer) return <></>;
if (
layer.options.canDrop &&
!layer.options.canDrop({
dragNodes: layer.dragEntities ?? [],
dropNode: from,
isBranch: false,
})
) {
return <></>;
}
return (
<div className="size-4 animate-bounce rounded-full bg-primary text-white shadow">
<div className="flex size-full items-center justify-center">
<IconChevronsDown size="1em" />
</div>
</div>
);
};
export default DraggingAdder;
@@ -0,0 +1,11 @@
import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor";
export interface NullProps {
node: FlowNodeEntity;
}
const Null = (_: NullProps) => {
return null;
};
export default Null;
@@ -0,0 +1,69 @@
import { useState } from "react";
import {
type CustomLabelProps,
FlowNodeRenderData,
FlowNodeTransformData,
FlowRendererRegistry,
FlowTextKey,
useBaseColor,
} from "@flowgram.ai/fixed-layout-editor";
export interface TryCatchCollapseProps extends CustomLabelProps {}
const TryCatchCollapse = ({ node, ...props }: TryCatchCollapseProps) => {
const { baseColor, baseActivatedColor } = useBaseColor();
const nodeActivateData = node.getData(FlowNodeRenderData)!;
const nodeTransformData = node.getData(FlowNodeTransformData)!;
const [hoverActivated, setHoverActivated] = useState(false);
const handleMouseEnter = () => {
setHoverActivated(true);
nodeActivateData.activated = true;
};
const handleMouseLeave = () => {
setHoverActivated(false);
nodeActivateData.activated = false;
};
if (!nodeTransformData || !nodeTransformData.parent) {
return <></>;
}
const width = nodeTransformData.inputPoint.x - nodeTransformData.parent.inputPoint.x;
const height = 40;
return (
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{
width,
height,
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 6,
}}
>
<div
data-label-id={props.labelId}
style={{
fontSize: 12,
color: hoverActivated ? baseActivatedColor : baseColor,
textAlign: "center",
lineHeight: "20px",
whiteSpace: "nowrap",
backgroundColor: "var(--g-editor-background)",
}}
>
{node.getService(FlowRendererRegistry).getText(FlowTextKey.CATCH_TEXT)}
</div>
{/* {renderCollapse()} */}
</div>
);
};
export default TryCatchCollapse;
@@ -0,0 +1,30 @@
import { FlowRendererKey } from "@flowgram.ai/fixed-layout-editor";
import Adder from "./Adder";
import BranchAdder from "./BranchAdder";
import Collapse from "./Collapse";
import DraggingAdder from "./DraggingAdder";
import DragHighlightAdder from "./DragHighlightAdder";
import DragNode from "./DragNode";
import Null from "./Null";
import TryCatchCollapse from "./TryCatchCollapse";
export const getFlowComponents = () => {
return {
[FlowRendererKey.ADDER]: Adder,
[FlowRendererKey.BRANCH_ADDER]: BranchAdder,
[FlowRendererKey.SLOT_ADDER]: Null,
[FlowRendererKey.COLLAPSE]: Collapse,
[FlowRendererKey.TRY_CATCH_COLLAPSE]: TryCatchCollapse,
[FlowRendererKey.SLOT_COLLPASE]: Null,
[FlowRendererKey.DRAG_NODE]: DragNode,
[FlowRendererKey.DRAG_HIGHLIGHT_ADDER]: DragHighlightAdder,
[FlowRendererKey.DRAG_BRANCH_HIGHLIGHT_ADDER]: DragHighlightAdder,
[FlowRendererKey.DRAGGABLE_ADDER]: DraggingAdder,
[FlowRendererKey.CONTEXT_MENU_POPOVER]: Null,
[FlowRendererKey.SELECTOR_BOX_POPOVER]: Null,
};
};
@@ -0,0 +1,22 @@
/* 隐藏 flowgram 的标签背景 */
/* flowgram 暂未提供相关配置项,这里需与 antd.colorBgContainer 保持一致 */
:root {
--g-editor-background: #fff;
}
.dark {
--g-editor-background: #262c2d;
}
/* 隐藏 flowgram 的折叠按钮 */
.flow-canvas-collapse-adder > .flow-canvas-collapse {
display: hidden !important;
position: absolute !important;
top: -99999px !important;
left: -99999px !important;
}
/* 控制 flowgram 线条样式 */
/* flowgram 暂未提供相关配置项 */
.flow-lines-container {
stroke-width: 1.5px;
}
@@ -0,0 +1,206 @@
import { getI18n } from "react-i18next";
import { IconCloudUpload, IconContract, IconDeviceDesktopSearch, IconPackage, IconSend } from "@tabler/icons-react";
import { Avatar, Typography } from "antd";
import { nanoid } from "nanoid";
import { deploymentProvidersMap, notificationProvidersMap } from "@/domain/provider";
import { BaseNode } from "./_shared";
import { type NodeRegistry, NodeType } from "./typings";
export const BizApplyNodeRegistry: NodeRegistry = {
type: NodeType.BizApply,
meta: {
helpText: getI18n().t("workflow_node.apply.help"),
icon: IconContract,
iconColor: "#fff",
iconBgColor: "#5b65f5",
},
formMeta: {
render: ({ form }) => {
const { t } = getI18n();
const fieldDomains = form.getValueIn<string>("config.domains");
return <BaseNode>{fieldDomains ? fieldDomains.split(";").join("; ") : t("workflow.detail.design.nodes.placeholder")}</BaseNode>;
},
},
onAdd: () => {
const { t } = getI18n();
return {
id: nanoid(),
type: NodeType.BizApply,
data: {
name: t("workflow_node.apply.default_name"),
},
};
},
};
export const BizUploadNodeRegistry: NodeRegistry = {
type: NodeType.BizUpload,
meta: {
helpText: getI18n().t("workflow_node.upload.help"),
icon: IconCloudUpload,
iconColor: "#fff",
iconBgColor: "#5b65f5",
},
formMeta: {
render: ({ form }) => {
const { t } = getI18n();
const fieldDomains = form.getValueIn<string>("config.domains");
return <BaseNode>{fieldDomains ? fieldDomains.split(";").join("; ") : t("workflow.detail.design.nodes.placeholder")}</BaseNode>;
},
},
onAdd: () => {
const { t } = getI18n();
return {
id: nanoid(),
type: NodeType.BizUpload,
data: {
name: t("workflow_node.upload.default_name"),
},
};
},
};
export const BizMonitorNodeRegistry: NodeRegistry = {
type: NodeType.BizMonitor,
meta: {
helpText: getI18n().t("workflow_node.monitor.help"),
icon: IconDeviceDesktopSearch,
iconColor: "#fff",
iconBgColor: "#5b65f5",
},
formMeta: {
render: ({ form }) => {
const { t } = getI18n();
const fieldDomain = form.getValueIn<string>("config.domain");
const fieldHost = form.getValueIn<string>("config.host");
return <BaseNode>{fieldDomain || fieldHost ? fieldDomain || fieldHost : t("workflow.detail.design.nodes.placeholder")}</BaseNode>;
},
},
onAdd: () => {
const { t } = getI18n();
return {
id: nanoid(),
type: NodeType.BizMonitor,
data: {
name: t("workflow_node.monitor.default_name"),
},
};
},
};
export const BizDeployNodeRegistry: NodeRegistry = {
type: NodeType.BizDeploy,
meta: {
helpText: getI18n().t("workflow_node.deploy.help"),
icon: IconPackage,
iconColor: "#fff",
iconBgColor: "#5b65f5",
},
formMeta: {
render: ({ form }) => {
const { t } = getI18n();
const fieldProvider = form.getValueIn<string>("config.provider");
return (
<BaseNode>
<div className="flex items-center justify-between gap-1">
{fieldProvider ? (
<>
<div className="flex-1 truncate">{t(deploymentProvidersMap.get(fieldProvider)?.name ?? "")}</div>
<Avatar shape="square" src={deploymentProvidersMap.get(fieldProvider)?.icon} size={20} />
</>
) : (
t("workflow.detail.design.nodes.placeholder")
)}
</div>
</BaseNode>
);
},
},
onAdd: () => {
const { t } = getI18n();
return {
id: nanoid(),
type: NodeType.BizMonitor,
data: {
name: t("workflow_node.deploy.default_name"),
},
};
},
};
export const BizNotifyNodeRegistry: NodeRegistry = {
type: NodeType.BizNotify,
meta: {
helpText: getI18n().t("workflow_node.notify.help"),
icon: IconSend,
iconColor: "#fff",
iconBgColor: "#0693d4",
},
formMeta: {
render: ({ form }) => {
const { t } = getI18n();
const fieldProvider = form.getValueIn<string>("config.provider");
return (
<BaseNode>
<div className="flex items-center justify-between gap-1">
{fieldProvider ? (
<>
<div className="flex-1 truncate">{t(notificationProvidersMap.get(fieldProvider)?.name ?? "")}</div>
<Avatar shape="square" src={notificationProvidersMap.get(fieldProvider)?.icon} size="small" />
</>
) : (
t("workflow.detail.design.nodes.placeholder")
)}
</div>
</BaseNode>
);
},
},
onAdd: () => {
const { t } = getI18n();
return {
id: nanoid(),
type: NodeType.BizNotify,
data: {
name: t("workflow_node.notify.default_name"),
},
};
},
};
@@ -0,0 +1,108 @@
import { getI18n } from "react-i18next";
import { FlowNodeBaseType, FlowNodeSplitType, ValidateTrigger } from "@flowgram.ai/fixed-layout-editor";
import { IconFilter, IconFilterFilled, IconSitemap } from "@tabler/icons-react";
import { nanoid } from "nanoid";
import { BaseNode, BlockNode } from "./_shared";
import { type NodeRegistry, NodeType } from "./typings";
export const ConditionNodeRegistry: NodeRegistry = {
type: NodeType.Condition,
extend: FlowNodeSplitType.DYNAMIC_SPLIT,
meta: {
helpText: getI18n().t("workflow_node.condition.help"),
icon: IconSitemap,
iconColor: "#fff",
iconBgColor: "#373c43",
expandable: false,
},
formMeta: {
validateTrigger: ValidateTrigger.onChange,
render: () => {
return <BaseNode></BaseNode>;
},
},
onAdd() {
const { t } = getI18n();
return {
id: nanoid(),
type: NodeType.Condition,
data: {
name: t("workflow_node.condition.default_name"),
},
blocks: [
{
id: nanoid(),
type: NodeType.BranchBlock,
blocks: [],
data: {
name: t("workflow_node.branch_block.default_name") + " 1",
},
},
{
id: nanoid(),
type: NodeType.BranchBlock,
data: {
name: t("workflow_node.branch_block.default_name") + " 2",
},
},
],
};
},
};
export const BranchBlockNodeRegistry: NodeRegistry = {
type: NodeType.BranchBlock,
extend: FlowNodeBaseType.BLOCK,
meta: {
addDisable: true,
copyDisable: true,
},
formMeta: {
validateTrigger: ValidateTrigger.onChange,
render: ({ form }) => {
const fieldExpr = form.getValueIn("config.expression");
return (
<BlockNode>
<div className="flex items-center justify-center gap-2">
<div className="flex items-center justify-center">
{fieldExpr ? <IconFilterFilled color="var(--color-primary)" size="1.25em" stroke="1.25" /> : <IconFilter size="1.25em" stroke="1.25" />}
</div>
<div className="truncate">{form.getValueIn<string>("name") || "\u00A0"}</div>
</div>
</BlockNode>
);
},
},
canAdd: () => {
return false;
},
canDelete: (_, node) => {
return node.parent!.blocks.length >= 2;
},
onAdd() {
const { t } = getI18n();
return {
id: nanoid(),
type: NodeType.BranchBlock,
data: {
name: t("workflow_node.branch_block.default_name"),
},
};
},
};
@@ -0,0 +1,75 @@
import { getI18n } from "react-i18next";
import { FlowNodeBaseType } from "@flowgram.ai/fixed-layout-editor";
import { IconLogout } from "@tabler/icons-react";
import { nanoid } from "nanoid";
import { BaseNode } from "./_shared";
import { type NodeRegistry, NodeType } from "./typings";
export const EndNodeRegistry: NodeRegistry = {
type: NodeType.End,
meta: {
helpText: getI18n().t("workflow_node.end.help"),
icon: IconLogout,
iconColor: "#fff",
iconBgColor: "#336df4",
isNodeEnd: true,
expandable: false,
selectable: false,
copyDisable: true,
},
formMeta: {
render: () => {
return <BaseNode></BaseNode>;
},
},
canAdd(_, from) {
// You can only add to the last node of the branch
if (!from.isLast) return false;
/**
* condition
* blockIcon
* inlineBlocks
* block1
* blockOrderIcon
* <---- [add end]
* block2
* blockOrderIcon
* end
*/
// originParent can determine whether it is condition , and then determine whether it is the last one
// https://github.com/bytedance/flowgram.ai/pull/146
if (from.parent && from.parent.parent?.flowNodeType === FlowNodeBaseType.INLINE_BLOCKS && from.parent.originParent && !from.parent.originParent.isLast) {
const allBranches = from.parent.parent!.blocks;
// Determine whether the last node of all branch is end, All branches are not allowed to be end
const branchEndCount = allBranches.filter((block) => block.blocks[block.blocks.length - 1]?.getNodeMeta().isNodeEnd).length;
return branchEndCount < allBranches.length - 1;
}
return true;
},
canDelete(ctx, node) {
return node.parent !== ctx.document.root;
},
onAdd() {
const { t } = getI18n();
return {
id: nanoid(),
type: NodeType.End,
data: {
name: t("workflow_node.end.default_name"),
},
};
},
};
@@ -0,0 +1,67 @@
import { getI18n } from "react-i18next";
import { IconRocket } from "@tabler/icons-react";
import { Typography } from "antd";
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
import { BaseNode } from "./_shared";
import { type NodeRegistry, NodeType } from "./typings";
export const StartNodeRegistry: NodeRegistry = {
type: NodeType.Start,
meta: {
helpText: getI18n().t("workflow_node.start.help"),
icon: IconRocket,
iconColor: "#fff",
iconBgColor: "#ed6d0c",
isStart: true,
expandable: false,
selectable: false,
addDisable: true,
copyDisable: true,
deleteDisable: true,
},
formMeta: {
render: ({ form }) => {
const { t } = getI18n();
const fieldTrigger = form.getValueIn<string>("config.trigger");
const fieldTriggerCron = form.getValueIn<string>("config.triggerCron");
return (
<BaseNode>
<div className="flex items-center justify-between gap-1">
{fieldTrigger ? (
<>
<div>
{fieldTrigger === WORKFLOW_TRIGGERS.SCHEDULED
? t("workflow.props.trigger.scheduled")
: fieldTrigger === WORKFLOW_TRIGGERS.MANUAL
? t("workflow.props.trigger.manual")
: "\u00A0"}
</div>
<div>{fieldTrigger === WORKFLOW_TRIGGERS.SCHEDULED ? fieldTriggerCron || "\u00A0" : ""}</div>
</>
) : (
t("workflow.detail.design.nodes.placeholder")
)}
</div>
</BaseNode>
);
},
},
canAdd: () => {
return false;
},
canDelete: () => {
return false;
},
};
@@ -0,0 +1,105 @@
import { getI18n } from "react-i18next";
import { ValidateTrigger } from "@flowgram.ai/fixed-layout-editor";
import { IconArrowsSplit, IconCircleX } from "@tabler/icons-react";
import { nanoid } from "nanoid";
import { BaseNode, BlockNode } from "./_shared";
import { type NodeRegistry, NodeType } from "./typings";
export const TryCatchNodeRegistry: NodeRegistry = {
type: NodeType.TryCatch,
meta: {
helpText: getI18n().t("workflow_node.try_catch.help"),
icon: IconArrowsSplit,
iconColor: "#fff",
iconBgColor: "#373c43",
expandable: false,
},
formMeta: {
validateTrigger: ValidateTrigger.onChange,
render: () => {
return <BaseNode></BaseNode>;
},
},
onAdd() {
const { t } = getI18n();
return {
id: nanoid(),
type: NodeType.TryCatch,
data: {
name: t("workflow_node.try_catch.default_name"),
},
blocks: [
{
id: nanoid(),
type: NodeType.TryBlock,
blocks: [],
},
{
id: nanoid(),
type: NodeType.CatchBlock,
blocks: [
{
id: nanoid(),
type: NodeType.End,
data: {
name: t("workflow_node.end.default_name"),
},
},
],
data: {
name: t("workflow_node.catch_block.default_name"),
},
},
],
};
},
};
export const CatchBlockNodeRegistry: NodeRegistry = {
type: NodeType.CatchBlock,
meta: {
addDisable: true,
copyDisable: true,
},
formMeta: {
validateTrigger: ValidateTrigger.onChange,
render: ({ form }) => {
return (
<BlockNode>
<div className="flex items-center justify-center gap-2">
<div className="flex items-center justify-center">
<IconCircleX color="var(--color-error)" size="1.25em" stroke="1.25" />
</div>
<div className="truncate">{form.getValueIn<string>("name") || "\u00A0"}</div>
</div>
</BlockNode>
);
},
},
canAdd: () => false,
canDelete: (_, node) => node.parent != null && node.parent.blocks.length >= 2,
onAdd() {
const { t } = getI18n();
return {
id: nanoid(),
type: NodeType.CatchBlock,
blocks: [],
data: {
name: t("workflow_node.catch_block.default_name"),
},
};
},
};
@@ -0,0 +1,63 @@
import { type FlowNodeRegistry } from "@flowgram.ai/fixed-layout-editor";
import { nanoid } from "nanoid";
/**
* 自定义节点注册
*/
export const nodeRegistry: FlowNodeRegistry = {
/**
* 自定义节点类型
*/
type: "condition",
/**
* 自定义节点扩展:
* - loop: 扩展为循环节点
* - start: 扩展为开始节点
* - dynamicSplit: 扩展为分支节点
* - end: 扩展为结束节点
* - tryCatch: 扩展为 tryCatch 节点
* - break: 分支断开
* - default: 扩展为普通节点 (默认)
*/
extend: "dynamicSplit",
/**
* 节点配置信息
*/
meta: {
isStart: false, // 是否为开始节点
isNodeEnd: false, // 是否为结束节点,结束节点后边无法再添加节点
draggable: false, // 是否可拖拽,如开始节点和结束节点无法拖拽
selectable: false, // 触发器等开始节点不能被框选
deleteDisable: true, // 禁止删除
copyDisable: true, // 禁止copy
addDisable: true, // 禁止添加
},
onAdd() {
return {
id: `condition_${nanoid(5)}`,
type: "condition",
data: {
title: "Condition",
},
blocks: [
{
id: nanoid(5),
type: "block",
data: {
title: "If_0",
},
},
{
id: nanoid(5),
type: "block",
data: {
title: "If_1",
},
},
],
};
},
};
@@ -0,0 +1,161 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useClientContext, useNodeRender } from "@flowgram.ai/fixed-layout-editor";
import { IconCopy, IconDotsVertical, IconGripVertical, IconLabel, IconX } from "@tabler/icons-react";
import { Button, type ButtonProps, Card, Dropdown, type MenuProps, Popover, Tooltip, Typography, theme } from "antd";
import { mergeCls } from "@/utils/css";
import { type NodeRegistry } from "./typings";
export const BaseNode = ({ className, style, children }: { className?: string; style?: React.CSSProperties; children?: React.ReactNode }) => {
const { token: themeToken } = theme.useToken();
const ctx = useClientContext();
const { playground } = ctx;
const nodeRender = useNodeRender();
const nodeRegistry = nodeRender.node.getNodeRegistry<NodeRegistry>();
const NodeIcon = nodeRegistry.meta?.icon;
const renderNodeIcon = () => {
return NodeIcon == null ? null : (
<div
className="mr-2 flex size-9 items-center justify-center rounded-lg bg-white text-primary shadow-md dark:bg-stone-200"
style={{
color: nodeRegistry.meta?.iconColor,
backgroundColor: nodeRegistry.meta?.iconBgColor,
}}
>
<NodeIcon size="1.75em" color={nodeRegistry.meta?.iconColor} stroke="1.25" />
</div>
);
};
return (
<div className={mergeCls("relative w-[320px] group/node", className)} style={style}>
<Card className="rounded-xl shadow-sm" styles={{ body: { padding: 0 } }} hoverable>
<div className="flex items-center gap-1 overflow-hidden p-3">
{nodeRegistry.meta?.helpText == null ? (
renderNodeIcon()
) : (
<Tooltip title={<span dangerouslySetInnerHTML={{ __html: nodeRegistry.meta.helpText }}></span>} mouseEnterDelay={1}>
<div className="cursor-help">{renderNodeIcon()}</div>
</Tooltip>
)}
<div className="flex-1 overflow-hidden">
<div className="truncate">
<Typography.Text>{nodeRender.data?.name || "\u00A0"}</Typography.Text>
</div>
{children != null && (
<div className="truncate text-xs" style={{ color: themeToken.colorTextTertiary }}>
{children}
</div>
)}
</div>
<div className="-mr-2 ml-1" onClick={(e) => e.stopPropagation()}>
<NodeMenuButton className="opacity-0 transition-opacity group-hover/node:opacity-100" size="small" />
</div>
</div>
</Card>
{!playground.config.readonlyOrDisabled && nodeRegistry.meta?.draggable === true && (
<div className="absolute top-1/2 -left-4 hidden -translate-y-1/2 group-hover/node:block">
<IconGripVertical size="1em" stroke="1" />
</div>
)}
</div>
);
};
export const BlockNode = ({ className, style, children }: { className?: string; style?: React.CSSProperties; children?: React.ReactNode }) => {
const ctx = useClientContext();
const { playground } = ctx;
const nodeRender = useNodeRender();
const nodeRegistry = nodeRender.node.getNodeRegistry<NodeRegistry>();
return (
<Popover classNames={{ root: "shadow-md" }} styles={{ body: { padding: 0 } }} arrow={false} content={<NodeMenuButton variant="text" />} placement="right">
<div className={mergeCls("relative w-[240px] group/node", className)} style={style}>
<Card className="rounded-xl shadow-sm" styles={{ body: { padding: 0 } }} hoverable>
<div className="overflow-hidden px-3 py-2">
<div className="truncate text-center">{children ?? (nodeRender.data?.name || "\u00A0")}</div>
</div>
</Card>
{!playground.config.readonlyOrDisabled && nodeRegistry.meta?.draggable === true && (
<div className="absolute top-1/2 -left-4 hidden -translate-y-1/2 group-hover/node:block">
<IconGripVertical size="1em" stroke="1" />
</div>
)}
</div>
</Popover>
);
};
export const NodeMenuButton = ({ className, style, ...props }: ButtonProps) => {
const ctx = useClientContext();
const { playground } = ctx;
const menuItems = useNodeMenuItems();
return playground.config.readonlyOrDisabled ? null : (
<Dropdown menu={{ items: menuItems }} trigger={["click"]} arrow={false}>
<Button className={className} style={style} ghost icon={<IconDotsVertical color="grey" size="1.25em" />} type="text" {...props} />
</Dropdown>
);
};
export const useNodeMenuItems = () => {
const { t } = useTranslation();
const ctx = useClientContext();
const { operation } = ctx;
const nodeRender = useNodeRender();
const nodeRegistry = nodeRender.node.getNodeRegistry<NodeRegistry>();
const nodeDeleteDisabled = useMemo(() => {
if (nodeRegistry.canDelete != null) {
return !nodeRegistry.canDelete(ctx, nodeRender.node);
}
return nodeRegistry.meta!.deleteDisable;
}, [nodeRegistry, nodeRender.node]);
const menuItems = useMemo<Required<MenuProps>["items"]>(() => {
return [
{
key: "rename",
label: t("workflow_node.action.rename_node"),
icon: <IconLabel size="1em" />,
onClick: () => {
operation.deleteNode(nodeRender.node);
alert("TODO: rename");
},
},
{
key: "duplicate",
label: t("workflow_node.action.duplicate_node"),
icon: <IconCopy size="1em" />,
onClick: () => {
operation.deleteNode(nodeRender.node);
alert("TODO: duplicate");
},
},
{
type: "divider",
},
{
key: "remove",
label: t("workflow_node.action.remove_node"),
icon: <IconX size="1em" />,
danger: true,
disabled: nodeDeleteDisabled,
onClick: () => {
operation.deleteNode(nodeRender.node);
alert("TODO: remove");
},
},
];
}, [nodeRender.node, nodeDeleteDisabled]);
return menuItems;
};
@@ -0,0 +1,23 @@
import { BizApplyNodeRegistry, BizDeployNodeRegistry, BizMonitorNodeRegistry, BizNotifyNodeRegistry, BizUploadNodeRegistry } from "./BusinessNode";
import { BranchBlockNodeRegistry, ConditionNodeRegistry } from "./ConditionNode";
import { EndNodeRegistry } from "./EndNode";
import { StartNodeRegistry } from "./StartNode";
import { CatchBlockNodeRegistry, TryCatchNodeRegistry } from "./TryCatchNode";
export const getFlowNodeRegistries = () => {
return [
StartNodeRegistry,
BizApplyNodeRegistry,
BizUploadNodeRegistry,
BizMonitorNodeRegistry,
BizDeployNodeRegistry,
BizNotifyNodeRegistry,
ConditionNodeRegistry,
BranchBlockNodeRegistry,
TryCatchNodeRegistry,
CatchBlockNodeRegistry,
EndNodeRegistry,
];
};
export type * from "./typings";
@@ -0,0 +1,55 @@
import { FlowNodeBaseType } from "@flowgram.ai/document";
import {
type FixedLayoutPluginContext,
type FlowNodeEntity,
type FlowNodeJSON,
type FlowNodeMeta,
type FlowNodeRegistry,
type FormMeta,
type FormRenderProps,
} from "@flowgram.ai/fixed-layout-editor";
export enum NodeType {
Start = FlowNodeBaseType.START,
End = FlowNodeBaseType.END,
Condition = "condition",
BranchBlock = "branchBlock",
TryCatch = "tryCatch",
TryBlock = "tryBlock",
CatchBlock = "catchBlock",
BizApply = "bizApply",
BizUpload = "bizUpload",
BizMonitor = "bizMonitor",
BizDeploy = "bizDeploy",
BizNotify = "bizNotify",
}
export interface NodeJSON extends FlowNodeJSON {
data: {
name?: string;
disabled?: boolean;
[key: string]: any;
};
}
export interface DocumentJSON {
nodes: NodeJSON[];
}
export interface NodeMeta extends FlowNodeMeta {
style?: React.CSSProperties;
helpText?: React.ReactNode;
icon?: React.Component;
iconColor?: string;
iconBgColor?: string;
}
export interface NodeRegistry<V extends NodeJSON["data"] = NodeJSON["data"]> extends FlowNodeRegistry<NodeMeta> {
meta?: FlowNodeMeta;
formMeta?: Omit<FormMeta<V>, "render"> & {
render: (props: FormRenderProps<V>) => React.ReactElement;
};
canAdd?: (ctx: FixedLayoutPluginContext, from: FlowNodeEntity) => boolean;
canDelete?: (ctx: FixedLayoutPluginContext, from: FlowNodeEntity) => boolean;
onAdd?: (ctx: FixedLayoutPluginContext, from: FlowNodeEntity) => FlowNodeJSON;
}
@@ -253,8 +253,8 @@ const SharedNodeBlock = ({ children, node, disabled, onClick }: SharedNodeBlockP
content={<SharedNodeMenu node={node} disabled={disabled} trigger={<Button color="primary" icon={<IconDotsVertical size="1em" />} variant="text" />} />}
placement="rightTop"
>
<div className="relative w-[256px] overflow-hidden">
<Card className="shadow-md" styles={{ body: { padding: 0 } }} hoverable>
<div className="relative w-[256px]">
<Card className="overflow-hidden shadow-md" styles={{ body: { padding: 0 } }} hoverable>
<div className="flex h-[48px] flex-col items-center justify-center truncate bg-primary px-4 py-2 text-white">
<SharedNodeTitle
className="overflow-hidden outline-hidden focus:rounded-xs focus:bg-background focus:text-foreground"
@@ -331,7 +331,7 @@ const SharedNodeConfigDrawer = ({
if (changed) {
modal.confirm({
title: t("common.text.operation_confirm"),
content: t("workflow_node.unsaved_changes.confirm"),
content: t("workflow.detail.design.unsaved_changes.confirm"),
onOk: () => resolve(void 0),
onCancel: () => reject(),
});
+14 -4
View File
@@ -28,7 +28,7 @@
"workflow.action.rollback.button": "Rollback",
"workflow.action.rollback.modal.title": "Rollback changes",
"workflow.action.rollback.modal.content": "Are you sure to rollback your changes to the last published version?",
"workflow.action.run.button": "Run at once",
"workflow.action.run.button": "Run",
"workflow.action.run.modal.title": "Run workflow",
"workflow.action.run.modal.content": "You have unpublished changes. Do you really want to run this workflow based on the last published version?",
"workflow.action.run.prompt": "Running... Please check the history later",
@@ -60,8 +60,18 @@
"workflow.detail.baseinfo.name.placeholder": "Please enter workflow name",
"workflow.detail.baseinfo.description.placeholder": "Please enter workflow description",
"workflow.detail.design.tab": "Orchestration",
"workflow.detail.design_legacy.tab": "Orchestration (Legacy)",
"workflow.detail.design.draft.alert": "The orchestration is not released yet.",
"workflow.detail.design_legacy.tab": "Orchestration (legacy)",
"workflow.detail.design.tab": "Orchestration (TODO)",
"workflow.detail.design.toolbar.zoom": "Zoom",
"workflow.detail.design.toolbar.zoomin": "Zoom in",
"workflow.detail.design.toolbar.zoomout": "Zoom out",
"workflow.detail.design.toolbar.auto_fit": "Auto fit view",
"workflow.detail.design.toolbar.horizontal_layout": "Horizontal Layout",
"workflow.detail.design.toolbar.vertical_layout": "Vertical Layout",
"workflow.detail.design.toolbar.hand_mode": "Hand mode",
"workflow.detail.design.toolbar.pointer_mode": "Pointer mode",
"workflow.detail.design.nodes.placeholder": "Please configure",
"workflow.detail.design.unpublished_draft.alert": "The orchestration is not released yet.",
"workflow.detail.design.unsaved_changes.confirm": "You have unsaved changes. Do you really want to close the panel and drop those changes?",
"workflow.detail.runs.tab": "History runs"
}
+35 -19
View File
@@ -9,9 +9,8 @@
"workflow_node.action.duplicate_branch": "Duplicate branch",
"workflow_node.action.remove_branch": "Delete branch",
"workflow_node.unsaved_changes.confirm": "You have unsaved changes. Do you really want to close the panel and drop those changes?",
"workflow_node.start.label": "Start",
"workflow_node.start.help": "Define the trigger for workflow initiation. A workflow can only have one start node.",
"workflow_node.start.default_name": "Start",
"workflow_node.start.form.trigger.label": "Trigger",
"workflow_node.start.form.trigger.placeholder": "Please select trigger",
@@ -24,7 +23,8 @@
"workflow_node.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:",
"workflow_node.start.form.trigger_cron.guide": "Tips: If you have multiple workflows, it is recommended to set them to run at different times of the day instead of always running at a specific time. And please don't always set it to midnight every day to avoid spikes in traffic. <br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Let’s Encrypt (ACME) client run at a random time?</a>",
"workflow_node.apply.label": "Obtain certificate",
"workflow_node.apply.label": "Request certificate",
"workflow_node.apply.help": "Apply for SSL certificate issuance from the certificate authority.",
"workflow_node.apply.default_name": "Application",
"workflow_node.apply.form.domains.label": "Domains",
"workflow_node.apply.form.domains.placeholder": "Please enter domains (separated by semicolons)",
@@ -104,6 +104,7 @@
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the issued certificate, otherwise the certificate may never be renewed.",
"workflow_node.upload.label": "Upload certificate",
"workflow_node.upload.help": "Upload the user's existing SSL certificate.",
"workflow_node.upload.default_name": "Uploading",
"workflow_node.upload.form.domains.label": "Domains",
"workflow_node.upload.form.domains.placholder": "Please select certificate file",
@@ -112,7 +113,21 @@
"workflow_node.upload.form.private_key.label": "Private key (PEM format)",
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.monitor.label": "Monitor certificate",
"workflow_node.monitor.help": "Obtain the SSL certificate of the website through HTTPS protocol.",
"workflow_node.monitor.default_name": "Monitoring",
"workflow_node.monitor.form.guide": "Tips: Certimate will send a HEAD request to the target address to obtain the certificate. Please ensure that the address is accessible through HTTPS protocol.",
"workflow_node.monitor.form.host.label": "Host",
"workflow_node.monitor.form.host.placeholder": "Please enter host",
"workflow_node.monitor.form.port.label": "Port",
"workflow_node.monitor.form.port.placeholder": "Please enter port",
"workflow_node.monitor.form.domain.label": "Domain (Optional)",
"workflow_node.monitor.form.domain.placeholder": "Please enter domain name",
"workflow_node.monitor.form.request_path.label": "Request path (Optional)",
"workflow_node.monitor.form.request_path.placeholder": "Please enter request path",
"workflow_node.deploy.label": "Deploy certificate",
"workflow_node.deploy.help": "Invoke the APIs of the service provider to deploy the SSL certificate.",
"workflow_node.deploy.default_name": "Deployment",
"workflow_node.deploy.form.provider.label": "Deploy target",
"workflow_node.deploy.form.provider.placeholder": "Please select deploy target",
@@ -938,19 +953,8 @@
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "skip",
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "not skip",
"workflow_node.monitor.label": "Monitor certificate",
"workflow_node.monitor.default_name": "Monitoring",
"workflow_node.monitor.form.guide": "Tips: Certimate will send a HEAD request to the target address to obtain the certificate. Please ensure that the address is accessible through HTTPS protocol.",
"workflow_node.monitor.form.host.label": "Host",
"workflow_node.monitor.form.host.placeholder": "Please enter host",
"workflow_node.monitor.form.port.label": "Port",
"workflow_node.monitor.form.port.placeholder": "Please enter port",
"workflow_node.monitor.form.domain.label": "Domain (Optional)",
"workflow_node.monitor.form.domain.placeholder": "Please enter domain name",
"workflow_node.monitor.form.request_path.label": "Request path (Optional)",
"workflow_node.monitor.form.request_path.placeholder": "Please enter request path",
"workflow_node.notify.label": "Send notification",
"workflow_node.notify.help": "Invoke the APIs of the service provider to push message notifications.",
"workflow_node.notify.default_name": "Notification",
"workflow_node.notify.form.subject.label": "Subject",
"workflow_node.notify.form.subject.placeholder": "Please enter subject",
@@ -989,13 +993,11 @@
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.on": "skip",
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.off": "not skip",
"workflow_node.end.label": "End",
"workflow_node.end.default_name": "End",
"workflow_node.branch.label": "Parallel/Conditional branch",
"workflow_node.branch.default_name": "Branch",
"workflow_node.condition.label": "Branch",
"workflow_node.condition.help": "When the specified conditions are met, enter the corresponding branch. The failure of a node in a certain branch does not affect the continuation of parallel branch execution.",
"workflow_node.condition.default_name": "Branch",
"workflow_node.condition.default_name.template_certtest_on_expire_soon": "If the certificate will expire soon ...",
"workflow_node.condition.default_name.template_certtest_on_expired": "If the certificate has expired ...",
@@ -1021,6 +1023,9 @@
"workflow_node.condition.form.expression.value.option.false.label": "False",
"workflow_node.condition.form.expression.add_condition.button": "Add condition",
"workflow_node.branch_block.label": "Branch",
"workflow_node.branch_block.default_name": "Branch",
"workflow_node.execute_result_branch.label": "Execution result branch",
"workflow_node.execute_result_branch.default_name": "Branch",
@@ -1028,5 +1033,16 @@
"workflow_node.execute_success.default_name": "On Succeeded",
"workflow_node.execute_failure.label": "If the previous node failed ...",
"workflow_node.execute_failure.default_name": "On Failed"
"workflow_node.execute_failure.default_name": "On Failed",
"workflow_node.try_catch.label": "Execution result branch",
"workflow_node.try_catch.help": "Attempt to execute subsequent nodes, and when any node fails to execute, interrupt and enter the execution failure branch.",
"workflow_node.try_catch.default_name": "Try...",
"workflow_node.catch_block.label": "Execution failure branch",
"workflow_node.catch_block.default_name": "On failed...",
"workflow_node.end.label": "End",
"workflow_node.end.help": "Suspend the workflow and exit. Usually the last node of a workflow.",
"workflow_node.end.default_name": "End"
}
+2 -2
View File
@@ -1,9 +1,9 @@
{
"certificate.page.title": "SSL 证书",
"certificate.page.subtitle": "SSL 证书含有网站的公钥和网站标识以及其他相关信息。它们来自于工作流的执行输出。",
"certificate.page.subtitle": "SSL 证书含有网站的公钥和网站标识以及其他相关信息。它们来自于工作流的运行输出。",
"certificate.nodata.title": "暂无证书",
"certificate.nodata.description": "当前未找到任何认证。请先执行一个工作流以生成证书。",
"certificate.nodata.description": "当前未找到任何认证。请先运行一个工作流以生成证书。",
"certificate.nodata.button": "前往工作流",
"certificate.search.placeholder": "按证书名称或序列号搜索……",
+2 -2
View File
@@ -7,8 +7,8 @@
"dashboard.statistics.all_workflows": "所有工作流",
"dashboard.statistics.enabled_workflows": "已启用工作流",
"dashboard.latest_workflow_runs": "最近执行的工作流",
"dashboard.latest_workflow_runs.nodata.description": "当前未找到任何工作流执行记录。请先执行一个工作流。",
"dashboard.latest_workflow_runs": "最近运行的工作流",
"dashboard.latest_workflow_runs.nodata.description": "当前未找到任何工作流运行历史。请先运行一个工作流。",
"dashboard.latest_workflow_runs.nodata.button": "前往工作流",
"dashboard.quick_actions": "快捷操作",
+2 -2
View File
@@ -56,8 +56,8 @@
"settings.persistence.tab": "数据持久化",
"settings.persistence.title": "定期清理数据",
"settings.persistence.form.workflow_runs_max_days.label": "工作流执行历史保留天数",
"settings.persistence.form.workflow_runs_max_days.placeholder": "请输入执行历史保留天数",
"settings.persistence.form.workflow_runs_max_days.label": "工作流运行历史保留天数",
"settings.persistence.form.workflow_runs_max_days.placeholder": "请输入运行历史保留天数",
"settings.persistence.form.workflow_runs_max_days.unit": "天",
"settings.persistence.form.workflow_runs_max_days.extra": "设置为 <b>0</b> 表示永久保留,不会自动清理。建议设置为 <b>180</b> 天以上。",
"settings.persistence.form.expired_certificates_max_days.label": "证书过期后保留天数",
+20 -10
View File
@@ -1,6 +1,6 @@
{
"workflow.page.title": "工作流",
"workflow.page.subtitle": "工作流是自动化流程的节点集合。当满足触发条件时,工作流开始顺序执行各节点以完成复杂的任务。",
"workflow.page.subtitle": "工作流是自动化流程的节点集合。当满足触发条件时,工作流开始顺序运行各节点以完成复杂的任务。",
"workflow.nodata.title": "暂无工作流",
"workflow.nodata.description": "当前未找到工作流。请先创建。",
@@ -28,17 +28,17 @@
"workflow.action.rollback.button": "回退更改",
"workflow.action.rollback.modal.title": "回退更改",
"workflow.action.rollback.modal.content": "确定要回退到最近一次发布的版本吗?",
"workflow.action.run.button": "立即执行",
"workflow.action.run.modal.title": "执行工作流",
"workflow.action.run.modal.content": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?",
"workflow.action.run.prompt": "执行中……请稍后查看执行历史",
"workflow.action.run.button": "运行",
"workflow.action.run.modal.title": "运行工作流",
"workflow.action.run.modal.content": "你有尚未发布的更改。确定要以最近一次发布的版本继续运行吗?",
"workflow.action.run.prompt": "运行中……请稍后查看运行历史",
"workflow.props.name": "名称",
"workflow.props.description": "描述",
"workflow.props.trigger": "触发方式",
"workflow.props.trigger.scheduled": "定时",
"workflow.props.trigger.manual": "手动",
"workflow.props.last_run_at": "最近执行时间",
"workflow.props.last_run_at": "最近运行时间",
"workflow.props.state": "是否启用",
"workflow.props.state.filter.all": "全部",
"workflow.props.state.filter.enabled": "启用",
@@ -60,8 +60,18 @@
"workflow.detail.baseinfo.name.placeholder": "请输入工作流名称",
"workflow.detail.baseinfo.description.placeholder": "请输入工作流描述",
"workflow.detail.design.tab": "流程编排",
"workflow.detail.design_legacy.tab": "旧版编辑器",
"workflow.detail.design.draft.alert": "当前编排有未发布的更改。",
"workflow.detail.runs.tab": "执行历史"
"workflow.detail.design_legacy.tab": "流程编排(旧版)",
"workflow.detail.design.tab": "流程编排 (TODO)",
"workflow.detail.design.toolbar.zoom": "缩放",
"workflow.detail.design.toolbar.zoomin": "放大",
"workflow.detail.design.toolbar.zoomout": "缩小",
"workflow.detail.design.toolbar.auto_fit": "自适应视图",
"workflow.detail.design.toolbar.horizontal_layout": "横向布局",
"workflow.detail.design.toolbar.vertical_layout": "竖向布局",
"workflow.detail.design.toolbar.hand_mode": "触摸模式",
"workflow.detail.design.toolbar.pointer_mode": "指针模式",
"workflow.detail.design.nodes.placeholder": "请完成配置",
"workflow.detail.design.unpublished_draft.alert": "当前编排有未发布的更改。",
"workflow.detail.design.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?",
"workflow.detail.runs.tab": "运行历史"
}
+37 -21
View File
@@ -9,9 +9,8 @@
"workflow_node.action.duplicate_branch": "复制分支",
"workflow_node.action.remove_branch": "删除分支",
"workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?",
"workflow_node.start.label": "开始",
"workflow_node.start.help": "定义工作流启动的触发条件。一个工作流只能有一个开始节点。",
"workflow_node.start.default_name": "开始",
"workflow_node.start.form.trigger.label": "触发方式",
"workflow_node.start.form.trigger.placeholder": "请选择触发方式",
@@ -21,10 +20,11 @@
"workflow_node.start.form.trigger_cron.placeholder": "请输入 Cron 表达式",
"workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式",
"workflow_node.start.form.trigger_cron.tooltip": "五段式表达式,支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式。时区以服务器设置为准。",
"workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
"workflow_node.start.form.trigger_cron.extra": "预计最近 5 次运行时间:",
"workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?</a>",
"workflow_node.apply.label": "申请签发证书",
"workflow_node.apply.help": "从证书颁发机构申请签发 SSL 证书。",
"workflow_node.apply.default_name": "申请",
"workflow_node.apply.form.domains.label": "域名",
"workflow_node.apply.form.domains.placeholder": "请输入域名(多个值请用半角分号隔开)",
@@ -98,11 +98,12 @@
"workflow_node.apply.form.skip_before_expiry_days.label": "重复申请",
"workflow_node.apply.form.skip_before_expiry_days.placeholder": "请输入续期间隔",
"workflow_node.apply.form.skip_before_expiry_days.prefix": "当上次签发的证书剩余有效期大于",
"workflow_node.apply.form.skip_before_expiry_days.suffix": "时,再次执行工作流时跳过此申请节点。",
"workflow_node.apply.form.skip_before_expiry_days.suffix": "时,再次运行工作流时跳过此申请节点。",
"workflow_node.apply.form.skip_before_expiry_days.unit": "天",
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过颁发的证书最大有效期,否则证书可能永远不会续期。",
"workflow_node.upload.label": "上传自有证书",
"workflow_node.upload.help": "上传用户已有的本地 SSL 证书。",
"workflow_node.upload.default_name": "上传",
"workflow_node.upload.form.domains.label": "域名",
"workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
@@ -111,7 +112,21 @@
"workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)",
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.monitor.label": "监控网站证书",
"workflow_node.monitor.help": "通过 HTTPS 协议获取指定网站的 SSL 证书。",
"workflow_node.monitor.default_name": "监控",
"workflow_node.monitor.form.guide": "小贴士:Certimate 将向目标地址发送一个 HEAD 请求来获取相应的域名证书,请确保该地址可通过 HTTPS 协议访问。",
"workflow_node.monitor.form.host.label": "主机地址",
"workflow_node.monitor.form.host.placeholder": "请输入主机地址(可以是域名或 IP)",
"workflow_node.monitor.form.port.label": "主机端口",
"workflow_node.monitor.form.port.placeholder": "请输入主机端口",
"workflow_node.monitor.form.domain.label": "域名(可选)",
"workflow_node.monitor.form.domain.placeholder": "请输入域名(仅当主机地址为 IP 时可选)",
"workflow_node.monitor.form.request_path.label": "请求路径(可选)",
"workflow_node.monitor.form.request_path.placeholder": "请输入请求路径",
"workflow_node.deploy.label": "部署证书到 ...",
"workflow_node.deploy.help": "调用服务提供商相关 API,将 SSL 证书部署到指定的目标。",
"workflow_node.deploy.default_name": "部署",
"workflow_node.deploy.form.provider.label": "部署目标",
"workflow_node.deploy.form.provider.placeholder": "请选择部署目标",
@@ -931,24 +946,13 @@
"workflow_node.deploy.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
"workflow_node.deploy.form.strategy_config.label": "执行策略",
"workflow_node.deploy.form.skip_on_last_succeeded.label": "重复部署",
"workflow_node.deploy.form.skip_on_last_succeeded.prefix": "当上次部署相同证书成功时,再次执行工作流时",
"workflow_node.deploy.form.skip_on_last_succeeded.prefix": "当上次部署相同证书成功时,再次运行工作流时",
"workflow_node.deploy.form.skip_on_last_succeeded.suffix": "此部署节点。",
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "跳过",
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "不跳过",
"workflow_node.monitor.label": "监控网站证书",
"workflow_node.monitor.default_name": "监控",
"workflow_node.monitor.form.guide": "小贴士:Certimate 将向目标地址发送一个 HEAD 请求来获取相应的域名证书,请确保该地址可通过 HTTPS 协议访问。",
"workflow_node.monitor.form.host.label": "主机地址",
"workflow_node.monitor.form.host.placeholder": "请输入主机地址(可以是域名或 IP)",
"workflow_node.monitor.form.port.label": "主机端口",
"workflow_node.monitor.form.port.placeholder": "请输入主机端口",
"workflow_node.monitor.form.domain.label": "域名(可选)",
"workflow_node.monitor.form.domain.placeholder": "请输入域名(仅当主机地址为 IP 时可选)",
"workflow_node.monitor.form.request_path.label": "请求路径(可选)",
"workflow_node.monitor.form.request_path.placeholder": "请输入请求路径",
"workflow_node.notify.label": "推送通知",
"workflow_node.notify.help": "调用服务提供商相关 API,将消息通知推送到指定的目标。",
"workflow_node.notify.default_name": "通知",
"workflow_node.notify.form.subject.label": "通知主题",
"workflow_node.notify.form.subject.placeholder": "请输入通知主题",
@@ -987,13 +991,11 @@
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.on": "跳过",
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.off": "不跳过",
"workflow_node.end.label": "结束",
"workflow_node.end.default_name": "结束",
"workflow_node.branch.label": "并行/条件分支",
"workflow_node.branch.default_name": "分支",
"workflow_node.condition.label": "分支",
"workflow_node.condition.help": "当满足指定的条件时,进入相应分支。某一分支中的节点执行失败不影响平行分支继续执行。",
"workflow_node.condition.default_name": "分支",
"workflow_node.condition.default_name.template_certtest_on_expire_soon": "若网站证书即将到期 ...",
"workflow_node.condition.default_name.template_certtest_on_expired": "若网站证书已到期 ...",
@@ -1019,6 +1021,9 @@
"workflow_node.condition.form.expression.value.option.false.label": "假",
"workflow_node.condition.form.expression.add_condition.button": "添加条件",
"workflow_node.branch_block.label": "分支",
"workflow_node.branch_block.default_name": "分支",
"workflow_node.execute_result_branch.label": "执行结果分支",
"workflow_node.execute_result_branch.default_name": "分支",
@@ -1026,5 +1031,16 @@
"workflow_node.execute_success.default_name": "执行成功",
"workflow_node.execute_failure.label": "若上一节点执行失败…",
"workflow_node.execute_failure.default_name": "执行失败"
"workflow_node.execute_failure.default_name": "执行失败",
"workflow_node.try_catch.label": "执行结果分支",
"workflow_node.try_catch.help": "尝试执行后续节点,当任一节点执行失败后,中断并进入执行失败分支。",
"workflow_node.try_catch.default_name": "尝试执行…",
"workflow_node.catch_block.label": "执行失败分支",
"workflow_node.catch_block.default_name": "若执行失败…",
"workflow_node.end.label": "结束",
"workflow_node.end.help": "中止工作流运行并退出。通常是一个工作流的最后一个节点。",
"workflow_node.end.default_name": "结束"
}
+14 -14
View File
@@ -1,27 +1,27 @@
{
"workflow_run.action.view.button": "查看详情",
"workflow_run.action.cancel.button": "取消执行",
"workflow_run.action.cancel.modal.title": "取消执行",
"workflow_run.action.cancel.modal.content": "确定要取消此执行吗?取消后仅会中止流程,但不会回滚已执行的节点。",
"workflow_run.action.delete.button": "删除执行",
"workflow_run.action.delete.modal.title": "删除执行「{{name}}」",
"workflow_run.action.delete.modal.content": "确定要删除该工作流执行记录吗?删除后仅会清除日志历史,但不会影响签发的证书。<br>注意此操作不可撤销,请谨慎操作。",
"workflow_run.action.cancel.button": "取消运行",
"workflow_run.action.cancel.modal.title": "取消运行",
"workflow_run.action.cancel.modal.content": "确定要取消此运行吗?取消后仅会中止流程,但不会回滚已执行的节点。",
"workflow_run.action.delete.button": "删除运行历史",
"workflow_run.action.delete.modal.title": "删除运行「{{name}}」",
"workflow_run.action.delete.modal.content": "确定要删除该工作流运行历史吗?删除后仅会清除日志历史,但不会影响签发的证书。<br>注意此操作不可撤销,请谨慎操作。",
"workflow_run.deletion.alert": "执行记录中包含工作流各节点的执行结果,删除后可能导致因找不到前次执行结果而触发重新申请或部署证书。如无必要请勿提前删除,建议保留至少 180 天。",
"workflow_run.cancellation.alert": "如遇进程意外中止、服务器超时等原因,你可以手动取消长时间挂起的执行,避免阻塞后续执行。",
"workflow_run.deletion.alert": "运行历史中包含工作流各节点的运行结果,删除后可能导致因找不到前次运行结果而触发重新申请或部署证书。如无必要请勿提前删除,建议保留至少 180 天。",
"workflow_run.cancellation.alert": "如遇进程意外中止、服务器超时等原因,你可以手动取消长时间挂起的运行,避免阻塞后续运行。",
"workflow_run.nodata.description": "当前未找到任何执行记录。请先执行此工作流。",
"workflow_run.nodata.description": "当前未找到任何运行历史。请先运行此工作流。",
"workflow_run.props.id": "ID",
"workflow_run.props.status": "状态",
"workflow_run.props.status.pending": "等待执行",
"workflow_run.props.status.running": "执行中",
"workflow_run.props.status.pending": "等待运行",
"workflow_run.props.status.running": "运行中",
"workflow_run.props.status.succeeded": "已成功",
"workflow_run.props.status.failed": "已失败",
"workflow_run.props.status.canceled": "已取消",
"workflow_run.props.trigger": "执行方式",
"workflow_run.props.trigger.scheduled": "定时执行",
"workflow_run.props.trigger.manual": "手动执行",
"workflow_run.props.trigger": "触发方式",
"workflow_run.props.trigger.scheduled": "定时",
"workflow_run.props.trigger.manual": "手动",
"workflow_run.props.started_at": "开始时间",
"workflow_run.props.ended_at": "完成时间",
+3 -3
View File
@@ -8,8 +8,8 @@ import {
IconHelpCircle,
IconHierarchy3,
IconHome,
IconLogout,
IconMenu2,
IconPower,
IconSettings,
} from "@tabler/icons-react";
import { Alert, Button, Drawer, Layout, Menu, type MenuProps, theme } from "antd";
@@ -88,7 +88,7 @@ const ConsoleLayout = () => {
danger: true,
icon: (
<span className="anticon scale-125" role="img">
<IconLogout size="1em" />
<IconPower size="1em" />
</span>
),
label: t("common.menu.logout"),
@@ -136,7 +136,7 @@ const ConsoleLayout = () => {
<AppVersion.Badge>
<Button icon={<IconBrandGithub size="1.25em" stroke="1.25" />} onClick={handleGitHubClick} />
</AppVersion.Badge>
<Button danger icon={<IconLogout size="1.25em" stroke="1.25" />} onClick={handleLogoutClick} />
<Button danger icon={<IconPower size="1.25em" stroke="1.25" />} onClick={handleLogoutClick} />
</div>
</div>
</Layout.Header>
+3 -3
View File
@@ -143,9 +143,9 @@ const AccessList = () => {
renderCell(checked, _, index, node) {
if (!checked) {
return (
<div className="group">
<div className="group-hover:hidden">{(page - 1) * pageSize + index + 1}</div>
<div className="hidden group-hover:block">{node}</div>
<div className="group/selection">
<div className="group-hover/selection:hidden">{(page - 1) * pageSize + index + 1}</div>
<div className="hidden group-hover/selection:block">{node}</div>
</div>
);
}
@@ -190,9 +190,9 @@ const CertificateList = () => {
renderCell(checked, _, index, node) {
if (!checked) {
return (
<div className="group">
<div className="group-hover:hidden">{(page - 1) * pageSize + index + 1}</div>
<div className="hidden group-hover:block">{node}</div>
<div className="group/selection">
<div className="group-hover/selection:hidden">{(page - 1) * pageSize + index + 1}</div>
<div className="hidden group-hover/selection:block">{node}</div>
</div>
);
}
+51 -28
View File
@@ -3,10 +3,11 @@ import { useTranslation } from "react-i18next";
import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom";
import { IconEdit, IconHistory, IconPlayerPlay, IconRobot } from "@tabler/icons-react";
import { useSize } from "ahooks";
import { App, Button, Input, type InputRef, Segmented, Skeleton, Space } from "antd";
import { App, Button, Input, type InputRef, Segmented, Skeleton } from "antd";
import { startRun as startWorkflowRun } from "@/api/workflows";
import Show from "@/components/Show";
import { isAllNodesValidated } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
@@ -19,10 +20,10 @@ const WorkflowDetail = () => {
const { t } = useTranslation();
const { message, modal } = App.useApp();
const { message, modal, notification } = App.useApp();
const { id: workflowId } = useParams();
const { workflow, initialized, ...workflowState } = useWorkflowStore(useZustandShallowSelector(["workflow", "initialized", "init", "destroy"]));
const { workflow, initialized, ...workflowState } = useWorkflowStore(useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled"]));
useEffect(() => {
workflowState.init(workflowId!);
@@ -35,6 +36,7 @@ const WorkflowDetail = () => {
const divHeaderSize = useSize(divHeaderRef);
const tabs = [
["design-legacy", "workflow.detail.design_legacy.tab", <IconRobot size="1em" />], // TODO: 正式发布时移除
["design", "workflow.detail.design.tab", <IconRobot size="1em" />],
["runs", "workflow.detail.runs.tab", <IconHistory size="1em" />],
] satisfies [string, string, React.ReactElement][];
@@ -90,6 +92,20 @@ const WorkflowDetail = () => {
});
};
const handleActiveClick = async () => {
try {
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
message.warning(t("workflow.action.enable.errmsg.uncompleted"));
return;
}
await workflowState.setEnabled(!workflow.enabled);
} catch (err) {
console.error(err);
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
}
};
return (
<div className="flex size-full flex-col">
<div className="px-6 py-4" ref={divHeaderRef}>
@@ -117,11 +133,14 @@ const WorkflowDetail = () => {
</div>
</div>
<div className="py-2">
<Space.Compact>
<Button disabled={runButtonDisabled} icon={<IconPlayerPlay size="1.25em" />} loading={runButtonLoading} type="primary" onClick={handleRunClick}>
{t("workflow.action.run.button")}
</Button>
</Space.Compact>
<Show when={initialized}>
<div className="flex items-center gap-2">
<Button onClick={handleActiveClick}>{workflow.enabled ? t("workflow.action.disable.button") : t("workflow.action.enable.button")}</Button>
<Button disabled={runButtonDisabled} icon={<IconPlayerPlay size="1.25em" />} loading={runButtonLoading} type="primary" onClick={handleRunClick}>
{t("workflow.action.run.button")}
</Button>
</div>
</Show>
</div>
</div>
</div>
@@ -152,7 +171,7 @@ const WorkflowDetailBaseName = () => {
const { notification } = App.useApp();
const { workflow, setBaseInfo: setWorkflowBaseInfo } = useWorkflowStore(useZustandShallowSelector(["workflow", "setBaseInfo"]));
const { workflow, initialized, setBaseInfo: setWorkflowBaseInfo } = useWorkflowStore(useZustandShallowSelector(["workflow", "initialized", "setBaseInfo"]));
const inputRef = useRef<InputRef>(null);
@@ -194,7 +213,7 @@ const WorkflowDetailBaseName = () => {
};
return (
<div className="group relative flex items-center gap-1">
<div className="group/input relative flex items-center gap-1">
<h1
className={mergeCls("break-all", {
invisible: editing,
@@ -202,14 +221,16 @@ const WorkflowDetailBaseName = () => {
>
{workflow.name || "\u00A0"}
</h1>
<Button
className={mergeCls("mb-2 opacity-0 transition-opacity group-hover:opacity-100", {
invisible: editing,
})}
icon={<IconEdit size="1.25em" stroke="1.25" />}
type="text"
onClick={handleEditClick}
/>
{initialized && (
<Button
className={mergeCls("mb-2 opacity-0 transition-opacity group-hover/input:opacity-100", {
invisible: editing,
})}
icon={<IconEdit size="1.25em" stroke="1.25" />}
type="text"
onClick={handleEditClick}
/>
)}
<Input
className={mergeCls("absolute top-0 left-0", editing ? "block" : "hidden")}
ref={inputRef}
@@ -231,7 +252,7 @@ const WorkflowDetailBaseDescription = () => {
const { notification } = App.useApp();
const { workflow, setBaseInfo: setWorkflowBaseInfo } = useWorkflowStore(useZustandShallowSelector(["workflow", "setBaseInfo"]));
const { workflow, initialized, setBaseInfo: setWorkflowBaseInfo } = useWorkflowStore(useZustandShallowSelector(["workflow", "initialized", "setBaseInfo"]));
const inputRef = useRef<InputRef>(null);
@@ -273,7 +294,7 @@ const WorkflowDetailBaseDescription = () => {
};
return (
<div className="group relative flex items-center gap-1">
<div className="group/input relative flex items-center gap-1">
<p
className={mergeCls("text-base text-gray-500", {
invisible: editing,
@@ -281,14 +302,16 @@ const WorkflowDetailBaseDescription = () => {
>
{workflow.description || "\u00A0"}
</p>
<Button
className={mergeCls("mb-4 opacity-0 transition-opacity group-hover:opacity-100", {
invisible: editing,
})}
icon={<IconEdit size="1.25em" stroke="1.25" />}
type="text"
onClick={handleEditClick}
/>
{initialized && (
<Button
className={mergeCls("mb-4 opacity-0 transition-opacity group-hover/input:opacity-100", {
invisible: editing,
})}
icon={<IconEdit size="1.25em" stroke="1.25" />}
type="text"
onClick={handleEditClick}
/>
)}
<Input
className={mergeCls("absolute top-0 left-0", editing ? "block" : "hidden")}
ref={inputRef}
+235 -37
View File
@@ -1,12 +1,16 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getI18n, useTranslation } from "react-i18next";
import { type FlowDocumentJSON } from "@flowgram.ai/document";
import { IconArrowBackUp, IconDots } from "@tabler/icons-react";
import { Alert, App, Button, Card, Dropdown, Space } from "antd";
import { useDeepCompareEffect } from "ahooks";
import { Alert, App, Button, Card, Dropdown, Space, theme } from "antd";
import { nanoid } from "nanoid";
import { isEqual } from "radash";
import Show from "@/components/Show";
import WorkflowElementsContainer from "@/components/workflow/WorkflowElementsContainer";
import { isAllNodesValidated } from "@/domain/workflow";
import WorkflowDesigner from "@/components/workflow/designer/Editor";
import WorkflowDesignerToolbar from "@/components/workflow/designer/Toolbar";
import { type WorkflowNode, WorkflowNodeType, isAllNodesValidated } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
@@ -15,6 +19,7 @@ import { getErrMsg } from "@/utils/error";
const WorkflowDetailDesign = () => {
const { t } = useTranslation();
const { token: themeToken } = theme.useToken();
const { message, modal, notification } = App.useApp();
const { workflow, ...workflowState } = useWorkflowStore(useZustandShallowSelector(["workflow", "init", "publish", "rollback"]));
@@ -35,6 +40,11 @@ const WorkflowDetailDesign = () => {
setAllowPublish(!isPendingOrRunning && hasChanges);
}, [workflow.content, workflow.draft, workflow.hasDraft, isPendingOrRunning]);
const [editorData, setEditorData] = useState<FlowDocumentJSON>();
useDeepCompareEffect(() => {
setEditorData({ nodes: compactWorkflowDraft(workflow.draft) });
}, [workflow.draft]);
const handleRollbackClick = () => {
modal.confirm({
title: t("workflow.action.rollback.modal.title"),
@@ -75,7 +85,7 @@ const WorkflowDetailDesign = () => {
};
return (
<div className="min-h-[360px] flex-1 overflow-hidden">
<div className="size-full">
<Card
className="size-full overflow-hidden"
styles={{
@@ -86,43 +96,231 @@ const WorkflowDetailDesign = () => {
},
}}
>
<div className="size-full pt-9">
<div className="absolute inset-x-6 z-2 mx-auto flex max-w-320 items-center justify-between gap-4">
<div className="flex-1 overflow-hidden">
<Show when={workflow.hasDraft!}>
<Alert message={<div className="truncate">{t("workflow.detail.design.draft.alert")}</div>} showIcon type="warning" />
</Show>
</div>
<div className="flex justify-end">
<Space.Compact>
<Button color="primary" disabled={!allowPublish} variant="outlined" onClick={handlePublishClick}>
{t("workflow.action.publish.button")}
</Button>
<Dropdown
menu={{
items: [
{
key: "rollback",
disabled: !allowRollback,
label: t("workflow.action.rollback.button"),
icon: <IconArrowBackUp size="1.25em" />,
onClick: handleRollbackClick,
},
],
}}
trigger={["click"]}
>
<Button color="primary" icon={<IconDots size="1.25em" />} variant="outlined" />
</Dropdown>
</Space.Compact>
<WorkflowDesigner initialData={editorData}>
<div className="absolute top-8 z-10 w-full px-4">
<div className="container">
<div className="flex items-center justify-end gap-4">
<div className="flex flex-1 items-center justify-end gap-4 overflow-hidden">
<div className="flex-1 overflow-hidden">
<Show when={workflow.hasDraft!}>
<Alert message={<div className="truncate">{t("workflow.detail.design.unpublished_draft.alert")}</div>} showIcon type="warning" />
</Show>
</div>
<Space.Compact>
<Button disabled={!allowPublish} ghost type="primary" onClick={handlePublishClick}>
{t("workflow.action.publish.button")}
</Button>
<Dropdown
menu={{
items: [
{
key: "rollback",
disabled: !allowRollback,
label: t("workflow.action.rollback.button"),
icon: <IconArrowBackUp size="1.25em" />,
onClick: handleRollbackClick,
},
],
}}
trigger={["click"]}
>
<Button icon={<IconDots size="1.25em" />} />
</Dropdown>
</Space.Compact>
</div>
</div>
</div>
</div>
<WorkflowElementsContainer className="pt-12" />
</div>
<div className="absolute bottom-8 z-10 w-full px-4">
<div className="container">
<div className="flex justify-end">
<WorkflowDesignerToolbar
style={{
backgroundColor: themeToken.colorBgContainer,
borderRadius: themeToken.borderRadius,
}}
/>
</div>
</div>
</div>
</WorkflowDesigner>
</Card>
</div>
);
};
const compactWorkflowDraft = (root: WorkflowNode | undefined) => {
const { t } = getI18n();
// TODO: 仅为兼容适配 v0.3.x 数据,正式上线后待删除
const res: FlowDocumentJSON["nodes"] = [];
if (!root) {
res.push({
id: nanoid(),
type: "start",
data: {
name: "Start",
},
});
} else {
const convert = (node: WorkflowNode | undefined) => {
const temp: typeof res = [];
let current: typeof node = node;
while (current) {
switch (current.type) {
case WorkflowNodeType.Start:
temp.push({
id: current.id,
type: "start",
data: {
name: current.name,
config: current.config,
},
});
break;
case WorkflowNodeType.Apply:
temp.push({
id: current.id,
type: "bizApply",
data: {
name: current.name,
config: current.config,
},
});
break;
case WorkflowNodeType.Upload:
temp.push({
id: current.id,
type: "bizUpload",
data: {
name: current.name,
config: current.config,
},
});
break;
case WorkflowNodeType.Monitor:
temp.push({
id: current.id,
type: "bizMonitor",
data: {
name: current.name,
config: current.config,
},
});
break;
case WorkflowNodeType.Deploy:
temp.push({
id: current.id,
type: "bizDeploy",
data: {
name: current.name,
config: current.config,
},
});
break;
case WorkflowNodeType.Notify:
temp.push({
id: current.id,
type: "bizNotify",
data: {
name: current.name,
config: current.config,
},
});
break;
case WorkflowNodeType.ExecuteResultBranch: {
const tryNode = temp.pop()!;
temp.push({
id: current.id,
type: "tryCatch",
blocks: [
{
id: current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteSuccess)?.id || nanoid(),
type: "tryBlock",
blocks: [tryNode],
data: {
name: current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteSuccess)?.name,
},
},
{
id: current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteFailure)?.id || nanoid(),
type: "catchBlock",
blocks: [
...convert(current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteFailure)?.next),
{
id: nanoid(),
type: "end",
data: {
name: t("workflow_node.end.default_name"),
},
},
],
data: {
name: current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteFailure)?.name,
},
},
],
data: {
name: current.name,
config: current.config,
},
});
current = current.branches?.find((b) => b.type === WorkflowNodeType.ExecuteSuccess);
break;
}
case WorkflowNodeType.Branch: {
temp.push({
id: current.id,
type: "condition",
blocks:
current.branches?.map((branch) => {
return {
id: branch.id,
type: "branchBlock",
blocks: convert(branch.next),
data: {
name: branch.name,
config: branch.config,
},
};
}) ?? [],
data: {
name: current.name,
config: current.config,
},
});
break;
}
}
current = current?.next;
}
return temp;
};
res.push(...convert(root));
}
res.push({
id: nanoid(),
type: "end",
data: {
name: t("workflow_node.end.default_name"),
},
});
return res;
};
export default WorkflowDetailDesign;
@@ -0,0 +1,132 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { IconArrowBackUp, IconDots } from "@tabler/icons-react";
import { Alert, App, Button, Card, Dropdown, Space } from "antd";
import { isEqual } from "radash";
import Show from "@/components/Show";
import WorkflowElementsContainer from "@/components/workflow/WorkflowElementsContainer";
import { isAllNodesValidated } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
import { getErrMsg } from "@/utils/error";
const WorkflowDetailDesign = () => {
const { t } = useTranslation();
const { message, modal, notification } = App.useApp();
const { workflow, ...workflowState } = useWorkflowStore(useZustandShallowSelector(["workflow", "init", "publish", "rollback"]));
const [isPendingOrRunning, setIsPendingOrRunning] = useState(false);
const [allowRollback, setAllowRollback] = useState(false);
const [allowPublish, setAllowPublish] = useState(false);
useEffect(() => {
const pending = workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.PENDING || workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING;
setIsPendingOrRunning(pending);
}, [workflow]);
useEffect(() => {
const hasContent = !!workflow.content;
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
setAllowRollback(!isPendingOrRunning && hasContent && hasChanges);
setAllowPublish(!isPendingOrRunning && hasChanges);
}, [workflow.content, workflow.draft, workflow.hasDraft, isPendingOrRunning]);
const handleRollbackClick = () => {
modal.confirm({
title: t("workflow.action.rollback.modal.title"),
content: t("workflow.action.rollback.modal.content"),
onOk: async () => {
try {
await workflowState.rollback();
message.success(t("common.text.operation_succeeded"));
} catch (err) {
console.error(err);
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
}
},
});
};
const handlePublishClick = () => {
if (!isAllNodesValidated(workflow.draft!)) {
message.warning(t("workflow.action.publish.errmsg.uncompleted"));
return;
}
modal.confirm({
title: t("workflow.action.publish.modal.title"),
content: t("workflow.action.publish.modal.content"),
onOk: async () => {
try {
await workflowState.publish();
message.success(t("common.text.operation_succeeded"));
} catch (err) {
console.error(err);
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
}
},
});
};
return (
<div className="min-h-[360px] flex-1 overflow-hidden">
<Card
className="size-full overflow-hidden"
styles={{
body: {
position: "relative",
height: "100%",
padding: 0,
},
}}
>
<div className="size-full pt-9">
<div className="absolute top-8 z-10 w-full px-4">
<div className="container">
<div className="flex items-center justify-end gap-4">
<div className="flex flex-1 items-center justify-end gap-4 overflow-hidden">
<div className="flex-1 overflow-hidden">
<Show when={workflow.hasDraft!}>
<Alert message={<div className="truncate">{t("workflow.detail.design.unpublished_draft.alert")}</div>} showIcon type="warning" />
</Show>
</div>
<Space.Compact>
<Button disabled={!allowPublish} type="primary" onClick={handlePublishClick}>
{t("workflow.action.publish.button")}
</Button>
<Dropdown
menu={{
items: [
{
key: "rollback",
disabled: !allowRollback,
label: t("workflow.action.rollback.button"),
icon: <IconArrowBackUp size="1.25em" />,
onClick: handleRollbackClick,
},
],
}}
trigger={["click"]}
>
<Button icon={<IconDots size="1.25em" />} />
</Dropdown>
</Space.Compact>
</div>
</div>
</div>
</div>
<WorkflowElementsContainer className="pt-12" />
</div>
</Card>
</div>
);
};
export default WorkflowDetailDesign;
+56 -46
View File
@@ -1,8 +1,8 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { IconBrowserShare, IconHistory, IconPlayerPause, IconTrash } from "@tabler/icons-react";
import { IconBrowserShare, IconDotsVertical, IconHistory, IconPlayerPause, IconTrash } from "@tabler/icons-react";
import { useRequest } from "ahooks";
import { Alert, App, Button, Skeleton, Table, type TableProps, Tooltip } from "antd";
import { Alert, App, Button, Dropdown, Skeleton, Table, type TableProps, Tooltip } from "antd";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
@@ -98,53 +98,63 @@ const WorkflowDetailRuns = () => {
key: "$action",
align: "end",
fixed: "right",
width: 120,
width: 64,
render: (_, record) => {
const allowCancel = record.status === WORKFLOW_RUN_STATUSES.PENDING || record.status === WORKFLOW_RUN_STATUSES.RUNNING;
const aloowDelete =
record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED ||
record.status === WORKFLOW_RUN_STATUSES.FAILED ||
record.status === WORKFLOW_RUN_STATUSES.CANCELED;
const cancelDisabled = !([WORKFLOW_RUN_STATUSES.PENDING, WORKFLOW_RUN_STATUSES.RUNNING] as string[]).includes(record.status);
const deleteDisabled = !cancelDisabled;
return (
<div className="flex items-center justify-end">
<Tooltip title={t("workflow_run.action.view.button")}>
<Button
color="primary"
icon={<IconBrowserShare size="1.25em" />}
variant="text"
onClick={(e) => {
e.stopPropagation();
handleRecordDetailClick(record);
}}
/>
</Tooltip>
<Tooltip title={t("workflow_run.action.cancel.button")}>
<Button
color="default"
disabled={!allowCancel}
icon={<IconPlayerPause size="1.25em" />}
variant="text"
onClick={(e) => {
e.stopPropagation();
handleRecordCancelClick(record);
}}
/>
</Tooltip>
<Tooltip title={t("workflow_run.action.delete.button")}>
<Button
color="danger"
danger
disabled={!aloowDelete}
icon={<IconTrash size="1.25em" />}
variant="text"
onClick={(e) => {
e.stopPropagation();
handleRecordDeleteClick(record);
}}
/>
</Tooltip>
</div>
<Dropdown
menu={{
items: [
{
key: "view",
label: t("workflow_run.action.view.button"),
icon: (
<span className="anticon scale-125">
<IconBrowserShare size="1em" />
</span>
),
onClick: () => {
handleRecordDetailClick(record);
},
},
{
key: "cancel",
label: t("workflow_run.action.cancel.button"),
icon: (
<span className="anticon scale-125">
<IconPlayerPause size="1em" />
</span>
),
disabled: cancelDisabled,
onClick: () => {
handleRecordCancelClick(record);
},
},
{
type: "divider",
},
{
key: "delete",
label: t("workflow_run.action.delete.button"),
icon: (
<span className="anticon scale-125">
<IconTrash size="1em" />
</span>
),
danger: true,
disabled: deleteDisabled,
onClick: () => {
handleRecordDeleteClick(record);
},
},
],
}}
trigger={["click"]}
>
<Button icon={<IconDotsVertical size="1.25em" />} type="text" />
</Dropdown>
);
},
onCell: () => {
+3 -3
View File
@@ -194,9 +194,9 @@ const WorkflowList = () => {
renderCell(checked, _, index, node) {
if (!checked) {
return (
<div className="group">
<div className="group-hover:hidden">{(page - 1) * pageSize + index + 1}</div>
<div className="hidden group-hover:block">{node}</div>
<div className="group/selection">
<div className="group-hover/selection:hidden">{(page - 1) * pageSize + index + 1}</div>
<div className="hidden group-hover/selection:block">{node}</div>
</div>
);
}
+5
View File
@@ -15,6 +15,7 @@ import SettingsPersistence from "@/pages/settings/SettingsPersistence";
import SettingsSSLProvider from "@/pages/settings/SettingsSSLProvider";
import WorkflowDetail from "@/pages/workflows/WorkflowDetail";
import WorkflowDetailDesign from "@/pages/workflows/WorkflowDetailDesign";
import WorkflowDetailDesignLegacy from "@/pages/workflows/WorkflowDetailDesignLegacy";
import WorkflowDetailRuns from "@/pages/workflows/WorkflowDetailRuns";
import WorkflowList from "@/pages/workflows/WorkflowList";
import WorkflowNew from "@/pages/workflows/WorkflowNew";
@@ -56,6 +57,10 @@ export const router = createHashRouter([
path: "/workflows/:id/design",
element: <WorkflowDetailDesign />,
},
{
path: "/workflows/:id/design-legacy",
element: <WorkflowDetailDesignLegacy />,
},
{
path: "/workflows/:id/runs",
element: <WorkflowDetailRuns />,