├── .editorconfig ├── .eslintrc.json ├── .github ├── cover.png └── workflows │ ├── deploy-examples.yml │ ├── main.yml │ └── packages.sh ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demos ├── vanilla-js-app │ ├── assets │ │ ├── favicon.ico │ │ ├── lib.js │ │ └── vanilla-js.js │ ├── package.json │ └── vanilla-js.html └── webpack-app │ ├── .gitignore │ ├── package.json │ ├── public │ ├── assets │ │ ├── editors.css │ │ ├── favicon.ico │ │ ├── i18n.css │ │ ├── icon-if.svg │ │ ├── icon-loop.svg │ │ ├── icon-task.svg │ │ ├── placement-restrictions.css │ │ └── playground.css │ ├── editors.html │ ├── i18n.html │ ├── placement-restrictions.html │ └── playground.html │ ├── src │ ├── editors │ │ ├── app.css │ │ ├── app.ts │ │ └── model │ │ │ ├── any-variables-step-model.ts │ │ │ ├── boolean-step-model.ts │ │ │ ├── choice-step-model.ts │ │ │ ├── definition-model.ts │ │ │ ├── dynamic-step-model.ts │ │ │ ├── generated-string-step-model.ts │ │ │ ├── nullable-any-variable-step-model.ts │ │ │ ├── nullable-variable-definition-step-model.ts │ │ │ ├── nullable-variable-step-model.ts │ │ │ ├── number-step-model.ts │ │ │ ├── root-model.ts │ │ │ ├── step-models.ts │ │ │ ├── string-dictionary-step-model.ts │ │ │ ├── string-step-model.ts │ │ │ └── variable-definitions-step-model.ts │ ├── i18n │ │ ├── app.ts │ │ └── definition-model.ts │ ├── placement-restrictions │ │ ├── app.ts │ │ └── definition-model.ts │ └── playground │ │ ├── app.ts │ │ ├── default-state.ts │ │ ├── editor-provider.ts │ │ ├── machine │ │ ├── activities │ │ │ ├── calculate-activity.ts │ │ │ ├── convert-value-activity.ts │ │ │ ├── if-activity.ts │ │ │ ├── log-activity.ts │ │ │ ├── loop-activity.ts │ │ │ └── set-string-value-activity.ts │ │ ├── activity-set.ts │ │ ├── global-state.ts │ │ ├── machine-executor.ts │ │ └── services │ │ │ ├── dynamics-service.ts │ │ │ ├── logger-service.ts │ │ │ └── variables-service.ts │ │ ├── model │ │ ├── calculate-step-model.ts │ │ ├── convert-value-step-model.ts │ │ ├── definition-model.ts │ │ ├── if-step-model.ts │ │ ├── log-step-model.ts │ │ ├── loop-step-model.ts │ │ ├── root-model.ts │ │ └── set-string-value-step-model.ts │ │ ├── playground.ts │ │ ├── storage.ts │ │ └── utilities │ │ └── text-variable-parser.ts │ ├── tsconfig.json │ └── webpack.config.js ├── docs └── I18N-KEYS.md ├── editor ├── .gitignore ├── css │ └── editor.css ├── karma.conf.cjs ├── package.json ├── rollup.config.mjs ├── src │ ├── components │ │ ├── button-component.spec.ts │ │ ├── button-component.ts │ │ ├── component.ts │ │ ├── dynamic-list-component.spec.ts │ │ ├── dynamic-list-component.ts │ │ ├── index.ts │ │ ├── input-component.spec.ts │ │ ├── input-component.ts │ │ ├── prepended-input-component.ts │ │ ├── property-validation-error-component.ts │ │ ├── row-component.spec.ts │ │ ├── row-component.ts │ │ ├── select-component.ts │ │ ├── textarea-component.ts │ │ ├── validation-error-component.spec.ts │ │ ├── validation-error-component.ts │ │ └── value-editor-container-component.ts │ ├── core │ │ ├── append-multiline-text.spec.ts │ │ ├── append-multiline-text.ts │ │ ├── filter-value-types.spec.ts │ │ ├── filter-value-types.ts │ │ ├── filter-variables-by-type.spec.ts │ │ ├── filter-variables-by-type.ts │ │ ├── html.spec.ts │ │ ├── html.ts │ │ ├── icons.ts │ │ ├── index.ts │ │ ├── sort-toolbox-groups.spec.ts │ │ ├── sort-toolbox-groups.ts │ │ ├── stacked-simple-event.spec.ts │ │ ├── stacked-simple-event.ts │ │ ├── step-i18n-prefix.ts │ │ ├── variable-name-formatter.spec.ts │ │ └── variable-name-formatter.ts │ ├── editor-extension.ts │ ├── editor-header.ts │ ├── editor-provider-configuration.ts │ ├── editor-provider.spec.ts │ ├── editor-provider.ts │ ├── editor.ts │ ├── external-types.ts │ ├── index.ts │ ├── property-editor │ │ ├── property-editor.ts │ │ └── property-hint.ts │ └── value-editors │ │ ├── any-variables │ │ ├── any-variable-item-component.ts │ │ ├── any-variable-selector-component.ts │ │ └── any-variables-value-editor.ts │ │ ├── boolean │ │ └── boolean-value-editor.ts │ │ ├── choice │ │ └── choice-value-editor.ts │ │ ├── dynamic │ │ └── dynamic-value-editor.ts │ │ ├── generated-string │ │ └── generated-string-value-editor.ts │ │ ├── hidden │ │ ├── hidden-value-editor.spec.ts │ │ └── hidden-value-editor.ts │ │ ├── index.ts │ │ ├── nullable-any-variable │ │ └── nullable-any-variable-editor.ts │ │ ├── nullable-variable-definition │ │ └── variable-definition-value-editor.ts │ │ ├── nullable-variable │ │ └── nullable-variable-value-editor.ts │ │ ├── number │ │ └── number-value-editor.ts │ │ ├── string-dictionary │ │ ├── string-dictionary-item-component.ts │ │ └── string-dictionary-value-editor.ts │ │ ├── string │ │ ├── index.ts │ │ ├── string-value-editor-configuration.ts │ │ ├── string-value-editor-extension.ts │ │ └── string-value-editor.ts │ │ ├── value-editor-factory-resolver.spec.ts │ │ ├── value-editor-factory-resolver.ts │ │ ├── value-editor.ts │ │ └── variable-definitions │ │ ├── variable-definition-item-component.ts │ │ └── variable-definitions-value-editor.ts └── tsconfig.json ├── model ├── .gitignore ├── README.md ├── jest.config.cjs ├── package.json ├── rollup.config.mjs ├── src │ ├── activator │ │ ├── index.ts │ │ ├── model-activator.spec.ts │ │ └── model-activator.ts │ ├── builders │ │ ├── branched-step-model-builder.ts │ │ ├── circular-dependency-detector.spec.ts │ │ ├── circular-dependency-detector.ts │ │ ├── definition-model-builder.ts │ │ ├── index.ts │ │ ├── property-model-builder.ts │ │ ├── root-model-builder.ts │ │ ├── sequential-step-model-builder.ts │ │ ├── step-model-builder.spec.ts │ │ └── step-model-builder.ts │ ├── context │ │ ├── default-value-context.ts │ │ ├── definition-context.ts │ │ ├── index.ts │ │ ├── property-context.ts │ │ ├── read-property-value.spec.ts │ │ ├── read-property-value.ts │ │ ├── scoped-property-context.ts │ │ ├── value-context.ts │ │ └── variables-provider.ts │ ├── core │ │ ├── index.ts │ │ ├── label-builder.spec.ts │ │ ├── label-builder.ts │ │ ├── path.spec.ts │ │ ├── path.ts │ │ ├── simple-event.spec.ts │ │ └── simple-event.ts │ ├── external-types.ts │ ├── i18n.spec.ts │ ├── i18n.ts │ ├── index.ts │ ├── model.ts │ ├── test-tools │ │ ├── definition-model-stub.ts │ │ ├── model-activator-stub.ts │ │ └── value-context-stub.ts │ ├── types.ts │ ├── validator │ │ ├── definition-validator.spec.ts │ │ ├── definition-validator.ts │ │ ├── index.ts │ │ ├── property-validator-context.ts │ │ ├── step-validator-context.ts │ │ ├── variable-name-validator.spec.ts │ │ └── variable-name-validator.ts │ └── value-models │ │ ├── any-variables │ │ ├── any-variables-value-model.ts │ │ └── index.ts │ │ ├── boolean │ │ ├── boolean-value-model-configuration.ts │ │ ├── boolean-value-model-validator.spec.ts │ │ ├── boolean-value-model-validator.ts │ │ ├── boolean-value-model.spec.ts │ │ ├── boolean-value-model.ts │ │ └── index.ts │ │ ├── branches │ │ ├── branches-value-model-configuration.ts │ │ ├── branches-value-model-validator.spec.ts │ │ ├── branches-value-model-validator.ts │ │ ├── branches-value-model.ts │ │ └── index.ts │ │ ├── choice │ │ ├── choice-value-model-configuration.ts │ │ ├── choice-value-model-validator.spec.ts │ │ ├── choice-value-model-validator.ts │ │ ├── choice-value-model.ts │ │ └── index.ts │ │ ├── dynamic │ │ ├── dynamic-value-model.ts │ │ └── index.ts │ │ ├── generated-string │ │ ├── generated-string-context.ts │ │ ├── generated-string-value-model.ts │ │ └── index.ts │ │ ├── index.ts │ │ ├── nullable-any-variable │ │ ├── index.ts │ │ └── nullable-any-variable-value-model.ts │ │ ├── nullable-variable-definition │ │ ├── index.ts │ │ └── nullable-variable-definition-value-model.ts │ │ ├── nullable-variable │ │ ├── index.ts │ │ └── nullable-variable-value-model.ts │ │ ├── number │ │ ├── index.ts │ │ ├── number-value-model-configuration.ts │ │ ├── number-value-model-validator.spec.ts │ │ ├── number-value-model-validator.ts │ │ └── number-value-model.ts │ │ ├── sequence │ │ ├── index.ts │ │ └── sequence-value-model.ts │ │ ├── string-dictionary │ │ ├── index.ts │ │ ├── string-dictionary-value-model-configuration.ts │ │ ├── string-dictionary-value-model-validator.ts │ │ └── string-dictionary-value-model.ts │ │ ├── string │ │ ├── index.ts │ │ ├── string-value-model-configuration.ts │ │ ├── string-value-model-validator.spec.ts │ │ ├── string-value-model-validator.ts │ │ └── string-value-model.ts │ │ └── variable-definitions │ │ ├── index.ts │ │ └── variable-definitions-value-model.ts └── tsconfig.json ├── package.json ├── scripts ├── generate-i18n-keys.cjs ├── publish.sh └── set-version.cjs └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | tab_width = 4 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.ts] 11 | quote_type = single 12 | 13 | [*.md] 14 | indent_style = space 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "no-mixed-spaces-and-tabs": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nocode-js/sequential-workflow-editor/3b259de7ecff0f8b8d95b8ed3da041a28a652c08/.github/cover.png -------------------------------------------------------------------------------- /.github/workflows/deploy-examples.yml: -------------------------------------------------------------------------------- 1 | name: deploy examples 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - feat/ci 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ${{matrix.os}} 11 | strategy: 12 | matrix: 13 | os: 14 | - ubuntu-latest 15 | node: 16 | - 16 17 | permissions: 18 | contents: read 19 | pages: write 20 | id-token: write 21 | environment: 22 | name: github-pages 23 | url: ${{ steps.deployment.outputs.page_url }} 24 | steps: 25 | - name: Checkout Repo 26 | uses: actions/checkout@v3 27 | - name: Setup Node 18 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: "18" 31 | - name: Install 32 | run: yarn install 33 | - name: Build 34 | run: yarn build 35 | - name: Upload artifact 36 | uses: actions/upload-pages-artifact@v3 37 | with: 38 | path: demos 39 | - name: Deploy 40 | id: deployment 41 | uses: actions/deploy-pages@v4 42 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ${{matrix.os}} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | node: [16] 17 | steps: 18 | - name: Checkout Repo 19 | uses: actions/checkout@v3 20 | - name: Setup Node 18 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: "18" 24 | - name: Packages 25 | run: bash .github/workflows/packages.sh 26 | -------------------------------------------------------------------------------- /.github/workflows/packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | SCRIPT_DIR=$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd) 5 | 6 | cd "$SCRIPT_DIR" 7 | yarn install 8 | yarn build 9 | yarn eslint 10 | yarn prettier 11 | yarn test:single 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn/ 2 | build/ 3 | node_modules/ 4 | coverage/ 5 | .vscode/ 6 | lib/ 7 | dist/ 8 | yarn-*.log 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "semi": true, 6 | "useTabs": true, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 N4NO.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /demos/vanilla-js-app/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nocode-js/sequential-workflow-editor/3b259de7ecff0f8b8d95b8ed3da041a28a652c08/demos/vanilla-js-app/assets/favicon.ico -------------------------------------------------------------------------------- /demos/vanilla-js-app/assets/lib.js: -------------------------------------------------------------------------------- 1 | /* global location, document */ 2 | 3 | function isTestEnv() { 4 | const hostname = location.hostname.toLowerCase(); 5 | return hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.'); 6 | } 7 | 8 | function embedScript(url) { 9 | document.write(``); 10 | } 11 | 12 | function embedStylesheet(url) { 13 | document.write(``); 14 | } 15 | 16 | const modelBaseUrl = isTestEnv() ? '../../model' : '//cdn.jsdelivr.net/npm/sequential-workflow-editor-model@0.11.3'; 17 | const editorBaseUrl = isTestEnv() ? '../../editor' : '//cdn.jsdelivr.net/npm/sequential-workflow-editor@0.11.3'; 18 | 19 | embedScript(`${modelBaseUrl}/dist/index.umd.js`); 20 | embedScript(`${editorBaseUrl}/dist/index.umd.js`); 21 | embedStylesheet(`${editorBaseUrl}/css/editor.css`); 22 | -------------------------------------------------------------------------------- /demos/vanilla-js-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-js-app-demo", 3 | "author": "b4rtaz", 4 | "license": "MIT", 5 | "private": true, 6 | "version": "1.0.0", 7 | "scripts": { 8 | "build": "echo Skipped", 9 | "eslint": "echo Skipped", 10 | "test:single": "echo Skipped", 11 | "prettier": "prettier --check ./*.html ./assets/*.js", 12 | "prettier:fix": "prettier --write ./*.html ./assets/*.js" 13 | }, 14 | "devDependencies": { 15 | "prettier": "^2.8.7" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demos/vanilla-js-app/vanilla-js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 🚢 Vanilla JS - Sequential Workflow Editor 6 | 7 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |

🚢 Vanilla JS - Sequential Workflow Editor

35 | 36 |

37 | This demo shows basic usage of 38 | Sequential Workflow Editor 39 | with Sequential Workflow Designer 40 | in a vanilla JavaScript application. 41 |

42 | 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /demos/webpack-app/.gitignore: -------------------------------------------------------------------------------- 1 | public/builds/ 2 | -------------------------------------------------------------------------------- /demos/webpack-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-app-demo", 3 | "author": "b4rtaz", 4 | "license": "MIT", 5 | "private": true, 6 | "version": "1.0.0", 7 | "scripts": { 8 | "clean": "rm -rf public/build", 9 | "start": "webpack --mode development --watch", 10 | "build": "yarn clean && webpack --mode production", 11 | "eslint": "eslint ./src --ext .ts", 12 | "test:single": "echo \"No tests yet\"", 13 | "prettier": "prettier --check ./src ./public/**/*.{html,css}", 14 | "prettier:fix": "prettier --write ./src ./public/**/*.{html,css}" 15 | }, 16 | "dependencies": { 17 | "xstate": "^4.38.2", 18 | "sequential-workflow-model": "^0.2.0", 19 | "sequential-workflow-designer": "^0.21.2", 20 | "sequential-workflow-machine": "^0.4.0", 21 | "sequential-workflow-editor-model": "^0.14.8", 22 | "sequential-workflow-editor": "^0.14.8" 23 | }, 24 | "devDependencies": { 25 | "ts-loader": "^9.4.2", 26 | "style-loader": "^3.3.1", 27 | "css-loader": "^6.7.3", 28 | "typescript": "^4.9.5", 29 | "webpack": "^5.75.0", 30 | "webpack-cli": "^5.0.1", 31 | "prettier": "^2.8.7", 32 | "@typescript-eslint/eslint-plugin": "^5.47.0", 33 | "@typescript-eslint/parser": "^5.47.0", 34 | "eslint": "^8.30.0" 35 | } 36 | } -------------------------------------------------------------------------------- /demos/webpack-app/public/assets/editors.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #designer { 4 | margin: 0; 5 | padding: 0; 6 | width: 100vw; 7 | height: 100vh; 8 | overflow: hidden; 9 | } 10 | body, 11 | input, 12 | textarea { 13 | font: 14px/1.3em Arial, Verdana, sans-serif; 14 | } 15 | .sqd-root-editor { 16 | padding: 10px; 17 | line-height: 1.3em; 18 | box-sizing: border-box; 19 | } 20 | a { 21 | color: #000; 22 | text-decoration: underline; 23 | } 24 | a:hover { 25 | text-decoration: none; 26 | } 27 | -------------------------------------------------------------------------------- /demos/webpack-app/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nocode-js/sequential-workflow-editor/3b259de7ecff0f8b8d95b8ed3da041a28a652c08/demos/webpack-app/public/assets/favicon.ico -------------------------------------------------------------------------------- /demos/webpack-app/public/assets/i18n.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | overflow: hidden; 8 | } 9 | body { 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | body, 14 | input, 15 | h1, 16 | textarea { 17 | font: 14px/1.3em Arial, Verdana, sans-serif; 18 | } 19 | .header { 20 | width: 100%; 21 | display: flex; 22 | align-items: center; 23 | background: #203fd2; 24 | color: #fff; 25 | } 26 | .header h1 { 27 | margin: 0; 28 | padding: 0; 29 | } 30 | .header a { 31 | color: #fff; 32 | } 33 | .header .column { 34 | padding: 10px; 35 | } 36 | .header .column.flex-1 { 37 | flex: 1; 38 | } 39 | .header .text-center { 40 | text-align: center; 41 | } 42 | .header .column.text-end { 43 | text-align: right; 44 | } 45 | @media only screen and (max-width: 700px) { 46 | .header .column.hidden-mobile { 47 | display: none; 48 | } 49 | } 50 | a { 51 | color: #000; 52 | text-decoration: underline; 53 | } 54 | a:hover { 55 | text-decoration: none; 56 | } 57 | #designer { 58 | flex: 1; 59 | } 60 | -------------------------------------------------------------------------------- /demos/webpack-app/public/assets/icon-if.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demos/webpack-app/public/assets/icon-loop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demos/webpack-app/public/assets/icon-task.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demos/webpack-app/public/assets/placement-restrictions.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #designer { 4 | margin: 0; 5 | padding: 0; 6 | width: 100vw; 7 | height: 100vh; 8 | overflow: hidden; 9 | } 10 | body, 11 | input, 12 | textarea { 13 | font: 14px/1.3em Arial, Verdana, sans-serif; 14 | } 15 | .sqd-root-editor { 16 | padding: 10px; 17 | line-height: 1.3em; 18 | box-sizing: border-box; 19 | } 20 | a { 21 | color: #000; 22 | text-decoration: underline; 23 | } 24 | a:hover { 25 | text-decoration: none; 26 | } 27 | -------------------------------------------------------------------------------- /demos/webpack-app/public/assets/playground.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | overflow: hidden; 8 | } 9 | body, 10 | input, 11 | textarea { 12 | font: 14px/1.3em Arial, Verdana, sans-serif; 13 | } 14 | body { 15 | display: flex; 16 | flex-direction: row; 17 | } 18 | #designer { 19 | flex: 3; 20 | } 21 | @media (max-width: 768px) { 22 | body { 23 | flex-direction: column; 24 | } 25 | #designer { 26 | flex: 2; 27 | } 28 | } 29 | #playground { 30 | flex: 1; 31 | padding: 10px; 32 | box-sizing: border-box; 33 | background-color: #ccc; 34 | overflow: auto; 35 | } 36 | #playground .title h2 { 37 | display: inline-block; 38 | margin: 0; 39 | padding: 10px 0; 40 | } 41 | #playground .title a { 42 | color: #333; 43 | text-decoration: underline; 44 | margin: 0 5px; 45 | } 46 | #playground .title a:hover { 47 | text-decoration: none; 48 | } 49 | #playground .explanation { 50 | font-size: 9pt; 51 | } 52 | #playground h4 { 53 | margin: 0; 54 | padding: 10px 0; 55 | } 56 | #playground .variables { 57 | padding: 0 5px 5px; 58 | background: #bbb; 59 | border-radius: 5px; 60 | } 61 | #playground .variable-row label { 62 | display: block; 63 | padding: 5px 0; 64 | } 65 | #playground .variable-row input[type='text'] { 66 | padding: 5px; 67 | width: 100%; 68 | box-sizing: border-box; 69 | border: 1px solid #999; 70 | outline: 0; 71 | border-radius: 5px; 72 | } 73 | #playground .logs { 74 | width: 100%; 75 | margin: 0; 76 | padding: 5px; 77 | height: 400px; 78 | overflow-y: auto; 79 | overflow-x: hidden; 80 | box-sizing: border-box; 81 | border: 1px solid #999; 82 | border-radius: 5px; 83 | outline: 0; 84 | background: #fff; 85 | } 86 | #playground .log { 87 | margin: 0; 88 | padding: 3px; 89 | border-top: 1px solid #ccc; 90 | list-style: none; 91 | } 92 | #playground .log:first-child { 93 | border-top: 0; 94 | } 95 | #playground .log.log-trace { 96 | font-size: 8pt; 97 | color: #999; 98 | } 99 | -------------------------------------------------------------------------------- /demos/webpack-app/public/editors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 📖 Editors - Sequential Workflow Editor 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /demos/webpack-app/public/i18n.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 🚩 I18n Example - Sequential Workflow Editor 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

🚩 I18n Example

15 |
16 |
17 | Language: 18 | 22 |
23 |
24 | GitHub 25 |
26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /demos/webpack-app/public/placement-restrictions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 🎯 Placement Restrictions - Sequential Workflow Editor 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /demos/webpack-app/public/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 🛠 Playground - Sequential Workflow Editor 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 |

🛠 Playground

17 | GitHub 18 |
19 | 20 |

21 | This demo enables you to design a workflow and test it. After any change, the workflow is re-executed. Additionally, you can 22 | adjust input values utilized in the workflow to observe how your algorithm operates with various data. Check the below 23 | console to view the execution logs. 24 |

25 | 26 |

Inputs

27 |
28 | 29 |

Console

30 | 31 | 32 |

Outputs

33 |
34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/app.css: -------------------------------------------------------------------------------- 1 | .string-magic-editor input { 2 | background: #e9e8ff; 3 | border-color: #c0bef0; 4 | } 5 | .string-magic-editor input:focus { 6 | border-color: #9794d9; 7 | } 8 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/app.ts: -------------------------------------------------------------------------------- 1 | import { Designer, Uid } from 'sequential-workflow-designer'; 2 | import { EditorProvider, StringValueEditorEditorExtension } from 'sequential-workflow-editor'; 3 | import { definitionModel } from './model/definition-model'; 4 | 5 | import 'sequential-workflow-designer/css/designer.css'; 6 | import 'sequential-workflow-designer/css/designer-light.css'; 7 | import 'sequential-workflow-editor/css/editor.css'; 8 | import './app.css'; 9 | 10 | export class App { 11 | public static create(): App { 12 | const placeholder = document.getElementById('designer') as HTMLElement; 13 | 14 | const editorProvider = EditorProvider.create(definitionModel, { 15 | uidGenerator: Uid.next, 16 | extensions: [ 17 | StringValueEditorEditorExtension.create({ 18 | editorId: 'string-magic', 19 | class: 'string-magic-editor' 20 | }) 21 | ] 22 | }); 23 | 24 | const designer = Designer.create(placeholder, editorProvider.activateDefinition(), { 25 | controlBar: true, 26 | editors: { 27 | rootEditorProvider: () => { 28 | const editor = document.createElement('div'); 29 | editor.innerHTML = 30 | 'This demo showcases all the supported editors by the Sequential Workflow Editor.

' + 31 | 'Start exploring by clicking on each step.'; 32 | return editor; 33 | }, 34 | stepEditorProvider: editorProvider.createStepEditorProvider() 35 | }, 36 | validator: { 37 | step: editorProvider.createStepValidator(), 38 | root: editorProvider.createRootValidator() 39 | }, 40 | steps: { 41 | iconUrlProvider: () => './assets/icon-task.svg' 42 | }, 43 | toolbox: { 44 | groups: editorProvider.getToolboxGroups(), 45 | labelProvider: editorProvider.createStepLabelProvider() 46 | } 47 | }); 48 | 49 | if (location.hash) { 50 | const type = location.hash.substring(1).toLowerCase(); 51 | const step = designer.getDefinition().sequence.find(s => s.type.toLowerCase() === type); 52 | if (step) { 53 | designer.selectStepById(step.id); 54 | } 55 | } 56 | 57 | return new App(); 58 | } 59 | } 60 | 61 | document.addEventListener('DOMContentLoaded', App.create, false); 62 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/any-variables-step-model.ts: -------------------------------------------------------------------------------- 1 | import { AnyVariables, createAnyVariablesValueModel, createStepModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface AnyVariablesStepModel extends Step { 5 | type: 'anyVariables'; 6 | componentType: 'task'; 7 | properties: { 8 | zeroConfig: AnyVariables; 9 | onlyBoolean: AnyVariables; 10 | }; 11 | } 12 | 13 | export const anyVariablesStepModel = createStepModel('anyVariables', 'task', step => { 14 | step.description('In this step, you can select a collection of variables of any type.'); 15 | 16 | step.property('zeroConfig').value(createAnyVariablesValueModel({})); 17 | step.property('onlyBoolean').value( 18 | createAnyVariablesValueModel({ 19 | valueTypes: ['boolean'] 20 | }) 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/boolean-step-model.ts: -------------------------------------------------------------------------------- 1 | import { createBooleanValueModel, createStepModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface BooleanStepModel extends Step { 5 | type: 'boolean'; 6 | componentType: 'task'; 7 | properties: { 8 | zeroConfig: boolean; 9 | defaultValueTrue: boolean; 10 | defaultValueFalse: boolean; 11 | }; 12 | } 13 | 14 | export const booleanStepModel = createStepModel('boolean', 'task', step => { 15 | step.description('This step demonstrates properties with boolean values.'); 16 | 17 | step.property('zeroConfig').value(createBooleanValueModel({})); 18 | step.property('defaultValueTrue').value( 19 | createBooleanValueModel({ 20 | defaultValue: true 21 | }) 22 | ); 23 | step.property('defaultValueFalse').value( 24 | createBooleanValueModel({ 25 | defaultValue: false 26 | }) 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/choice-step-model.ts: -------------------------------------------------------------------------------- 1 | import { createChoiceValueModel, createStepModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface ChoiceStepModel extends Step { 5 | type: 'choice'; 6 | componentType: 'task'; 7 | properties: { 8 | minimalConfig: string; 9 | defaultValueAllow: 'allow' | 'ignore' | 'deny'; 10 | }; 11 | } 12 | 13 | export const choiceStepModel = createStepModel('choice', 'task', step => { 14 | step.description('In this step, you can see properties that allow you to select a value from a predefined list.'); 15 | 16 | step.property('minimalConfig').value(createChoiceValueModel({ choices: ['red', 'blue', 'green'] })); 17 | 18 | step.property('defaultValueAllow').value( 19 | createChoiceValueModel({ 20 | choices: ['allow', 'ignore'], 21 | defaultValue: 'ignore' 22 | }) 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/definition-model.ts: -------------------------------------------------------------------------------- 1 | import { createDefinitionModel } from 'sequential-workflow-editor-model'; 2 | import { stepModels } from './step-models'; 3 | import { rootModel } from './root-model'; 4 | 5 | export const definitionModel = createDefinitionModel(model => { 6 | model.valueTypes(['string', 'number', 'boolean']); 7 | model.root(rootModel); 8 | model.steps(stepModels); 9 | }); 10 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/dynamic-step-model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dynamic, 3 | createBooleanValueModel, 4 | createStepModel, 5 | createDynamicValueModel, 6 | createStringValueModel, 7 | StringDictionary, 8 | createStringDictionaryValueModel 9 | } from 'sequential-workflow-editor-model'; 10 | import { Step } from 'sequential-workflow-model'; 11 | 12 | export interface DynamicStepModel extends Step { 13 | type: 'dynamic'; 14 | componentType: 'task'; 15 | properties: { 16 | example: Dynamic; 17 | twoHeaderControls: Dynamic; 18 | }; 19 | } 20 | 21 | export const dynamicStepModel = createStepModel('dynamic', 'task', step => { 22 | step.description( 23 | 'This step has properties with dynamic values. For each property, you can change the value type by selecting the desired type.' 24 | ); 25 | 26 | step.property('example').value( 27 | createDynamicValueModel({ 28 | models: [createStringValueModel({}), createBooleanValueModel({})] 29 | }) 30 | ); 31 | 32 | step.property('twoHeaderControls').value( 33 | createDynamicValueModel({ 34 | models: [createStringDictionaryValueModel({}), createStringValueModel({})] 35 | }) 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/generated-string-step-model.ts: -------------------------------------------------------------------------------- 1 | import { createStepModel, createGeneratedStringValueModel, createNumberValueModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface GeneratedStringStepModel extends Step { 5 | type: 'generatedString'; 6 | componentType: 'task'; 7 | properties: { 8 | x: number; 9 | example: string; 10 | }; 11 | } 12 | 13 | export const generatedStringStepModel = createStepModel('generatedString', 'task', step => { 14 | step.description( 15 | 'This step has a property whose value is generated using data from another property. To see how it works, please change the value of the "X" property to 0, 1, 2, etc.' 16 | ); 17 | 18 | step.property('x').value(createNumberValueModel({})); 19 | 20 | step.property('example') 21 | .dependentProperty('x') 22 | .value( 23 | createGeneratedStringValueModel({ 24 | generator(context) { 25 | const x = context.getPropertyValue('x'); 26 | switch (x) { 27 | case 0: 28 | return 'Only zero :('; 29 | case 1: 30 | return 'One! Nice! :)'; 31 | case 2: 32 | return 'Two! Cool number! :))))'; 33 | } 34 | if (x < 0) { 35 | return 'No no no! Negative number :('; 36 | } 37 | return 'Give me other number!'; 38 | } 39 | }) 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/nullable-any-variable-step-model.ts: -------------------------------------------------------------------------------- 1 | import { NullableAnyVariable, createStepModel, createNullableAnyVariableValueModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface NullableAnyVariableStepModel extends Step { 5 | type: 'nullableAnyVariable'; 6 | componentType: 'task'; 7 | properties: { 8 | zeroConfig: NullableAnyVariable; 9 | required: NullableAnyVariable; 10 | onlyNumber: NullableAnyVariable; 11 | }; 12 | } 13 | 14 | export const nullableAnyVariableStepModel = createStepModel('nullableAnyVariable', 'task', step => { 15 | step.property('zeroConfig').value(createNullableAnyVariableValueModel({})); 16 | step.property('required').value( 17 | createNullableAnyVariableValueModel({ 18 | isRequired: true 19 | }) 20 | ); 21 | step.property('onlyNumber').value( 22 | createNullableAnyVariableValueModel({ 23 | valueTypes: ['number'] 24 | }) 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/nullable-variable-definition-step-model.ts: -------------------------------------------------------------------------------- 1 | import { NullableVariableDefinition, createStepModel, createNullableVariableDefinitionValueModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface NullableVariableDefinitionStepModel extends Step { 5 | type: 'nullableVariableDefinition'; 6 | componentType: 'task'; 7 | properties: { 8 | minimalConfig: NullableVariableDefinition; 9 | required: NullableVariableDefinition; 10 | defaultValue: NullableVariableDefinition; 11 | }; 12 | } 13 | 14 | export const nullableVariableDefinitionStepModel = createStepModel( 15 | 'nullableVariableDefinition', 16 | 'task', 17 | step => { 18 | step.property('minimalConfig').value( 19 | createNullableVariableDefinitionValueModel({ 20 | valueType: 'number' 21 | }) 22 | ); 23 | step.property('required').value( 24 | createNullableVariableDefinitionValueModel({ 25 | valueType: 'number', 26 | isRequired: true 27 | }) 28 | ); 29 | 30 | step.property('defaultValue').value( 31 | createNullableVariableDefinitionValueModel({ 32 | valueType: 'number', 33 | defaultValue: { 34 | name: 'index', 35 | type: 'number' 36 | } 37 | }) 38 | ); 39 | } 40 | ); 41 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/nullable-variable-step-model.ts: -------------------------------------------------------------------------------- 1 | import { NullableVariable, createStepModel, createNullableVariableValueModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface NullableVariableStepModel extends Step { 5 | type: 'nullableVariable'; 6 | componentType: 'task'; 7 | properties: { 8 | minimalConfig: NullableVariable; 9 | required: NullableVariable; 10 | }; 11 | } 12 | 13 | export const nullableVariableStepModel = createStepModel('nullableVariable', 'task', step => { 14 | step.property('minimalConfig').value( 15 | createNullableVariableValueModel({ 16 | valueType: 'number' 17 | }) 18 | ); 19 | step.property('required').value( 20 | createNullableVariableValueModel({ 21 | valueType: 'number', 22 | isRequired: true 23 | }) 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/number-step-model.ts: -------------------------------------------------------------------------------- 1 | import { createStepModel, createNumberValueModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface NumberStepModel extends Step { 5 | type: 'number'; 6 | componentType: 'task'; 7 | properties: { 8 | zeroConfig: number; 9 | defaultValue10: number; 10 | min10: number; 11 | max20: number; 12 | }; 13 | } 14 | 15 | export const numberStepModel = createStepModel('number', 'task', step => { 16 | step.property('zeroConfig').value(createNumberValueModel({})); 17 | step.property('defaultValue10').value( 18 | createNumberValueModel({ 19 | defaultValue: 10 20 | }) 21 | ); 22 | step.property('min10').value( 23 | createNumberValueModel({ 24 | min: 10 25 | }) 26 | ); 27 | step.property('max20').value( 28 | createNumberValueModel({ 29 | max: 20 30 | }) 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/root-model.ts: -------------------------------------------------------------------------------- 1 | import { createRootModel, createSequenceValueModel, createVariableDefinitionsValueModel } from 'sequential-workflow-editor-model'; 2 | import { stepModels } from './step-models'; 3 | 4 | export const rootModel = createRootModel(root => { 5 | root.sequence().value( 6 | createSequenceValueModel({ 7 | sequence: stepModels.map(s => s.type) 8 | }) 9 | ); 10 | root.property('x').value( 11 | createVariableDefinitionsValueModel({ 12 | defaultValue: { 13 | variables: [ 14 | { 15 | name: 'counter', 16 | type: 'number' 17 | }, 18 | { 19 | name: 'userName', 20 | type: 'string' 21 | }, 22 | { 23 | name: 'isEnabled', 24 | type: 'boolean' 25 | } 26 | ] 27 | } 28 | }) 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/step-models.ts: -------------------------------------------------------------------------------- 1 | import { stringStepModel } from './string-step-model'; 2 | import { stringDictionaryStepModel } from './string-dictionary-step-model'; 3 | import { booleanStepModel } from './boolean-step-model'; 4 | import { choiceStepModel } from './choice-step-model'; 5 | import { numberStepModel } from './number-step-model'; 6 | import { anyVariablesStepModel } from './any-variables-step-model'; 7 | import { dynamicStepModel } from './dynamic-step-model'; 8 | import { generatedStringStepModel } from './generated-string-step-model'; 9 | import { nullableAnyVariableStepModel } from './nullable-any-variable-step-model'; 10 | import { nullableVariableStepModel } from './nullable-variable-step-model'; 11 | import { nullableVariableDefinitionStepModel } from './nullable-variable-definition-step-model'; 12 | import { variableDefinitionsStepModel } from './variable-definitions-step-model'; 13 | 14 | export const stepModels = [ 15 | anyVariablesStepModel, 16 | booleanStepModel, 17 | choiceStepModel, 18 | dynamicStepModel, 19 | generatedStringStepModel, 20 | nullableAnyVariableStepModel, 21 | nullableVariableDefinitionStepModel, 22 | nullableVariableStepModel, 23 | numberStepModel, 24 | stringDictionaryStepModel, 25 | stringStepModel, 26 | variableDefinitionsStepModel 27 | ]; 28 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/string-dictionary-step-model.ts: -------------------------------------------------------------------------------- 1 | import { StringDictionary, createStepModel, createStringDictionaryValueModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface StringDictionaryStepModel extends Step { 5 | type: 'stringDictionary'; 6 | componentType: 'task'; 7 | properties: { 8 | zeroConfig: StringDictionary; 9 | uniqueKeys: StringDictionary; 10 | valueMinLength3: StringDictionary; 11 | }; 12 | } 13 | 14 | export const stringDictionaryStepModel = createStepModel('stringDictionary', 'task', step => { 15 | step.property('zeroConfig').value(createStringDictionaryValueModel({})); 16 | step.property('uniqueKeys').value( 17 | createStringDictionaryValueModel({ 18 | uniqueKeys: true 19 | }) 20 | ); 21 | step.property('valueMinLength3').value( 22 | createStringDictionaryValueModel({ 23 | valueMinLength: 3 24 | }) 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/string-step-model.ts: -------------------------------------------------------------------------------- 1 | import { createStepModel, createStringValueModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface StringStepModel extends Step { 5 | type: 'string'; 6 | componentType: 'task'; 7 | properties: { 8 | zeroConfig: string; 9 | defaultValue: string; 10 | minLength3: string; 11 | patternYear: string; 12 | multiLine: string; 13 | }; 14 | } 15 | 16 | export const stringStepModel = createStepModel('string', 'task', step => { 17 | step.property('zeroConfig').value(createStringValueModel({})); 18 | step.property('defaultValue').value( 19 | createStringValueModel({ 20 | defaultValue: 'Some default value' 21 | }) 22 | ); 23 | step.property('minLength3') 24 | .value( 25 | createStringValueModel({ 26 | minLength: 3, 27 | editorId: 'string-magic' 28 | }) 29 | ) 30 | .hint('This editor has a different color than the others, because it uses the string editor with custom CSS class.'); 31 | step.property('patternYear').value( 32 | createStringValueModel({ 33 | pattern: /^\d{4}$/ 34 | }) 35 | ); 36 | step.property('multiLine').value( 37 | createStringValueModel({ 38 | multiline: true 39 | }) 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /demos/webpack-app/src/editors/model/variable-definitions-step-model.ts: -------------------------------------------------------------------------------- 1 | import { VariableDefinitions, createStepModel, createVariableDefinitionsValueModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface VariableDefinitionsStepModel extends Step { 5 | type: 'variableDefinitions'; 6 | componentType: 'task'; 7 | properties: { 8 | zeroConfig: VariableDefinitions; 9 | numberAndBooleanOnly: VariableDefinitions; 10 | defaultValue: VariableDefinitions; 11 | }; 12 | } 13 | 14 | export const variableDefinitionsStepModel = createStepModel('variableDefinitions', 'task', step => { 15 | step.property('zeroConfig').value(createVariableDefinitionsValueModel({})); 16 | step.property('numberAndBooleanOnly').value( 17 | createVariableDefinitionsValueModel({ 18 | valueTypes: ['number', 'boolean'] 19 | }) 20 | ); 21 | step.property('defaultValue').value( 22 | createVariableDefinitionsValueModel({ 23 | defaultValue: { 24 | variables: [ 25 | { name: 'x', type: 'number' }, 26 | { name: 'y', type: 'string' } 27 | ] 28 | } 29 | }) 30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /demos/webpack-app/src/i18n/definition-model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dynamic, 3 | StringDictionary, 4 | createBooleanValueModel, 5 | createChoiceValueModel, 6 | createDefinitionModel, 7 | createDynamicValueModel, 8 | createNumberValueModel, 9 | createStepModel, 10 | createStringDictionaryValueModel, 11 | createStringValueModel 12 | } from 'sequential-workflow-editor-model'; 13 | import { Definition, Step } from 'sequential-workflow-model'; 14 | 15 | export interface I18nDefinition extends Definition { 16 | properties: { 17 | timeout: number; 18 | debug: boolean; 19 | }; 20 | } 21 | 22 | export interface ChownStep extends Step { 23 | type: 'chown'; 24 | componentType: 'task'; 25 | properties: { 26 | stringOrNumber: Dynamic; 27 | users: StringDictionary; 28 | mode: string; 29 | }; 30 | } 31 | 32 | export const definitionModel = createDefinitionModel(model => { 33 | model.root(root => { 34 | root.property('timeout').value( 35 | createNumberValueModel({ 36 | min: 100, 37 | max: 200, 38 | defaultValue: 150 39 | }) 40 | ); 41 | root.property('debug').value( 42 | createBooleanValueModel({ 43 | defaultValue: false 44 | }) 45 | ); 46 | }); 47 | model.steps([ 48 | createStepModel('chown', 'task', step => { 49 | step.property('stringOrNumber').value( 50 | createDynamicValueModel({ 51 | models: [ 52 | createStringValueModel({ 53 | pattern: /^[a-zA-Z0-9]+$/ 54 | }), 55 | createNumberValueModel({ 56 | min: 1, 57 | max: 100, 58 | defaultValue: 50 59 | }) 60 | ] 61 | }) 62 | ); 63 | step.property('users').value( 64 | createStringDictionaryValueModel({ 65 | valueMinLength: 1, 66 | uniqueKeys: true 67 | }) 68 | ); 69 | step.property('mode').value( 70 | createChoiceValueModel({ 71 | choices: ['Read', 'Write', 'Execute'], 72 | defaultValue: 'Read' 73 | }) 74 | ); 75 | }) 76 | ]); 77 | }); 78 | -------------------------------------------------------------------------------- /demos/webpack-app/src/placement-restrictions/app.ts: -------------------------------------------------------------------------------- 1 | import { EditorProvider } from 'sequential-workflow-editor'; 2 | import { SocketStep, definitionModel } from './definition-model'; 3 | import { Designer, Uid } from 'sequential-workflow-designer'; 4 | 5 | import 'sequential-workflow-designer/css/designer.css'; 6 | import 'sequential-workflow-designer/css/designer-light.css'; 7 | import 'sequential-workflow-editor/css/editor.css'; 8 | 9 | export class App { 10 | public static create() { 11 | const placeholder = document.getElementById('designer') as HTMLElement; 12 | 13 | const editorProvider = EditorProvider.create(definitionModel, { 14 | uidGenerator: Uid.next 15 | }); 16 | 17 | const definition = editorProvider.activateDefinition(); 18 | const loop = editorProvider.activateStep('socket') as SocketStep; 19 | loop.sequence.push(editorProvider.activateStep('writeSocket')); 20 | const break_ = editorProvider.activateStep('writeSocket'); 21 | definition.sequence.push(loop); 22 | definition.sequence.push(break_); 23 | 24 | Designer.create(placeholder, definition, { 25 | controlBar: true, 26 | editors: { 27 | rootEditorProvider: () => { 28 | const editor = document.createElement('div'); 29 | editor.innerHTML = 30 | 'This example shows how to restrict the placement of steps. The write socket step can only be placed inside a socket step. GitHub'; 31 | return editor; 32 | }, 33 | stepEditorProvider: editorProvider.createStepEditorProvider() 34 | }, 35 | validator: { 36 | step: editorProvider.createStepValidator(), 37 | root: editorProvider.createRootValidator() 38 | }, 39 | steps: { 40 | iconUrlProvider: () => './assets/icon-task.svg' 41 | }, 42 | toolbox: { 43 | groups: editorProvider.getToolboxGroups(), 44 | labelProvider: editorProvider.createStepLabelProvider() 45 | } 46 | }); 47 | } 48 | } 49 | 50 | document.addEventListener('DOMContentLoaded', App.create, false); 51 | -------------------------------------------------------------------------------- /demos/webpack-app/src/placement-restrictions/definition-model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createDefinitionModel, 3 | createNumberValueModel, 4 | createSequentialStepModel, 5 | createStepModel, 6 | createStringValueModel 7 | } from 'sequential-workflow-editor-model'; 8 | import { SequentialStep } from 'sequential-workflow-model'; 9 | 10 | export interface SocketStep extends SequentialStep { 11 | type: 'socket'; 12 | componentType: 'container'; 13 | properties: { 14 | ip: string; 15 | port: number; 16 | }; 17 | } 18 | 19 | export interface WriteSocketStep extends SequentialStep { 20 | type: 'writeSocket'; 21 | componentType: 'task'; 22 | properties: { 23 | data: string; 24 | }; 25 | } 26 | 27 | export const definitionModel = createDefinitionModel(model => { 28 | model.root(() => { 29 | // 30 | }); 31 | model.steps([ 32 | createSequentialStepModel('socket', 'container', step => { 33 | step.property('ip').value( 34 | createStringValueModel({ 35 | defaultValue: '127.0.0.1' 36 | }) 37 | ); 38 | step.property('port').value( 39 | createNumberValueModel({ 40 | defaultValue: 5000 41 | }) 42 | ); 43 | }), 44 | createStepModel('writeSocket', 'task', step => { 45 | step.property('data').value( 46 | createStringValueModel({ 47 | defaultValue: 'Hello World!' 48 | }) 49 | ); 50 | 51 | step.validator({ 52 | validate(context) { 53 | const parentTypes = context.getParentStepTypes(); 54 | return parentTypes.includes('socket') ? null : 'The write socket step must be inside a socket.'; 55 | } 56 | }); 57 | }) 58 | ]); 59 | }); 60 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/editor-provider.ts: -------------------------------------------------------------------------------- 1 | import { EditorProvider } from 'sequential-workflow-editor'; 2 | import { Uid } from 'sequential-workflow-designer'; 3 | import { definitionModel } from './model/definition-model'; 4 | 5 | export const editorProvider = EditorProvider.create(definitionModel, { 6 | uidGenerator: Uid.next 7 | }); 8 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/activities/calculate-activity.ts: -------------------------------------------------------------------------------- 1 | import { createAtomActivity } from 'sequential-workflow-machine'; 2 | import { GlobalState } from '../global-state'; 3 | import { CalculateStep } from '../../model/calculate-step-model'; 4 | 5 | export const calculateActivity = createAtomActivity('calculate', { 6 | init: () => ({}), 7 | handler: async (step: CalculateStep, { $variables, $dynamics }: GlobalState) => { 8 | if (!step.properties.result) { 9 | throw new Error('Result variable is not defined'); 10 | } 11 | 12 | const a = $dynamics.readNumber(step.properties.a); 13 | const b = $dynamics.readNumber(step.properties.b); 14 | 15 | const result = calculate(a, b, step.properties.operator); 16 | $variables.set(step.properties.result.name, result); 17 | } 18 | }); 19 | 20 | function calculate(a: number, b: number, operator: string): number { 21 | switch (operator) { 22 | case '+': 23 | return a + b; 24 | case '-': 25 | return a - b; 26 | case '*': 27 | return a * b; 28 | case '/': 29 | return a / b; 30 | case '%': 31 | return a % b; 32 | default: 33 | throw new Error(`Unknown operator: ${operator}`); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/activities/convert-value-activity.ts: -------------------------------------------------------------------------------- 1 | import { createAtomActivity } from 'sequential-workflow-machine'; 2 | import { GlobalState } from '../global-state'; 3 | import { ConvertValueStep } from '../../model/convert-value-step-model'; 4 | 5 | export const convertValueActivity = createAtomActivity('convertValue', { 6 | init: () => ({}), 7 | handler: async (step: ConvertValueStep, { $variables }: GlobalState) => { 8 | if (!step.properties.source) { 9 | throw new Error('Source variable is required'); 10 | } 11 | if (!step.properties.target) { 12 | throw new Error('Target variable is required'); 13 | } 14 | 15 | const value = $variables.read(step.properties.source.name); 16 | 17 | let convertedValue: unknown; 18 | switch (step.properties.target.type) { 19 | case 'number': 20 | convertedValue = Number(value); 21 | break; 22 | case 'string': 23 | convertedValue = String(value); 24 | break; 25 | default: 26 | throw new Error(`Unsupported target type: ${step.properties.target.type}`); 27 | } 28 | 29 | $variables.set(step.properties.target.name, convertedValue); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/activities/if-activity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { branchName, createForkActivity } from 'sequential-workflow-machine'; 3 | import { IfStep } from '../../model/if-step-model'; 4 | import { GlobalState } from '../global-state'; 5 | 6 | export const ifActivity = createForkActivity('if', { 7 | init: () => ({}), 8 | handler: async (step: IfStep, { $dynamics }: GlobalState) => { 9 | const a = $dynamics.readAny(step.properties.a); 10 | const b = $dynamics.readAny(step.properties.b); 11 | 12 | const result = compare(a, b, step.properties.operator); 13 | return branchName(result ? 'true' : 'false'); 14 | } 15 | }); 16 | 17 | function compare(a: any, b: any, operator: string): boolean { 18 | switch (operator) { 19 | case '==': 20 | return a == b; 21 | case '===': 22 | return a === b; 23 | case '!=': 24 | return a != b; 25 | case '!==': 26 | return a !== b; 27 | case '>': 28 | return a > b; 29 | case '>=': 30 | return a >= b; 31 | case '<': 32 | return a < b; 33 | case '<=': 34 | return a <= b; 35 | default: 36 | throw new Error(`Unknown operator: ${operator}`); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/activities/log-activity.ts: -------------------------------------------------------------------------------- 1 | import { createAtomActivityFromHandler } from 'sequential-workflow-machine'; 2 | import { GlobalState } from '../global-state'; 3 | import { LogStep } from '../../model/log-step-model'; 4 | import { formatVariableName } from 'sequential-workflow-editor'; 5 | 6 | export const logActivity = createAtomActivityFromHandler( 7 | 'log', 8 | async (step: LogStep, { $variables, $dynamics, $logger }: GlobalState) => { 9 | let message = $dynamics.readString(step.properties.message); 10 | 11 | for (const variable of step.properties.variables.variables) { 12 | const value = $variables.isSet(variable.name) ? $variables.read(variable.name) || '' : ''; 13 | const type = typeof value; 14 | const name = formatVariableName(variable.name); 15 | message += `\n${name}=${value} (${type})`; 16 | } 17 | 18 | $logger.log(message); 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/activities/loop-activity.ts: -------------------------------------------------------------------------------- 1 | import { createLoopActivity } from 'sequential-workflow-machine'; 2 | import { LoopStep } from '../../model/loop-step-model'; 3 | import { GlobalState } from '../global-state'; 4 | 5 | interface LoopActivityState { 6 | indexVariableName: string; 7 | } 8 | 9 | export const loopActivity = createLoopActivity('loop', { 10 | loopName: step => `LOOP.${step.id}`, 11 | init: (step: LoopStep) => { 12 | if (!step.properties.indexVariable) { 13 | throw new Error('Index variable is not defined'); 14 | } 15 | return { 16 | indexVariableName: step.properties.indexVariable.name 17 | }; 18 | }, 19 | onEnter: (step: LoopStep, { $variables, $dynamics }: GlobalState, { indexVariableName }: LoopActivityState) => { 20 | const startIndex = $dynamics.readNumber(step.properties.from); 21 | 22 | $variables.set(indexVariableName, startIndex); 23 | }, 24 | onLeave: (_, { $variables }: GlobalState, { indexVariableName }: LoopActivityState) => { 25 | $variables.delete(indexVariableName); 26 | }, 27 | condition: async (step: LoopStep, { $variables, $dynamics }: GlobalState, { indexVariableName }: LoopActivityState) => { 28 | const from = $dynamics.readNumber(step.properties.to); 29 | const increment = $dynamics.readNumber(step.properties.increment); 30 | if (increment === 0) { 31 | throw new Error('Increment cannot be 0'); 32 | } 33 | 34 | const currentIndex = $variables.read(indexVariableName); 35 | 36 | let canContinue: boolean; 37 | switch (step.properties.operator) { 38 | case '<': 39 | canContinue = currentIndex < from; 40 | break; 41 | case '<=': 42 | canContinue = currentIndex <= from; 43 | break; 44 | default: 45 | throw new Error('Comparison is not supported'); 46 | } 47 | 48 | const newIndex = currentIndex + increment; 49 | 50 | $variables.set(indexVariableName, newIndex); 51 | return canContinue; 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/activities/set-string-value-activity.ts: -------------------------------------------------------------------------------- 1 | import { createAtomActivity } from 'sequential-workflow-machine'; 2 | import { GlobalState } from '../global-state'; 3 | import { SetStringValueStep } from '../../model/set-string-value-step-model'; 4 | import { TextVariableParser } from '../../utilities/text-variable-parser'; 5 | 6 | export const setStringValueActivity = createAtomActivity('setStringValue', { 7 | init: () => ({}), 8 | handler: async (step: SetStringValueStep, { $variables, $dynamics }: GlobalState) => { 9 | if (!step.properties.variable) { 10 | throw new Error('Variable is not set'); 11 | } 12 | 13 | let value = $dynamics.readString(step.properties.value); 14 | 15 | value = TextVariableParser.replace(value, variableName => { 16 | return String($variables.read(variableName)); 17 | }); 18 | 19 | $variables.set(step.properties.variable.name, value); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/activity-set.ts: -------------------------------------------------------------------------------- 1 | import { createActivitySet } from 'sequential-workflow-machine'; 2 | import { setStringValueActivity } from './activities/set-string-value-activity'; 3 | import { loopActivity } from './activities/loop-activity'; 4 | import { logActivity } from './activities/log-activity'; 5 | import { ifActivity } from './activities/if-activity'; 6 | import { calculateActivity } from './activities/calculate-activity'; 7 | import { convertValueActivity } from './activities/convert-value-activity'; 8 | 9 | export const activitySet = createActivitySet([ 10 | calculateActivity, 11 | convertValueActivity, 12 | ifActivity, 13 | logActivity, 14 | loopActivity, 15 | setStringValueActivity 16 | ]); 17 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/global-state.ts: -------------------------------------------------------------------------------- 1 | import { DynamicsService } from './services/dynamics-service'; 2 | import { LoggerService } from './services/logger-service'; 3 | import { VariableState, VariablesService } from './services/variables-service'; 4 | 5 | export interface GlobalState { 6 | startTime: Date; 7 | variablesState: VariableState; 8 | 9 | $variables: VariablesService; 10 | $dynamics: DynamicsService; 11 | $logger: LoggerService; 12 | } 13 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/machine-executor.ts: -------------------------------------------------------------------------------- 1 | import { WorkflowMachineSnapshot, createWorkflowMachineBuilder } from 'sequential-workflow-machine'; 2 | import { GlobalState } from './global-state'; 3 | import { activitySet } from './activity-set'; 4 | import { MyDefinition } from '../model/definition-model'; 5 | import { VariableState, VariablesService, createVariableState } from './services/variables-service'; 6 | import { DynamicsService } from './services/dynamics-service'; 7 | import { LoggerService } from './services/logger-service'; 8 | 9 | const builder = createWorkflowMachineBuilder(activitySet); 10 | 11 | export function executeMachine( 12 | definition: MyDefinition, 13 | variableValues: VariableState, 14 | onStateChanged: (path: string[]) => void, 15 | onLog: (message: string) => void 16 | ): Promise> { 17 | const machine = builder.build(definition); 18 | const interpreter = machine.create({ 19 | init: () => { 20 | const variablesState = createVariableState(variableValues); 21 | const $variables = new VariablesService(variablesState); 22 | const $dynamics = new DynamicsService($variables); 23 | const $logger = new LoggerService(); 24 | $logger.onLog.subscribe(onLog); 25 | 26 | return { 27 | startTime: new Date(), 28 | variablesState, 29 | 30 | $variables, 31 | $dynamics, 32 | $logger 33 | }; 34 | } 35 | }); 36 | 37 | return new Promise((resolve, reject) => { 38 | try { 39 | interpreter.onChange(() => { 40 | const snapshot = interpreter.getSnapshot(); 41 | onStateChanged(snapshot.statePath); 42 | }); 43 | interpreter.onDone(() => { 44 | resolve(interpreter.getSnapshot()); 45 | }); 46 | interpreter.start(); 47 | } catch (e) { 48 | reject(e); 49 | } 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/services/dynamics-service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dynamic, 3 | NullableAnyVariable, 4 | NullableVariable, 5 | booleanValueModelId, 6 | nullableAnyVariableValueModelId, 7 | nullableVariableValueModelId, 8 | numberValueModelId, 9 | stringValueModelId 10 | } from 'sequential-workflow-editor-model'; 11 | import { VariablesService } from './variables-service'; 12 | 13 | export class DynamicsService { 14 | public constructor(private readonly $variables: VariablesService) {} 15 | 16 | public readAny(dynamic: Dynamic): TValue { 17 | switch (dynamic.modelId) { 18 | case stringValueModelId: 19 | case numberValueModelId: 20 | case booleanValueModelId: 21 | return dynamic.value as TValue; 22 | case nullableVariableValueModelId: 23 | case nullableAnyVariableValueModelId: { 24 | const variable = dynamic.value as NullableVariable | NullableAnyVariable; 25 | if (!variable || !variable.name) { 26 | throw new Error('Variable is not set'); 27 | } 28 | return this.$variables.read(variable.name); 29 | } 30 | } 31 | throw new Error(`Dynamic model is not supported: ${dynamic.modelId}`); 32 | } 33 | 34 | public readString(dynamic: Dynamic): string { 35 | const value = this.readAny(dynamic); 36 | if (typeof value !== 'string') { 37 | throw new Error('Value is not a string'); 38 | } 39 | return value; 40 | } 41 | 42 | public readNumber(dynamic: Dynamic): number { 43 | const value = this.readAny(dynamic); 44 | if (typeof value !== 'number') { 45 | throw new Error('Value is not a number'); 46 | } 47 | return value; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/services/logger-service.ts: -------------------------------------------------------------------------------- 1 | import { SimpleEvent } from 'sequential-workflow-editor-model'; 2 | 3 | export class LoggerService { 4 | public readonly onLog = new SimpleEvent(); 5 | 6 | public log(message: string) { 7 | this.onLog.forward(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/machine/services/variables-service.ts: -------------------------------------------------------------------------------- 1 | export type VariableState = Record; 2 | 3 | export function createVariableState(state?: VariableState): VariableState { 4 | return state ?? {}; 5 | } 6 | 7 | export class VariablesService { 8 | public constructor(private readonly state: VariableState) {} 9 | 10 | public read(name: string): TValue { 11 | const value = this.state[name]; 12 | if (value === undefined) { 13 | throw new Error(`Cannot read unset variable: ${name}`); 14 | } 15 | return value as TValue; 16 | } 17 | 18 | public set(name: string, value: TValue) { 19 | if (value === undefined) { 20 | throw new Error('Cannot set variable to undefined'); 21 | } 22 | this.state[name] = value; 23 | } 24 | 25 | public isSet(name: string): boolean { 26 | return this.state[name] !== undefined; 27 | } 28 | 29 | public delete(name: string) { 30 | delete this.state[name]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/model/convert-value-step-model.ts: -------------------------------------------------------------------------------- 1 | import { NullableAnyVariable, createStepModel, createNullableAnyVariableValueModel } from 'sequential-workflow-editor-model'; 2 | import { Step } from 'sequential-workflow-model'; 3 | 4 | export interface ConvertValueStep extends Step { 5 | type: 'convertValue'; 6 | componentType: 'task'; 7 | properties: { 8 | source: NullableAnyVariable; 9 | target: NullableAnyVariable; 10 | }; 11 | } 12 | 13 | export const convertValueStepModel = createStepModel('convertValue', 'task', step => { 14 | step.category('Values'); 15 | step.description('Convert value from one variable to another.'); 16 | 17 | step.property('source') 18 | .value( 19 | createNullableAnyVariableValueModel({ 20 | isRequired: true 21 | }) 22 | ) 23 | .label('Source variable'); 24 | step.property('target') 25 | .value( 26 | createNullableAnyVariableValueModel({ 27 | isRequired: true 28 | }) 29 | ) 30 | .label('Target variable'); 31 | }); 32 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/model/definition-model.ts: -------------------------------------------------------------------------------- 1 | import { VariableDefinitions, createDefinitionModel } from 'sequential-workflow-editor-model'; 2 | import { rootModel } from './root-model'; 3 | import { logStepModel } from './log-step-model'; 4 | import { loopStepModel } from './loop-step-model'; 5 | import { setStringValueStepModel } from './set-string-value-step-model'; 6 | import { Definition } from 'sequential-workflow-model'; 7 | import { ifStepModel } from './if-step-model'; 8 | import { calculateStepModel } from './calculate-step-model'; 9 | import { convertValueStepModel } from './convert-value-step-model'; 10 | 11 | export interface MyDefinition extends Definition { 12 | properties: { 13 | inputs: VariableDefinitions; 14 | outputs: VariableDefinitions; 15 | }; 16 | } 17 | 18 | export const definitionModel = createDefinitionModel(model => { 19 | model.valueTypes(['string', 'number']); 20 | model.root(rootModel); 21 | model.steps([calculateStepModel, convertValueStepModel, ifStepModel, logStepModel, loopStepModel, setStringValueStepModel]); 22 | }); 23 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/model/if-step-model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dynamic, 3 | NullableVariable, 4 | createBooleanValueModel, 5 | createBranchesValueModel, 6 | createChoiceValueModel, 7 | createBranchedStepModel, 8 | createDynamicValueModel, 9 | createNullableAnyVariableValueModel, 10 | createNumberValueModel, 11 | createStringValueModel 12 | } from 'sequential-workflow-editor-model'; 13 | import { BranchedStep } from 'sequential-workflow-model'; 14 | 15 | export interface IfStep extends BranchedStep { 16 | type: 'if'; 17 | componentType: 'switch'; 18 | properties: { 19 | a: Dynamic; 20 | operator: string; 21 | b: Dynamic; 22 | }; 23 | } 24 | 25 | export const ifStepModel = createBranchedStepModel('if', 'switch', step => { 26 | step.category('Logic'); 27 | step.description('Check condition and execute different branches.'); 28 | 29 | const ab = createDynamicValueModel({ 30 | models: [ 31 | createNumberValueModel({}), 32 | createStringValueModel({}), 33 | createBooleanValueModel({}), 34 | createNullableAnyVariableValueModel({ 35 | isRequired: true 36 | }) 37 | ] 38 | }); 39 | 40 | step.property('a').value(ab).label('A').hint('Left side of comparison.'); 41 | 42 | step.property('operator') 43 | .label('Operator') 44 | .value( 45 | createChoiceValueModel({ 46 | choices: ['==', '===', '!=', '!==', '>', '>=', '<', '<='] 47 | }) 48 | ) 49 | .hint('Comparison operator.\nStep supports strict and non-strict operators.'); 50 | 51 | step.property('b').value(ab).label('B').hint('Right side of comparison.'); 52 | 53 | step.branches().value( 54 | createBranchesValueModel({ 55 | branches: { 56 | true: [], 57 | false: [] 58 | } 59 | }) 60 | ); 61 | }); 62 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/model/log-step-model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AnyVariables, 3 | Dynamic, 4 | GeneratedStringContext, 5 | NullableVariable, 6 | WellKnownValueType, 7 | createAnyVariablesValueModel, 8 | createDynamicValueModel, 9 | createGeneratedStringValueModel, 10 | createNullableVariableValueModel, 11 | createStepModel, 12 | createStringValueModel 13 | } from 'sequential-workflow-editor-model'; 14 | import { Step } from 'sequential-workflow-model'; 15 | 16 | export interface LogStep extends Step { 17 | type: 'log'; 18 | componentType: 'task'; 19 | properties: { 20 | message: Dynamic; 21 | variables: AnyVariables; 22 | note: Dynamic; 23 | }; 24 | } 25 | 26 | export const logStepModel = createStepModel('log', 'task', step => { 27 | step.property('message') 28 | .value( 29 | createDynamicValueModel({ 30 | models: [ 31 | createStringValueModel({ 32 | minLength: 1 33 | }), 34 | createNullableVariableValueModel({ 35 | isRequired: true, 36 | valueType: WellKnownValueType.string 37 | }) 38 | ] 39 | }) 40 | ) 41 | .label('Text'); 42 | 43 | step.property('variables').value(createAnyVariablesValueModel({})).label('Log variables'); 44 | 45 | step.property('note') 46 | .dependentProperty('variables') 47 | .value( 48 | createDynamicValueModel({ 49 | models: [ 50 | createGeneratedStringValueModel({ 51 | generator: (context: GeneratedStringContext) => { 52 | // TODO: if the type would be deleted from arguments, then the auto type is wrong. 53 | const variables = context.getPropertyValue('variables'); 54 | return `Dumped ${variables.variables.length} variables`; 55 | } 56 | }), 57 | createStringValueModel({}) 58 | ] 59 | }) 60 | ); 61 | }); 62 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/model/root-model.ts: -------------------------------------------------------------------------------- 1 | import { createRootModel, createSequenceValueModel, createVariableDefinitionsValueModel } from 'sequential-workflow-editor-model'; 2 | import { MyDefinition } from './definition-model'; 3 | 4 | export const rootModel = createRootModel(root => { 5 | root.property('inputs') 6 | .hint('Variables passed to the workflow from the outside.') 7 | .value(createVariableDefinitionsValueModel({})) 8 | .dependentProperty('outputs') 9 | .validator({ 10 | validate(context) { 11 | const inputs = context.getPropertyValue('outputs'); 12 | return inputs.variables.length > 0 ? null : 'At least one input is required'; 13 | } 14 | }); 15 | 16 | root.property('outputs').hint('Variables returned from the workflow.').value(createVariableDefinitionsValueModel({})).label('Outputs'); 17 | 18 | root.sequence().value( 19 | createSequenceValueModel({ 20 | sequence: [] 21 | }) 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/model/set-string-value-step-model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dynamic, 3 | NullableVariable, 4 | WellKnownValueType, 5 | createStepModel, 6 | createDynamicValueModel, 7 | createNullableVariableValueModel, 8 | createStringValueModel 9 | } from 'sequential-workflow-editor-model'; 10 | import { Step } from 'sequential-workflow-model'; 11 | import { TextVariableParser } from '../utilities/text-variable-parser'; 12 | 13 | export interface SetStringValueStep extends Step { 14 | type: 'setStringValue'; 15 | componentType: 'task'; 16 | properties: { 17 | variable: NullableVariable; 18 | value: Dynamic; 19 | }; 20 | } 21 | 22 | export const setStringValueStepModel = createStepModel('setStringValue', 'task', step => { 23 | step.category('Values'); 24 | 25 | step.property('variable') 26 | .value( 27 | createNullableVariableValueModel({ 28 | valueType: WellKnownValueType.string, 29 | isRequired: true 30 | }) 31 | ) 32 | .label('Target variable'); 33 | step.property('value') 34 | .value( 35 | createDynamicValueModel({ 36 | models: [ 37 | createStringValueModel({ 38 | minLength: 1 39 | }), 40 | createNullableVariableValueModel({ 41 | valueType: WellKnownValueType.string, 42 | isRequired: true 43 | }) 44 | ] 45 | }) 46 | ) 47 | .label('Value') 48 | .validator({ 49 | validate(context) { 50 | const value = context.getValue(); 51 | if (value.modelId === 'string') { 52 | const variableNames = TextVariableParser.parse(value.value as string); 53 | const undefinedVariableName = context.findFirstUndefinedVariable(variableNames); 54 | if (undefinedVariableName) { 55 | return `Variable $${undefinedVariableName} is not defined`; 56 | } 57 | } 58 | return null; 59 | } 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/storage.ts: -------------------------------------------------------------------------------- 1 | import { MyDefinition } from './model/definition-model'; 2 | import { RawInputData } from './playground'; 3 | 4 | const version = 3; 5 | const definitionKey = `definition_${version}`; 6 | const inputDataKey = `inputData_${version}`; 7 | 8 | export interface AppState { 9 | definition: MyDefinition; 10 | inputData: RawInputData; 11 | } 12 | 13 | export class AppStorage { 14 | public static tryGet(): AppState | null { 15 | const definition = localStorage[definitionKey]; 16 | const inputData = localStorage[inputDataKey]; 17 | if (definition && inputData) { 18 | return { 19 | definition: JSON.parse(definition), 20 | inputData: JSON.parse(inputData) 21 | }; 22 | } 23 | return null; 24 | } 25 | 26 | public static set(definition: MyDefinition, inputData: RawInputData) { 27 | localStorage[definitionKey] = JSON.stringify(definition); 28 | localStorage[inputDataKey] = JSON.stringify(inputData); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /demos/webpack-app/src/playground/utilities/text-variable-parser.ts: -------------------------------------------------------------------------------- 1 | const regexp = /\$[A-Za-z][a-zA-Z_0-9-]*/g; 2 | 3 | export class TextVariableParser { 4 | public static parse(text: string): string[] { 5 | return (text.match(regexp) || []).map(v => v.substring(1)); 6 | } 7 | 8 | public static replace(text: string, valueProvider: (variableName: string) => string): string { 9 | return text.replace(regexp, v => { 10 | const variableName = v.substring(1); 11 | return valueProvider(variableName); 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /demos/webpack-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "noImplicitAny": true, 5 | "target": "es6", 6 | "module": "es2015", 7 | "sourceMap": false, 8 | "strict": true, 9 | "allowJs": false, 10 | "declaration": false, 11 | "moduleResolution": "node", 12 | "lib": ["es2015", "dom"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /demos/webpack-app/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | function bundle(name) { 4 | return { 5 | entry: `./src/${name}/app.ts`, 6 | cache: false, 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.ts$/, 11 | use: 'ts-loader', 12 | exclude: /node_modules/, 13 | }, 14 | { 15 | test: /\.css$/i, 16 | use: ['style-loader', 'css-loader'], 17 | }, 18 | ], 19 | }, 20 | resolve: { 21 | extensions: ['.tsx', '.ts', '.js'], 22 | }, 23 | output: { 24 | filename: `${name}.js`, 25 | path: path.resolve(__dirname, 'public/builds'), 26 | } 27 | } 28 | } 29 | 30 | module.exports = [ 31 | bundle('editors'), 32 | bundle('i18n'), 33 | bundle('placement-restrictions'), 34 | bundle('playground'), 35 | ]; 36 | -------------------------------------------------------------------------------- /editor/.gitignore: -------------------------------------------------------------------------------- 1 | README.md 2 | LICENSE 3 | -------------------------------------------------------------------------------- /editor/karma.conf.cjs: -------------------------------------------------------------------------------- 1 | module.exports = config => { 2 | config.set({ 3 | frameworks: [ 4 | 'jasmine', 5 | 'karma-typescript' 6 | ], 7 | plugins: [ 8 | require('karma-jasmine'), 9 | require('karma-chrome-launcher'), 10 | require('karma-spec-reporter'), 11 | require('karma-typescript') 12 | ], 13 | files: [ 14 | { pattern: 'src/**/*.ts' } 15 | ], 16 | preprocessors: { 17 | 'src/**/*.ts': 'karma-typescript' 18 | }, 19 | reporters: [ 20 | 'progress', 21 | 'karma-typescript' 22 | ], 23 | browsers: [ 24 | 'ChromeHeadless' 25 | ], 26 | karmaTypescriptConfig: { 27 | compilerOptions: { 28 | skipLibCheck: true 29 | }, 30 | bundlerOptions: { 31 | transforms: [ 32 | require("karma-typescript-es6-transform")() 33 | ] 34 | } 35 | } 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /editor/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import dts from 'rollup-plugin-dts'; 2 | import typescript from 'rollup-plugin-typescript2'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | import fs from 'fs'; 5 | 6 | const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 7 | const external = Object.keys(packageJson.dependencies); 8 | 9 | const ts = typescript({ 10 | useTsconfigDeclarationDir: true 11 | }); 12 | 13 | export default [ 14 | { 15 | input: './src/index.ts', 16 | plugins: [ts], 17 | external, 18 | cache: false, 19 | output: [ 20 | { 21 | file: './lib/esm/index.js', 22 | format: 'es' 23 | }, 24 | { 25 | file: './lib/cjs/index.cjs', 26 | format: 'cjs' 27 | } 28 | ] 29 | }, 30 | { 31 | input: './build/index.d.ts', 32 | output: [ 33 | { 34 | file: './lib/index.d.ts', 35 | format: 'es' 36 | } 37 | ], 38 | plugins: [dts()], 39 | }, 40 | { 41 | input: './src/index.ts', 42 | plugins: [ 43 | nodeResolve({ 44 | browser: true, 45 | }), 46 | ts 47 | ], 48 | external: [ 49 | 'sequential-workflow-editor-model' 50 | ], 51 | output: [ 52 | { 53 | file: './dist/index.umd.js', 54 | format: 'umd', 55 | name: 'sequentialWorkflowEditor' 56 | } 57 | ] 58 | } 59 | ]; 60 | -------------------------------------------------------------------------------- /editor/src/components/button-component.spec.ts: -------------------------------------------------------------------------------- 1 | import { buttonComponent } from './button-component'; 2 | 3 | describe('ButtonComponent', () => { 4 | it('triggers onClick event when clicked', () => { 5 | let clicked = false; 6 | 7 | const button = buttonComponent('Some button'); 8 | button.onClick.subscribe(() => (clicked = true)); 9 | 10 | button.view.dispatchEvent(new Event('click')); 11 | 12 | expect(button.view.innerText).toBe('Some button'); 13 | expect(clicked).toBe(true); 14 | }); 15 | 16 | it('replaces icon', () => { 17 | const button = buttonComponent('Icon button', { icon: 'm200' }); 18 | const getD = () => button.view.children[0].children[0].getAttribute('d'); 19 | 20 | expect(getD()).toBe('m200'); 21 | 22 | button.setIcon('m100'); 23 | 24 | expect(getD()).toBe('m100'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /editor/src/components/button-component.ts: -------------------------------------------------------------------------------- 1 | import { SimpleEvent } from 'sequential-workflow-editor-model'; 2 | import { Html } from '../core/html'; 3 | import { Component } from './component'; 4 | import { Icons } from '../core/icons'; 5 | 6 | export interface ButtonComponent extends Component { 7 | onClick: SimpleEvent; 8 | setIcon(d: string): void; 9 | setLabel(label: string): void; 10 | } 11 | 12 | export interface ButtonComponentConfiguration { 13 | size?: 'small'; 14 | theme?: 'secondary'; 15 | icon?: string; 16 | } 17 | 18 | export function buttonComponent(label: string, configuration?: ButtonComponentConfiguration): ButtonComponent { 19 | function onClicked(e: Event) { 20 | e.preventDefault(); 21 | onClick.forward(); 22 | } 23 | 24 | function setIcon(d: string) { 25 | if (icon) { 26 | icon.getElementsByTagName('path')[0].setAttribute('d', d); 27 | } else { 28 | throw new Error('This button does not have icon'); 29 | } 30 | } 31 | 32 | function setLabel(label: string) { 33 | if (configuration?.icon) { 34 | throw new Error('Cannot change label on button with icon'); 35 | } else { 36 | view.innerText = label; 37 | } 38 | } 39 | 40 | const onClick = new SimpleEvent(); 41 | 42 | let className = 'swe-button'; 43 | if (configuration?.size) { 44 | className += ` swe-button-${configuration.size}`; 45 | } 46 | if (configuration?.theme) { 47 | className += ` swe-button-${configuration.theme}`; 48 | } 49 | const view = Html.element('button', { 50 | class: className, 51 | title: label, 52 | 'aria-label': label 53 | }); 54 | let icon: SVGElement | undefined; 55 | if (configuration?.icon) { 56 | icon = Icons.createSvg(configuration.icon, 'swe-button-icon'); 57 | view.appendChild(icon); 58 | } else { 59 | view.innerText = label; 60 | } 61 | view.addEventListener('click', onClicked, false); 62 | 63 | return { 64 | view, 65 | onClick, 66 | setIcon, 67 | setLabel 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /editor/src/components/component.ts: -------------------------------------------------------------------------------- 1 | export interface Component { 2 | view: HTMLElement; 3 | } 4 | -------------------------------------------------------------------------------- /editor/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './button-component'; 2 | export * from './component'; 3 | export * from './dynamic-list-component'; 4 | export * from './input-component'; 5 | export * from './prepended-input-component'; 6 | export * from './row-component'; 7 | export * from './select-component'; 8 | export * from './textarea-component'; 9 | export * from './validation-error-component'; 10 | export * from './value-editor-container-component'; 11 | -------------------------------------------------------------------------------- /editor/src/components/input-component.spec.ts: -------------------------------------------------------------------------------- 1 | import { inputComponent } from './input-component'; 2 | 3 | describe('InputComponent', () => { 4 | it('triggers onChanged event when new character is added to field', () => { 5 | let count = 0; 6 | 7 | const input = inputComponent('Foo'); 8 | input.onChanged.subscribe(value => { 9 | expect(value).toBe('FooB'); 10 | count++; 11 | }); 12 | 13 | (input.view as HTMLInputElement).value = 'FooB'; 14 | input.view.dispatchEvent(new Event('input')); 15 | 16 | expect(count).toBe(1); 17 | }); 18 | 19 | it('renders input[type=text] by default', () => { 20 | const input = inputComponent('x'); 21 | expect(input.view.getAttribute('type')).toBe('text'); 22 | }); 23 | 24 | it('renders input[type=number] when configuration is set', () => { 25 | const input = inputComponent('x', { type: 'number' }); 26 | expect(input.view.getAttribute('type')).toBe('number'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /editor/src/components/input-component.ts: -------------------------------------------------------------------------------- 1 | import { SimpleEvent } from 'sequential-workflow-editor-model'; 2 | import { Component } from './component'; 3 | import { Html } from '../core'; 4 | 5 | export interface InputComponent extends Component { 6 | onChanged: SimpleEvent; 7 | setValue(value: string): void; 8 | getValue(): string; 9 | setReadonly(readonly: boolean): void; 10 | } 11 | 12 | export interface InputConfiguration { 13 | type?: 'text' | 'number' | 'password'; 14 | isReadonly?: boolean; 15 | placeholder?: string; 16 | } 17 | 18 | export function inputComponent(startValue: string, configuration?: InputConfiguration): InputComponent { 19 | const onChanged = new SimpleEvent(); 20 | 21 | function setValue(value: string) { 22 | view.value = value; 23 | } 24 | 25 | function getValue(): string { 26 | return view.value; 27 | } 28 | 29 | function setReadonly(readonly: boolean) { 30 | if (readonly) { 31 | view.setAttribute('readonly', 'readonly'); 32 | } else { 33 | view.removeAttribute('readonly'); 34 | } 35 | } 36 | 37 | const view = Html.element('input', { 38 | class: 'swe-input swe-stretched', 39 | type: configuration?.type ?? 'text' 40 | }); 41 | if (configuration?.placeholder) { 42 | view.setAttribute('placeholder', configuration.placeholder); 43 | } 44 | if (configuration?.isReadonly) { 45 | setReadonly(true); 46 | } 47 | view.value = startValue; 48 | view.addEventListener('input', () => { 49 | onChanged.forward(view.value); 50 | }); 51 | 52 | return { 53 | view, 54 | onChanged, 55 | setValue, 56 | getValue, 57 | setReadonly 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /editor/src/components/prepended-input-component.ts: -------------------------------------------------------------------------------- 1 | import { Html } from '../core'; 2 | import { Component } from './component'; 3 | 4 | export function prependedInputComponent(prefix: string, component: TComponent): TComponent { 5 | const view = Html.element('div', { 6 | class: 'swe-prepended-input' 7 | }); 8 | 9 | const pref = Html.element('span', { 10 | class: 'swe-prepended-input-prefix' 11 | }); 12 | pref.innerText = prefix; 13 | view.appendChild(pref); 14 | view.appendChild(component.view); 15 | 16 | return { 17 | ...component, 18 | view 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /editor/src/components/property-validation-error-component.ts: -------------------------------------------------------------------------------- 1 | import { PropertyValidator, PropertyValidatorContext } from 'sequential-workflow-editor-model'; 2 | import { Component } from './component'; 3 | import { validationErrorComponent } from './validation-error-component'; 4 | 5 | export interface PropertyValidationErrorComponent extends Component { 6 | validate(): void; 7 | isHidden(): boolean; 8 | } 9 | 10 | export function propertyValidationErrorComponent( 11 | validator: PropertyValidator, 12 | context: PropertyValidatorContext 13 | ): PropertyValidationErrorComponent { 14 | const validation = validationErrorComponent(); 15 | 16 | function validate() { 17 | const error = validator.validate(context); 18 | validation.setError(error); 19 | } 20 | 21 | validate(); 22 | 23 | return { 24 | view: validation.view, 25 | validate, 26 | isHidden: validation.isHidden 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /editor/src/components/row-component.spec.ts: -------------------------------------------------------------------------------- 1 | import { Html } from '../core/html'; 2 | import { rowComponent } from './row-component'; 3 | 4 | describe('RowComponent', () => { 5 | it('when cols not defined sets swe-col-1 class', () => { 6 | const a = Html.element('p'); 7 | const b = Html.element('p'); 8 | 9 | const row = rowComponent([a, b]); 10 | 11 | expect(row.view.children[0].className).toBe('swe-col swe-col-1'); 12 | expect(row.view.children[1].className).toBe('swe-col swe-col-1'); 13 | }); 14 | 15 | it('when cols is defined sets appropriate classes', () => { 16 | const a = Html.element('p'); 17 | const b = Html.element('p'); 18 | const c = Html.element('p'); 19 | 20 | const row = rowComponent([a, b, c], { 21 | cols: [1, 2, null] 22 | }); 23 | 24 | expect(row.view.children[0].className).toBe('swe-col swe-col-1'); 25 | expect(row.view.children[1].className).toBe('swe-col swe-col-2'); 26 | expect(row.view.children[2].className).toBe('swe-col'); 27 | }); 28 | 29 | it('adds custom class', () => { 30 | const row = rowComponent([], { 31 | class: 'custom-class' 32 | }); 33 | 34 | expect(row.view.className).toBe('swe-row custom-class'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /editor/src/components/row-component.ts: -------------------------------------------------------------------------------- 1 | import { Html } from '../core/html'; 2 | import { Component } from './component'; 3 | 4 | export interface RowComponentConfiguration { 5 | cols?: (number | null)[]; 6 | class?: string; 7 | } 8 | 9 | export function rowComponent(elements: HTMLElement[], configuration?: RowComponentConfiguration): Component { 10 | let viewClass = 'swe-row'; 11 | if (configuration && configuration.class) { 12 | viewClass += ' ' + configuration.class; 13 | } 14 | 15 | const view = Html.element('div', { 16 | class: viewClass 17 | }); 18 | elements.forEach((element, index) => { 19 | const grow = configuration && configuration.cols ? configuration.cols[index] : 1; 20 | let className = 'swe-col'; 21 | if (grow) { 22 | className += ` swe-col-${grow}`; 23 | } 24 | 25 | const col = Html.element('div', { 26 | class: className 27 | }); 28 | col.appendChild(element); 29 | view.appendChild(col); 30 | }); 31 | return { 32 | view 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /editor/src/components/select-component.ts: -------------------------------------------------------------------------------- 1 | import { SimpleEvent } from 'sequential-workflow-editor-model'; 2 | import { Html } from '../core/html'; 3 | import { Component } from './component'; 4 | 5 | export interface SelectComponent extends Component { 6 | setValues(values: string[]): void; 7 | getSelectedIndex(): number; 8 | selectIndex(index: number): void; 9 | onSelected: SimpleEvent; 10 | } 11 | 12 | export interface SelectConfiguration { 13 | size?: 'small'; 14 | stretched?: boolean; 15 | } 16 | 17 | export function selectComponent(configuration?: SelectConfiguration): SelectComponent { 18 | function setValues(values: string[]) { 19 | options.forEach(option => view.removeChild(option)); 20 | options.length = 0; 21 | 22 | for (let i = 0; i < values.length; i++) { 23 | const option = document.createElement('option'); 24 | option.value = values[i]; 25 | option.innerText = values[i]; 26 | view.appendChild(option); 27 | options.push(option); 28 | } 29 | } 30 | 31 | function getSelectedIndex(): number { 32 | return view.selectedIndex; 33 | } 34 | 35 | function selectIndex(index: number) { 36 | view.selectedIndex = index; 37 | } 38 | 39 | function onSelectChanged() { 40 | onSelected.forward(getSelectedIndex()); 41 | } 42 | 43 | const onSelected = new SimpleEvent(); 44 | 45 | let className = 'swe-select'; 46 | if (configuration?.size) { 47 | className += ` swe-select-${configuration.size}`; 48 | } 49 | if (configuration?.stretched) { 50 | className += ' swe-stretched'; 51 | } 52 | const view = Html.element('select', { 53 | class: className 54 | }); 55 | const options: HTMLOptionElement[] = []; 56 | view.addEventListener('change', onSelectChanged, false); 57 | 58 | return { 59 | view, 60 | setValues, 61 | getSelectedIndex, 62 | selectIndex, 63 | onSelected 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /editor/src/components/textarea-component.ts: -------------------------------------------------------------------------------- 1 | import { SimpleEvent } from 'sequential-workflow-editor-model'; 2 | import { Component } from './component'; 3 | import { Html } from '../core'; 4 | 5 | export interface TextareaComponent extends Component { 6 | onChanged: SimpleEvent; 7 | setValue(value: string): void; 8 | getValue(): string; 9 | } 10 | 11 | export interface TextareaConfiguration { 12 | placeholder?: string; 13 | rows: number; 14 | } 15 | 16 | export function textareaComponent(startValue: string, configuration: TextareaConfiguration): TextareaComponent { 17 | const onChanged = new SimpleEvent(); 18 | 19 | function setValue(value: string) { 20 | view.value = value; 21 | } 22 | 23 | function getValue(): string { 24 | return view.value; 25 | } 26 | 27 | const view = Html.element('textarea', { 28 | class: 'swe-textarea swe-stretched', 29 | rows: configuration?.rows?.toString() ?? '4' 30 | }); 31 | if (configuration?.placeholder) { 32 | view.setAttribute('placeholder', configuration.placeholder); 33 | } 34 | view.value = startValue; 35 | view.addEventListener('input', () => { 36 | onChanged.forward(view.value); 37 | }); 38 | 39 | return { 40 | view, 41 | onChanged, 42 | setValue, 43 | getValue 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /editor/src/components/validation-error-component.spec.ts: -------------------------------------------------------------------------------- 1 | import { validationErrorComponent } from './validation-error-component'; 2 | 3 | describe('ValidationErrorComponent', () => { 4 | it('returns correct value for isHidden() and emits changes', () => { 5 | let emitted: boolean | null = null; 6 | const component = validationErrorComponent(); 7 | component.onIsHiddenChanged.subscribe(v => (emitted = v)); 8 | 9 | // test 1 10 | expect(component.isHidden()).toBe(true); 11 | expect(component.view.children.length).toBe(0); 12 | expect(emitted).toBeNull(); 13 | 14 | // test 2 15 | emitted = null; 16 | component.setDefaultError({ $: 'Expected 2 characters' }); 17 | 18 | expect(component.isHidden()).toBe(false); 19 | expect(component.view.children.length).toBeGreaterThan(0); 20 | expect(emitted).toBe(false); 21 | 22 | // test 3 23 | emitted = null; 24 | component.setDefaultError({ $: 'Expected 3 characters' }); 25 | 26 | expect(component.isHidden()).toBe(false); 27 | expect(component.view.children.length).toBeGreaterThan(0); 28 | expect(emitted).toBeNull(); // Visibility did not change 29 | 30 | // test 4 31 | emitted = null; 32 | component.setDefaultError(null); 33 | 34 | expect(component.isHidden()).toBe(true); 35 | expect(component.view.children.length).toBe(0); 36 | expect(emitted).toBe(true); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /editor/src/components/validation-error-component.ts: -------------------------------------------------------------------------------- 1 | import { SimpleEvent, ValidationResult } from 'sequential-workflow-editor-model'; 2 | import { Component } from './component'; 3 | import { Html } from '../core/html'; 4 | 5 | export interface ValidationErrorComponent extends Component { 6 | onIsHiddenChanged: SimpleEvent; 7 | isHidden(): boolean; 8 | setError(error: string | null): void; 9 | setDefaultError(result: ValidationResult): void; 10 | } 11 | 12 | export function validationErrorComponent(): ValidationErrorComponent { 13 | const view = Html.element('div', { 14 | class: 'swe-validation-error' 15 | }); 16 | const onIsHiddenChanged = new SimpleEvent(); 17 | let child: HTMLElement | null = null; 18 | 19 | function isHidden() { 20 | return child === null; 21 | } 22 | 23 | function setError(error: string | null) { 24 | const oldState = isHidden(); 25 | 26 | if (child) { 27 | view.removeChild(child); 28 | child = null; 29 | } 30 | if (error) { 31 | child = Html.element('div', { 32 | class: 'swe-validation-error-text' 33 | }); 34 | child.textContent = error; 35 | view.appendChild(child); 36 | } 37 | 38 | const newState = isHidden(); 39 | if (oldState !== newState) { 40 | onIsHiddenChanged.forward(newState); 41 | } 42 | } 43 | 44 | function setDefaultError(result: ValidationResult) { 45 | setError(result && result['$']); 46 | } 47 | 48 | return { 49 | onIsHiddenChanged, 50 | view, 51 | isHidden, 52 | setError, 53 | setDefaultError 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /editor/src/components/value-editor-container-component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from './component'; 2 | 3 | export function valueEditorContainerComponent(elements: HTMLElement[]): Component { 4 | const view = document.createElement('div'); 5 | view.className = 'swe-value-editor-container'; 6 | elements.forEach(element => view.appendChild(element)); 7 | return { view }; 8 | } 9 | -------------------------------------------------------------------------------- /editor/src/core/append-multiline-text.spec.ts: -------------------------------------------------------------------------------- 1 | import { appendMultilineText } from './append-multiline-text'; 2 | 3 | describe('appendMultilineText()', () => { 4 | let parent: HTMLElement; 5 | 6 | beforeEach(() => { 7 | parent = document.createElement('div'); 8 | }); 9 | 10 | it('appends correctly if passed text with \\n', () => { 11 | appendMultilineText(parent, 'Hello\nWorld\nNow'); 12 | 13 | expect(parent.innerHTML).toBe('Hello
World
Now'); 14 | }); 15 | 16 | it('appends correctly if passed text with \\r\\n', () => { 17 | appendMultilineText(parent, 'Hello\r\nWorld\r\nToday'); 18 | 19 | expect(parent.innerHTML).toBe('Hello
World
Today'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /editor/src/core/append-multiline-text.ts: -------------------------------------------------------------------------------- 1 | export function appendMultilineText(target: HTMLElement, text: string) { 2 | const lines = text.split(/\r?\n/g); 3 | for (let i = 0; i < lines.length; i++) { 4 | if (i > 0) { 5 | target.appendChild(document.createElement('br')); 6 | } 7 | const line = document.createTextNode(lines[i]); 8 | target.appendChild(line); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /editor/src/core/filter-value-types.spec.ts: -------------------------------------------------------------------------------- 1 | import { filterValueTypes } from './filter-value-types'; 2 | 3 | describe('filterValueTypes', () => { 4 | it('filters correctly if passed array', () => { 5 | const result = filterValueTypes(['number', 'string'], ['number']); 6 | expect(result.length).toBe(1); 7 | expect(result[0]).toBe('number'); 8 | }); 9 | 10 | it('filters correctly if passed empty array', () => { 11 | const result = filterValueTypes(['number', 'string'], []); 12 | expect(result.length).toBe(0); 13 | }); 14 | 15 | it('does not filter if second argument is undefined', () => { 16 | const result = filterValueTypes(['number', 'string'], undefined); 17 | expect(result.length).toBe(2); 18 | expect(result[0]).toBe('number'); 19 | expect(result[1]).toBe('string'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /editor/src/core/filter-value-types.ts: -------------------------------------------------------------------------------- 1 | import { ValueType } from 'sequential-workflow-editor-model'; 2 | 3 | export function filterValueTypes(types: ValueType[], allowedTypes: ValueType[] | undefined): ValueType[] { 4 | if (!allowedTypes) { 5 | return types; 6 | } 7 | const result: ValueType[] = []; 8 | for (const type of types) { 9 | if (allowedTypes.includes(type)) { 10 | result.push(type); 11 | } 12 | } 13 | return result; 14 | } 15 | -------------------------------------------------------------------------------- /editor/src/core/filter-variables-by-type.spec.ts: -------------------------------------------------------------------------------- 1 | import { ContextVariable, Path } from 'sequential-workflow-editor-model'; 2 | import { filterVariablesByType } from './filter-variables-by-type'; 3 | 4 | describe('filterVariablesByType', () => { 5 | const variables: ContextVariable[] = [ 6 | { 7 | name: 'blue', 8 | stepId: '0x1', 9 | type: 'string', 10 | valueModelPath: Path.root() 11 | }, 12 | { 13 | name: 'green', 14 | stepId: '0x2', 15 | type: 'number', 16 | valueModelPath: Path.root() 17 | }, 18 | { 19 | name: 'pink', 20 | stepId: '0x3', 21 | type: 'boolean', 22 | valueModelPath: Path.root() 23 | } 24 | ]; 25 | 26 | it('returns filtered list when passed string', () => { 27 | const result = filterVariablesByType(variables, 'string'); 28 | expect(result.length).toBe(1); 29 | expect(result[0].name).toBe('blue'); 30 | }); 31 | 32 | it('returns filtered list when passed array', () => { 33 | const result = filterVariablesByType(variables, ['number', 'boolean']); 34 | expect(result.length).toBe(2); 35 | expect(result[0].name).toBe('green'); 36 | expect(result[1].name).toBe('pink'); 37 | }); 38 | 39 | it('returns empty array if expected types are not found', () => { 40 | const result = filterVariablesByType(variables, ['date', 'time']); 41 | expect(result.length).toBe(0); 42 | }); 43 | 44 | it('returns source list if filter is not passed', () => { 45 | const result = filterVariablesByType(variables, undefined); 46 | expect(result.length).toBe(3); 47 | expect(result[0].name).toBe('blue'); 48 | expect(result[1].name).toBe('green'); 49 | expect(result[2].name).toBe('pink'); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /editor/src/core/filter-variables-by-type.ts: -------------------------------------------------------------------------------- 1 | import { ContextVariable, ValueType } from 'sequential-workflow-editor-model'; 2 | 3 | export function filterVariablesByType(variables: ContextVariable[], valueType: ValueType | ValueType[] | undefined): ContextVariable[] { 4 | if (!valueType) { 5 | return variables; 6 | } 7 | const filter = Array.isArray(valueType) 8 | ? (variable: ContextVariable) => valueType.includes(variable.type) 9 | : (variable: ContextVariable) => variable.type === valueType; 10 | return variables.filter(filter); 11 | } 12 | -------------------------------------------------------------------------------- /editor/src/core/html.spec.ts: -------------------------------------------------------------------------------- 1 | import { Html } from './html'; 2 | 3 | describe('Html', () => { 4 | it('creates element', () => { 5 | const element = Html.element('div', { 6 | 'data-test': '1', 7 | class: 'foo' 8 | }); 9 | 10 | expect(element).toBeDefined(); 11 | expect(element.getAttribute('data-test')).toBe('1'); 12 | expect(element.className).toBe('foo'); 13 | }); 14 | 15 | it('sets attribute', () => { 16 | const element = document.createElement('div'); 17 | 18 | expect(element.getAttribute('data-test')).toBeNull(); 19 | 20 | Html.attrs(element, { 21 | 'data-test': '555' 22 | }); 23 | 24 | expect(element.getAttribute('data-test')).toBe('555'); 25 | }); 26 | 27 | it('toggles class', () => { 28 | const element = document.createElement('div'); 29 | 30 | Html.toggleClass(element, true, 'foo'); 31 | 32 | expect(element.classList.contains('foo')).toBe(true); 33 | 34 | Html.toggleClass(element, false, 'foo'); 35 | 36 | expect(element.classList.contains('foo')).toBe(false); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /editor/src/core/html.ts: -------------------------------------------------------------------------------- 1 | export interface Attributes { 2 | [name: string]: string | number; 3 | } 4 | 5 | export class Html { 6 | public static attrs(element: Element, attributes: Attributes) { 7 | Object.keys(attributes).forEach(name => { 8 | const value = attributes[name]; 9 | element.setAttribute(name, typeof value === 'string' ? value : value.toString()); 10 | }); 11 | } 12 | 13 | public static element(name: T, attributes?: Attributes): HTMLElementTagNameMap[T] { 14 | const element = document.createElement(name); 15 | if (attributes) { 16 | Html.attrs(element, attributes); 17 | } 18 | return element; 19 | } 20 | 21 | public static toggleClass(element: Element, isEnabled: boolean, className: string) { 22 | if (isEnabled) { 23 | element.classList.add(className); 24 | } else { 25 | element.classList.remove(className); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /editor/src/core/icons.ts: -------------------------------------------------------------------------------- 1 | const ns = 'http://www.w3.org/2000/svg'; 2 | 3 | export class Icons { 4 | public static help = 5 | 'M419-334q1-87 20.5-129t65.5-76q39-31 57.5-61.109T581-666q0-39-25.5-64.5T486-756q-46 0-75 26t-43 67l-120-52q27-74 87-120.5T485.756-882q109.228 0 168.236 62.148Q713-757.703 713-669q0 60-21 105.5T625-478q-46 40-57 65.5T557-334H419Zm66.788 282Q447-52 420-79t-27-65.496q0-38.495 26.92-65.5Q446.841-237 485.92-237 525-237 552-209.996q27 27.005 27 65.5Q579-106 551.788-79q-27.213 27-66 27Z'; 6 | public static close = 'm249-183-66-66 231-231-231-231 66-66 231 231 231-231 66 66-231 231 231 231-66 66-231-231-231 231Z'; 7 | public static add = 'M433-183v-250H183v-94h250v-250h94v250h250v94H527v250h-94Z'; 8 | 9 | public static createSvg(icon: string, cls: string): SVGElement { 10 | const svg = document.createElementNS(ns, 'svg'); 11 | svg.setAttribute('viewBox', '0 -960 960 960'); 12 | svg.classList.add(cls); 13 | const path = document.createElementNS(ns, 'path'); 14 | path.setAttribute('d', icon); 15 | svg.appendChild(path); 16 | return svg; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /editor/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './append-multiline-text'; 2 | export * from './filter-value-types'; 3 | export * from './filter-value-types'; 4 | export * from './html'; 5 | export * from './icons'; 6 | export * from './stacked-simple-event'; 7 | export * from './variable-name-formatter'; 8 | -------------------------------------------------------------------------------- /editor/src/core/sort-toolbox-groups.spec.ts: -------------------------------------------------------------------------------- 1 | import { Step } from 'sequential-workflow-model'; 2 | import { sortToolboxGroups } from './sort-toolbox-groups'; 3 | import { ToolboxGroup } from '../editor-provider-configuration'; 4 | 5 | function createStep(name: string): Step { 6 | return { 7 | id: name, 8 | type: name, 9 | name, 10 | componentType: 'task', 11 | properties: {} 12 | }; 13 | } 14 | 15 | describe('sortToolboxGroups', () => { 16 | it('sorts correctly', () => { 17 | const groups: ToolboxGroup[] = [ 18 | { 19 | name: 'B', 20 | steps: [createStep('U'), createStep('B'), createStep('A')] 21 | }, 22 | { 23 | name: 'A', 24 | steps: [createStep('G'), createStep('F'), createStep('C')] 25 | } 26 | ]; 27 | 28 | sortToolboxGroups(groups); 29 | 30 | expect(groups[0].name).toBe('A'); 31 | expect(groups[0].steps[0].name).toBe('C'); 32 | expect(groups[0].steps[1].name).toBe('F'); 33 | expect(groups[0].steps[2].name).toBe('G'); 34 | 35 | expect(groups[1].name).toBe('B'); 36 | expect(groups[1].steps[0].name).toBe('A'); 37 | expect(groups[1].steps[1].name).toBe('B'); 38 | expect(groups[1].steps[2].name).toBe('U'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /editor/src/core/sort-toolbox-groups.ts: -------------------------------------------------------------------------------- 1 | import { EditorToolboxSorter, ToolboxGroup } from '../editor-provider-configuration'; 2 | 3 | export const sortToolboxGroups: EditorToolboxSorter = (groups: ToolboxGroup[]) => { 4 | groups.forEach(group => { 5 | group.steps.sort((a, b) => a.name.localeCompare(b.name)); 6 | }); 7 | groups.sort((a, b) => a.name.localeCompare(b.name)); 8 | }; 9 | -------------------------------------------------------------------------------- /editor/src/core/stacked-simple-event.spec.ts: -------------------------------------------------------------------------------- 1 | import { StackedSimpleEvent } from './stacked-simple-event'; 2 | 3 | describe('StackedSimpleEvent', () => { 4 | let event: StackedSimpleEvent; 5 | 6 | beforeEach(() => { 7 | event = new StackedSimpleEvent(); 8 | }); 9 | 10 | it('should emit an event with a single value when a single value is pushed', done => { 11 | event.subscribe(values => { 12 | expect(values).toEqual([1]); 13 | done(); 14 | }); 15 | 16 | event.push(1); 17 | }); 18 | 19 | it('should emit an event with multiple values when multiple values are pushed', done => { 20 | event.subscribe(values => { 21 | expect(values).toEqual([1, 2, 3]); 22 | done(); 23 | }); 24 | 25 | event.push(1); 26 | event.push(2); 27 | event.push(3); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /editor/src/core/stacked-simple-event.ts: -------------------------------------------------------------------------------- 1 | import { SimpleEvent, SimpleEventListener } from 'sequential-workflow-editor-model'; 2 | 3 | export class StackedSimpleEvent { 4 | private readonly event = new SimpleEvent(); 5 | 6 | private readonly stack: T[] = []; 7 | private to: ReturnType | null = null; 8 | 9 | public push(value: T) { 10 | this.stack.push(value); 11 | 12 | if (this.to) { 13 | return; 14 | } 15 | 16 | this.to = setTimeout(() => { 17 | this.to = null; 18 | this.event.forward(this.stack); 19 | this.stack.length = 0; 20 | }); 21 | } 22 | 23 | public subscribe(listener: SimpleEventListener) { 24 | this.event.subscribe(listener); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /editor/src/core/step-i18n-prefix.ts: -------------------------------------------------------------------------------- 1 | export function createStepI18nPrefix(stepType: string | null): string { 2 | return stepType ? `step.${stepType}.property:` : 'root.property:'; 3 | } 4 | -------------------------------------------------------------------------------- /editor/src/core/variable-name-formatter.spec.ts: -------------------------------------------------------------------------------- 1 | import { formatVariableName, formatVariableNameWithType } from './variable-name-formatter'; 2 | 3 | describe('formatVariableName', () => { 4 | it('returns proper name', () => { 5 | expect(formatVariableName('lastName')).toBe('$lastName'); 6 | }); 7 | }); 8 | 9 | describe('formatVariableNameWithType', () => { 10 | it('returns proper name', () => { 11 | expect(formatVariableNameWithType('foo', 'string')).toBe('$foo (string)'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /editor/src/core/variable-name-formatter.ts: -------------------------------------------------------------------------------- 1 | export function formatVariableName(name: string): string { 2 | return `$${name}`; 3 | } 4 | 5 | export function formatVariableNameWithType(name: string, type: string): string { 6 | return `${formatVariableName(name)} (${type})`; 7 | } 8 | -------------------------------------------------------------------------------- /editor/src/editor-extension.ts: -------------------------------------------------------------------------------- 1 | import { ValueEditorFactory } from './value-editors'; 2 | 3 | export interface EditorExtension { 4 | valueEditors?: ValueEditorExtension[]; 5 | } 6 | 7 | export interface ValueEditorExtension { 8 | editorId: string; 9 | factory: ValueEditorFactory; 10 | } 11 | -------------------------------------------------------------------------------- /editor/src/editor-header.ts: -------------------------------------------------------------------------------- 1 | import { I18n } from 'sequential-workflow-editor-model'; 2 | import { Component } from './components/component'; 3 | import { Html } from './core'; 4 | import { appendMultilineText } from './core/append-multiline-text'; 5 | 6 | export interface EditorHeaderData { 7 | label: string; 8 | description?: string; 9 | } 10 | 11 | export class EditorHeader implements Component { 12 | public static create(data: EditorHeaderData, stepType: string, i18n: I18n): EditorHeader { 13 | const view = Html.element('div', { class: 'swe-editor-header' }); 14 | 15 | const title = Html.element('h3', { class: 'swe-editor-header-title' }); 16 | title.textContent = i18n(`step.${stepType}.name`, data.label); 17 | view.appendChild(title); 18 | 19 | if (data.description) { 20 | const description = i18n(`step.${stepType}.description`, data.description); 21 | const p = Html.element('p', { class: 'swe-editor-header-description' }); 22 | appendMultilineText(p, description); 23 | view.appendChild(p); 24 | } 25 | return new EditorHeader(view); 26 | } 27 | 28 | private constructor(public readonly view: HTMLElement) {} 29 | } 30 | -------------------------------------------------------------------------------- /editor/src/editor-provider-configuration.ts: -------------------------------------------------------------------------------- 1 | import { I18n, UidGenerator } from 'sequential-workflow-editor-model'; 2 | import { DefinitionWalker, Step } from 'sequential-workflow-model'; 3 | import { EditorExtension } from './editor-extension'; 4 | 5 | export interface EditorProviderConfiguration { 6 | /** 7 | * A generator of unique identifiers. 8 | */ 9 | uidGenerator: UidGenerator; 10 | 11 | /** 12 | * The definition walker. If it's not set the editor uses the default definition walker from the `sequential-workflow-model` package. 13 | */ 14 | definitionWalker?: DefinitionWalker; 15 | 16 | /** 17 | * The translation service for the editor. 18 | */ 19 | i18n?: I18n; 20 | 21 | /** 22 | * Determines whether the header of the editor is hidden. 23 | */ 24 | isHeaderHidden?: boolean; 25 | 26 | /** 27 | * Sorter for the toolbox groups. By default, the groups are sorted alphabetically. 28 | */ 29 | toolboxSorter?: EditorToolboxSorter; 30 | 31 | /** 32 | * Extensions for the editor. 33 | */ 34 | extensions?: EditorExtension[]; 35 | } 36 | 37 | export interface ToolboxGroup { 38 | /** 39 | * The name of the group. 40 | */ 41 | name: string; 42 | 43 | /** 44 | * The steps in the group. 45 | */ 46 | steps: Step[]; 47 | } 48 | 49 | export type EditorToolboxSorter = (groups: ToolboxGroup[]) => void; 50 | -------------------------------------------------------------------------------- /editor/src/editor-provider.spec.ts: -------------------------------------------------------------------------------- 1 | import { createDefinitionModel, createRootModel } from 'sequential-workflow-editor-model'; 2 | import { EditorProvider } from './editor-provider'; 3 | 4 | describe('EditorProvider', () => { 5 | it('creates instance', () => { 6 | const definitionModel = createDefinitionModel(model => { 7 | model.root( 8 | createRootModel(() => { 9 | // empty model. 10 | }) 11 | ); 12 | }); 13 | const provider = EditorProvider.create(definitionModel, { 14 | uidGenerator: () => '0x1' 15 | }); 16 | 17 | expect(provider).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /editor/src/external-types.ts: -------------------------------------------------------------------------------- 1 | import { Definition, Step } from 'sequential-workflow-model'; 2 | 3 | export interface StepEditorContext { 4 | notifyNameChanged(): void; 5 | notifyPropertiesChanged(): void; 6 | notifyChildrenChanged(): void; 7 | } 8 | 9 | export interface GlobalEditorContext { 10 | notifyPropertiesChanged(): void; 11 | } 12 | 13 | export type RootEditorProvider = (definition: Definition, context: GlobalEditorContext) => HTMLElement; 14 | export type StepEditorProvider = (step: Step, context: StepEditorContext, definition: Definition) => HTMLElement; 15 | 16 | export type StepLabelProvider = (step: { type: string }) => string; 17 | 18 | export type StepValidator = (step: Step, _: unknown, definition: Definition) => boolean; 19 | export type RootValidator = (definition: Definition) => boolean; 20 | -------------------------------------------------------------------------------- /editor/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './core'; 3 | export * from './value-editors'; 4 | export * from './editor-extension'; 5 | export * from './editor-provider-configuration'; 6 | export * from './editor-provider'; 7 | -------------------------------------------------------------------------------- /editor/src/property-editor/property-hint.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../components/component'; 2 | import { Html } from '../core'; 3 | import { appendMultilineText } from '../core/append-multiline-text'; 4 | 5 | export interface PropertyHintComponent extends Component { 6 | toggle(): void; 7 | } 8 | 9 | export function propertyHint(text: string): PropertyHintComponent { 10 | let content: HTMLElement | null = null; 11 | const view = Html.element('div', { 12 | class: 'swe-property-hint' 13 | }); 14 | 15 | function toggle() { 16 | if (content) { 17 | view.removeChild(content); 18 | content = null; 19 | } else { 20 | content = Html.element('div', { 21 | class: 'swe-property-hint-text' 22 | }); 23 | appendMultilineText(content, text); 24 | view.appendChild(content); 25 | } 26 | } 27 | 28 | return { 29 | view, 30 | toggle 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /editor/src/value-editors/any-variables/any-variable-item-component.ts: -------------------------------------------------------------------------------- 1 | import { AnyVariable, I18n, SimpleEvent } from 'sequential-workflow-editor-model'; 2 | import { Html } from '../../core/html'; 3 | import { validationErrorComponent } from '../../components/validation-error-component'; 4 | import { buttonComponent } from '../../components/button-component'; 5 | import { rowComponent } from '../../components/row-component'; 6 | import { formatVariableNameWithType } from '../../core/variable-name-formatter'; 7 | import { DynamicListItemComponent } from '../../components/dynamic-list-component'; 8 | import { Icons } from '../../core/icons'; 9 | 10 | export type AnyVariableItemComponent = DynamicListItemComponent; 11 | 12 | export function anyVariableItemComponent(variable: AnyVariable, i18n: I18n): AnyVariableItemComponent { 13 | function validate(error: string | null) { 14 | validation.setError(error); 15 | } 16 | 17 | const onDeleteClicked = new SimpleEvent(); 18 | 19 | const view = Html.element('div'); 20 | 21 | const name = Html.element('span'); 22 | name.innerText = formatVariableNameWithType(variable.name, variable.type); 23 | 24 | const deleteButton = buttonComponent(i18n('anyVariable.delete', 'Delete'), { 25 | size: 'small', 26 | theme: 'secondary', 27 | icon: Icons.close 28 | }); 29 | deleteButton.onClick.subscribe(() => onDeleteClicked.forward()); 30 | 31 | const validation = validationErrorComponent(); 32 | 33 | const row = rowComponent([name, deleteButton.view], { 34 | cols: [1, null] 35 | }); 36 | 37 | view.appendChild(row.view); 38 | view.appendChild(validation.view); 39 | 40 | return { 41 | view, 42 | onDeleteClicked, 43 | onItemChanged: new SimpleEvent(), 44 | validate 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /editor/src/value-editors/any-variables/any-variables-value-editor.ts: -------------------------------------------------------------------------------- 1 | import { AnyVariable, AnyVariablesValueModel, ValueContext } from 'sequential-workflow-editor-model'; 2 | import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; 3 | import { ValueEditor } from '../value-editor'; 4 | import { anyVariableItemComponent } from './any-variable-item-component'; 5 | import { anyVariableSelectorComponent } from './any-variable-selector-component'; 6 | import { dynamicListComponent } from '../../components/dynamic-list-component'; 7 | 8 | export const anyVariablesValueEditorId = 'anyVariables'; 9 | 10 | export function anyVariablesValueEditor(context: ValueContext): ValueEditor { 11 | function onChanged(variables: AnyVariable[]) { 12 | context.setValue({ 13 | variables 14 | }); 15 | } 16 | 17 | function onNewAdded(newVariable: AnyVariable) { 18 | if (context.getValue().variables.some(v => v.name === newVariable.name)) { 19 | // TODO: variable is already added, some message? 20 | return; 21 | } 22 | list.add(newVariable); 23 | } 24 | 25 | const selector = anyVariableSelectorComponent(context); 26 | selector.onAdded.subscribe(onNewAdded); 27 | 28 | const list = dynamicListComponent(context.getValue().variables, anyVariableItemComponent, context, { 29 | emptyMessage: context.i18n('anyVariables.noVariablesSelected', 'No variables selected') 30 | }); 31 | list.onChanged.subscribe(onChanged); 32 | 33 | const container = valueEditorContainerComponent([selector.view, list.view]); 34 | 35 | return { 36 | view: container.view 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /editor/src/value-editors/boolean/boolean-value-editor.ts: -------------------------------------------------------------------------------- 1 | import { BooleanValueModel, ValueContext } from 'sequential-workflow-editor-model'; 2 | import { ValueEditor } from '../value-editor'; 3 | import { validationErrorComponent } from '../../components/validation-error-component'; 4 | import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; 5 | import { rowComponent } from '../../components/row-component'; 6 | import { selectComponent } from '../../components/select-component'; 7 | 8 | export const booleanValueEditorId = 'boolean'; 9 | 10 | export function booleanValueEditor(context: ValueContext): ValueEditor { 11 | function validate() { 12 | validation.setDefaultError(context.validate()); 13 | } 14 | 15 | function onSelected(index: number) { 16 | context.setValue(index === 1); 17 | validate(); 18 | } 19 | 20 | const select = selectComponent({ 21 | stretched: true 22 | }); 23 | select.setValues([context.i18n('boolean.false', 'False'), context.i18n('boolean.true', 'True')]); 24 | select.selectIndex(context.getValue() ? 1 : 0); 25 | select.onSelected.subscribe(onSelected); 26 | 27 | const row = rowComponent([select.view]); 28 | 29 | const validation = validationErrorComponent(); 30 | const container = valueEditorContainerComponent([row.view, validation.view]); 31 | 32 | validate(); 33 | 34 | return { 35 | view: container.view 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /editor/src/value-editors/choice/choice-value-editor.ts: -------------------------------------------------------------------------------- 1 | import { ChoiceValueModel, ValueContext } from 'sequential-workflow-editor-model'; 2 | import { ValueEditor } from '../value-editor'; 3 | import { validationErrorComponent } from '../../components/validation-error-component'; 4 | import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; 5 | import { rowComponent } from '../../components/row-component'; 6 | import { selectComponent } from '../../components/select-component'; 7 | import { createStepI18nPrefix } from '../../core/step-i18n-prefix'; 8 | 9 | export const choiceValueEditorId = 'choice'; 10 | 11 | export function choiceValueEditor(context: ValueContext): ValueEditor { 12 | function validate() { 13 | validation.setDefaultError(context.validate()); 14 | } 15 | 16 | function onSelected(index: number) { 17 | const value = choices[index]; 18 | context.setValue(value); 19 | validate(); 20 | } 21 | 22 | const select = selectComponent({ 23 | stretched: true 24 | }); 25 | 26 | const stepType = context.tryGetStepType(); 27 | const i18nPrefix = createStepI18nPrefix(stepType); 28 | 29 | const choices = context.model.configuration.choices; 30 | const translatedChoices = choices.map(choice => { 31 | const pathStr = context.model.path.toString(); 32 | const key = `${i18nPrefix}${pathStr}:choice:${choice}`; 33 | return context.i18n(key, choice); 34 | }); 35 | 36 | select.setValues(translatedChoices); 37 | const startIndex = choices.indexOf(context.getValue()); 38 | select.selectIndex(startIndex); 39 | select.onSelected.subscribe(onSelected); 40 | 41 | const row = rowComponent([select.view]); 42 | 43 | const validation = validationErrorComponent(); 44 | const container = valueEditorContainerComponent([row.view, validation.view]); 45 | 46 | validate(); 47 | 48 | return { 49 | view: container.view 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /editor/src/value-editors/generated-string/generated-string-value-editor.ts: -------------------------------------------------------------------------------- 1 | import { GeneratedStringContext, GeneratedStringVariableValueModel, ValueContext } from 'sequential-workflow-editor-model'; 2 | import { ValueEditor } from '../value-editor'; 3 | import { validationErrorComponent } from '../../components/validation-error-component'; 4 | import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; 5 | import { rowComponent } from '../../components/row-component'; 6 | import { inputComponent } from '../../components/input-component'; 7 | 8 | export const generatedStringValueEditorId = 'generatedString'; 9 | 10 | export function generatedStringValueEditor( 11 | context: ValueContext 12 | ): ValueEditor { 13 | const generatedContext = GeneratedStringContext.create(context); 14 | 15 | function validate() { 16 | validation.setDefaultError(context.validate()); 17 | } 18 | 19 | function reloadDependencies() { 20 | generate(); 21 | validate(); 22 | } 23 | 24 | function generate() { 25 | const generated = context.model.configuration.generator(generatedContext); 26 | if (input.getValue() !== generated) { 27 | input.setValue(generated); 28 | context.setValue(generated); 29 | } 30 | } 31 | 32 | const startValue = context.getValue(); 33 | const input = inputComponent(startValue, { 34 | isReadonly: true 35 | }); 36 | const row = rowComponent([input.view]); 37 | 38 | const validation = validationErrorComponent(); 39 | const container = valueEditorContainerComponent([row.view, validation.view]); 40 | 41 | validate(); 42 | 43 | return { 44 | view: container.view, 45 | reloadDependencies 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /editor/src/value-editors/hidden/hidden-value-editor.spec.ts: -------------------------------------------------------------------------------- 1 | import { hiddenValueEditor } from './hidden-value-editor'; 2 | 3 | describe('hiddenValueEditor', () => { 4 | it('is hidden', () => { 5 | const editor = hiddenValueEditor(); 6 | 7 | expect(editor.view).toBeDefined(); 8 | expect(editor.isHidden).toBeDefined(); 9 | expect((editor.isHidden as () => boolean)()).toBe(true); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /editor/src/value-editors/hidden/hidden-value-editor.ts: -------------------------------------------------------------------------------- 1 | import { ValueModel } from 'sequential-workflow-editor-model'; 2 | import { ValueEditor } from '../value-editor'; 3 | import { valueEditorContainerComponent } from '../../components'; 4 | 5 | export function hiddenValueEditor(): ValueEditor { 6 | const container = valueEditorContainerComponent([]); 7 | return { 8 | view: container.view, 9 | isHidden: () => true 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /editor/src/value-editors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './string'; 2 | export * from './value-editor-factory-resolver'; 3 | export * from './value-editor'; 4 | -------------------------------------------------------------------------------- /editor/src/value-editors/nullable-variable-definition/variable-definition-value-editor.ts: -------------------------------------------------------------------------------- 1 | import { NullableVariableDefinitionValueModel, ValueContext } from 'sequential-workflow-editor-model'; 2 | import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; 3 | import { ValueEditor } from '../value-editor'; 4 | import { validationErrorComponent } from '../../components/validation-error-component'; 5 | import { rowComponent } from '../../components/row-component'; 6 | import { inputComponent } from '../../components/input-component'; 7 | import { prependedInputComponent } from '../../components/prepended-input-component'; 8 | 9 | export const nullableVariableDefinitionValueEditorId = 'nullableVariableDefinition'; 10 | 11 | export function nullableVariableDefinitionValueEditor( 12 | context: ValueContext 13 | ): ValueEditor { 14 | function validate() { 15 | validation.setDefaultError(context.validate()); 16 | } 17 | 18 | const startValue = context.getValue()?.name || ''; 19 | const input = prependedInputComponent('$', inputComponent(startValue)); 20 | input.onChanged.subscribe(value => { 21 | context.setValue( 22 | value 23 | ? { 24 | name: value, 25 | type: context.model.configuration.valueType 26 | } 27 | : null 28 | ); 29 | validate(); 30 | }); 31 | 32 | const row = rowComponent([input.view]); 33 | const validation = validationErrorComponent(); 34 | const container = valueEditorContainerComponent([row.view, validation.view]); 35 | 36 | validate(); 37 | 38 | return { 39 | view: container.view 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /editor/src/value-editors/nullable-variable/nullable-variable-value-editor.ts: -------------------------------------------------------------------------------- 1 | import { NullableVariableValueModel, ValueContext } from 'sequential-workflow-editor-model'; 2 | import { ValueEditor } from '../value-editor'; 3 | import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; 4 | import { validationErrorComponent } from '../../components/validation-error-component'; 5 | import { rowComponent } from '../../components/row-component'; 6 | import { selectComponent } from '../../components/select-component'; 7 | import { formatVariableNameWithType } from '../../core/variable-name-formatter'; 8 | import { filterVariablesByType } from '../../core/filter-variables-by-type'; 9 | 10 | export const nullableVariableValueEditorId = 'nullableVariable'; 11 | 12 | export function nullableVariableValueEditor(context: ValueContext): ValueEditor { 13 | function validate() { 14 | validation.setDefaultError(context.validate()); 15 | } 16 | 17 | function onChanged(selectedIndex: number) { 18 | if (selectedIndex === 0) { 19 | context.setValue(null); 20 | } else { 21 | context.setValue({ 22 | name: variables[selectedIndex - 1].name 23 | }); 24 | } 25 | validate(); 26 | } 27 | 28 | const startValue = context.getValue(); 29 | const variables = filterVariablesByType(context.getVariables(), context.model.configuration.valueType); 30 | 31 | const select = selectComponent({ 32 | stretched: true 33 | }); 34 | select.setValues([ 35 | context.i18n('nullableVariable.selectType', '- Select: :type -', { 36 | type: context.model.configuration.valueType 37 | }), 38 | ...variables.map(variable => formatVariableNameWithType(variable.name, variable.type)) 39 | ]); 40 | if (startValue) { 41 | select.selectIndex(variables.findIndex(variable => variable.name === startValue.name) + 1); 42 | } else { 43 | select.selectIndex(0); 44 | } 45 | select.onSelected.subscribe(index => onChanged(index)); 46 | 47 | const row = rowComponent([select.view]); 48 | 49 | const validation = validationErrorComponent(); 50 | const container = valueEditorContainerComponent([row.view, validation.view]); 51 | 52 | validate(); 53 | 54 | return { 55 | view: container.view 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /editor/src/value-editors/number/number-value-editor.ts: -------------------------------------------------------------------------------- 1 | import { NumberValueModel, ValueContext } from 'sequential-workflow-editor-model'; 2 | import { ValueEditor } from '../value-editor'; 3 | import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; 4 | import { validationErrorComponent } from '../../components/validation-error-component'; 5 | import { rowComponent } from '../../components/row-component'; 6 | import { inputComponent } from '../../components/input-component'; 7 | 8 | export const numberValueEditorId = 'number'; 9 | 10 | export function numberValueEditor(context: ValueContext): ValueEditor { 11 | function validate() { 12 | validation.setDefaultError(context.validate()); 13 | } 14 | 15 | const startValue = String(context.getValue()); 16 | const input = inputComponent(startValue, { 17 | type: 'number' 18 | }); 19 | input.onChanged.subscribe(value => { 20 | const num = value.length > 0 ? Number(value) : NaN; 21 | context.setValue(num); 22 | validate(); 23 | }); 24 | 25 | const row = rowComponent([input.view]); 26 | 27 | const validation = validationErrorComponent(); 28 | const container = valueEditorContainerComponent([row.view, validation.view]); 29 | 30 | validate(); 31 | 32 | return { 33 | view: container.view 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /editor/src/value-editors/string-dictionary/string-dictionary-item-component.ts: -------------------------------------------------------------------------------- 1 | import { I18n, SimpleEvent, StringDictionaryItem } from 'sequential-workflow-editor-model'; 2 | import { validationErrorComponent } from '../../components/validation-error-component'; 3 | import { Html } from '../../core'; 4 | import { rowComponent } from '../../components/row-component'; 5 | import { inputComponent } from '../../components/input-component'; 6 | import { buttonComponent } from '../../components/button-component'; 7 | import { DynamicListItemComponent } from '../../components/dynamic-list-component'; 8 | import { Icons } from '../../core/icons'; 9 | 10 | export type StringDictionaryItemComponent = DynamicListItemComponent; 11 | 12 | export function stringDictionaryItemComponent(item: StringDictionaryItem, i18n: I18n): StringDictionaryItemComponent { 13 | function validate(error: string | null) { 14 | validation.setError(error); 15 | } 16 | 17 | function onChanged() { 18 | onItemChanged.forward({ key: keyInput.getValue(), value: valueInput.getValue() }); 19 | } 20 | 21 | const onItemChanged = new SimpleEvent(); 22 | const onDeleteClicked = new SimpleEvent(); 23 | 24 | const keyInput = inputComponent(item.key, { 25 | placeholder: i18n('stringDictionary.key', 'Key') 26 | }); 27 | keyInput.onChanged.subscribe(onChanged); 28 | 29 | const valueInput = inputComponent(item.value, { 30 | placeholder: i18n('stringDictionary.value', 'Value') 31 | }); 32 | valueInput.onChanged.subscribe(onChanged); 33 | 34 | const deleteButton = buttonComponent(i18n('stringDictionary.delete', 'Delete'), { 35 | size: 'small', 36 | theme: 'secondary', 37 | icon: Icons.close 38 | }); 39 | deleteButton.onClick.subscribe(onDeleteClicked.forward); 40 | 41 | const row = rowComponent([keyInput.view, valueInput.view, deleteButton.view], { 42 | cols: [2, 3, null] 43 | }); 44 | 45 | const validation = validationErrorComponent(); 46 | 47 | const view = Html.element('div', { 48 | class: 'swe-dictionary-item' 49 | }); 50 | view.appendChild(row.view); 51 | view.appendChild(validation.view); 52 | 53 | return { 54 | view, 55 | onItemChanged, 56 | onDeleteClicked, 57 | validate 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /editor/src/value-editors/string-dictionary/string-dictionary-value-editor.ts: -------------------------------------------------------------------------------- 1 | import { StringDictionaryItem, StringDictionaryValueModel, ValueContext } from 'sequential-workflow-editor-model'; 2 | import { ValueEditor } from '../value-editor'; 3 | import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; 4 | import { dynamicListComponent } from '../../components/dynamic-list-component'; 5 | import { stringDictionaryItemComponent } from './string-dictionary-item-component'; 6 | import { buttonComponent } from '../../components/button-component'; 7 | import { Icons } from '../../core/icons'; 8 | 9 | export const stringDictionaryValueEditorId = 'stringDictionary'; 10 | 11 | export function stringDictionaryValueEditor(context: ValueContext): ValueEditor { 12 | function onChanged(items: StringDictionaryItem[]) { 13 | context.setValue({ 14 | items 15 | }); 16 | } 17 | 18 | function onAddClicked() { 19 | list.add({ 20 | key: '', 21 | value: '' 22 | }); 23 | } 24 | 25 | const list = dynamicListComponent(context.getValue().items, stringDictionaryItemComponent, context, { 26 | emptyMessage: context.i18n('stringDictionary.noItems', 'No items') 27 | }); 28 | list.onChanged.subscribe(onChanged); 29 | 30 | const container = valueEditorContainerComponent([list.view]); 31 | 32 | const addButton = buttonComponent(context.i18n('stringDictionary.addItem', 'Add item'), { 33 | size: 'small', 34 | icon: Icons.add 35 | }); 36 | addButton.onClick.subscribe(onAddClicked); 37 | 38 | return { 39 | view: container.view, 40 | controlView: addButton.view 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /editor/src/value-editors/string/index.ts: -------------------------------------------------------------------------------- 1 | export * from './string-value-editor-configuration'; 2 | export * from './string-value-editor-extension'; 3 | export * from './string-value-editor'; 4 | -------------------------------------------------------------------------------- /editor/src/value-editors/string/string-value-editor-configuration.ts: -------------------------------------------------------------------------------- 1 | export interface StringValueEditorConfiguration { 2 | editorId: string; 3 | class?: string; 4 | } 5 | -------------------------------------------------------------------------------- /editor/src/value-editors/string/string-value-editor-extension.ts: -------------------------------------------------------------------------------- 1 | import { EditorExtension, ValueEditorExtension } from '../../editor-extension'; 2 | import { ValueEditorFactory } from '../value-editor'; 3 | import { createStringValueEditor } from './string-value-editor'; 4 | import { StringValueEditorConfiguration } from './string-value-editor-configuration'; 5 | 6 | export class StringValueEditorEditorExtension implements EditorExtension { 7 | public static create(configuration: StringValueEditorConfiguration) { 8 | return new StringValueEditorEditorExtension(configuration); 9 | } 10 | 11 | private constructor(private readonly configuration: StringValueEditorConfiguration) {} 12 | 13 | public readonly valueEditors: ValueEditorExtension[] = [ 14 | { 15 | editorId: this.configuration.editorId, 16 | factory: createStringValueEditor(this.configuration) as ValueEditorFactory 17 | } 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /editor/src/value-editors/string/string-value-editor.ts: -------------------------------------------------------------------------------- 1 | import { StringValueModel, ValueContext } from 'sequential-workflow-editor-model'; 2 | import { ValueEditor } from '../value-editor'; 3 | import { validationErrorComponent } from '../../components/validation-error-component'; 4 | import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; 5 | import { rowComponent } from '../../components/row-component'; 6 | import { InputComponent, inputComponent } from '../../components/input-component'; 7 | import { TextareaComponent, textareaComponent } from '../../components/textarea-component'; 8 | import { StringValueEditorConfiguration } from './string-value-editor-configuration'; 9 | 10 | export const stringValueEditorId = 'string'; 11 | 12 | const defaultMultiline = 4; 13 | 14 | export function createStringValueEditor(configuration?: StringValueEditorConfiguration) { 15 | return (context: ValueContext): ValueEditor => { 16 | function validate() { 17 | validation.setDefaultError(context.validate()); 18 | } 19 | 20 | const startValue = context.getValue(); 21 | const multiline = context.model.configuration.multiline; 22 | 23 | const input: InputComponent | TextareaComponent = multiline 24 | ? textareaComponent(startValue, { 25 | rows: multiline === true ? defaultMultiline : multiline 26 | }) 27 | : inputComponent(startValue); 28 | 29 | input.onChanged.subscribe(value => { 30 | context.setValue(value); 31 | validate(); 32 | }); 33 | 34 | const row = rowComponent([input.view], { 35 | class: configuration?.class 36 | }); 37 | 38 | const validation = validationErrorComponent(); 39 | const container = valueEditorContainerComponent([row.view, validation.view]); 40 | 41 | validate(); 42 | 43 | return { 44 | view: container.view 45 | }; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /editor/src/value-editors/value-editor-factory-resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { ValueEditorFactoryResolver } from './value-editor-factory-resolver'; 2 | 3 | describe('ValueEditorFactoryResolver', () => { 4 | describe('default', () => { 5 | const resolver = ValueEditorFactoryResolver.create(); 6 | 7 | describe('resolve()', () => { 8 | it('resolves string', () => { 9 | const factory = resolver.resolve('string', undefined); 10 | expect(factory).toBeDefined(); 11 | }); 12 | 13 | it('throws error when editor is not found', () => { 14 | expect(() => resolver.resolve('some_unknown_mode_id', undefined)).toThrowError( 15 | 'Editor id some_unknown_mode_id is not supported' 16 | ); 17 | }); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /editor/src/value-editors/value-editor.ts: -------------------------------------------------------------------------------- 1 | import { ModelActivator, ValueModel, ValueContext, I18n, SimpleEvent } from 'sequential-workflow-editor-model'; 2 | import { ValueEditorFactoryResolver } from './value-editor-factory-resolver'; 3 | import { Component } from '../components/component'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | export interface ValueEditor extends Component { 7 | onIsHiddenChanged?: SimpleEvent; 8 | controlView?: HTMLElement; 9 | reloadDependencies?: () => void; 10 | isHidden?: () => boolean; 11 | } 12 | 13 | export type ValueEditorFactory = ( 14 | context: ValueContext, 15 | services: EditorServices 16 | ) => ValueEditor; 17 | 18 | export interface EditorServices { 19 | valueEditorFactoryResolver: ValueEditorFactoryResolver; 20 | activator: ModelActivator; 21 | i18n: I18n; 22 | } 23 | -------------------------------------------------------------------------------- /editor/src/value-editors/variable-definitions/variable-definitions-value-editor.ts: -------------------------------------------------------------------------------- 1 | import { ValueContext, VariableDefinition, VariableDefinitionsValueModel } from 'sequential-workflow-editor-model'; 2 | import { ValueEditor } from '../value-editor'; 3 | import { variableDefinitionItemComponent } from './variable-definition-item-component'; 4 | import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; 5 | import { buttonComponent } from '../../components/button-component'; 6 | import { dynamicListComponent } from '../../components/dynamic-list-component'; 7 | import { Icons } from '../../core/icons'; 8 | 9 | export const variableDefinitionsValueEditorId = 'variableDefinitions'; 10 | 11 | export function variableDefinitionsValueEditor( 12 | context: ValueContext 13 | ): ValueEditor { 14 | function onChanged(variables: VariableDefinition[]) { 15 | context.setValue({ 16 | variables 17 | }); 18 | } 19 | 20 | function onAddClicked() { 21 | list.add({ 22 | name: '', 23 | type: context.getValueTypes()[0] 24 | }); 25 | } 26 | 27 | const list = dynamicListComponent( 28 | context.getValue().variables, 29 | item => variableDefinitionItemComponent(item, context), 30 | context, 31 | { 32 | emptyMessage: context.i18n('variableDefinitions.noVariablesDefined', 'No variables defined') 33 | } 34 | ); 35 | list.onChanged.subscribe(onChanged); 36 | 37 | const addButton = buttonComponent(context.i18n('variableDefinitions.newVariable', 'New variable'), { 38 | size: 'small', 39 | icon: Icons.add 40 | }); 41 | addButton.onClick.subscribe(onAddClicked); 42 | 43 | const container = valueEditorContainerComponent([list.view]); 44 | 45 | return { 46 | view: container.view, 47 | controlView: addButton.view 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /editor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build/", 4 | "noImplicitAny": true, 5 | "target": "es6", 6 | "module": "es2015", 7 | "sourceMap": false, 8 | "strict": true, 9 | "allowJs": false, 10 | "declaration": true, 11 | "declarationDir": "./build/", 12 | "moduleResolution": "node", 13 | "lib": [ 14 | "es2015", 15 | "dom" 16 | ] 17 | }, 18 | "include": [ 19 | "./src/" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /model/.gitignore: -------------------------------------------------------------------------------- 1 | LICENSE 2 | -------------------------------------------------------------------------------- /model/README.md: -------------------------------------------------------------------------------- 1 | # Sequential Workflow Editor Model 2 | 3 | This package contains the model for [Sequential Workflow Editor](https://github.com/nocode-js/sequential-workflow-editor). 4 | 5 | ## 💡 License 6 | 7 | #### Commercial license 8 | 9 | You are creating a closed source application and you are not distributing our library in source form. You may use this project under the terms of the [Commercial License](./LICENSE). To purchase license check the [pricing](https://nocode-js.com/sequential-workflow-editor/pricing). 10 | 11 | #### Open source license 12 | 13 | If you are developing a freely available software program using a license that aligns with the GNU General Public License version 3, you are permitted to utilize this project while abiding by the provisions outlined in the GPLv3. 14 | -------------------------------------------------------------------------------- /model/jest.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | testMatch: ['**/?(*.)+(spec|test).ts?(x)'], 5 | transform: { 6 | '^.+\\.(ts|js)x?$': 'ts-jest', 7 | }, 8 | moduleNameMapper: {}, 9 | transformIgnorePatterns: [ 10 | 'node_modules/(?!sequential-workflow-model)' 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequential-workflow-editor-model", 3 | "version": "0.14.8", 4 | "homepage": "https://nocode-js.com/", 5 | "author": { 6 | "name": "NoCode JS", 7 | "url": "https://nocode-js.com/" 8 | }, 9 | "license": "MIT", 10 | "type": "module", 11 | "main": "./lib/esm/index.js", 12 | "types": "./lib/index.d.ts", 13 | "exports": { 14 | ".": { 15 | "types": { 16 | "require": "./lib/index.d.ts", 17 | "default": "./lib/index.d.ts" 18 | }, 19 | "default": { 20 | "require": "./lib/cjs/index.cjs", 21 | "default": "./lib/esm/index.js" 22 | } 23 | } 24 | }, 25 | "sideEffects": false, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/nocode-js/sequential-workflow-editor.git" 29 | }, 30 | "files": [ 31 | "lib/", 32 | "dist/" 33 | ], 34 | "publishConfig": { 35 | "registry": "https://registry.npmjs.org/" 36 | }, 37 | "scripts": { 38 | "prepare": "cp ../LICENSE LICENSE", 39 | "clean": "rm -rf lib && rm -rf node_modules/.cache/rollup-plugin-typescript2", 40 | "build": "yarn clean && rollup -c", 41 | "start": "rollup -c --watch", 42 | "eslint": "eslint ./src --ext .ts", 43 | "prettier": "prettier --check ./src", 44 | "prettier:fix": "prettier --write ./src", 45 | "test:single": "jest", 46 | "test": "jest --clearCache && jest --watchAll" 47 | }, 48 | "dependencies": { 49 | "sequential-workflow-model": "^0.2.0" 50 | }, 51 | "peerDependencies": { 52 | "sequential-workflow-model": "^0.2.0" 53 | }, 54 | "devDependencies": { 55 | "rollup": "^4.4.0", 56 | "rollup-plugin-dts": "^6.1.0", 57 | "rollup-plugin-typescript2": "^0.36.0", 58 | "@rollup/plugin-node-resolve": "^15.2.3", 59 | "typescript": "^4.9.5", 60 | "prettier": "^3.1.0", 61 | "@typescript-eslint/eslint-plugin": "^5.47.0", 62 | "@typescript-eslint/parser": "^5.47.0", 63 | "eslint": "^8.30.0", 64 | "@types/jest": "^29.5.1", 65 | "jest": "^29.5.0", 66 | "ts-jest": "^29.1.0" 67 | }, 68 | "keywords": [ 69 | "workflow", 70 | "model", 71 | "nocode", 72 | "lowcode", 73 | "flow" 74 | ] 75 | } -------------------------------------------------------------------------------- /model/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import dts from 'rollup-plugin-dts'; 2 | import typescript from 'rollup-plugin-typescript2'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | import fs from 'fs'; 5 | 6 | const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 7 | const external = Object.keys(packageJson.dependencies); 8 | 9 | const ts = typescript({ 10 | useTsconfigDeclarationDir: true 11 | }); 12 | 13 | export default [ 14 | { 15 | input: './src/index.ts', 16 | plugins: [ 17 | typescript({ 18 | useTsconfigDeclarationDir: true 19 | }) 20 | ], 21 | cache: false, 22 | external, 23 | output: [ 24 | { 25 | file: './lib/cjs/index.cjs', 26 | format: 'cjs' 27 | }, 28 | { 29 | file: './lib/esm/index.js', 30 | format: 'es' 31 | } 32 | ] 33 | }, 34 | { 35 | input: './build/index.d.ts', 36 | output: [ 37 | { 38 | file: './lib/index.d.ts', 39 | format: 'es' 40 | } 41 | ], 42 | plugins: [dts()], 43 | }, 44 | { 45 | input: './src/index.ts', 46 | plugins: [ 47 | nodeResolve({ 48 | browser: true, 49 | }), 50 | ts 51 | ], 52 | output: [ 53 | { 54 | file: './dist/index.umd.js', 55 | format: 'umd', 56 | name: 'sequentialWorkflowEditorModel' 57 | } 58 | ] 59 | } 60 | ]; 61 | -------------------------------------------------------------------------------- /model/src/activator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model-activator'; 2 | -------------------------------------------------------------------------------- /model/src/activator/model-activator.spec.ts: -------------------------------------------------------------------------------- 1 | import { createDefinitionModel, createRootModel, createStepModel } from '../builders'; 2 | import { createNumberValueModel, createStringValueModel } from '../value-models'; 3 | import { ModelActivator } from './model-activator'; 4 | import { Definition, Step } from 'sequential-workflow-model'; 5 | 6 | interface TestDefinition extends Definition { 7 | properties: { 8 | size: number; 9 | }; 10 | } 11 | 12 | interface TestStep extends Step { 13 | type: 'test'; 14 | componentType: 'task'; 15 | properties: { 16 | password: string; 17 | }; 18 | } 19 | 20 | describe('ModelActivator', () => { 21 | const definitionModel = createDefinitionModel(model => { 22 | model.root( 23 | createRootModel(root => { 24 | root.property('size').value( 25 | createNumberValueModel({ 26 | defaultValue: 20 27 | }) 28 | ); 29 | }) 30 | ); 31 | 32 | model.steps([ 33 | createStepModel('test', 'task', step => { 34 | step.property('password').value( 35 | createStringValueModel({ 36 | defaultValue: 'lorem ipsum' 37 | }) 38 | ); 39 | }) 40 | ]); 41 | }); 42 | 43 | it('activates definition correctly', () => { 44 | const definition = ModelActivator.create(definitionModel, () => '1').activateDefinition(); 45 | 46 | expect(definition.properties.size).toBe(20); 47 | expect(Array.isArray(definition.sequence)).toBe(true); 48 | }); 49 | 50 | it('activates step correctly', () => { 51 | const uidGenerator = () => '321'; 52 | const step = ModelActivator.create(definitionModel, uidGenerator).activateStep('test'); 53 | 54 | expect(step.id).toBe('321'); 55 | expect(step.type).toBe('test'); 56 | expect(step.componentType).toBe('task'); 57 | expect(step.name).toBe('Test'); 58 | expect(step.properties.password).toBe('lorem ipsum'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /model/src/builders/branched-step-model-builder.ts: -------------------------------------------------------------------------------- 1 | import { BranchedStep, Branches } from 'sequential-workflow-model'; 2 | import { StepModelBuilder } from './step-model-builder'; 3 | import { StepModel } from '../model'; 4 | import { Path } from '../core/path'; 5 | import { PropertyModelBuilder } from './property-model-builder'; 6 | 7 | const branchesPath = Path.create('branches'); 8 | 9 | export class BranchedStepModelBuilder extends StepModelBuilder { 10 | private readonly branchesBuilder = new PropertyModelBuilder(branchesPath, this.circularDependencyDetector); 11 | 12 | /** 13 | * @returns the builder for the branches property. 14 | * @example `builder.branches().value(createBranchesValueModel(...));` 15 | */ 16 | public branches(): PropertyModelBuilder { 17 | return this.branchesBuilder; 18 | } 19 | 20 | public build(): StepModel { 21 | if (!this.branchesBuilder.hasValue()) { 22 | throw new Error(`Branches configuration is not set for ${this.type}`); 23 | } 24 | 25 | const model = super.build(); 26 | model.properties.push(this.branchesBuilder.build()); 27 | return model; 28 | } 29 | } 30 | 31 | export function createBranchedStepModel( 32 | type: TStep['type'], 33 | componentType: TStep['componentType'], 34 | build: (builder: BranchedStepModelBuilder) => void 35 | ): StepModel { 36 | const builder = new BranchedStepModelBuilder(type, componentType); 37 | build(builder); 38 | return builder.build(); 39 | } 40 | -------------------------------------------------------------------------------- /model/src/builders/circular-dependency-detector.spec.ts: -------------------------------------------------------------------------------- 1 | import { Path } from '../core/path'; 2 | import { CircularDependencyDetector } from './circular-dependency-detector'; 3 | 4 | describe('CircularDependencyDetector', () => { 5 | let detector: CircularDependencyDetector; 6 | 7 | beforeEach(() => { 8 | detector = new CircularDependencyDetector(); 9 | }); 10 | 11 | it('detects circular dependency', () => { 12 | detector.check(Path.create('pink'), Path.create('red')); 13 | 14 | expect(() => detector.check(Path.create('red'), Path.create('pink'))).toThrowError( 15 | 'It is not allowed to depend on dependency with dependency: red <-> pink' 16 | ); 17 | }); 18 | 19 | it('detects when try to depend on dependency with dependency', () => { 20 | const a = () => detector.check(Path.create('white'), Path.create('gray')); 21 | const b = () => detector.check(Path.create('black'), Path.create('white')); 22 | const c = () => detector.check(Path.create('red'), Path.create('black')); 23 | 24 | const expectedError = 'It is not allowed to depend on dependency with dependency'; 25 | expect(() => { 26 | a(); 27 | b(); 28 | c(); 29 | }).toThrowError(expectedError); 30 | 31 | expect(() => { 32 | c(); 33 | a(); 34 | b(); 35 | }).toThrowError(expectedError); 36 | 37 | expect(() => { 38 | c(); 39 | b(); 40 | a(); 41 | }).toThrowError(expectedError); 42 | }); 43 | 44 | it('does not throw error if there is no circular dependency', () => { 45 | detector.check(Path.create('green'), Path.create('blue')); 46 | detector.check(Path.create('green'), Path.create('violet')); 47 | detector.check(Path.create('red'), Path.create('violet')); 48 | detector.check(Path.create('black'), Path.create('white')); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /model/src/builders/circular-dependency-detector.ts: -------------------------------------------------------------------------------- 1 | import { Path } from '../core/path'; 2 | 3 | export class CircularDependencyDetector { 4 | private readonly dependencies: { source: Path; target: Path }[] = []; 5 | 6 | public check(source: Path, target: Path) { 7 | if (this.dependencies.some(dep => dep.source.equals(target))) { 8 | throw new Error(`It is not allowed to depend on dependency with dependency: ${source.toString()} <-> ${target.toString()}`); 9 | } 10 | 11 | this.dependencies.push({ source, target }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /model/src/builders/definition-model-builder.ts: -------------------------------------------------------------------------------- 1 | import { Definition } from 'sequential-workflow-model'; 2 | import { DefinitionModel, RootModel, StepModel, StepModels } from '../model'; 3 | import { ValueType } from '../types'; 4 | import { RootModelBuilder, createRootModel } from './root-model-builder'; 5 | 6 | export class DefinitionModelBuilder { 7 | private rootModel?: RootModel; 8 | private readonly stepModels: StepModels = {}; 9 | private _valueTypes?: ValueType[]; 10 | 11 | public constructor() { 12 | // Nothing... 13 | } 14 | 15 | public root(modelOrCallback: RootModel | ((builder: RootModelBuilder) => void)): this { 16 | if (this.rootModel) { 17 | throw new Error('Root model is already defined'); 18 | } 19 | if (typeof modelOrCallback === 'function') { 20 | this.rootModel = createRootModel(modelOrCallback); 21 | } else { 22 | this.rootModel = modelOrCallback; 23 | } 24 | return this; 25 | } 26 | 27 | public steps(models: StepModel[]): this { 28 | for (const model of models) { 29 | this.step(model); 30 | } 31 | return this; 32 | } 33 | 34 | public step(model: StepModel): this { 35 | if (this.stepModels[model.type]) { 36 | throw new Error(`Step model with type ${model.type} is already defined`); 37 | } 38 | this.stepModels[model.type] = model; 39 | return this; 40 | } 41 | 42 | public valueTypes(valueTypes: ValueType[]): this { 43 | if (this._valueTypes) { 44 | throw new Error('Value types are already set'); 45 | } 46 | this._valueTypes = valueTypes; 47 | return this; 48 | } 49 | 50 | public build(): DefinitionModel { 51 | if (!this.rootModel) { 52 | throw new Error('Root model is not defined'); 53 | } 54 | 55 | return { 56 | root: this.rootModel, 57 | steps: this.stepModels, 58 | valueTypes: this._valueTypes ?? [] 59 | }; 60 | } 61 | } 62 | 63 | export function createDefinitionModel( 64 | build: (builder: DefinitionModelBuilder) => void 65 | ): DefinitionModel { 66 | const builder = new DefinitionModelBuilder(); 67 | build(builder); 68 | return builder.build(); 69 | } 70 | -------------------------------------------------------------------------------- /model/src/builders/index.ts: -------------------------------------------------------------------------------- 1 | export * from './branched-step-model-builder'; 2 | export * from './definition-model-builder'; 3 | export * from './root-model-builder'; 4 | export * from './property-model-builder'; 5 | export * from './sequential-step-model-builder'; 6 | export * from './step-model-builder'; 7 | -------------------------------------------------------------------------------- /model/src/builders/sequential-step-model-builder.ts: -------------------------------------------------------------------------------- 1 | import { Sequence, SequentialStep } from 'sequential-workflow-model'; 2 | import { StepModelBuilder } from './step-model-builder'; 3 | import { createSequenceValueModel } from '../value-models'; 4 | import { Path } from '../core/path'; 5 | import { StepModel } from '../model'; 6 | import { PropertyModelBuilder } from './property-model-builder'; 7 | 8 | const sequencePath = Path.create('sequence'); 9 | 10 | export class SequentialStepModelBuilder extends StepModelBuilder { 11 | private readonly sequenceBuilder = new PropertyModelBuilder( 12 | sequencePath, 13 | this.circularDependencyDetector 14 | ); 15 | 16 | /** 17 | * @returns the builder for the sequence property. 18 | * @example `builder.sequence().value(createSequenceValueModel(...));` 19 | */ 20 | public sequence(): PropertyModelBuilder { 21 | return this.sequenceBuilder; 22 | } 23 | 24 | public build(): StepModel { 25 | if (!this.sequenceBuilder.hasValue()) { 26 | this.sequenceBuilder.value( 27 | createSequenceValueModel({ 28 | sequence: [] 29 | }) 30 | ); 31 | } 32 | 33 | const model = super.build(); 34 | model.properties.push(this.sequenceBuilder.build()); 35 | return model; 36 | } 37 | } 38 | 39 | export function createSequentialStepModel( 40 | type: TStep['type'], 41 | componentType: TStep['componentType'], 42 | build: (builder: SequentialStepModelBuilder) => void 43 | ): StepModel { 44 | const builder = new SequentialStepModelBuilder(type, componentType); 45 | build(builder); 46 | return builder.build(); 47 | } 48 | -------------------------------------------------------------------------------- /model/src/context/default-value-context.ts: -------------------------------------------------------------------------------- 1 | import { Properties } from 'sequential-workflow-model'; 2 | import { ModelActivator } from '../activator'; 3 | import { PropertyContext } from './property-context'; 4 | 5 | export class DefaultValueContext { 6 | public static create( 7 | activator: ModelActivator, 8 | propertyContext: PropertyContext 9 | ): DefaultValueContext { 10 | return new DefaultValueContext(activator, propertyContext); 11 | } 12 | 13 | private constructor( 14 | private readonly activator: ModelActivator, 15 | public readonly propertyContext: PropertyContext 16 | ) {} 17 | 18 | public readonly getPropertyValue = this.propertyContext.getPropertyValue; 19 | public readonly formatPropertyValue = this.propertyContext.formatPropertyValue; 20 | public readonly activateStep = this.activator.activateStep; 21 | } 22 | -------------------------------------------------------------------------------- /model/src/context/definition-context.ts: -------------------------------------------------------------------------------- 1 | import { Definition, DefinitionWalker, Step } from 'sequential-workflow-model'; 2 | import { DefinitionModel } from '../model'; 3 | import { ParentsProvider } from './variables-provider'; 4 | import { I18n } from '../i18n'; 5 | 6 | export class DefinitionContext { 7 | public static createForStep( 8 | step: Step, 9 | definition: Definition, 10 | definitionModel: DefinitionModel, 11 | definitionWalker: DefinitionWalker, 12 | i18n: I18n 13 | ): DefinitionContext { 14 | const parentsProvider = ParentsProvider.createForStep(step, definition, definitionModel, definitionWalker, i18n); 15 | return new DefinitionContext(step, definition, definitionModel, parentsProvider); 16 | } 17 | 18 | public static createForRoot( 19 | definition: Definition, 20 | definitionModel: DefinitionModel, 21 | definitionWalker: DefinitionWalker, 22 | i18n: I18n 23 | ): DefinitionContext { 24 | const parentsProvider = ParentsProvider.createForRoot(definition, definitionModel, definitionWalker, i18n); 25 | return new DefinitionContext(definition, definition, definitionModel, parentsProvider); 26 | } 27 | 28 | private constructor( 29 | public readonly object: Step | Definition, 30 | public readonly definition: Definition, 31 | public readonly definitionModel: DefinitionModel, 32 | public readonly parentsProvider: ParentsProvider 33 | ) {} 34 | } 35 | -------------------------------------------------------------------------------- /model/src/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './default-value-context'; 2 | export * from './property-context'; 3 | export * from './scoped-property-context'; 4 | export * from './value-context'; 5 | export * from './definition-context'; 6 | -------------------------------------------------------------------------------- /model/src/context/read-property-value.spec.ts: -------------------------------------------------------------------------------- 1 | import { Path } from '../core'; 2 | import { PropertyModel } from '../model'; 3 | import { readPropertyValue } from './read-property-value'; 4 | 5 | describe('readPropertyValue', () => { 6 | const model = { 7 | dependencies: [Path.create('properties/blue'), Path.create('properties/green')] 8 | } as unknown as PropertyModel; 9 | const object = { 10 | properties: { 11 | blue: 1, 12 | green: { 13 | red: 2 14 | }, 15 | black: 3 16 | } 17 | }; 18 | 19 | it('reads correctly', () => { 20 | const blue = readPropertyValue('blue', model, object); 21 | expect(blue).toBe(1); 22 | 23 | const green = readPropertyValue('green', model, object); 24 | expect(green).toEqual({ red: 2 }); 25 | }); 26 | 27 | it('throws error if property is not registered as dependency', () => { 28 | expect(() => readPropertyValue('black', model, object)).toThrowError('Property black is not registered as dependency'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /model/src/context/read-property-value.ts: -------------------------------------------------------------------------------- 1 | import { Properties } from 'sequential-workflow-model'; 2 | import { Path } from '../core'; 3 | import { PropertyModel } from '../model'; 4 | 5 | export function readPropertyValue( 6 | name: Key, 7 | model: PropertyModel, 8 | object: object 9 | ): TProperties[Key] { 10 | const nameStr = String(name); 11 | const path = Path.create(['properties', nameStr]); 12 | 13 | if (!model.dependencies.some(dep => dep.equals(path))) { 14 | throw new Error(`Property ${nameStr} is not registered as dependency`); 15 | } 16 | 17 | return path.read(object); 18 | } 19 | -------------------------------------------------------------------------------- /model/src/context/scoped-property-context.ts: -------------------------------------------------------------------------------- 1 | import { Properties } from 'sequential-workflow-model'; 2 | import { ContextVariable } from '../model'; 3 | import { ParentsProvider } from './variables-provider'; 4 | import { PropertyContext } from './property-context'; 5 | import { ValueType } from '../types'; 6 | import { I18n } from '../i18n'; 7 | 8 | export class ScopedPropertyContext { 9 | public static create( 10 | propertyContext: PropertyContext, 11 | parentsProvider: ParentsProvider, 12 | i18n: I18n 13 | ): ScopedPropertyContext { 14 | return new ScopedPropertyContext(propertyContext, i18n, parentsProvider); 15 | } 16 | 17 | private constructor( 18 | public readonly propertyContext: PropertyContext, 19 | public readonly i18n: I18n, 20 | private readonly parentsProvider: ParentsProvider 21 | ) {} 22 | 23 | public readonly tryGetStepType = this.propertyContext.tryGetStepType; 24 | public readonly getPropertyValue = this.propertyContext.getPropertyValue; 25 | public readonly formatPropertyValue = this.propertyContext.formatPropertyValue; 26 | public readonly getValueTypes = this.propertyContext.getValueTypes; 27 | 28 | public readonly hasVariable = (variableName: string, valueType: string | null): boolean => { 29 | return this.getVariables().some(v => v.name === variableName && (valueType === null || v.type === valueType)); 30 | }; 31 | 32 | public readonly findFirstUndefinedVariable = (variableNames: string[]): string | undefined => { 33 | const variables = new Set(this.getVariables().map(v => v.name)); 34 | return variableNames.find(name => !variables.has(name)); 35 | }; 36 | 37 | public readonly isVariableDuplicated = (variableName: string): boolean => { 38 | return this.getVariables().filter(v => v.name === variableName).length > 1; 39 | }; 40 | 41 | public readonly tryGetVariableType = (variableName: string): ValueType | null => { 42 | const variable = this.getVariables().find(v => v.name === variableName); 43 | return variable ? variable.type : null; 44 | }; 45 | 46 | public readonly getVariables = (): ContextVariable[] => { 47 | return this.parentsProvider.getVariables(); 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /model/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './path'; 2 | export * from './simple-event'; 3 | -------------------------------------------------------------------------------- /model/src/core/label-builder.spec.ts: -------------------------------------------------------------------------------- 1 | import { buildLabel } from './label-builder'; 2 | 3 | describe('buildLabel', () => { 4 | it('creates label', () => { 5 | expect(buildLabel('test')).toBe('Test'); 6 | expect(buildLabel('TEST')).toBe('TEST'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /model/src/core/label-builder.ts: -------------------------------------------------------------------------------- 1 | export function buildLabel(value: string): string { 2 | return value.charAt(0).toUpperCase() + value.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /model/src/core/path.spec.ts: -------------------------------------------------------------------------------- 1 | import { Path } from './path'; 2 | 3 | describe('Path', () => { 4 | it('creates correctly', () => { 5 | const p1 = Path.create('a/b/c'); 6 | 7 | expect(p1.parts[0]).toBe('a'); 8 | expect(p1.parts[1]).toBe('b'); 9 | expect(p1.parts[2]).toBe('c'); 10 | }); 11 | 12 | it('equals() returns correct value', () => { 13 | const abc = Path.create('a/b/c'); 14 | expect(abc.equals('a/b')).toBe(false); 15 | expect(abc.equals('a/b/c')).toBe(true); 16 | expect(abc.equals('a')).toBe(false); 17 | expect(abc.equals('a/b/c/d')).toBe(false); 18 | }); 19 | 20 | it('startWith() returns correct value', () => { 21 | const abc = Path.create('a/b/c'); 22 | expect(abc.startsWith('a/b')).toBe(true); 23 | expect(abc.startsWith('a/b/c')).toBe(true); 24 | expect(abc.startsWith('a')).toBe(true); 25 | 26 | expect(abc.startsWith('a/b/c/d')).toBe(false); 27 | expect(abc.startsWith('a/q')).toBe(false); 28 | expect(abc.startsWith('q/w/e')).toBe(false); 29 | expect(abc.startsWith('q')).toBe(false); 30 | }); 31 | 32 | it('write() writes correctly', () => { 33 | const obj = { 34 | a: { 35 | b: { 36 | c: 1 37 | } 38 | } 39 | }; 40 | 41 | Path.create('a/b/c').write(obj, 2); 42 | Path.create('a/b/d').write(obj, 3); 43 | 44 | expect(obj.a.b.c).toBe(2); 45 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 46 | expect((obj.a.b as any).d).toBe(3); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /model/src/core/path.ts: -------------------------------------------------------------------------------- 1 | const SEPARATOR = '/'; 2 | 3 | export class Path { 4 | public static create(path: string[] | string): Path { 5 | if (typeof path === 'string') { 6 | path = path.split(SEPARATOR); 7 | } 8 | return new Path(path); 9 | } 10 | 11 | public static root(): Path { 12 | return new Path([]); 13 | } 14 | 15 | private constructor(public readonly parts: string[]) {} 16 | 17 | public read(object: object): TValue { 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | let result = object as any; 20 | for (const part of this.parts) { 21 | result = result[part]; 22 | if (result === undefined) { 23 | throw new Error(`Cannot read path: ${this.parts.join(SEPARATOR)}`); 24 | } 25 | } 26 | return result as TValue; 27 | } 28 | 29 | public write(object: object, value: TValue) { 30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 31 | let result = object as any; 32 | for (let i = 0; i < this.parts.length - 1; i++) { 33 | result = result[this.parts[i]]; 34 | if (result === undefined) { 35 | throw new Error(`Cannot write path: ${this.toString()}`); 36 | } 37 | } 38 | result[this.last()] = value; 39 | } 40 | 41 | public equals(other: Path | string): boolean { 42 | if (typeof other === 'string') { 43 | other = Path.create(other); 44 | } 45 | return this.parts.length === other.parts.length && this.startsWith(other); 46 | } 47 | 48 | public add(part: string): Path { 49 | return new Path([...this.parts, part]); 50 | } 51 | 52 | public last(): string { 53 | if (this.parts.length === 0) { 54 | throw new Error('Root path has no last part'); 55 | } 56 | return this.parts[this.parts.length - 1]; 57 | } 58 | 59 | public startsWith(other: Path | string): boolean { 60 | if (typeof other === 'string') { 61 | other = Path.create(other); 62 | } 63 | 64 | if (this.parts.length < other.parts.length) { 65 | return false; 66 | } 67 | for (let i = 0; i < other.parts.length; i++) { 68 | if (this.parts[i] !== other.parts[i]) { 69 | return false; 70 | } 71 | } 72 | return true; 73 | } 74 | 75 | public toString(): string { 76 | return this.parts.join(SEPARATOR); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /model/src/core/simple-event.spec.ts: -------------------------------------------------------------------------------- 1 | import { SimpleEvent } from './simple-event'; 2 | 3 | describe('SimpleEvent', () => { 4 | it('forward() works as expected', () => { 5 | const e = new SimpleEvent(); 6 | 7 | let counter = 0; 8 | function listener() { 9 | counter++; 10 | } 11 | 12 | e.subscribe(listener); 13 | e.forward(); 14 | 15 | expect(counter).toEqual(1); 16 | expect(e.count()).toEqual(1); 17 | 18 | e.unsubscribe(listener); 19 | e.forward(); 20 | 21 | expect(counter).toEqual(1); 22 | expect(e.count()).toEqual(0); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /model/src/core/simple-event.ts: -------------------------------------------------------------------------------- 1 | export class SimpleEvent { 2 | private readonly listeners: SimpleEventListener[] = []; 3 | 4 | public subscribe(listener: SimpleEventListener) { 5 | this.listeners.push(listener); 6 | } 7 | 8 | public unsubscribe(listener: SimpleEventListener) { 9 | const index = this.listeners.indexOf(listener); 10 | if (index >= 0) { 11 | this.listeners.splice(index, 1); 12 | } else { 13 | throw new Error('Unknown listener'); 14 | } 15 | } 16 | 17 | public readonly forward = (value: T) => { 18 | if (this.listeners.length > 0) { 19 | this.listeners.forEach(listener => listener(value)); 20 | } 21 | }; 22 | 23 | public count(): number { 24 | return this.listeners.length; 25 | } 26 | } 27 | 28 | export type SimpleEventListener = (value: T) => void; 29 | -------------------------------------------------------------------------------- /model/src/external-types.ts: -------------------------------------------------------------------------------- 1 | export type UidGenerator = () => string; 2 | -------------------------------------------------------------------------------- /model/src/i18n.spec.ts: -------------------------------------------------------------------------------- 1 | import { defaultI18n } from './i18n'; 2 | 3 | describe('defaultI18n', () => { 4 | it('returns expected value', () => { 5 | expect(defaultI18n('key', 'test')).toBe('test'); 6 | expect( 7 | defaultI18n('key', 'We need :min users', { 8 | min: '10' 9 | }) 10 | ).toBe('We need 10 users'); 11 | expect( 12 | defaultI18n('key', 'Your name :name should have :n characters', { 13 | name: 'Alice', 14 | n: '4' 15 | }) 16 | ).toBe('Your name Alice should have 4 characters'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /model/src/i18n.ts: -------------------------------------------------------------------------------- 1 | export type I18n = (key: string, defaultValue: string, replacements?: Record) => string; 2 | 3 | export const defaultI18n: I18n = (_, defaultValue, replacements) => { 4 | if (replacements) { 5 | let result = defaultValue; 6 | Object.keys(replacements).forEach(key => { 7 | result = result.replace(':' + key, replacements[key]); 8 | }); 9 | return result; 10 | } 11 | return defaultValue; 12 | }; 13 | -------------------------------------------------------------------------------- /model/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './activator'; 2 | export * from './builders'; 3 | export * from './context'; 4 | export * from './core'; 5 | export * from './validator'; 6 | export * from './value-models'; 7 | export * from './external-types'; 8 | export * from './i18n'; 9 | export * from './model'; 10 | export * from './types'; 11 | -------------------------------------------------------------------------------- /model/src/test-tools/definition-model-stub.ts: -------------------------------------------------------------------------------- 1 | import { Path } from '../core'; 2 | import { DefinitionModel } from '../model'; 3 | 4 | export function createDefinitionModelStub(): DefinitionModel { 5 | const path = Path.create(['sequence']); 6 | return { 7 | root: { 8 | properties: [], 9 | sequence: { 10 | path, 11 | dependencies: [], 12 | label: 'Stub sequence', 13 | value: { 14 | id: 'stub', 15 | label: 'Stub', 16 | path, 17 | configuration: {}, 18 | getDefaultValue: () => [], 19 | getVariableDefinitions: () => null, 20 | validate: () => null 21 | } 22 | } 23 | }, 24 | steps: {}, 25 | valueTypes: [] 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /model/src/test-tools/model-activator-stub.ts: -------------------------------------------------------------------------------- 1 | import { ModelActivator } from '../activator'; 2 | import { createDefinitionModelStub } from './definition-model-stub'; 3 | 4 | export function createModelActivatorStub(): ModelActivator { 5 | let index = 0; 6 | return ModelActivator.create(createDefinitionModelStub(), () => `0x${index++}`); 7 | } 8 | -------------------------------------------------------------------------------- /model/src/test-tools/value-context-stub.ts: -------------------------------------------------------------------------------- 1 | import { ValueContext } from '../context'; 2 | import { defaultI18n } from '../i18n'; 3 | import { ValueModel } from '../model'; 4 | 5 | export function createValueContextStub( 6 | value: unknown, 7 | configuration: TValueModel['configuration'] 8 | ): ValueContext { 9 | return { 10 | getValue: () => value, 11 | model: { 12 | configuration 13 | }, 14 | i18n: defaultI18n 15 | } as unknown as ValueContext; 16 | } 17 | -------------------------------------------------------------------------------- /model/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface VariableDefinitions { 2 | variables: VariableDefinition[]; 3 | } 4 | 5 | export interface VariableDefinition { 6 | name: string; 7 | type: ValueType; 8 | } 9 | 10 | export type NullableVariableDefinition = VariableDefinition | null; 11 | 12 | export interface Variable { 13 | name: string; 14 | } 15 | 16 | export type NullableVariable = Variable | null; 17 | 18 | export interface AnyVariable { 19 | name: string; 20 | type: ValueType; 21 | } 22 | 23 | export type NullableAnyVariable = AnyVariable | null; 24 | 25 | export interface AnyVariables { 26 | variables: AnyVariable[]; 27 | } 28 | 29 | export enum WellKnownValueType { 30 | string = 'string', 31 | number = 'number', 32 | boolean = 'boolean' 33 | } 34 | 35 | export type ValueType = WellKnownValueType | string; 36 | 37 | export interface Dynamic { 38 | modelId: ValueModelId; 39 | value: TValue; 40 | } 41 | 42 | export type ValueModelId = string; 43 | 44 | export interface StringDictionary { 45 | items: StringDictionaryItem[]; 46 | } 47 | export interface StringDictionaryItem { 48 | key: string; 49 | value: string; 50 | } 51 | -------------------------------------------------------------------------------- /model/src/validator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition-validator'; 2 | export * from './property-validator-context'; 3 | export * from './step-validator-context'; 4 | export * from './variable-name-validator'; 5 | -------------------------------------------------------------------------------- /model/src/validator/property-validator-context.ts: -------------------------------------------------------------------------------- 1 | import { Properties, PropertyValue } from 'sequential-workflow-model'; 2 | import { ValueModel } from '../model'; 3 | import { ValueContext } from '../context'; 4 | 5 | export class PropertyValidatorContext { 6 | public static create( 7 | valueContext: ValueContext 8 | ): PropertyValidatorContext { 9 | return new PropertyValidatorContext(valueContext); 10 | } 11 | 12 | protected constructor(private readonly valueContext: ValueContext) {} 13 | 14 | public readonly getPropertyValue = this.valueContext.getPropertyValue; 15 | public readonly formatPropertyValue = this.valueContext.formatPropertyValue; 16 | public readonly getSupportedValueTypes = this.valueContext.getValueTypes; 17 | public readonly hasVariable = this.valueContext.hasVariable; 18 | public readonly findFirstUndefinedVariable = this.valueContext.findFirstUndefinedVariable; 19 | public readonly isVariableDuplicated = this.valueContext.isVariableDuplicated; 20 | public readonly tryGetVariableType = this.valueContext.tryGetVariableType; 21 | public readonly getVariables = this.valueContext.getVariables; 22 | 23 | public getValue(): TValue { 24 | return this.valueContext.getValue() as TValue; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /model/src/validator/step-validator-context.ts: -------------------------------------------------------------------------------- 1 | import { DefinitionContext } from '../context'; 2 | import { ParentsProvider } from '../context/variables-provider'; 3 | 4 | export class StepValidatorContext { 5 | public static create(definitionContext: DefinitionContext): StepValidatorContext { 6 | return new StepValidatorContext(definitionContext.parentsProvider); 7 | } 8 | 9 | private constructor(private readonly parentsProvider: ParentsProvider) {} 10 | 11 | /** 12 | * @returns The parent step types. 13 | * @example `['loop', 'if']` 14 | */ 15 | public readonly getParentStepTypes = this.parentsProvider.getParentStepTypes; 16 | } 17 | -------------------------------------------------------------------------------- /model/src/validator/variable-name-validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { defaultI18n } from '../i18n'; 2 | import { variableNameValidator } from './variable-name-validator'; 3 | 4 | describe('VariableNameValidator', () => { 5 | const i18n = defaultI18n; 6 | 7 | it('creates correctly', () => { 8 | expect(variableNameValidator(i18n, 'a')).toBeNull(); 9 | expect(variableNameValidator(i18n, 'ab')).toBeNull(); 10 | expect(variableNameValidator(i18n, 'a-b-c')).toBeNull(); 11 | expect(variableNameValidator(i18n, 'FooBar')).toBeNull(); 12 | expect(variableNameValidator(i18n, 'foo_bar')).toBeNull(); 13 | expect(variableNameValidator(i18n, 'item1')).toBeNull(); 14 | expect(variableNameValidator(i18n, 'item_1')).toBeNull(); 15 | expect(variableNameValidator(i18n, 'Item_1')).toBeNull(); 16 | expect(variableNameValidator(i18n, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_b')).toBeNull(); 17 | 18 | expect(variableNameValidator(i18n, '1')).toContain('invalid characters'); 19 | expect(variableNameValidator(i18n, 'fooBar&')).toContain('invalid characters'); 20 | expect(variableNameValidator(i18n, '1_')).toContain('invalid characters'); 21 | expect(variableNameValidator(i18n, '')).toContain('is required'); 22 | expect(variableNameValidator(i18n, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_bc')).toContain('32 characters or less'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /model/src/validator/variable-name-validator.ts: -------------------------------------------------------------------------------- 1 | import { I18n } from '../i18n'; 2 | 3 | const MAX_LENGTH = 32; 4 | 5 | export function variableNameValidator(i18n: I18n, name: string): string | null { 6 | if (!name) { 7 | return i18n('variableName.required', 'Variable name is required'); 8 | } 9 | if (name.length > MAX_LENGTH) { 10 | return i18n('variableName.maxLength', 'Variable name must be :n characters or less', { 11 | n: String(MAX_LENGTH) 12 | }); 13 | } 14 | if (!/^[A-Za-z][a-zA-Z_0-9-]*$/.test(name)) { 15 | return i18n('variableName.invalidCharacters', 'Variable name contains invalid characters'); 16 | } 17 | return null; 18 | } 19 | -------------------------------------------------------------------------------- /model/src/value-models/any-variables/any-variables-value-model.ts: -------------------------------------------------------------------------------- 1 | import { ValueModel, ValueModelFactoryFromModel, ValidationResult } from '../../model'; 2 | import { Path } from '../../core/path'; 3 | import { AnyVariables, ValueType } from '../../types'; 4 | import { ValueContext } from '../../context'; 5 | 6 | export interface AnyVariablesValueModelConfiguration { 7 | label?: string; 8 | valueTypes?: ValueType[]; 9 | editorId?: string; 10 | } 11 | 12 | export type AnyVariablesValueModel = ValueModel; 13 | 14 | export const anyVariablesValueModelId = 'anyVariables'; 15 | 16 | export const createAnyVariablesValueModel = ( 17 | configuration: AnyVariablesValueModelConfiguration 18 | ): ValueModelFactoryFromModel => ({ 19 | create: (path: Path) => ({ 20 | id: anyVariablesValueModelId, 21 | label: configuration.label ?? 'Variables', 22 | editorId: configuration.editorId, 23 | path, 24 | configuration, 25 | getDefaultValue() { 26 | return { 27 | variables: [] 28 | }; 29 | }, 30 | getVariableDefinitions: () => null, 31 | validate(context: ValueContext): ValidationResult { 32 | const errors: Record = {}; 33 | const value = context.getValue(); 34 | 35 | value.variables.forEach((variable, index) => { 36 | if (!context.hasVariable(variable.name, variable.type)) { 37 | errors[index] = context.i18n('anyVariables.variableIsLost', 'Variable :name is lost', { 38 | name: variable.name 39 | }); 40 | return; 41 | } 42 | if (configuration.valueTypes && !configuration.valueTypes.includes(variable.type)) { 43 | errors[index] = context.i18n('anyVariables.invalidVariableType', 'Variable :name has invalid type', { 44 | name: variable.name 45 | }); 46 | return; 47 | } 48 | }); 49 | 50 | return Object.keys(errors).length > 0 ? errors : null; 51 | } 52 | }) 53 | }); 54 | -------------------------------------------------------------------------------- /model/src/value-models/any-variables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './any-variables-value-model'; 2 | -------------------------------------------------------------------------------- /model/src/value-models/boolean/boolean-value-model-configuration.ts: -------------------------------------------------------------------------------- 1 | export interface BooleanValueModelConfiguration { 2 | label?: string; 3 | defaultValue?: boolean; 4 | editorId?: string; 5 | } 6 | -------------------------------------------------------------------------------- /model/src/value-models/boolean/boolean-value-model-validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { createValueContextStub } from '../../test-tools/value-context-stub'; 2 | import { BooleanValueModel } from './boolean-value-model'; 3 | import { booleanValueModelValidator } from './boolean-value-model-validator'; 4 | 5 | describe('booleanValueModelValidator', () => { 6 | it('returns null if value is boolean', () => { 7 | const context = createValueContextStub(true, {}); 8 | const error = booleanValueModelValidator(context); 9 | expect(error).toBeNull(); 10 | }); 11 | 12 | it('returns "The value must be a boolean" if value is not a boolean', () => { 13 | const context = createValueContextStub('this is not a boolean', {}); 14 | const error = booleanValueModelValidator(context); 15 | expect(error?.$).toBe('The value must be a boolean'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /model/src/value-models/boolean/boolean-value-model-validator.ts: -------------------------------------------------------------------------------- 1 | import { ValueContext } from '../../context'; 2 | import { ValidationResult, createValidationSingleError } from '../../model'; 3 | import { BooleanValueModel } from './boolean-value-model'; 4 | 5 | export function booleanValueModelValidator(context: ValueContext): ValidationResult { 6 | const value = context.getValue(); 7 | if (typeof value !== 'boolean') { 8 | return createValidationSingleError(context.i18n('boolean.invalidType', 'The value must be a boolean')); 9 | } 10 | return null; 11 | } 12 | -------------------------------------------------------------------------------- /model/src/value-models/boolean/boolean-value-model.spec.ts: -------------------------------------------------------------------------------- 1 | import { DefaultValueContext } from '../../context/default-value-context'; 2 | import { PropertyContext } from '../../context/property-context'; 3 | import { Path } from '../../core'; 4 | import { createDefinitionModelStub } from '../../test-tools/definition-model-stub'; 5 | import { createModelActivatorStub } from '../../test-tools/model-activator-stub'; 6 | import { createBooleanValueModel } from './boolean-value-model'; 7 | import { BooleanValueModelConfiguration } from './boolean-value-model-configuration'; 8 | 9 | describe('booleanValueModel', () => { 10 | const definitionModel = createDefinitionModelStub(); 11 | const modelActivator = createModelActivatorStub(); 12 | const propertyContext = PropertyContext.create({}, definitionModel.root.sequence, definitionModel); 13 | const context = DefaultValueContext.create(modelActivator, propertyContext); 14 | 15 | function getModel(configuration: BooleanValueModelConfiguration) { 16 | return createBooleanValueModel(configuration).create(Path.create('test')); 17 | } 18 | 19 | describe('getDefaultValue()', () => { 20 | it('returns false as default', () => { 21 | const value = getModel({}).getDefaultValue(context); 22 | 23 | expect(value).toBe(false); 24 | }); 25 | 26 | it('returns the configured default value', () => { 27 | const valueTrue = getModel({ defaultValue: true }).getDefaultValue(context); 28 | expect(valueTrue).toBe(true); 29 | 30 | const valueFalse = getModel({ defaultValue: false }).getDefaultValue(context); 31 | expect(valueFalse).toBe(false); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /model/src/value-models/boolean/boolean-value-model.ts: -------------------------------------------------------------------------------- 1 | import { ValueModel, ValueModelFactoryFromModel } from '../../model'; 2 | import { Path } from '../../core/path'; 3 | import { BooleanValueModelConfiguration } from './boolean-value-model-configuration'; 4 | import { booleanValueModelValidator } from './boolean-value-model-validator'; 5 | 6 | export type BooleanValueModel = ValueModel; 7 | 8 | export const booleanValueModelId = 'boolean'; 9 | 10 | export const createBooleanValueModel = (configuration: BooleanValueModelConfiguration): ValueModelFactoryFromModel => ({ 11 | create: (path: Path) => ({ 12 | id: booleanValueModelId, 13 | editorId: configuration.editorId, 14 | label: configuration.label ?? 'Boolean', 15 | path, 16 | configuration, 17 | getDefaultValue() { 18 | if (configuration.defaultValue !== undefined) { 19 | return configuration.defaultValue; 20 | } 21 | return false; 22 | }, 23 | getVariableDefinitions: () => null, 24 | validate: booleanValueModelValidator 25 | }) 26 | }); 27 | -------------------------------------------------------------------------------- /model/src/value-models/boolean/index.ts: -------------------------------------------------------------------------------- 1 | export * from './boolean-value-model-configuration'; 2 | export * from './boolean-value-model'; 3 | -------------------------------------------------------------------------------- /model/src/value-models/branches/branches-value-model-configuration.ts: -------------------------------------------------------------------------------- 1 | export interface BranchesValueModelConfiguration { 2 | /** 3 | * @description Branches of the branched step. Each branch is a list of step types. 4 | */ 5 | branches: Record; 6 | 7 | /** 8 | * @description If true, the branches are dynamic and can be changed by the user. 9 | */ 10 | dynamic?: boolean; 11 | 12 | /** 13 | * @description Override the default editor for the branches. 14 | */ 15 | editorId?: string; 16 | } 17 | -------------------------------------------------------------------------------- /model/src/value-models/branches/branches-value-model-validator.ts: -------------------------------------------------------------------------------- 1 | import { Branches } from 'sequential-workflow-model'; 2 | import { BranchesValueModel } from './branches-value-model'; 3 | import { ValueContext } from '../../context'; 4 | import { ValidationResult, createValidationSingleError } from '../../model'; 5 | 6 | export function branchesValueModelValidator( 7 | context: ValueContext> 8 | ): ValidationResult { 9 | const configuration = context.model.configuration; 10 | const branches = context.getValue(); 11 | 12 | if (typeof branches !== 'object') { 13 | return createValidationSingleError(context.i18n('branches.mustBeObject', 'The value must be object')); 14 | } 15 | const branchNames = Object.keys(branches); 16 | if (branchNames.length === 0) { 17 | return createValidationSingleError(context.i18n('branches.empty', 'No branches defined')); 18 | } 19 | if (!configuration.dynamic) { 20 | const configurationBranchNames = Object.keys(configuration.branches); 21 | if (branchNames.length !== configurationBranchNames.length) { 22 | return createValidationSingleError(context.i18n('branches.invalidLength', 'Invalid number of branches')); 23 | } 24 | const missingBranchName = configurationBranchNames.find(branchName => !branchNames.includes(branchName)); 25 | if (missingBranchName) { 26 | return createValidationSingleError( 27 | context.i18n('branches.missingBranch', 'Missing branch: :name', { name: missingBranchName }) 28 | ); 29 | } 30 | } 31 | return null; 32 | } 33 | -------------------------------------------------------------------------------- /model/src/value-models/branches/branches-value-model.ts: -------------------------------------------------------------------------------- 1 | import { Branches, Sequence } from 'sequential-workflow-model'; 2 | import { ValueModel, ValueModelFactoryFromModel } from '../../model'; 3 | import { Path } from '../../core/path'; 4 | import { DefaultValueContext } from '../../context/default-value-context'; 5 | import { BranchesValueModelConfiguration } from './branches-value-model-configuration'; 6 | import { branchesValueModelValidator } from './branches-value-model-validator'; 7 | 8 | export type BranchesValueModel = ValueModel; 9 | 10 | type BranchesOf = Record; 11 | 12 | export const branchesValueModelId = 'branches'; 13 | 14 | export const createBranchesValueModel = ( 15 | configuration: TConfiguration 16 | ): ValueModelFactoryFromModel>> => ({ 17 | create: (path: Path) => ({ 18 | id: branchesValueModelId, 19 | editorId: configuration.editorId, 20 | label: 'Branches', 21 | path, 22 | configuration, 23 | getDefaultValue(context: DefaultValueContext): BranchesOf { 24 | const branches = Object.keys(configuration.branches).reduce((result, branchName) => { 25 | result[branchName] = configuration.branches[branchName].map(type => context.activateStep(type)); 26 | return result; 27 | }, {}); 28 | return branches as BranchesOf; 29 | }, 30 | getVariableDefinitions: () => null, 31 | validate: branchesValueModelValidator 32 | }) 33 | }); 34 | -------------------------------------------------------------------------------- /model/src/value-models/branches/index.ts: -------------------------------------------------------------------------------- 1 | export * from './branches-value-model-configuration'; 2 | export * from './branches-value-model'; 3 | -------------------------------------------------------------------------------- /model/src/value-models/choice/choice-value-model-configuration.ts: -------------------------------------------------------------------------------- 1 | export interface ChoiceValueModelConfiguration { 2 | /** 3 | * Label. If not provided, the label is generated from the property name. 4 | */ 5 | label?: string; 6 | /** 7 | * Supported choices. 8 | */ 9 | choices: TValue[]; 10 | /** 11 | * Default value. 12 | */ 13 | defaultValue?: TValue; 14 | /** 15 | * Custom editor ID. 16 | */ 17 | editorId?: string; 18 | } 19 | -------------------------------------------------------------------------------- /model/src/value-models/choice/choice-value-model-validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { createValueContextStub } from '../../test-tools/value-context-stub'; 2 | import { ChoiceValueModel } from './choice-value-model'; 3 | import { ChoiceValueModelConfiguration } from './choice-value-model-configuration'; 4 | import { choiceValueModelValidator } from './choice-value-model-validator'; 5 | 6 | describe('choiceValueModelValidator', () => { 7 | it('returns correct response', () => { 8 | const configuration: ChoiceValueModelConfiguration = { 9 | choices: ['x', 'y'] 10 | }; 11 | 12 | const context1 = createValueContextStub('z', configuration); 13 | const error1 = choiceValueModelValidator(context1); 14 | expect(error1?.$).toBe('Value is not supported'); 15 | 16 | const context2 = createValueContextStub('x', configuration); 17 | const error2 = choiceValueModelValidator(context2); 18 | expect(error2).toBe(null); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /model/src/value-models/choice/choice-value-model-validator.ts: -------------------------------------------------------------------------------- 1 | import { ValueContext } from '../../context'; 2 | import { createValidationSingleError, ValidationResult } from '../../model'; 3 | import { ChoiceValueModel } from './choice-value-model'; 4 | 5 | export function choiceValueModelValidator(context: ValueContext): ValidationResult { 6 | const value = context.getValue(); 7 | const configuration = context.model.configuration; 8 | if (!configuration.choices.includes(value)) { 9 | return createValidationSingleError(context.i18n('choice.notSupportedValue', 'Value is not supported')); 10 | } 11 | return null; 12 | } 13 | -------------------------------------------------------------------------------- /model/src/value-models/choice/choice-value-model.ts: -------------------------------------------------------------------------------- 1 | import { ValueModel, ValueModelFactory } from '../../model'; 2 | import { Path } from '../../core/path'; 3 | import { ChoiceValueModelConfiguration } from './choice-value-model-configuration'; 4 | import { choiceValueModelValidator } from './choice-value-model-validator'; 5 | 6 | export type ChoiceValueModel = ValueModel>; 7 | 8 | export const choiceValueModelId = 'choice'; 9 | 10 | export function createChoiceValueModel( 11 | configuration: ChoiceValueModelConfiguration 12 | ): ValueModelFactory> { 13 | if (configuration.choices.length < 1) { 14 | throw new Error('At least one choice must be provided.'); 15 | } 16 | 17 | return { 18 | create: (path: Path) => ({ 19 | id: choiceValueModelId, 20 | label: configuration.label ?? 'Choice', 21 | editorId: configuration.editorId, 22 | path, 23 | configuration, 24 | getDefaultValue() { 25 | if (configuration.defaultValue) { 26 | if (!configuration.choices.includes(configuration.defaultValue)) { 27 | throw new Error(`Default value "${configuration.defaultValue}" does not match any of the choices`); 28 | } 29 | return configuration.defaultValue; 30 | } 31 | return configuration.choices[0]; 32 | }, 33 | getVariableDefinitions: () => null, 34 | validate: choiceValueModelValidator 35 | }) 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /model/src/value-models/choice/index.ts: -------------------------------------------------------------------------------- 1 | export * from './choice-value-model-configuration'; 2 | export * from './choice-value-model'; 3 | -------------------------------------------------------------------------------- /model/src/value-models/dynamic/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dynamic-value-model'; 2 | -------------------------------------------------------------------------------- /model/src/value-models/generated-string/generated-string-context.ts: -------------------------------------------------------------------------------- 1 | import { Properties } from 'sequential-workflow-model'; 2 | import { ValueContext } from '../../context'; 3 | import { GeneratedStringVariableValueModel } from './generated-string-value-model'; 4 | import { DefaultValueContext } from '../../context/default-value-context'; 5 | 6 | export class GeneratedStringContext { 7 | public static create( 8 | context: ValueContext, TProps> | DefaultValueContext 9 | ) { 10 | return new GeneratedStringContext(context); 11 | } 12 | 13 | private constructor( 14 | private readonly context: 15 | | ValueContext, TProperties> 16 | | DefaultValueContext 17 | ) {} 18 | 19 | public readonly getPropertyValue = this.context.getPropertyValue; 20 | public readonly formatPropertyValue = this.context.formatPropertyValue; 21 | } 22 | -------------------------------------------------------------------------------- /model/src/value-models/generated-string/generated-string-value-model.ts: -------------------------------------------------------------------------------- 1 | import { Properties } from 'sequential-workflow-model'; 2 | import { ValueContext } from '../../context'; 3 | import { Path } from '../../core'; 4 | import { ValidationResult, ValueModel, ValueModelFactory, createValidationSingleError } from '../../model'; 5 | import { GeneratedStringContext } from './generated-string-context'; 6 | import { DefaultValueContext } from '../../context/default-value-context'; 7 | 8 | export interface GeneratedStringValueModelConfiguration { 9 | label?: string; 10 | generator(context: GeneratedStringContext): string; 11 | editorId?: string; 12 | } 13 | 14 | export type GeneratedStringVariableValueModel = ValueModel< 15 | string, 16 | GeneratedStringValueModelConfiguration 17 | >; 18 | 19 | export const generatedStringValueModelId = 'generatedString'; 20 | 21 | export function createGeneratedStringValueModel( 22 | configuration: GeneratedStringValueModelConfiguration 23 | ): ValueModelFactory, TProperties> { 24 | return { 25 | create: (path: Path) => ({ 26 | id: generatedStringValueModelId, 27 | label: configuration.label ?? 'Generated string', 28 | editorId: configuration.editorId, 29 | path, 30 | configuration, 31 | getDefaultValue(context: DefaultValueContext) { 32 | const subContext = GeneratedStringContext.create(context); 33 | return configuration.generator(subContext); 34 | }, 35 | getVariableDefinitions: () => null, 36 | validate(context: ValueContext, TProperties>): ValidationResult { 37 | const subContext = GeneratedStringContext.create(context); 38 | const value = configuration.generator(subContext); 39 | if (context.getValue() !== value) { 40 | return createValidationSingleError( 41 | context.i18n('generatedString.differentValue', 'Generator returns different value than the current value') 42 | ); 43 | } 44 | return null; 45 | } 46 | }) 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /model/src/value-models/generated-string/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generated-string-context'; 2 | export * from './generated-string-value-model'; 3 | -------------------------------------------------------------------------------- /model/src/value-models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './any-variables'; 2 | export * from './boolean'; 3 | export * from './branches'; 4 | export * from './choice'; 5 | export * from './dynamic'; 6 | export * from './generated-string'; 7 | export * from './nullable-any-variable'; 8 | export * from './nullable-variable'; 9 | export * from './nullable-variable-definition'; 10 | export * from './number'; 11 | export * from './sequence'; 12 | export * from './string'; 13 | export * from './string-dictionary'; 14 | export * from './variable-definitions'; 15 | -------------------------------------------------------------------------------- /model/src/value-models/nullable-any-variable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nullable-any-variable-value-model'; 2 | -------------------------------------------------------------------------------- /model/src/value-models/nullable-any-variable/nullable-any-variable-value-model.ts: -------------------------------------------------------------------------------- 1 | import { ValueModel, ValueModelFactoryFromModel, ValidationResult, createValidationSingleError } from '../../model'; 2 | import { Path } from '../../core/path'; 3 | import { NullableAnyVariable, ValueType } from '../../types'; 4 | import { ValueContext } from '../../context'; 5 | 6 | export interface NullableAnyVariableValueModelConfiguration { 7 | label?: string; 8 | isRequired?: boolean; 9 | valueTypes?: ValueType[]; 10 | editorId?: string; 11 | } 12 | 13 | export type NullableAnyVariableValueModel = ValueModel; 14 | 15 | export const nullableAnyVariableValueModelId = 'nullableAnyVariable'; 16 | 17 | export const createNullableAnyVariableValueModel = ( 18 | configuration: NullableAnyVariableValueModelConfiguration 19 | ): ValueModelFactoryFromModel => ({ 20 | create: (path: Path) => ({ 21 | id: nullableAnyVariableValueModelId, 22 | label: configuration.label ?? 'Variable', 23 | editorId: configuration.editorId, 24 | path, 25 | configuration, 26 | getDefaultValue() { 27 | return null; 28 | }, 29 | getVariableDefinitions: () => null, 30 | validate(context: ValueContext): ValidationResult { 31 | const value = context.getValue(); 32 | if (configuration.isRequired && !value) { 33 | return createValidationSingleError(context.i18n('nullableAnyVariable.variableIsRequired', 'The variable is required')); 34 | } 35 | if (value) { 36 | if (!context.hasVariable(value.name, value.type)) { 37 | return createValidationSingleError( 38 | context.i18n('nullableAnyVariable.variableIsLost', 'The variable :name is lost', { name: value.name }) 39 | ); 40 | } 41 | if (configuration.valueTypes && !configuration.valueTypes.includes(value.type)) { 42 | return createValidationSingleError( 43 | context.i18n('nullableAnyVariable.invalidVariableType', 'The variable :name has invalid type', { name: value.name }) 44 | ); 45 | } 46 | } 47 | return null; 48 | } 49 | }) 50 | }); 51 | -------------------------------------------------------------------------------- /model/src/value-models/nullable-variable-definition/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nullable-variable-definition-value-model'; 2 | -------------------------------------------------------------------------------- /model/src/value-models/nullable-variable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nullable-variable-value-model'; 2 | -------------------------------------------------------------------------------- /model/src/value-models/nullable-variable/nullable-variable-value-model.ts: -------------------------------------------------------------------------------- 1 | import { ValueModel, ValueModelFactoryFromModel, ValidationResult, createValidationSingleError } from '../../model'; 2 | import { Path } from '../../core/path'; 3 | import { NullableVariable } from '../../types'; 4 | import { ValueType } from '../../types'; 5 | import { ValueContext } from '../../context'; 6 | 7 | export interface NullableVariableValueModelConfiguration { 8 | label?: string; 9 | valueType: ValueType; 10 | isRequired?: boolean; 11 | editorId?: string; 12 | } 13 | 14 | export type NullableVariableValueModel = ValueModel; 15 | 16 | export const nullableVariableValueModelId = 'nullableVariable'; 17 | 18 | export const createNullableVariableValueModel = ( 19 | configuration: NullableVariableValueModelConfiguration 20 | ): ValueModelFactoryFromModel => ({ 21 | create: (path: Path) => ({ 22 | id: nullableVariableValueModelId, 23 | label: configuration.label ?? 'Variable', 24 | editorId: configuration.editorId, 25 | path, 26 | configuration, 27 | getDefaultValue(): NullableVariable { 28 | return null; 29 | }, 30 | getVariableDefinitions: () => null, 31 | validate(context: ValueContext): ValidationResult { 32 | const value = context.getValue(); 33 | if (configuration.isRequired && !value) { 34 | return createValidationSingleError(context.i18n('nullableVariable.variableIsRequired', 'The variable is required')); 35 | } 36 | if (value && value.name) { 37 | if (!context.hasVariable(value.name, configuration.valueType)) { 38 | return createValidationSingleError( 39 | context.i18n('nullableVariable.variableIsLost', 'The variable :name is not found', { 40 | name: value.name 41 | }) 42 | ); 43 | } 44 | } 45 | return null; 46 | } 47 | }) 48 | }); 49 | -------------------------------------------------------------------------------- /model/src/value-models/number/index.ts: -------------------------------------------------------------------------------- 1 | export * from './number-value-model-configuration'; 2 | export * from './number-value-model'; 3 | -------------------------------------------------------------------------------- /model/src/value-models/number/number-value-model-configuration.ts: -------------------------------------------------------------------------------- 1 | export interface NumberValueModelConfiguration { 2 | label?: string; 3 | defaultValue?: number; 4 | min?: number; 5 | max?: number; 6 | editorId?: string; 7 | } 8 | -------------------------------------------------------------------------------- /model/src/value-models/number/number-value-model-validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { createValueContextStub } from '../../test-tools/value-context-stub'; 2 | import { NumberValueModel } from './number-value-model'; 3 | import { NumberValueModelConfiguration } from './number-value-model-configuration'; 4 | import { numberValueModelValidator } from './number-value-model-validator'; 5 | 6 | describe('numberValueModelValidator', () => { 7 | it('returns error when value is not a number', () => { 8 | const context1 = createValueContextStub(NaN, {}); 9 | const error1 = numberValueModelValidator(context1); 10 | expect(error1?.$).toBe('The value must be a number'); 11 | 12 | const context2 = createValueContextStub('10', {}); 13 | const error2 = numberValueModelValidator(context2); 14 | expect(error2?.$).toBe('The value must be a number'); 15 | }); 16 | 17 | it('returns error when value is too small', () => { 18 | const configuration: NumberValueModelConfiguration = { 19 | min: 10 20 | }; 21 | 22 | const context = createValueContextStub(5, configuration); 23 | const error = numberValueModelValidator(context); 24 | expect(error?.$).toBe('The value must be at least 10'); 25 | }); 26 | 27 | it('returns error when value is too big', () => { 28 | const configuration: NumberValueModelConfiguration = { 29 | max: 10 30 | }; 31 | 32 | const context = createValueContextStub(15, configuration); 33 | const error = numberValueModelValidator(context); 34 | expect(error?.$).toBe('The value must be at most 10'); 35 | }); 36 | 37 | it('returns null when value is correct', () => { 38 | const configuration: NumberValueModelConfiguration = { 39 | min: 1, 40 | max: 10 41 | }; 42 | 43 | const context = createValueContextStub(5, configuration); 44 | const error = numberValueModelValidator(context); 45 | expect(error).toBe(null); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /model/src/value-models/number/number-value-model-validator.ts: -------------------------------------------------------------------------------- 1 | import { ValueContext } from '../../context'; 2 | import { ValidationResult, createValidationSingleError } from '../../model'; 3 | import { NumberValueModel } from './number-value-model'; 4 | 5 | export function numberValueModelValidator(context: ValueContext): ValidationResult { 6 | const value = context.getValue(); 7 | const configuration = context.model.configuration; 8 | 9 | if (isNaN(value) || typeof value !== 'number') { 10 | return createValidationSingleError(context.i18n('number.valueMustBeNumber', 'The value must be a number')); 11 | } 12 | if (configuration.min !== undefined && value < configuration.min) { 13 | return createValidationSingleError( 14 | context.i18n('number.valueTooLow', 'The value must be at least :min', { 15 | min: String(configuration.min) 16 | }) 17 | ); 18 | } 19 | if (configuration.max !== undefined && value > configuration.max) { 20 | return createValidationSingleError( 21 | context.i18n('number.valueTooHigh', 'The value must be at most :max', { 22 | max: String(configuration.max) 23 | }) 24 | ); 25 | } 26 | return null; 27 | } 28 | -------------------------------------------------------------------------------- /model/src/value-models/number/number-value-model.ts: -------------------------------------------------------------------------------- 1 | import { ValueModel, ValueModelFactoryFromModel } from '../../model'; 2 | import { Path } from '../../core/path'; 3 | import { NumberValueModelConfiguration } from './number-value-model-configuration'; 4 | import { numberValueModelValidator } from './number-value-model-validator'; 5 | 6 | export type NumberValueModel = ValueModel; 7 | 8 | export const numberValueModelId = 'number'; 9 | 10 | export const createNumberValueModel = (configuration: NumberValueModelConfiguration): ValueModelFactoryFromModel => ({ 11 | create: (path: Path) => ({ 12 | id: numberValueModelId, 13 | label: configuration.label ?? 'Number', 14 | editorId: configuration.editorId, 15 | path, 16 | configuration, 17 | getDefaultValue() { 18 | if (configuration.defaultValue !== undefined) { 19 | return configuration.defaultValue; 20 | } 21 | if (configuration.min !== undefined) { 22 | return configuration.min; 23 | } 24 | return 0; 25 | }, 26 | getVariableDefinitions: () => null, 27 | validate: numberValueModelValidator 28 | }) 29 | }); 30 | -------------------------------------------------------------------------------- /model/src/value-models/sequence/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sequence-value-model'; 2 | -------------------------------------------------------------------------------- /model/src/value-models/sequence/sequence-value-model.ts: -------------------------------------------------------------------------------- 1 | import { Sequence } from 'sequential-workflow-model'; 2 | import { ValueModel, ValueModelFactoryFromModel } from '../../model'; 3 | import { Path } from '../../core/path'; 4 | import { DefaultValueContext } from '../../context/default-value-context'; 5 | 6 | export interface SequenceValueModelConfiguration { 7 | sequence: string[]; 8 | } 9 | 10 | export type SequenceValueModel = ValueModel; 11 | 12 | export const sequenceValueModelId = 'sequence'; 13 | 14 | export const createSequenceValueModel = ( 15 | configuration: SequenceValueModelConfiguration 16 | ): ValueModelFactoryFromModel => ({ 17 | create: (path: Path) => ({ 18 | id: sequenceValueModelId, 19 | label: 'Sequence', 20 | path, 21 | configuration, 22 | getDefaultValue(context: DefaultValueContext): Sequence { 23 | return configuration.sequence.map(type => context.activateStep(type)); 24 | }, 25 | getVariableDefinitions: () => null, 26 | validate: () => null 27 | }) 28 | }); 29 | -------------------------------------------------------------------------------- /model/src/value-models/string-dictionary/index.ts: -------------------------------------------------------------------------------- 1 | export * from './string-dictionary-value-model'; 2 | export * from './string-dictionary-value-model-configuration'; 3 | -------------------------------------------------------------------------------- /model/src/value-models/string-dictionary/string-dictionary-value-model-configuration.ts: -------------------------------------------------------------------------------- 1 | export interface StringDictionaryValueModelConfiguration { 2 | label?: string; 3 | uniqueKeys?: boolean; 4 | valueMinLength?: number; 5 | editorId?: string; 6 | } 7 | -------------------------------------------------------------------------------- /model/src/value-models/string-dictionary/string-dictionary-value-model-validator.ts: -------------------------------------------------------------------------------- 1 | import { ValueContext } from '../../context'; 2 | import { ValidationError, ValidationResult } from '../../model'; 3 | import { StringDictionaryValueModel } from './string-dictionary-value-model'; 4 | 5 | export function stringDictionaryValueModelValidator(context: ValueContext): ValidationResult { 6 | const errors: ValidationError = {}; 7 | const value = context.getValue(); 8 | const configuration = context.model.configuration; 9 | const count = value.items.length; 10 | 11 | if (configuration.uniqueKeys) { 12 | for (let index = 0; index < count; index++) { 13 | const key = value.items[index].key; 14 | const duplicate = value.items.findIndex((item, i) => i !== index && item.key === key); 15 | if (duplicate >= 0) { 16 | errors[index] = context.i18n('stringDictionary.duplicatedKey', 'Key name is duplicated'); 17 | } 18 | } 19 | } 20 | 21 | for (let index = 0; index < count; index++) { 22 | const item = value.items[index]; 23 | if (!item.key) { 24 | errors[index] = context.i18n('stringDictionary.keyIsRequired', 'Key is required'); 25 | } 26 | if (configuration.valueMinLength !== undefined && item.value.length < configuration.valueMinLength) { 27 | errors[index] = context.i18n('stringDictionary.valueTooShort', 'Value must be at least :min characters long', { 28 | min: String(configuration.valueMinLength) 29 | }); 30 | } 31 | } 32 | 33 | return Object.keys(errors).length > 0 ? errors : null; 34 | } 35 | -------------------------------------------------------------------------------- /model/src/value-models/string-dictionary/string-dictionary-value-model.ts: -------------------------------------------------------------------------------- 1 | import { ValueModel, ValueModelFactoryFromModel } from '../../model'; 2 | import { Path } from '../../core/path'; 3 | import { StringDictionary } from '../../types'; 4 | import { StringDictionaryValueModelConfiguration } from './string-dictionary-value-model-configuration'; 5 | import { stringDictionaryValueModelValidator } from './string-dictionary-value-model-validator'; 6 | 7 | export type StringDictionaryValueModel = ValueModel; 8 | 9 | export const stringDictionaryValueModelId = 'stringDictionary'; 10 | 11 | export const createStringDictionaryValueModel = ( 12 | configuration: StringDictionaryValueModelConfiguration 13 | ): ValueModelFactoryFromModel => ({ 14 | create: (path: Path) => ({ 15 | id: stringDictionaryValueModelId, 16 | label: configuration.label ?? 'Dictionary', 17 | editorId: configuration.editorId, 18 | path, 19 | configuration, 20 | getDefaultValue() { 21 | return { 22 | items: [] 23 | }; 24 | }, 25 | getVariableDefinitions: () => null, 26 | validate: stringDictionaryValueModelValidator 27 | }) 28 | }); 29 | -------------------------------------------------------------------------------- /model/src/value-models/string/index.ts: -------------------------------------------------------------------------------- 1 | export * from './string-value-model'; 2 | export * from './string-value-model-configuration'; 3 | -------------------------------------------------------------------------------- /model/src/value-models/string/string-value-model-configuration.ts: -------------------------------------------------------------------------------- 1 | export interface StringValueModelConfiguration { 2 | label?: string; 3 | minLength?: number; 4 | defaultValue?: string; 5 | pattern?: RegExp; 6 | multiline?: boolean | number; 7 | editorId?: string; 8 | } 9 | -------------------------------------------------------------------------------- /model/src/value-models/string/string-value-model-validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { createValueContextStub } from '../../test-tools/value-context-stub'; 2 | import { StringValueModel } from './string-value-model'; 3 | import { StringValueModelConfiguration } from './string-value-model-configuration'; 4 | import { stringValueModelValidator } from './string-value-model-validator'; 5 | 6 | describe('stringValueModelValidator', () => { 7 | it('returns correct response when minLength is set', () => { 8 | const configuration: StringValueModelConfiguration = { 9 | minLength: 2 10 | }; 11 | 12 | const context1 = createValueContextStub('', configuration); 13 | const error1 = stringValueModelValidator(context1); 14 | expect(error1?.$).toBe('The value must be at least 2 characters long'); 15 | 16 | const context2 = createValueContextStub('fo', configuration); 17 | const error2 = stringValueModelValidator(context2); 18 | expect(error2).toBe(null); 19 | }); 20 | 21 | it('returns error when value is not string', () => { 22 | const context1 = createValueContextStub(0x123, {}); 23 | const error1 = stringValueModelValidator(context1); 24 | expect(error1?.$).toBe('The value must be a string'); 25 | }); 26 | 27 | it('returns correct response when pattern is set', () => { 28 | const configuration: StringValueModelConfiguration = { 29 | pattern: /^[a-z]$/ 30 | }; 31 | 32 | const context1 = createValueContextStub('1', configuration); 33 | const error1 = stringValueModelValidator(context1); 34 | expect(error1?.$).toBe('The value does not match the required pattern'); 35 | 36 | const context2 = createValueContextStub('a', configuration); 37 | const error2 = stringValueModelValidator(context2); 38 | expect(error2).toBe(null); 39 | }); 40 | 41 | it('returns success if configuration does not have restrictions', () => { 42 | for (const text of ['', 'a', 'ab']) { 43 | const context = createValueContextStub(text, {}); 44 | const error = stringValueModelValidator(context); 45 | expect(error).toBe(null); 46 | } 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /model/src/value-models/string/string-value-model-validator.ts: -------------------------------------------------------------------------------- 1 | import { ValueContext } from '../../context'; 2 | import { ValidationResult, createValidationSingleError } from '../../model'; 3 | import { StringValueModel } from './string-value-model'; 4 | 5 | export function stringValueModelValidator(context: ValueContext): ValidationResult { 6 | const value = context.getValue(); 7 | const configuration = context.model.configuration; 8 | 9 | if (typeof value !== 'string') { 10 | return createValidationSingleError(context.i18n('string.valueMustBeString', 'The value must be a string')); 11 | } 12 | if (configuration.minLength !== undefined && value.length < configuration.minLength) { 13 | return createValidationSingleError( 14 | context.i18n('string.valueTooShort', 'The value must be at least :min characters long', { 15 | min: String(configuration.minLength) 16 | }) 17 | ); 18 | } 19 | if (configuration.pattern && !configuration.pattern.test(value)) { 20 | return createValidationSingleError( 21 | context.i18n('string.valueDoesNotMatchPattern', 'The value does not match the required pattern') 22 | ); 23 | } 24 | return null; 25 | } 26 | -------------------------------------------------------------------------------- /model/src/value-models/string/string-value-model.ts: -------------------------------------------------------------------------------- 1 | import { ValueModel, ValueModelFactoryFromModel } from '../../model'; 2 | import { Path } from '../../core/path'; 3 | import { StringValueModelConfiguration } from './string-value-model-configuration'; 4 | import { stringValueModelValidator } from './string-value-model-validator'; 5 | 6 | export type StringValueModel = ValueModel; 7 | 8 | export const stringValueModelId = 'string'; 9 | 10 | export const createStringValueModel = (configuration: StringValueModelConfiguration): ValueModelFactoryFromModel => ({ 11 | create: (path: Path) => ({ 12 | id: stringValueModelId, 13 | label: configuration.label ?? 'String', 14 | editorId: configuration.editorId, 15 | path, 16 | configuration, 17 | getDefaultValue() { 18 | return configuration.defaultValue || ''; 19 | }, 20 | getVariableDefinitions: () => null, 21 | validate: stringValueModelValidator 22 | }) 23 | }); 24 | -------------------------------------------------------------------------------- /model/src/value-models/variable-definitions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './variable-definitions-value-model'; 2 | -------------------------------------------------------------------------------- /model/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build", 4 | "noImplicitAny": true, 5 | "target": "es6", 6 | "module": "es2015", 7 | "sourceMap": false, 8 | "strict": true, 9 | "allowJs": false, 10 | "declaration": true, 11 | "declarationDir": "./build", 12 | "moduleResolution": "node", 13 | "esModuleInterop": true, 14 | "lib": ["es2015"] 15 | }, 16 | "include": [ 17 | "./src/" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "yarn workspaces run build", 5 | "test:single": "yarn workspaces run test:single", 6 | "eslint": "yarn workspaces run eslint", 7 | "prettier": "yarn workspaces run prettier", 8 | "prettier:fix": "yarn workspaces run prettier:fix", 9 | "serve": "http-server -c-1 -p 8999 ./", 10 | "ci": "bash .github/workflows/packages.sh" 11 | }, 12 | "workspaces": [ 13 | "model", 14 | "editor", 15 | "demos/vanilla-js-app", 16 | "demos/webpack-app" 17 | ], 18 | "devDependencies": { 19 | "http-server": "^14.1.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/generate-i18n-keys.cjs: -------------------------------------------------------------------------------- 1 | // Usage: node generate-i18n-keys.cjs 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | function* walkDir(dirPath) { 7 | const files = fs.readdirSync(dirPath, { withFileTypes: true }); 8 | for (const file of files) { 9 | if (file.isDirectory()) { 10 | yield* walkDir(path.join(dirPath, file.name)); 11 | } else { 12 | yield path.join(dirPath, file.name); 13 | } 14 | } 15 | } 16 | 17 | function processDir(dirPath) { 18 | const dict = {}; 19 | for (const file of walkDir(path.join(__dirname, dirPath))) { 20 | if (file.endsWith('.ts')) { 21 | const content = fs.readFileSync(file, 'utf8'); 22 | const items = content.match(/i18n\s*\([^)]+/g); 23 | if (items) { 24 | items.forEach(item => { 25 | const values = item.match(/'([^']+)'/g); 26 | if (values?.length === 2) { 27 | dict[values[0].slice(1, -1)] = values[1].slice(1, -1); 28 | } 29 | }); 30 | } 31 | } 32 | } 33 | return dict; 34 | } 35 | 36 | function sortDict(dict) { 37 | const keys = Object.keys(dict); 38 | keys.sort((a, b) => a.localeCompare(b)); 39 | return keys.reduce((result, key) => { 40 | result[key] = dict[key]; 41 | return result; 42 | }, {}); 43 | 44 | } 45 | 46 | const sortedDict = sortDict({ 47 | ...processDir('../model/src'), 48 | ...processDir('../editor/src') 49 | }); 50 | 51 | let output = '# I18N Keys\n\n'; 52 | output += 'This document lists all the I18N keys used in the Sequential Workflow Editor.\n\n'; 53 | output += '```json\n' + JSON.stringify(sortedDict, null, 2) + '\n```\n'; 54 | 55 | fs.writeFileSync(path.join(__dirname, '../docs/I18N-KEYS.md'), output); 56 | 57 | console.log(sortedDict); 58 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "${BASH_SOURCE[0]}")" 4 | 5 | cd ../model 6 | yarn build 7 | npm publish 8 | 9 | cd ../editor 10 | yarn build 11 | npm publish 12 | -------------------------------------------------------------------------------- /scripts/set-version.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const version = process.argv[2]; 5 | if (!version || !(/^\d+\.\d+\.\d+$/.test(version))) { 6 | console.log('Usage: node set-version.js 1.2.3'); 7 | return; 8 | } 9 | 10 | const dependencies = [ 11 | 'sequential-workflow-editor-model', 12 | 'sequential-workflow-editor' 13 | ]; 14 | 15 | function updateDependencies(deps) { 16 | if (!deps) { 17 | return; 18 | } 19 | for (const name in deps) { 20 | if (dependencies.includes(name)) { 21 | deps[name] = `^${version}`; 22 | } 23 | } 24 | } 25 | 26 | function updatePackage(filePath, setVersion) { 27 | filePath = path.join(__dirname, '..', filePath); 28 | const json = JSON.parse(fs.readFileSync(filePath, 'utf-8')); 29 | 30 | if (setVersion) { 31 | json['version'] = version; 32 | } 33 | updateDependencies(json['dependencies']); 34 | updateDependencies(json['peerDependencies']); 35 | updateDependencies(json['devDependencies']); 36 | 37 | fs.writeFileSync(filePath, JSON.stringify(json, null, '\t'), 'utf-8'); 38 | } 39 | 40 | updatePackage('model/package.json', true); 41 | updatePackage('editor/package.json', true); 42 | updatePackage('demos/webpack-app/package.json', false); 43 | --------------------------------------------------------------------------------