├── .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 |
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 |
--------------------------------------------------------------------------------