Add files from webapp-workflow with client/web/workflow prefix
25
client/web/workflow/.eslintrc.js
Normal file
@@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
root: false,
|
||||
env: {
|
||||
es6: true,
|
||||
node: true,
|
||||
mocha: true,
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/recommended',
|
||||
'@vue/standard',
|
||||
],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'new-cap': 'off',
|
||||
'import/no-named-default': 'off',
|
||||
'vue/component-name-in-template-casing': ['error', 'kebab-case'],
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/order-in-components': ['error'],
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
},
|
||||
}
|
||||
20
client/web/workflow/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
*.log
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.iml
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
|
||||
.nyc_output
|
||||
coverage
|
||||
|
||||
/.act*
|
||||
/workflow
|
||||
37
client/web/workflow/Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
||||
# build-stage
|
||||
FROM node:12.14-alpine as build-stage
|
||||
|
||||
ENV PATH /app/node_modules/.bin:$PATH
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
COPY package.json ./
|
||||
COPY yarn.lock ./
|
||||
|
||||
RUN yarn install
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN yarn build
|
||||
|
||||
|
||||
# deploy stage
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY CONTRIBUTING.* DCO LICENSE README.* ./
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=30s --start-period=10s --timeout=30s \
|
||||
CMD wget --quiet --tries=1 --spider "http://127.0.0.1:80/config.js" || exit 1
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
42
client/web/workflow/Makefile
Normal file
@@ -0,0 +1,42 @@
|
||||
.PHONY: dep test build release upload
|
||||
|
||||
|
||||
YARN_FLAGS ?= --non-interactive --no-progress --silent --emoji false
|
||||
YARN = yarn $(YARN_FLAGS)
|
||||
|
||||
REPO_NAME ?= corteza-webapp-workflow
|
||||
|
||||
BUILD_FLAVOUR ?= corteza
|
||||
BUILD_FLAGS ?= --production
|
||||
BUILD_DEST_DIR = dist
|
||||
BUILD_TIME ?= $(shell date +%FT%T%z)
|
||||
BUILD_VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||
|
||||
BUILD_NAME = $(REPO_NAME)-$(BUILD_VERSION)
|
||||
|
||||
RELEASE_NAME = $(BUILD_NAME).tar.gz
|
||||
RELEASE_EXTRA_FILES ?= README.md LICENSE CONTRIBUTING.md DCO
|
||||
RELEASE_PKEY ?= .upload-rsa
|
||||
|
||||
dep:
|
||||
$(YARN) install
|
||||
|
||||
test:
|
||||
$(YARN) lint
|
||||
$(YARN) test:unit
|
||||
|
||||
build:
|
||||
export BUILD_VERSION=${BUILD_VERSION} && $(YARN) build $(BUILD_FLAGS)
|
||||
|
||||
release:
|
||||
@ echo $(RELEASE_NAME)
|
||||
@ cp $(RELEASE_EXTRA_FILES) $(BUILD_DEST_DIR)
|
||||
@ tar -C $(BUILD_DEST_DIR) -czf $(RELEASE_NAME) $(dir $(BUILD_DEST_DIR))
|
||||
|
||||
upload: $(RELEASE_PKEY)
|
||||
@ echo "put *.tar.gz" | sftp -q -o "StrictHostKeyChecking no" -i $(RELEASE_PKEY) $(RELEASE_SFTP_URI)
|
||||
@ rm -f $(RELEASE_PKEY)
|
||||
|
||||
$(RELEASE_PKEY):
|
||||
@ echo $(RELEASE_SFTP_KEY) | base64 -d > $(RELEASE_PKEY)
|
||||
@ chmod 0400 $@
|
||||
12
client/web/workflow/babel.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
],
|
||||
env: {
|
||||
test: {
|
||||
plugins: [
|
||||
['istanbul', { useInlineSourceMaps: false }],
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
5
client/web/workflow/crowdin.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
files:
|
||||
- source: /src/i18n/en
|
||||
ignore:
|
||||
- /src/i18n/en/index.js
|
||||
translation: /src/i18n/%locale%
|
||||
42
client/web/workflow/entrypoint.sh
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
if [ ! -z "${1:-}" ]; then
|
||||
exec "$@"
|
||||
else
|
||||
# check if config.js is present (via volume)
|
||||
# or if it's missing
|
||||
if [ ! -f "./config.js" ]; then
|
||||
# config.js missing, generate it
|
||||
if [ ! -z "${CONFIGJS:-}" ]; then
|
||||
# use $CONFIGJS variable that might be passed to the container:
|
||||
# --env CONFIGJS="$(cat public/config.example.js)"
|
||||
echo "${CONFIGJS}" > ./config.js
|
||||
else
|
||||
# Try to guess where the API is located by using DOMAIN or VIRTUAL_HOST and prefix it with "api."
|
||||
API_HOST=${API_HOST:-"api.${VIRTUAL_HOST:-"${DOMAIN:-"local.cortezaproject.org"}"}"}
|
||||
API_BASEURL=${API_FULL_URL:-"//${API_HOST}"}
|
||||
|
||||
echo "window.CortezaAPI = '${API_BASEURL}'" > ./config.js
|
||||
fi
|
||||
fi
|
||||
|
||||
BASE_PATH=${BASE_PATH:-"/"}
|
||||
if [ $BASE_PATH != "/" ]; then
|
||||
BASE_PATH_LENGTH=${#BASE_PATH}
|
||||
BASE_PATH_LAST_CHAR=${BASE_PATH:BASE_PATH_LENGTH-1:1}
|
||||
|
||||
if [ $BASE_PATH_LAST_CHAR != "/" ]; then
|
||||
BASE_PATH="$BASE_PATH/"
|
||||
fi
|
||||
fi
|
||||
|
||||
sed -i "s|<base href=/ >|<base href=\"$BASE_PATH\">|g" ./index.html
|
||||
sed -i "s|<base href=\"/\">|<base href=\"$BASE_PATH\">|g" ./index.html
|
||||
|
||||
sed -i "s|{{BASE_PATH}}|$BASE_PATH|g" /etc/nginx/nginx.conf
|
||||
|
||||
|
||||
nginx -g "daemon off;"
|
||||
fi
|
||||
46
client/web/workflow/nginx.conf
Normal file
@@ -0,0 +1,46 @@
|
||||
user nginx;
|
||||
worker_processes 1;
|
||||
|
||||
error_log /dev/stdout warn;
|
||||
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format json escape=json
|
||||
'{'
|
||||
'"@timestamp":"$time_iso8601",'
|
||||
'"remote_addr":"$remote_addr",'
|
||||
'"request":"$request",'
|
||||
'"status":$status,'
|
||||
'"body_bytes_sent":$body_bytes_sent,'
|
||||
'"request_time":$request_time,'
|
||||
'"http_referrer":"$http_referer",'
|
||||
'"http_user_agent":"$http_user_agent"'
|
||||
'}';
|
||||
|
||||
access_log /dev/stdout json;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 300;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
|
||||
index index.html;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location {{BASE_PATH}} {
|
||||
try_files $uri {{BASE_PATH}}index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
client/web/workflow/package.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"name": "corteza-workflow",
|
||||
"version": "2022.9.2",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"dev-env": "vue-cli-service serve src/dev-env.js",
|
||||
"build": "vue-cli-service build --mode production",
|
||||
"lint": "vue-cli-service lint && stylelint '**/*.{vue,scss}' --fix",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"test:unit:cc": "nyc vue-cli-service test:unit",
|
||||
"cdeps": "yarn upgrade @cortezaproject/corteza-js @cortezaproject/corteza-vue"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "yarn lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cortezaproject/corteza-js": "^2022.9.2",
|
||||
"@cortezaproject/corteza-vue": "^2022.9.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.21",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.10.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.10.1",
|
||||
"@fortawesome/vue-fontawesome": "^0.1.9",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"mxgraph": "^4.2.0",
|
||||
"portal-vue": "^2.1.7",
|
||||
"v-jsoneditor": "^1.4.2",
|
||||
"vue": "2.6.14",
|
||||
"vue-router": "3.1.5",
|
||||
"vue-select": "^3.11.2",
|
||||
"vue2-ace-editor": "^0.0.15",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.5.13",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"@vue/eslint-config-standard": "^5.1.0",
|
||||
"@vue/test-utils": "^1.0.0-beta.25",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-plugin-istanbul": "^5.2.0",
|
||||
"babel-preset-vue": "^2.0.2",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.0.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"flush-promises": "^1.0.2",
|
||||
"null-loader": "^4.0.1",
|
||||
"nyc": "^14.1.1",
|
||||
"resolve-url-loader": "^3.1.2",
|
||||
"sass": "^1.49.9",
|
||||
"sass-loader": "^10",
|
||||
"sinon": "^7.3.2",
|
||||
"stylelint": "^8.0.0",
|
||||
"stylelint-config-standard": "^18.3.0",
|
||||
"stylelint-scss": "^3.14.2",
|
||||
"stylelint-webpack-plugin": "^0.10.5",
|
||||
"vue-template-compiler": "2.6.14"
|
||||
},
|
||||
"stylelint": {
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
],
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"color-hex-case": null,
|
||||
"color-hex-length": null,
|
||||
"no-empty-source": null,
|
||||
"selector-list-comma-newline-after": null
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
],
|
||||
"nyc": {
|
||||
"all": true,
|
||||
"reporter": [
|
||||
"lcov",
|
||||
"text"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.{js,vue}"
|
||||
],
|
||||
"exclude": [
|
||||
"**/index.js",
|
||||
"**/*.spec.js"
|
||||
],
|
||||
"extension": [
|
||||
".js",
|
||||
".vue"
|
||||
],
|
||||
"check-coverage": true,
|
||||
"per-file": true,
|
||||
"branches": 0,
|
||||
"lines": 0,
|
||||
"functions": 0,
|
||||
"statements": 0
|
||||
}
|
||||
}
|
||||
1
client/web/workflow/public/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
config.js
|
||||
12
client/web/workflow/public/config.example.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// Corteza API location
|
||||
window.CortezaAPI = 'https://api.cortezaproject.your-domain.tld';
|
||||
|
||||
// CortezaAuth can be autoconfigured by replacing /api with /auth in CortezaAPI
|
||||
// or by appending /auth to the end of CortezaAPI string
|
||||
// When this is not possible and your configuration is more exotic you can set it
|
||||
// explicitly:
|
||||
// window.CortezaAuth = 'https://api.cortezaproject.your-domain.tld/auth';
|
||||
|
||||
// Configure CortezaWebapp when your web applications are not placed on the root.
|
||||
// This is autoconfigured from the value of <base> tag href attribute in most cases.
|
||||
// window.CortezaWebapp = 'https://cortezaproject.your-domain.tld';
|
||||
BIN
client/web/workflow/public/favicon32x32.png
Normal file
|
After Width: | Height: | Size: 702 B |
1
client/web/workflow/public/icons/break.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="32px" height="32px" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048"><path d="M1022.25 99.354c-450.277-9.22-870.449 360.942-917.287 809.188c-49.497 379.099 158.63 778.965 505.847 944.176c327.865 164.75 753.014 115.134 1024.927-134.893c274.593-240.828 389.319-650.62 262.28-995.771c-116.8-340.195-443.475-599.282-804.796-620.01a950.215 950.215 0 0 0-70.971-2.69zm-10.482 99.76c412.893-15.55 800.952 328.748 833.306 741.03c39.736 363.606-189.04 740.293-537.188 860.127c-338.432 125.141-753.857 3.445-958.257-297.536c-209.788-293.857-201.456-727.62 36.58-1003.308c152.352-183.514 386.115-299.42 625.559-300.314zm16.396 89.923c-381.953-12.147-733.35 323.18-740.25 705.154c-16.837 345.168 238.98 683.903 580.72 749.641c316.644 69.318 670.444-93.359 811.717-387.88c146.107-287.645 80.764-670.441-168.526-880.558c-132.088-117.004-306.804-186.158-483.66-186.357zm-4.494 98.049c336.708-8.916 643.367 294.108 637.967 631.146c5.952 311.574-244.821 607.697-557.659 639.11c-293.715 38.565-603.394-149.77-687.837-437.104c-88.514-273.766 27.776-603.496 282.864-744.453c97.803-57.623 211.099-88.88 324.665-88.7zM794.723 615.498L615.498 794.723l230.63 230.633l-226.464 226.466l176.38 176.381l226.468-226.467l230.765 230.766l179.225-179.225l-230.766-230.765l226.473-226.475l-176.38-176.38c-75.492 75.49-150.983 150.981-226.473 226.472L794.723 615.498zm-.127 70.68l230.765 230.765l226.467-226.464l105.695 105.695l-226.466 226.465l230.633 230.632l-108.41 108.41l-230.632-230.632l-226.466 226.467l-105.696-105.696l226.467-226.467l-230.765-230.765l108.408-108.41z" fill="#2D2D2D"/><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
client/web/workflow/public/icons/clock-danger.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="#E54122" d="M256,8C119,8,8,119,8,256S119,504,256,504,504,393,504,256,393,8,256,8Zm92.49,313h0l-20,25a16,16,0,0,1-22.49,2.5h0l-67-49.72a40,40,0,0,1-15-31.23V112a16,16,0,0,1,16-16h32a16,16,0,0,1,16,16V256l58,42.5A16,16,0,0,1,348.49,321Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 504 B |
1
client/web/workflow/public/icons/clock-success.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="#719430" d="M256,8C119,8,8,119,8,256S119,504,256,504,504,393,504,256,393,8,256,8Zm92.49,313h0l-20,25a16,16,0,0,1-22.49,2.5h0l-67-49.72a40,40,0,0,1-15-31.23V112a16,16,0,0,1,16-16h32a16,16,0,0,1,16,16V256l58,42.5A16,16,0,0,1,348.49,321Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 504 B |
1
client/web/workflow/public/icons/cog.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z" fill="#4D7281"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
8
client/web/workflow/public/icons/connection-point.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100"
|
||||
height="100"
|
||||
version="1.1"
|
||||
>
|
||||
<circle cx="50" cy="50" r="25" fill="#A7D0E3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 146 B |
1
client/web/workflow/public/icons/continue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="32px" height="32px" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048"><path d="M1022.25 99.36C571.973 90.17 151.801 460.306 104.963 908.553c-49.497 379.097 158.63 778.958 505.847 944.173c327.865 164.75 753.014 115.136 1024.928-134.893c274.592-240.828 389.318-650.625 262.28-995.771c-116.8-340.193-443.475-599.288-804.797-620.012c-23.612-1.742-47.291-2.71-70.97-2.71zm-10.482 99.762c412.892-15.548 800.952 328.747 833.307 741.03c39.736 363.608-189.04 740.296-537.189 860.124c-338.432 125.14-753.857 3.483-958.257-297.534c-209.788-293.858-201.456-727.62 36.58-1003.309c152.352-183.51 386.115-299.42 625.559-300.31zm16.396 89.922c-381.953-12.152-733.35 323.184-740.25 705.155c-16.837 345.166 238.98 683.897 580.72 749.641c316.644 69.314 670.444-93.357 811.717-387.882c146.108-287.647 80.764-670.44-168.525-880.558c-132.09-117.003-306.805-186.162-483.662-186.356zm-4.494 98.05c336.708-8.902 643.367 294.109 637.967 631.148c5.952 311.573-244.82 607.695-557.658 639.11c-293.716 38.566-603.396-149.773-687.84-437.11c-88.514-273.762 27.778-603.496 282.866-744.455c97.803-57.616 211.099-88.877 324.665-88.693zm102.98 236.909v205.745H664v388.53h462.65v205.716c112.45-133.325 224.9-266.65 337.35-399.986l-337.35-400.005zm56.356 149.56l208.967 248.52l-208.967 248.52v-111.876H726.938V887.45h456.068V773.563z" fill="#2D2D2D"/><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
client/web/workflow/public/icons/debug.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="bug" class="svg-inline--fa fa-bug fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" width="28px" height="28px" viewBox="0 0 512 512"><path fill="#2D2D2D" d="M511.988 288.9c-.478 17.43-15.217 31.1-32.653 31.1H424v16c0 21.864-4.882 42.584-13.6 61.145l60.228 60.228c12.496 12.497 12.496 32.758 0 45.255-12.498 12.497-32.759 12.496-45.256 0l-54.736-54.736C345.886 467.965 314.351 480 280 480V236c0-6.627-5.373-12-12-12h-24c-6.627 0-12 5.373-12 12v244c-34.351 0-65.886-12.035-90.636-32.108l-54.736 54.736c-12.498 12.497-32.759 12.496-45.256 0-12.496-12.497-12.496-32.758 0-45.255l60.228-60.228C92.882 378.584 88 357.864 88 336v-16H32.666C15.23 320 .491 306.33.013 288.9-.484 270.816 14.028 256 32 256h56v-58.745l-46.628-46.628c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0L141.255 160h229.489l54.627-54.627c12.498-12.497 32.758-12.497 45.256 0 12.496 12.497 12.496 32.758 0 45.255L424 197.255V256h56c17.972 0 32.484 14.816 31.988 32.9zM257 0c-61.856 0-112 50.144-112 112h224C369 50.144 318.856 0 257 0z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
client/web/workflow/public/icons/delay.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="32px" height="32px" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048"><path d="M1024 99.004c-510.27 0-925 414.72-925 924.994s414.729 925.004 925 925.004s925-414.73 925-925.004c0-510.273-414.73-924.994-925-924.994zm0 100c456.228 0 825 368.764 825 824.994s-368.773 825.004-825 825.004s-825-368.774-825-825.004s368.772-824.994 825-824.994zm-8.29 385.945c-171.528 1.212-335.1 113.885-398.317 273.285c-65.79 155.801-29.42 347.57 88.884 468.442c115.027 123.83 304.555 169.464 463.307 111.428c166.36-56.675 288.573-221.231 292.976-397.19c8.863-170.945-93.874-339.97-248.558-412.566c-59.027-28.58-124.545-43.458-190.117-43.371a394.838 394.838 0 0 0-8.174-.028zm15.643 79.973c156.707.7 303.715 116.52 340.258 269.176c40.7 149.018-27.87 318.936-161.361 397.015c-136.3 85.544-328.47 62.745-440.395-53.11c-118.113-113.943-139.147-311.084-48.53-447.733c64.178-101.947 182.05-166.467 302.56-165.293c2.492-.048 4.981-.066 7.468-.055zm105.952 102.072l-119.903 217.527c-37.032 3.938-45.83 60.122-11.662 75.106c22.57 15.666 40.247-10.392 60.793-9.494h183.293v-50H1065.89l115.205-209.004l-43.79-24.135z" fill="#2D2D2D"/><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
client/web/workflow/public/icons/error-handler.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="32px" height="32px" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048"><path d="M1024 99c-363.757-4.934-714.249 229.17-852.226 565.192c-144.118 330.153-69.236 740.283 182.862 997.771c242.464 260.992 641.95 356.786 976.52 234.67c346.74-118.073 602.724-458.592 616.639-825.09c23.34-360.131-188.536-719.422-512.954-876.293C1307.807 131.856 1165.95 98.797 1024 99zm0 100c359.283-6.249 700.984 251.836 793.747 598.93c97.03 326.908-34.215 705.405-315.907 899.024C1209.227 1912.03 775.3 1896.47 498.977 1660.8c-278.414-221.98-378.073-632.576-235.228-958.182C387.557 401.605 698.338 195.179 1024 199zm354.53 430.836l-210.237 426.18l-261.406-340.014l-237.416 702.162l266.172-342.129l274.562 314.17l168.324-760.37zm-448.397 224.86l248.558 315.126l62.078-138.195l-59.431 239.111l-253.695-298.441l-102.926 159.117l105.416-276.719z" fill="#2D2D2D"/><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
client/web/workflow/public/icons/error.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="32px" height="32px" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048"><path d="M1005.303 98.988c-447.59.377-859.063 373.342-901.291 819.483c-45.314 379.987 168.535 777.154 517.567 938.558c324.137 158.423 740.176 108.288 1009.01-135.647c273.526-236.69 393.041-640.13 272.893-984.32c-114.258-351.15-451.46-619.981-822.842-636.252c-25.079-1.64-50.213-1.947-75.337-1.822zm18.334 289.733c329.628-9.588 631.825 281.137 635.693 610.898c14.255 301.673-212.385 597.907-513.018 648.484c-290.913 58.275-613.563-109.854-715.39-392.282c-109.622-279.29 2.217-632.478 269.045-779.065c97.56-57.284 210.508-88.176 323.67-88.035zm354.892 241.117l-210.236 426.178L906.887 716L669.47 1418.162l266.174-342.127l274.562 314.172l168.322-760.37z" fill="#2D2D2D"/><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
client/web/workflow/public/icons/exec-workflow.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32px" height="32px" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);"><g transform="translate(0 995.638)"><rect width="1699.302" height="1400.778" x="174.349" y="-672.027" rx="266.951" fill="none" stroke="#2D2D2D" stroke-width="100" stroke-linecap="round" stroke-linejoin="round"/></g><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 563 B |
10
client/web/workflow/public/icons/expressions.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<g id="Group_6221" data-name="Group 6221" transform="translate(-378 -5834)">
|
||||
<rect id="Rectangle_261" data-name="Rectangle 261" width="32" height="32" transform="translate(378 5834)" fill="rgba(255,255,255,0)"/>
|
||||
<g id="Group_6219" data-name="Group 6219" transform="translate(381 5845)">
|
||||
<path id="Path_5690" data-name="Path 5690" d="M6.877,0,4.709,3.643,2.382,0H0L3.424,5.353,0,10.613H2.312L4.461,6.991l2.311,3.623H9.148L5.746,5.3,9.17,0Z" fill="#2D2D2D"/>
|
||||
<path id="Path_5691" data-name="Path 5691" d="M10.855,0V4H6.908V5.882h3.947v4h1.973v-4h3.949V4H12.828V0Z" transform="translate(3.815)" fill="#2D2D2D"/>
|
||||
<path id="Path_5692" data-name="Path 5692" d="M14.266,0V1.9h1.527v8.717h2.044V0Z" transform="translate(7.877)" fill="#2D2D2D"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 872 B |
18
client/web/workflow/public/icons/function.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="Rectangle_262" data-name="Rectangle 262" width="26.102" height="16.706" fill="#2D2D2D"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="Group_6220" data-name="Group 6220" transform="translate(-333 -5834)">
|
||||
<rect id="Rectangle_260" data-name="Rectangle 260" width="32" height="32" transform="translate(333 5834)" fill="rgba(255,255,255,0)"/>
|
||||
<g id="Group_6218" data-name="Group 6218" transform="translate(336 5842)">
|
||||
<g id="Group_6217" data-name="Group 6217" transform="translate(0 0)" clip-path="url(#clip-path)">
|
||||
<path id="Path_5686" data-name="Path 5686" d="M5.664,5.292v-1.6H3.719v-.6a1.556,1.556,0,0,1,.344-1.149A1.882,1.882,0,0,1,5.353,1.6h.176V0H5.353A4.067,4.067,0,0,0,2.684.736a2.941,2.941,0,0,0-.874,2.35v.6H.609v1.6h1.2V13.62a1.565,1.565,0,0,1-.343,1.149A1.9,1.9,0,0,1,.176,15.1H0v1.606H.176a4.064,4.064,0,0,0,2.669-.736,2.937,2.937,0,0,0,.874-2.35V5.292Z" transform="translate(0 0.001)" fill="#2D2D2D"/>
|
||||
<path id="Path_5687" data-name="Path 5687" d="M8.146,12.7a10.534,10.534,0,0,1,0-8.562A9.09,9.09,0,0,1,10.9.658L10.971.6V.22H9.411L9.365.254A8.615,8.615,0,0,0,6.681,3.667a11.287,11.287,0,0,0-1,4.745,11.322,11.322,0,0,0,1,4.751,8.6,8.6,0,0,0,2.684,3.42l.045.034h1.561v-.385l-.068-.054A9,9,0,0,1,8.146,12.7" transform="translate(2.328 0.091)" fill="#2D2D2D"/>
|
||||
<path id="Path_5688" data-name="Path 5688" d="M14.359,7.428l3.108-4.813H15.386L13.417,5.924,11.3,2.616H9.143l3.106,4.862-3.11,4.777h2.1l1.952-3.29,2.1,3.29h2.16Z" transform="translate(3.744 1.072)" fill="#2D2D2D"/>
|
||||
<path id="Path_5689" data-name="Path 5689" d="M19.054,3.667A8.612,8.612,0,0,0,16.371.254L16.325.22H14.764V.6l.066.054A9.213,9.213,0,0,1,17.583,4.14a10.489,10.489,0,0,1,0,8.562,9.082,9.082,0,0,1-2.753,3.475l-.066.054v.385h1.561l.047-.034a8.6,8.6,0,0,0,2.683-3.42,11.31,11.31,0,0,0,1-4.751,11.275,11.275,0,0,0-1-4.745" transform="translate(6.048 0.091)" fill="#2D2D2D"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
1
client/web/workflow/public/icons/gateway-exclusive.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="32px" height="32px" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048"><path d="M1024.022 99.36c-19.324-.017-38.646 7.15-52.98 21.55L120.937 971.023c-28.67 28.668-28.537 77.295.132 105.963l849.971 849.965c28.67 28.678 77.294 28.804 105.963 0l850.106-850.1c28.669-28.667 28.536-77.296-.135-105.964L1077.002 120.91c-14.334-14.334-33.657-21.534-52.98-21.55zm-.065 126.045l798.66 798.666l-798.66 798.657l-798.66-798.657l798.66-798.666zM725.686 669.792c-.014 0-9.612 1.838-9.62 1.838c-.01 0-8.144 5.513-8.15 5.513l-30.732 30.739c-.01 0-5.61 8.225-5.614 8.322c0 .01-1.737 9.48-1.736 9.48c0 .01 1.868 9.385 1.871 9.385c0 .01 5.338 8.322 5.344 8.322l280.707 280.7l-280.572 280.574v-.088c0 .01-5.61 8.32-5.614 8.32c0 .01-1.736 9.483-1.736 9.483c0 .02 1.868 9.385 1.871 9.385c0 0 5.339 8.223 5.344 8.32l30.734 30.728c.01.01 8.411 5.516 8.418 5.516c.01 0 9.346 1.838 9.354 1.838c.01 0 9.479-1.74 9.486-1.74c.01 0 8.28-5.614 8.285-5.614l280.576-280.582l280.637 280.641c.01.01 8.412 5.516 8.418 5.516c.01 0 9.346 1.838 9.354 1.838c.01 0 9.48-1.743 9.488-1.743c.01 0 8.276-5.61 8.281-5.61l30.735-30.73c.01-.01 5.475-8.126 5.48-8.126c0-.01 1.871-9.58 1.871-9.676c0-.01-1.869-9.385-1.873-9.385c0 0-5.472-8.418-5.478-8.418l-280.606-280.611l280.608-280.604c.01 0 5.473-8.127 5.478-8.127c0-.01 1.871-9.578 1.871-9.578c0-.02-1.868-9.385-1.87-9.385c0-.01-5.606-8.322-5.612-8.322l-30.735-30.738c-.01 0-8.143-5.514-8.15-5.514c-.01 0-9.345-1.84-9.353-1.84c-.01 0-9.613 1.84-9.62 1.84c-.01 0-8.145 5.514-8.15 5.514l-280.613 280.613l-280.739-280.748v-.088c-.01 0-8.278-5.32-8.285-5.32c-.01 0-9.34-1.837-9.351-1.838h-.002z" fill="#2D2D2D"/><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
client/web/workflow/public/icons/gateway-inclusive.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32px" height="32px" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);"><path d="M1024.022 99.36c-19.324-.017-38.646 7.15-52.98 21.55L120.937 971.023c-28.67 28.668-28.537 77.295.132 105.963l849.971 849.965c28.67 28.678 77.294 28.804 105.963 0l850.106-850.1c28.669-28.667 28.536-77.296-.135-105.964L1077.002 120.91c-14.334-14.334-33.657-21.534-52.98-21.55zm-.065 126.045l798.66 798.666l-798.66 798.657l-798.66-798.657l798.66-798.666zm.043 368.6c-237.232 0-430 192.78-430 430.008c0 237.228 192.768 430 430 430s430-192.772 430-430c0-237.229-192.768-430.008-430-430.008zm0 47.69c211.408 0 382.323 170.912 382.323 382.318c0 211.405-170.915 382.33-382.323 382.33c-211.407 0-382.322-170.925-382.322-382.33c0-211.406 170.915-382.319 382.322-382.319z" fill="#2D2D2D"/><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
client/web/workflow/public/icons/gateway-parallel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32px" height="32px" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);"><path d="M1024.022 99.36c-19.324-.017-38.646 7.15-52.98 21.55L120.937 971.023c-28.67 28.668-28.537 77.295.132 105.963l849.971 849.965c28.67 28.678 77.294 28.804 105.963 0l850.106-850.1c28.669-28.667 28.536-77.296-.135-105.964L1077.002 120.91c-14.334-14.334-33.657-21.534-52.98-21.55zm-.065 126.045l798.66 798.666l-798.66 798.657l-798.66-798.657l798.66-798.666zm-21.803 329.82c0 .01-9.657 1.838-9.662 1.838c-.01 0-7.908 5.323-7.914 5.323c-.01.01-5.497 8.127-5.502 8.127c0 .01-1.86 9.675-1.863 9.675V977.04H580.188l-.067-.078c0 .01-9.618 2.129-9.623 2.129c-.01.01-7.907 5.322-7.912 5.322l.008-.098c-.01.01-5.497 8.127-5.502 8.127c-.01 0-1.861 9.676-1.865 9.676v43.47s1.848 9.783 1.914 9.85c0 .01 5.478 7.934 5.478 7.934c.01.01 7.957 5.322 7.96 5.322c.01.01 9.656 2.127 9.66 2.127h396.978v396.785l-.063-.058c0 .01 1.916 9.85 1.915 9.85c0 .01 5.476 7.933 5.476 7.933c.01.01 7.96 5.32 7.961 5.32c0 0 9.59 2.032 9.662 2.13l43.461-.01c.011 0 9.846-2.032 9.852-2.032c0 0 7.908-5.32 7.914-5.32c.01-.01 5.47-7.936 5.476-7.936c0 0 1.887-9.82 1.89-9.82V1070.86h396.88c.011.01 9.847-2.031 9.851-2.031c.01 0 7.91-5.322 7.914-5.322c.01-.01 5.471-7.934 5.477-7.934c0 0 1.886-9.82 1.89-9.82v-43.461c0-.01-1.877-9.58-1.874-9.676c-.01-.01-5.45-8.127-5.518-8.127c-.01-.01-7.958-5.32-7.96-5.32c0 .01-9.82-2.13-9.825-2.033h-396.838V580.294c0-.01-1.878-9.577-1.875-9.674c-.01-.01-5.45-8.129-5.45-8.129c-.01-.01-7.956-5.32-7.958-5.32c-.01 0-9.848-1.936-9.852-1.936l-43.469-.01z" fill="#2D2D2D"/><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
13
client/web/workflow/public/icons/grid.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="8192" height="8192" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="smallGrid" width="8" height="8" patternUnits="userSpaceOnUse">
|
||||
<path d="M 8 0 L 0 0 0 8" fill="none" stroke="#c4c4c4" stroke-width="0.5"/>
|
||||
</pattern>
|
||||
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<rect width="80" height="80" fill="url(#smallGrid)"/>
|
||||
<path d="M 80 0 L 0 0 0 80" fill="none" stroke="#c4c4c4" stroke-width="1"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="8192" height="8192" fill="url(#grid)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 561 B |
4
client/web/workflow/public/icons/issue.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
||||
<!-- Font Awesome Free 5.15.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->
|
||||
<path fill="#E54122" d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 736 B |
5
client/web/workflow/public/icons/iterator.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="32px" height="32px" style="-ms-transform:rotate(360deg);-webkit-transform:rotate(360deg);transform:rotate(360deg)" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2000 2000">
|
||||
<path d="M429.65 210C214.934 210 40 384.196 40 598.5v803c0 214.304 174.934 388.5 389.65 388.5h1140.7c214.716 0 389.65-174.196 389.65-388.5v-803c0-214.304-174.934-388.5-389.65-388.5H429.65zm0 120h1140.7C1720.887 330 1840 448.826 1840 598.5v803c0 149.674-119.113 268.5-269.65 268.5H429.65C279.113 1670 160 1551.174 160 1401.5v-803C160 448.826 279.113 330 429.65 330z" fill="#2D2D2D"/>
|
||||
<path d="M1057.07 410.836C805.11 407.3 563.447 583.065 491.134 824.983c-55.584 173.977-23.105 373.061 85.522 520.027l-269.086-52.09l-22.804 117.814l483.865 93.664l93.687-479.986l-117.779-22.988l-56.861 291.316c-138.733-165.6-136.73-427.773 4.367-591.379c137.417-171.716 399.203-221.007 590.733-114c183.232 94.568 284.888 318.597 234.896 518.746c-44.77 208.91-247.404 367.34-460.604 363.053c-55.485-3.935-83.374 76.196-37.436 107.561c40.104 24.986 90.846 7.364 134.808 4.475c248.181-37.748 457.52-249.452 489.52-498.91c36.994-238.025-91.384-488.935-304.803-600.241c-86.168-46.769-184.068-71.364-282.089-71.21z" fill="#2D2D2D"/>
|
||||
<rect x="0" y="0" width="2000" height="2000" fill="rgba(0, 0, 0, 0)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
client/web/workflow/public/icons/play.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="#719430" d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"></path></svg>
|
||||
|
After Width: | Height: | Size: 392 B |
1
client/web/workflow/public/icons/prompt.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="32px" height="32px" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2000 2000"><g transform="translate(0 947.638)"><rect width="1800" height="1460" x="100" y="-677.638" rx="329.651" ry="328.5" fill="transparent" stroke="#2D2D2D" stroke-width="120" stroke-linecap="round"/><path d="M782.99 94.728h320.662c31.894 0 46.426-18.387 52.19-46.737c8.96-43.893-13.184-85.435-52.814-85.533c-97.378-.489-320.04-.196-320.04-.196v.223s340.368.268 379.564 0c39.195-.27 53.287-33.884 53.405-71.521c.12-37.54-18.907-70.744-53.287-70.793c-117.153-.098-379.68-.635-379.68-.635v-.194s199.412.465 285.185.465c35.051-.098 53.445-30.314 53.327-68.293c-.237-37.197-19.42-63.45-53.722-63.45c-137.086-.244-428.878 1.895-437.468-.146c-2.274-.54-4.22-4.971-2.723-6.794c14.054-17.122 80.562-79.478 95.325-98.59c18.512-23.803 20.486-56.943 6.394-78.401c-16.341-24.977-38.406-24.44-58.418-11.536c-57.669 37.344-260.516 178.067-305.237 208.763c-35.525 24.39-59.84 61.245-74.287 107.045c-14.604 46.435-13.105 100.51-12.947 147.875c.158 35.095.949 63.478 9.554 98.573c24.827 102.01 90.39 154.164 186.464 154.457c182.282.684 364.643.929 546.964 0c29.801-.195 46.221-19.702 46.655-55.482c.474-36.854-16.696-58.591-46.695-58.787c-70.181-.489-238.37 0-238.37 0z" fill="none" stroke="#2D2D2D" stroke-width="43.663900000000005" stroke-linecap="round"/></g><rect x="0" y="0" width="2000" height="2000" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
client/web/workflow/public/icons/stop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path fill="#E54122" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z"/></svg>
|
||||
|
After Width: | Height: | Size: 337 B |
1
client/web/workflow/public/icons/swimlane.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32px" height="32px" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);"><path d="M96 402v1280h1856V402zm1756 97.7v1091.542H480V499.702l1372-.003zM192 499.7h192v1091.541H192z" fill="#2D2D2D"/><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 467 B |
1
client/web/workflow/public/icons/termination.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="32px" height="32px" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048"><path d="M1005.303 98.988c-447.59.377-859.063 373.342-901.291 819.483c-45.314 379.987 168.535 777.154 517.567 938.558c324.137 158.423 740.176 108.288 1009.01-135.647c273.526-236.69 393.041-640.13 272.893-984.32c-114.258-351.15-451.46-619.981-822.842-636.252c-25.079-1.64-50.213-1.947-75.337-1.822zm18.334 289.733c329.628-9.588 631.825 281.137 635.693 610.898c14.255 301.673-212.385 597.907-513.018 648.484c-290.913 58.275-613.563-109.854-715.39-392.282c-109.622-279.29 2.217-632.478 269.045-779.065c97.56-57.284 210.508-88.176 323.67-88.035zM1024 554.17c-304.682-11.568-549.567 321.3-448.846 608.674c79.046 294.298 469.266 430.4 714.132 248.923c257.896-162.49 272.52-575.472 26.765-755.798c-82.202-65.575-186.913-102.06-292.051-101.8z" fill="#2D2D2D"/><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
client/web/workflow/public/icons/trigger.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32px" height="32px" preserveAspectRatio="xMidYMid meet" viewBox="0 0 2048 2048" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);"><path d="M1899 1023.999c0-483.252-391.75-874.995-875-874.995S149 540.747 149 1024c0 483.251 391.75 875.004 875 875.004s875-391.753 875-875.004z" fill="none" stroke="#2D2D2D" stroke-width="100.04248826"/><rect x="0" y="0" width="2048" height="2048" fill="rgba(0, 0, 0, 0)" /></svg>
|
||||
|
After Width: | Height: | Size: 551 B |
40
client/web/workflow/public/index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!--
|
||||
base location is root by default for all webapps
|
||||
|
||||
href value should be modified when app is placed under subdir
|
||||
-->
|
||||
<base href="/" />
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" id="favicon">
|
||||
<link rel="preload" href="<%= BASE_URL %>config.js" as="script" />
|
||||
<title><%= FLAVOUR %></title>
|
||||
<style type="text/css">
|
||||
html {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>
|
||||
JavaScript is disabled in your browser!
|
||||
</strong>
|
||||
<p>
|
||||
We're sorry but Corteza doesn't work properly without JavaScript enabled.
|
||||
Please enable it to continue.
|
||||
</p>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
87
client/web/workflow/src/app.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import './config-check'
|
||||
import './console-splash'
|
||||
|
||||
import './filters'
|
||||
import './plugins'
|
||||
import './mixins'
|
||||
import './components'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
|
||||
import { i18n } from '@cortezaproject/corteza-vue'
|
||||
|
||||
export default (options = {}) => {
|
||||
options = {
|
||||
el: '#app',
|
||||
name: 'workflow',
|
||||
template: '<div v-if="loaded && i18nLoaded" class="h-100"><router-view/></div>',
|
||||
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
i18nLoaded: false,
|
||||
}),
|
||||
|
||||
async created () {
|
||||
this.$i18n.i18next.on('loaded', () => {
|
||||
this.i18nLoaded = true
|
||||
})
|
||||
return this.$auth.vue(this).handle().then(({ accessTokenFn, user }) => {
|
||||
if (user.meta.preferredLanguage) {
|
||||
// After user is authenticated, get his preferred language
|
||||
// and instruct i18next to change it
|
||||
this.$i18n.i18next.changeLanguage(user.meta.preferredLanguage)
|
||||
}
|
||||
|
||||
// Load effective permissions
|
||||
this.$store.dispatch('rbac/load')
|
||||
|
||||
this.$Settings.init({ api: this.$SystemAPI }).then(() => {
|
||||
this.loaded = true
|
||||
|
||||
// This bit removes code from the query params
|
||||
//
|
||||
// Vue router can't be used here because when on any child route there is no
|
||||
// guarantee that the route has loaded and so it may redirect us to the root page.
|
||||
//
|
||||
// @todo dig a bit deeper if there is a better vue-like solution; atm none were ok.
|
||||
const url = new URL(window.location.href)
|
||||
if (url.searchParams.get('code')) {
|
||||
url.searchParams.delete('code')
|
||||
window.location.replace(url.toString())
|
||||
}
|
||||
})
|
||||
}).catch((err) => {
|
||||
if (err instanceof Error && err.message === 'Unauthenticated') {
|
||||
// user not logged-in,
|
||||
// start with authentication flow
|
||||
this.$auth.startAuthenticationFlow()
|
||||
return
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
},
|
||||
|
||||
router,
|
||||
store,
|
||||
i18n: i18n(Vue,
|
||||
{ app: 'corteza-webapp-workflow' },
|
||||
'configurator',
|
||||
'editor',
|
||||
'help',
|
||||
'general',
|
||||
'navigation',
|
||||
'notification',
|
||||
'permissions',
|
||||
'configurator',
|
||||
'steps',
|
||||
),
|
||||
|
||||
// Any additional options we want to merge
|
||||
...options,
|
||||
}
|
||||
|
||||
return new Vue(options)
|
||||
}
|
||||
5
client/web/workflow/src/components/C3.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { default as WorkflowEditor } from './WorkflowEditor.c3'
|
||||
|
||||
export default {
|
||||
WorkflowEditor,
|
||||
}
|
||||
85
client/web/workflow/src/components/Configurator/Delay.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-card
|
||||
class="flex-grow-1 border-bottom border-light rounded-0"
|
||||
>
|
||||
<b-card-header
|
||||
header-tag="header"
|
||||
class="bg-white p-0 mb-3"
|
||||
>
|
||||
<h5
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('configurator:configuration') }}
|
||||
</h5>
|
||||
</b-card-header>
|
||||
<b-card-body
|
||||
class="p-0"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general:offset-expression')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<expression-editor
|
||||
:value.sync="item.config.arguments[0].expr"
|
||||
lang="javascript"
|
||||
font-size="18px"
|
||||
show-line-numbers
|
||||
:border="false"
|
||||
@input="valueChanged"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import ExpressionEditor from '../ExpressionEditor'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExpressionEditor,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
watch: {
|
||||
'item.config.stepID': {
|
||||
immediate: true,
|
||||
handler () {
|
||||
let args = [{
|
||||
target: 'offset',
|
||||
type: 'Duration',
|
||||
expr: '',
|
||||
}]
|
||||
|
||||
if (this.item.config.arguments && this.item.config.arguments.length) {
|
||||
args = this.item.config.arguments.map(({ target, type, value, expr }) => {
|
||||
return {
|
||||
target,
|
||||
type,
|
||||
expr: expr || (value ? `"${value}"` : ''),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.$set(this.item.config, 'arguments', args)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
valueChanged (value) {
|
||||
this.$emit('update-default-value', {
|
||||
value: `Delay workflow execution for ${value}`,
|
||||
force: !this.item.node.value,
|
||||
})
|
||||
|
||||
this.$root.$emit('change-detected')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
13
client/web/workflow/src/components/Configurator/Edge.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
{{ $t('configurator:edge') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
extends: base,
|
||||
}
|
||||
</script>
|
||||
76
client/web/workflow/src/components/Configurator/Error.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<b-card
|
||||
class="flex-grow-1 border-bottom border-light rounded-0"
|
||||
>
|
||||
<b-card-header
|
||||
header-tag="header"
|
||||
class="bg-white p-0 mb-3"
|
||||
>
|
||||
<h5
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('configurator:configuration') }}
|
||||
</h5>
|
||||
</b-card-header>
|
||||
<b-card-body
|
||||
class="p-0"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('general:error-expression')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<expression-editor
|
||||
:value.sync="item.config.arguments[0].expr"
|
||||
lang="javascript"
|
||||
font-size="18px"
|
||||
show-line-numbers
|
||||
:border="false"
|
||||
@input="valueChanged"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import ExpressionEditor from '../ExpressionEditor'
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
ExpressionEditor,
|
||||
},
|
||||
extends: base,
|
||||
|
||||
created () {
|
||||
let args = [{
|
||||
target: 'message',
|
||||
type: 'String',
|
||||
expr: '',
|
||||
}]
|
||||
|
||||
if (this.item.config.arguments && this.item.config.arguments.length) {
|
||||
args = this.item.config.arguments.map(({ target, type, value, expr }) => {
|
||||
return {
|
||||
target,
|
||||
type,
|
||||
expr: expr || (value ? `"${value}"` : ''),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.$set(this.item.config, 'arguments', args)
|
||||
},
|
||||
|
||||
methods: {
|
||||
valueChanged (value) {
|
||||
this.$emit('update-default-value', {
|
||||
value: `Stop workflow with error: ${value}`,
|
||||
force: !this.item.node.value,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script>
|
||||
import Function from './Function'
|
||||
|
||||
export default {
|
||||
extends: Function,
|
||||
|
||||
data () {
|
||||
return {
|
||||
showFunctionList: false,
|
||||
expressionResults: true,
|
||||
functionRef: 'exec-workflow',
|
||||
|
||||
workflowOptions: [],
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (!this.workflowOptions.length) {
|
||||
this.searchWorkflows('', () => {})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getFunctionTypes () {
|
||||
this.functions = [
|
||||
{
|
||||
ref: 'exec-workflow',
|
||||
kind: 'function',
|
||||
meta: {
|
||||
short: 'Execute a workflow',
|
||||
},
|
||||
parameters: [
|
||||
{
|
||||
name: 'workflow',
|
||||
types: [
|
||||
'ID',
|
||||
'Handle',
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'scope',
|
||||
types: [
|
||||
'Vars',
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
results: [],
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
searchWorkflows (query = '', loading) {
|
||||
loading(true)
|
||||
|
||||
this.$AutomationAPI.workflowList({ query, subWorkflow: 2 })
|
||||
.then(({ set }) => {
|
||||
this.workflowOptions = set.map(m => Object.freeze(m))
|
||||
})
|
||||
.finally(() => {
|
||||
loading(false)
|
||||
})
|
||||
},
|
||||
|
||||
getWorkflowLabel ({ workflowID, handle, meta = {} }) {
|
||||
return meta.name || handle || workflowID
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
217
client/web/workflow/src/components/Configurator/Expressions.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-card
|
||||
class="flex-grow-1 rounded-0"
|
||||
body-class="p-0"
|
||||
>
|
||||
<b-card-header
|
||||
header-tag="header"
|
||||
class="d-flex align-items-center bg-white py-4"
|
||||
>
|
||||
<h5
|
||||
class="d-flex align-items-center mb-0"
|
||||
>
|
||||
{{ $t('steps:expressions.label') }}
|
||||
<a
|
||||
:href="documentationURL"
|
||||
target="_blank"
|
||||
class="d-flex align-items-center h6 mb-0 ml-1"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'question-circle']"
|
||||
/>
|
||||
</a>
|
||||
</h5>
|
||||
|
||||
<portal to="sidebar-footer">
|
||||
<b-button
|
||||
variant="primary"
|
||||
class="align-top border-0 ml-auto"
|
||||
@click="addArgument()"
|
||||
>
|
||||
{{ $t('steps:expressions.configurator.add-expression') }}
|
||||
</b-button>
|
||||
</portal>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body
|
||||
v-if="hasArguments"
|
||||
class="p-0"
|
||||
>
|
||||
<expression-table
|
||||
value-field="expr"
|
||||
:items="item.config.arguments"
|
||||
:fields="argumentFields"
|
||||
:types="fieldTypes"
|
||||
@remove="removeArgument"
|
||||
@open-editor="openInEditor"
|
||||
/>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<b-modal
|
||||
id="expression-editor"
|
||||
:visible="!!expressionEditor.currentExpression"
|
||||
:title="$t('editor:editor')"
|
||||
size="lg"
|
||||
:ok-title="$t('general:save')"
|
||||
:cancel-title="$t('general:cancel')"
|
||||
body-class="p-0"
|
||||
@ok="saveExpression"
|
||||
@hidden="resetExpression"
|
||||
>
|
||||
<expression-editor
|
||||
:value.sync="currentExpressionValue"
|
||||
height="500"
|
||||
lang="javascript"
|
||||
font-size="18px"
|
||||
show-line-numbers
|
||||
:border="false"
|
||||
:show-popout="false"
|
||||
/>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import ExpressionEditor from '../ExpressionEditor.vue'
|
||||
import ExpressionTable from '../ExpressionTable.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExpressionEditor,
|
||||
ExpressionTable,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
fieldTypes: [],
|
||||
|
||||
expressionEditor: {
|
||||
currentIndex: undefined,
|
||||
currentExpression: undefined,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentExpressionValue: {
|
||||
get () {
|
||||
return this.expressionEditor.currentExpression ? this.expressionEditor.currentExpression.expr : ''
|
||||
},
|
||||
|
||||
set (value) {
|
||||
if (this.expressionEditor.currentExpression) {
|
||||
this.expressionEditor.currentExpression.expr = value
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
argumentFields () {
|
||||
return [
|
||||
{
|
||||
key: 'target',
|
||||
thClass: 'pl-3 py-2',
|
||||
tdClass: 'text-truncate pointer',
|
||||
formatter: (value, key, item) => {
|
||||
return `${item.target}(${item.type})`
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
key: 'expr',
|
||||
label: this.$t('steps:expressions.configurator.expression'),
|
||||
thClass: 'py-2 pr-3',
|
||||
tdClass: 'position-relative pointer',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
hasArguments () {
|
||||
const { config } = this.item || {}
|
||||
return (config && (config.arguments || []).length) || []
|
||||
},
|
||||
|
||||
documentationURL () {
|
||||
// eslint-disable-next-line no-undef
|
||||
const [year, month] = VERSION.split('.')
|
||||
return `https://docs.cortezaproject.org/corteza-docs/${year}.${month}/integrator-guide/expr/index.html`
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'item.config.stepID': {
|
||||
immediate: true,
|
||||
handler () {
|
||||
this.$set(this.item.config, 'arguments', this.item.config.arguments || [])
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.getTypes()
|
||||
},
|
||||
|
||||
methods: {
|
||||
addArgument () {
|
||||
this.item.config.arguments.push({
|
||||
target: '',
|
||||
expr: '',
|
||||
type: 'Any',
|
||||
_showDetails: true,
|
||||
})
|
||||
this.$root.$emit('change-detected')
|
||||
},
|
||||
|
||||
removeArgument (index) {
|
||||
this.item.config.arguments.splice(index, 1)
|
||||
this.$root.$emit('change-detected')
|
||||
},
|
||||
|
||||
openInEditor (index = -1) {
|
||||
this.expressionEditor = {
|
||||
currentIndex: index >= -1 ? index : undefined,
|
||||
currentExpression: index >= 0 ? { ...this.item.config.arguments[index] } : undefined,
|
||||
}
|
||||
},
|
||||
|
||||
saveExpression () {
|
||||
if (this.expressionEditor.currentIndex >= 0) {
|
||||
const args = [...this.item.config.arguments]
|
||||
args[this.expressionEditor.currentIndex] = this.expressionEditor.currentExpression
|
||||
this.$set(this.item.config, 'arguments', args)
|
||||
this.$root.$emit('change-detected')
|
||||
}
|
||||
|
||||
this.resetExpression()
|
||||
},
|
||||
|
||||
resetExpression () {
|
||||
this.expressionEditor = {
|
||||
currentIndex: undefined,
|
||||
currentExpression: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
async getTypes () {
|
||||
return this.$AutomationAPI.typeList()
|
||||
.then(({ set }) => {
|
||||
this.fieldTypes = set
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:fetch-types-failed')))
|
||||
},
|
||||
|
||||
getTypeDescription (type) {
|
||||
// This will be moved to backend field type information
|
||||
const typeDescriptions = {
|
||||
ID: 'Make sure to provide the ID in double quotes if you\'re using a literal value. Example "123"',
|
||||
}
|
||||
|
||||
return typeDescriptions[type]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
663
client/web/workflow/src/components/Configurator/Function.vue
Normal file
@@ -0,0 +1,663 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!processing"
|
||||
>
|
||||
<b-card
|
||||
v-if="showFunctionList"
|
||||
class="flex-grow-1 border-bottom border-light rounded-0"
|
||||
>
|
||||
<b-card-header
|
||||
header-tag="header"
|
||||
class="bg-white p-0 mb-3"
|
||||
>
|
||||
<h5
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('configurator:configuration') }}
|
||||
</h5>
|
||||
</b-card-header>
|
||||
<b-card-body
|
||||
v-if="functionTypes.length"
|
||||
class="p-0"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('steps:function.configurator.type*')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<vue-select
|
||||
v-model="functionRef"
|
||||
:options="functionTypes"
|
||||
label="text"
|
||||
:selectable="f => !f.disabled"
|
||||
:reduce="f => f.value"
|
||||
:filter="functionFilter"
|
||||
:placeholder="$t('steps:function.configurator.select-function')"
|
||||
@input="functionChanged"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<p
|
||||
v-if="functionDescription"
|
||||
class="mt-3 mb-0"
|
||||
>
|
||||
{{ functionDescription }}
|
||||
</p>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<b-card
|
||||
v-if="args.length"
|
||||
class="flex-grow-1 border-bottom border-light rounded-0"
|
||||
body-class="p-0"
|
||||
>
|
||||
<b-card-header
|
||||
header-tag="header"
|
||||
class="d-flex align-items-center bg-white p-4"
|
||||
>
|
||||
<h5
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('steps:function.configurator.arguments') }}
|
||||
</h5>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body
|
||||
class="p-0"
|
||||
>
|
||||
<b-table
|
||||
id="arguments"
|
||||
fixed
|
||||
borderless
|
||||
hover
|
||||
head-row-variant="secondary"
|
||||
details-td-class="bg-white"
|
||||
class="mb-4"
|
||||
:items="args"
|
||||
:fields="argumentFields"
|
||||
:tbody-tr-class="rowClass"
|
||||
@row-clicked="item=>$set(item, '_showDetails', !item._showDetails)"
|
||||
>
|
||||
<template #cell(target)="{ item: a }">
|
||||
<var>{{ `${a.target}${a.required ? '*' : ''}` }}</var>
|
||||
<samp v-if="!isWhileIterator"> ({{ a.type }})</samp>
|
||||
</template>
|
||||
|
||||
<template #cell(type)="{ item: a }">
|
||||
<var>{{ a.type }}</var>
|
||||
</template>
|
||||
|
||||
<template #cell(value)="{ item: a }">
|
||||
<samp>{{ a[a.valueType] }}</samp>
|
||||
</template>
|
||||
|
||||
<template #row-details="{ item: a, index }">
|
||||
<div class="arrow-up" />
|
||||
|
||||
<b-card
|
||||
class="bg-light"
|
||||
body-class="px-4 pb-3"
|
||||
>
|
||||
<b-form-group
|
||||
v-if="(paramTypes[functionRef][a.target] || []).length > 1"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<vue-select
|
||||
v-model="a.type"
|
||||
:options="(paramTypes[functionRef][a.target] || [])"
|
||||
:filter="argTypeFilter"
|
||||
:clearable="false"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
label-class="d-flex align-items-center text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<div
|
||||
v-if="a.valueType === 'value'"
|
||||
>
|
||||
<vue-select
|
||||
v-if="a.target === 'workflow'"
|
||||
key="workflowID"
|
||||
v-model="a.value"
|
||||
:options="workflowOptions"
|
||||
:get-option-label="getWorkflowLabel"
|
||||
:reduce="wf => a.type === 'ID' ? wf.workflowID : wf.handle"
|
||||
clearable
|
||||
placeholder="Search for a workflow"
|
||||
class="bg-white rounded"
|
||||
@input="$root.$emit('change-detected')"
|
||||
@search="searchWorkflows"
|
||||
/>
|
||||
|
||||
<vue-select
|
||||
v-else-if="a.input.type === 'select'"
|
||||
v-model="a.value"
|
||||
:options="a.input.properties.options"
|
||||
label="text"
|
||||
:filter="varFilter"
|
||||
:reduce="a => a.value"
|
||||
:placeholder="$t('steps:function.configurator.option-select')"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
|
||||
<b-form-checkbox
|
||||
v-else-if="a.type === 'Boolean'"
|
||||
v-model="a.value"
|
||||
value="true"
|
||||
size="md"
|
||||
unchecked-value="false"
|
||||
@input="$root.$emit('change-detected')"
|
||||
>
|
||||
{{ a.target }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<expression-editor
|
||||
v-else
|
||||
:value.sync="a.value"
|
||||
@open="openInEditor(index)"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<expression-editor
|
||||
v-else-if="a.valueType === 'expr'"
|
||||
:value.sync="a.expr"
|
||||
lang="javascript"
|
||||
show-line-numbers
|
||||
@open="openInEditor(index)"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-checkbox
|
||||
v-if="!isWhileIterator"
|
||||
v-model="a.valueType"
|
||||
value="expr"
|
||||
unchecked-value="value"
|
||||
switch
|
||||
size="sm"
|
||||
class="float-right mr-2 mt-2"
|
||||
@change="valueTypeChanged($event, index)"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
{{ $t('steps:function.configurator.expression') }}
|
||||
<a
|
||||
:href="documentationURL"
|
||||
target="_blank"
|
||||
class="d-flex align-items-center h6 mb-0 ml-1"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'question-circle']"
|
||||
class="ml-1"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</b-form-checkbox>
|
||||
</b-card>
|
||||
</template>
|
||||
</b-table>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<b-card
|
||||
v-if="expressionResults || results.length"
|
||||
class="flex-grow-1 border-bottom border-light rounded-0"
|
||||
body-class="p-0"
|
||||
>
|
||||
<b-card-header
|
||||
header-tag="header"
|
||||
class="d-flex align-items-center bg-white p-4"
|
||||
>
|
||||
<h5
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('steps:function.configurator.results') }}
|
||||
</h5>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body
|
||||
v-if="results.length"
|
||||
class="p-0"
|
||||
>
|
||||
<expression-table
|
||||
v-if="expressionResults"
|
||||
value-field="expr"
|
||||
:items="results"
|
||||
:fields="resultFields"
|
||||
:types="fieldTypes"
|
||||
@remove="removeResult"
|
||||
@open-editor="openInEditor"
|
||||
/>
|
||||
|
||||
<b-table
|
||||
v-else
|
||||
id="results"
|
||||
fixed
|
||||
borderless
|
||||
hover
|
||||
head-row-variant="secondary"
|
||||
details-td-class="bg-white"
|
||||
class="mb-4"
|
||||
:items="results"
|
||||
:fields="resultFields"
|
||||
:tbody-tr-class="rowClass"
|
||||
@row-clicked="item=>$set(item, '_showDetails', !item._showDetails)"
|
||||
>
|
||||
<template #cell(type)="{ item: a }">
|
||||
<var>{{ a.type }}</var>
|
||||
</template>
|
||||
|
||||
<template #cell(value)="{ item: a }">
|
||||
<samp>{{ a.expr }}</samp>
|
||||
</template>
|
||||
|
||||
<template #row-details="{ item: a }">
|
||||
<div class="arrow-up" />
|
||||
|
||||
<b-card
|
||||
class="bg-light"
|
||||
body-class="px-4 pb-3"
|
||||
>
|
||||
<b-form-group
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="a.target"
|
||||
:placeholder="$t('configurator:target')"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-card>
|
||||
</template>
|
||||
</b-table>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<portal to="sidebar-footer">
|
||||
<b-button
|
||||
v-if="expressionResults"
|
||||
variant="primary"
|
||||
class="align-top border-0 ml-auto"
|
||||
@click="addResult()"
|
||||
>
|
||||
{{ $t('steps:function.configurator.add-result') }}
|
||||
</b-button>
|
||||
</portal>
|
||||
|
||||
<b-modal
|
||||
id="expression-editor"
|
||||
:visible="!!expressionEditor.currentExpression"
|
||||
:title="$t('editor:editor')"
|
||||
size="lg"
|
||||
:ok-title="$t('general:save')"
|
||||
:cancel-title="$t('general:cancel')"
|
||||
body-class="p-0"
|
||||
@ok="saveExpression"
|
||||
@hidden="resetExpression"
|
||||
>
|
||||
<expression-editor
|
||||
:value.sync="currentExpressionValue"
|
||||
:lang="expressionEditor.lang"
|
||||
height="500"
|
||||
font-size="18px"
|
||||
show-line-numbers
|
||||
:border="false"
|
||||
:show-popout="false"
|
||||
/>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import { VueSelect } from 'vue-select'
|
||||
import ExpressionEditor from '../ExpressionEditor.vue'
|
||||
import ExpressionTable from '../ExpressionTable.vue'
|
||||
import { objectSearchMaker, stringSearchMaker } from '../../lib/filter'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VueSelect,
|
||||
ExpressionEditor,
|
||||
ExpressionTable,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: true,
|
||||
|
||||
showFunctionList: true,
|
||||
expressionResults: false,
|
||||
functionRef: undefined,
|
||||
|
||||
functions: [],
|
||||
args: [],
|
||||
results: [],
|
||||
|
||||
fieldTypes: [],
|
||||
|
||||
paramTypes: {},
|
||||
resultTypes: {},
|
||||
|
||||
expressionEditor: {
|
||||
currentIndex: undefined,
|
||||
currentExpression: undefined,
|
||||
lang: 'javascript',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Used for expression editor modal
|
||||
currentExpressionValue: {
|
||||
get () {
|
||||
const { currentExpression } = this.expressionEditor
|
||||
return currentExpression ? currentExpression[currentExpression.valueType] : ''
|
||||
},
|
||||
|
||||
set (value) {
|
||||
const { currentExpression } = this.expressionEditor
|
||||
|
||||
if (currentExpression) {
|
||||
currentExpression[currentExpression.valueType] = value
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
functionTypes () {
|
||||
return this.functions.map(({ ref, meta, disabled = false }) => ({ value: ref, text: meta.short, disabled }))
|
||||
},
|
||||
|
||||
argumentFields () {
|
||||
return [
|
||||
{
|
||||
key: 'target',
|
||||
label: this.$t('steps:function.configurator.name'),
|
||||
thClass: 'pl-3 py-2',
|
||||
tdClass: 'text-truncate pointer',
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
thClass: 'pr-3 py-2',
|
||||
tdClass: 'text-truncate pointer',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
resultFields () {
|
||||
return [
|
||||
{
|
||||
key: 'target',
|
||||
thClass: 'pl-3 py-2',
|
||||
tdClass: 'text-truncate pointer',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
thClass: 'py-2',
|
||||
tdClass: 'text-truncate pointer',
|
||||
},
|
||||
{
|
||||
key: 'expr',
|
||||
label: this.$t('steps:function.configurator.result'),
|
||||
thClass: 'pr-3 py-2',
|
||||
tdClass: 'position-relative pointer',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
valueTypes () {
|
||||
return [
|
||||
{ text: this.$t('steps:function.configurator.expression'), value: 'expr' },
|
||||
{ text: this.$t('steps:function.configurator.constant'), value: 'value' },
|
||||
]
|
||||
},
|
||||
|
||||
defaultOptions () {
|
||||
return [{ value: null, text: this.$t('steps:function.configurator.option-select'), disabled: true }]
|
||||
},
|
||||
|
||||
functionDescription () {
|
||||
return (this.functions.find(({ ref }) => ref === this.functionRef) || { meta: {} }).meta.description
|
||||
},
|
||||
|
||||
isWhileIterator () {
|
||||
if (this.item.config) {
|
||||
return this.item.config.kind === 'iterator' && this.functionRef === 'loopDo'
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
documentationURL () {
|
||||
// eslint-disable-next-line no-undef
|
||||
const [year, month] = VERSION.split('.')
|
||||
return `https://docs.cortezaproject.org/corteza-docs/${year}.${month}/integrator-guide/expr/index.html`
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'item.config.stepID': {
|
||||
immediate: true,
|
||||
async handler () {
|
||||
this.processing = true
|
||||
|
||||
this.$set(this.item.config, 'arguments', this.item.config.arguments || [])
|
||||
this.$set(this.item.config, 'results', this.item.config.results || [])
|
||||
|
||||
await this.getFunctionTypes()
|
||||
await this.getTypes()
|
||||
|
||||
this.functionRef = this.item.config.ref || this.functionRef
|
||||
|
||||
this.setParams(this.functionRef, true)
|
||||
|
||||
this.processing = false
|
||||
},
|
||||
},
|
||||
|
||||
args: {
|
||||
deep: true,
|
||||
handler (args) {
|
||||
this.item.config.arguments = args.filter(({ value, source, expr }) => value || source || expr)
|
||||
.map(arg => {
|
||||
const argMapped = {
|
||||
target: arg.target,
|
||||
type: arg.type,
|
||||
}
|
||||
|
||||
argMapped[arg.valueType] = arg[arg.valueType]
|
||||
|
||||
return argMapped
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
results: {
|
||||
deep: true,
|
||||
handler (res) {
|
||||
this.item.config.results = res.filter(({ target }) => target).map(({ target, expr, type }) => ({ target, type, expr }))
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
functionFilter: objectSearchMaker('text'),
|
||||
argTypeFilter: stringSearchMaker(),
|
||||
varFilter: objectSearchMaker('text'),
|
||||
|
||||
setParams (fName, immediate = false) {
|
||||
this.args = []
|
||||
this.results = []
|
||||
|
||||
if (!immediate) {
|
||||
this.$root.$emit('change-detected')
|
||||
}
|
||||
|
||||
if (fName) {
|
||||
const func = this.functions.find(({ ref }) => ref === fName)
|
||||
|
||||
// Set parameters
|
||||
if (!this.paramTypes[func.ref] && func.parameters) {
|
||||
this.paramTypes[func.ref] = {}
|
||||
func.parameters.forEach(({ name, types }) => {
|
||||
this.paramTypes[func.ref][name] = types || []
|
||||
})
|
||||
}
|
||||
|
||||
this.args = func.parameters?.map(param => {
|
||||
const arg = this.item.config.arguments.find(({ target }) => target === param.name) || {}
|
||||
const { input = {} } = (param.meta || {}).visual || {}
|
||||
return {
|
||||
name: param.name,
|
||||
target: param.name,
|
||||
type: arg.type || this.paramTypes[func.ref][param.name][0],
|
||||
valueType: this.getValueType(arg, arg.type || this.paramTypes[func.ref][param.name][0], input),
|
||||
value: arg.value || null,
|
||||
expr: arg.expr || arg.source || null,
|
||||
required: param.required || false,
|
||||
input,
|
||||
}
|
||||
}) || []
|
||||
|
||||
// Set results
|
||||
if (!this.expressionResults) {
|
||||
if (!this.resultTypes[func.ref] && func.results) {
|
||||
this.resultTypes[func.ref] = {}
|
||||
func.results.forEach(({ name, types }) => {
|
||||
this.resultTypes[func.ref][name] = types || []
|
||||
})
|
||||
}
|
||||
|
||||
this.results = func.results?.map(result => {
|
||||
const res = this.item.config.results.find(({ expr }) => expr === result.name) || {}
|
||||
return {
|
||||
name: result.name,
|
||||
valueType: 'expr',
|
||||
target: res.target || undefined,
|
||||
type: this.resultTypes[func.ref][result.name][0],
|
||||
expr: res.expr || result.name,
|
||||
}
|
||||
}) || []
|
||||
} else {
|
||||
this.results = this.item.config.results.map(({ target, type, expr }) => {
|
||||
return {
|
||||
valueType: 'expr',
|
||||
target,
|
||||
type,
|
||||
expr,
|
||||
}
|
||||
}) || []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
openInEditor (index = -1) {
|
||||
this.expressionEditor = {
|
||||
currentIndex: index >= -1 ? index : undefined,
|
||||
currentExpression: index >= 0 ? { ...this.args[index] } : undefined,
|
||||
}
|
||||
|
||||
this.expressionEditor.lang = this.expressionEditor.currentExpression.valueType === 'expr' ? 'javascript' : 'text'
|
||||
},
|
||||
|
||||
saveExpression () {
|
||||
const { currentIndex = -1, currentExpression } = this.expressionEditor
|
||||
if (currentIndex >= 0) {
|
||||
this.args[currentIndex] = currentExpression
|
||||
this.$set(this.args, currentIndex, currentExpression)
|
||||
this.$root.$emit('change-detected')
|
||||
}
|
||||
|
||||
this.resetExpression()
|
||||
},
|
||||
|
||||
resetExpression () {
|
||||
this.expressionEditor = {
|
||||
currentIndex: undefined,
|
||||
currentExpression: undefined,
|
||||
lang: 'javascript',
|
||||
}
|
||||
},
|
||||
|
||||
async getFunctionTypes () {
|
||||
return this.$AutomationAPI.functionList()
|
||||
.then(({ set }) => {
|
||||
this.functions = set.filter(({ kind = '' }) => kind !== 'iterator').sort((a, b) => a.meta.short.localeCompare(b.meta.short))
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:failed-fetch-functions')))
|
||||
},
|
||||
|
||||
async getTypes () {
|
||||
return this.$AutomationAPI.typeList()
|
||||
.then(({ set }) => {
|
||||
this.fieldTypes = set
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:fetch-types-failed')))
|
||||
},
|
||||
|
||||
functionChanged (functionRef) {
|
||||
this.item.config.ref = functionRef
|
||||
|
||||
this.setParams(functionRef)
|
||||
|
||||
this.$emit('update-default-value', {
|
||||
value: (this.functionTypes.find(({ value }) => value === functionRef) || { meta: {} }).text,
|
||||
force: !this.item.node.value,
|
||||
})
|
||||
},
|
||||
|
||||
valueTypeChanged (valueType, index) {
|
||||
const oldType = valueType === 'value' ? 'expr' : 'value'
|
||||
this.args[index][valueType] = this.args[index][oldType]
|
||||
|
||||
if (!this.args[index].value && this.args[index].type === 'Boolean' && valueType === 'value') {
|
||||
this.args[index].value = 'false'
|
||||
}
|
||||
|
||||
this.$root.$emit('change-detected')
|
||||
},
|
||||
|
||||
getValueType (item, type, input = {}) {
|
||||
if (['Boolean'].includes(type) || ['select'].includes(input.type) || this.isWhileIterator) {
|
||||
return item.expr ? 'expr' : 'value'
|
||||
} else {
|
||||
return item.value ? 'value' : 'expr'
|
||||
}
|
||||
},
|
||||
|
||||
rowClass (item, type) {
|
||||
return item._showDetails && type === 'row' ? 'border-thick' : 'border-thick-transparent'
|
||||
},
|
||||
|
||||
addResult () {
|
||||
this.results.push({
|
||||
target: '',
|
||||
expr: '',
|
||||
type: 'Any',
|
||||
_showDetails: true,
|
||||
})
|
||||
this.$root.$emit('change-detected')
|
||||
},
|
||||
|
||||
removeResult (index) {
|
||||
this.results.splice(index, 1)
|
||||
this.$root.$emit('change-detected')
|
||||
},
|
||||
|
||||
getTypeDescription (type) {
|
||||
// This will be moved to backend field type information
|
||||
const typeDescriptions = {
|
||||
ID: 'Make sure to provide the ID in double quotes if you\'re using a literal value. Example "123"',
|
||||
}
|
||||
|
||||
return typeDescriptions[type]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
95
client/web/workflow/src/components/Configurator/Gateway.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-card
|
||||
v-if="['incl', 'excl'].includes(gatewayKind)"
|
||||
class="flex-grow-1 border-bottom border-light rounded-0"
|
||||
>
|
||||
<b-card-header
|
||||
header-tag="header"
|
||||
class="bg-white p-0 mb-3"
|
||||
>
|
||||
<h5
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('configurator:configuration') }}
|
||||
</h5>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body
|
||||
class="p-0"
|
||||
>
|
||||
<var
|
||||
v-if="outEdges < 2"
|
||||
>
|
||||
{{ $t('steps:gateway.configurator.two-paths') }}
|
||||
</var>
|
||||
|
||||
<div
|
||||
v-else
|
||||
>
|
||||
<b-form-group
|
||||
v-for="edge in gatewayEdges"
|
||||
:key="edge.id"
|
||||
:label="edge.value"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<expression-editor
|
||||
:value.sync="edge.expr"
|
||||
lang="javascript"
|
||||
height="60"
|
||||
show-line-numbers
|
||||
:show-popout="false"
|
||||
@input="updateEdge(edge.id, $event)"
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import ExpressionEditor from '../ExpressionEditor.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExpressionEditor,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
gatewayKind () {
|
||||
return this.item.config.ref
|
||||
},
|
||||
|
||||
gatewayEdges () {
|
||||
const edges = []
|
||||
if (['incl', 'excl'].includes(this.gatewayKind)) {
|
||||
if (this.outEdges && this.item.node.edges) {
|
||||
this.item.node.edges.forEach(({ id, source, target, value = '' }) => {
|
||||
if (source.id === this.item.node.id) {
|
||||
edges.push({
|
||||
id,
|
||||
source: source.id,
|
||||
target: target.id,
|
||||
value,
|
||||
expr: this.edges[id].config.expr || '',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return edges
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateEdge (id, expr) {
|
||||
this.edges[id].config.expr = expr
|
||||
this.$root.$emit('change-detected')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
17
client/web/workflow/src/components/Configurator/Iterator.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import Function from './Function'
|
||||
|
||||
export default {
|
||||
extends: Function,
|
||||
|
||||
methods: {
|
||||
async getFunctionTypes () {
|
||||
return this.$AutomationAPI.functionList()
|
||||
.then(({ set }) => {
|
||||
this.functions = set.filter(({ kind = '' }) => kind === 'iterator').sort((a, b) => a.meta.short.localeCompare(b.meta.short))
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:failed-fetch-functions')))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
14
client/web/workflow/src/components/Configurator/Prompt.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import Function from './Function'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
export default {
|
||||
extends: Function,
|
||||
|
||||
methods: {
|
||||
async getFunctionTypes () {
|
||||
this.functions = components.promptDefinitions || []
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
475
client/web/workflow/src/components/Configurator/Trigger.vue
Normal file
@@ -0,0 +1,475 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-card
|
||||
class="flex-grow-1 border-bottom border-light rounded-0"
|
||||
>
|
||||
<b-card-header
|
||||
header-tag="header"
|
||||
class="bg-white p-0 mb-3"
|
||||
>
|
||||
<h5
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('configurator:configuration') }}
|
||||
</h5>
|
||||
</b-card-header>
|
||||
<b-card-body
|
||||
class="p-0"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('steps:trigger.configurator.resource*')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<vue-select
|
||||
v-model="item.triggers.resourceType"
|
||||
:options="resourceTypeOptions"
|
||||
label="text"
|
||||
:reduce="r => r.value"
|
||||
:filter="resTypeFilter"
|
||||
:placeholder="$t('steps:trigger.configurator.select-resource-type')"
|
||||
@input="resourceChanged"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
v-if="item.triggers.resourceType"
|
||||
label-class="text-primary"
|
||||
:label="$t('steps:trigger.configurator.event*')"
|
||||
>
|
||||
<vue-select
|
||||
v-model="item.triggers.eventType"
|
||||
:options="eventTypeOptions"
|
||||
label="eventType"
|
||||
:reduce="e => e.eventType"
|
||||
:filter="evtTypeFilter"
|
||||
:placeholder="$t('steps:trigger.configurator.select-event-type')"
|
||||
@input="eventChanged"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="item.triggers.enabled"
|
||||
:disabled="isSubworkflow && !item.triggers.enabled"
|
||||
class="text-primary"
|
||||
@change="enabledChanged()"
|
||||
>
|
||||
{{ $t('general:enabled') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<b-card
|
||||
v-if="showConstraints"
|
||||
class="flex-grow-1 border-bottom border-light rounded-0"
|
||||
body-class="p-0"
|
||||
>
|
||||
<b-card-header
|
||||
header-tag="header"
|
||||
class="d-flex align-items-center bg-white p-4"
|
||||
>
|
||||
<h5
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('steps:trigger.configurator.constraints') }}
|
||||
</h5>
|
||||
<b-button
|
||||
v-if="constraintNameTypes.length"
|
||||
variant="primary"
|
||||
class="align-top border-0 ml-3"
|
||||
@click="addConstraint()"
|
||||
>
|
||||
{{ $t('steps:trigger.configurator.add-constraints') }}
|
||||
</b-button>
|
||||
</b-card-header>
|
||||
<b-card-body
|
||||
class="p-0"
|
||||
>
|
||||
<b-table
|
||||
v-if="constraintNameTypes.length"
|
||||
id="constraints"
|
||||
fixed
|
||||
borderless
|
||||
hover
|
||||
head-row-variant="secondary"
|
||||
details-td-class="bg-white"
|
||||
:items="item.triggers.constraints"
|
||||
:fields="constraintFields"
|
||||
:tbody-tr-class="rowClass"
|
||||
@row-clicked="item=>$set(item, '_showDetails', !item._showDetails)"
|
||||
>
|
||||
<template #cell(name)="{ item: c }">
|
||||
<samp v-if="c.name">
|
||||
{{ c.name.split('.').map(s => {
|
||||
return s[0].toUpperCase() + s.slice(1).toLowerCase()
|
||||
}).join(' ')
|
||||
}}
|
||||
</samp>
|
||||
</template>
|
||||
|
||||
<template #cell(values)="{ item: c, index }">
|
||||
<div
|
||||
class="text-truncate"
|
||||
:class="{ 'w-75': c._showDetails}"
|
||||
>
|
||||
<samp>{{ c.values.join(' or ') }}</samp>
|
||||
</div>
|
||||
|
||||
<b-button
|
||||
v-if="c._showDetails"
|
||||
variant="outline-danger"
|
||||
class="position-absolute trash border-0"
|
||||
@click="removeConstraint(index)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'trash-alt']"
|
||||
/>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #row-details="{ item: c }">
|
||||
<div class="arrow-up" />
|
||||
<b-card
|
||||
class="bg-light"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('steps:trigger.configurator.resource')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<vue-select
|
||||
v-model="c.name"
|
||||
:options="constraintNameTypes"
|
||||
label="text"
|
||||
:reduce="c => c.value"
|
||||
:filter="constrFilter"
|
||||
:placeholder="$t('steps:trigger.configurator.select-constraint-type')"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('steps:trigger.configurator.operator')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<vue-select
|
||||
v-model="c.op"
|
||||
:options="constraintOperatorTypes"
|
||||
label="text"
|
||||
:reduce="c => c.value"
|
||||
:placeholder="$t('steps:trigger.configurator.select-operator')"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group>
|
||||
<template #label>
|
||||
<div
|
||||
class="d-flex text-primary"
|
||||
>
|
||||
Values
|
||||
<b-button
|
||||
variant="link"
|
||||
class="align-top border-0 p-0 ml-2"
|
||||
@click="c.values.push('')"
|
||||
>
|
||||
{{ $t('steps:trigger.configurator.add') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<b-input-group
|
||||
v-for="(value, index) in c.values"
|
||||
:key="index"
|
||||
class="mb-2"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="c.values[index]"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
variant="outline-danger"
|
||||
class="ml-1 border-0"
|
||||
@click="c.values.splice(index, 1)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'trash-alt']"
|
||||
/>
|
||||
</b-button>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</b-card>
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<b-form-group
|
||||
v-else-if="item.triggers.constraints[0]"
|
||||
:label="item.triggers.eventType.replace('on', '')"
|
||||
label-class="text-primary"
|
||||
class="mt-0 mb-4 mx-4"
|
||||
>
|
||||
<c-input-date-time
|
||||
v-if="item.triggers.eventType === 'onTimestamp'"
|
||||
v-model="item.triggers.constraints[0].values[0]"
|
||||
:labels="{
|
||||
clear: $t('general:clear'),
|
||||
none: $t('general:none'),
|
||||
now: $t('general:now'),
|
||||
today: $t('general:today'),
|
||||
}"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
|
||||
<b-form-input
|
||||
v-else
|
||||
v-model="item.triggers.constraints[0].values[0]"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<b-card
|
||||
v-if="(eventType.properties || []).length"
|
||||
class="flex-grow-1 rounded-0"
|
||||
body-class="p-0"
|
||||
>
|
||||
<b-card-header
|
||||
header-tag="header"
|
||||
class="bg-white p-4"
|
||||
>
|
||||
<h5
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('steps:trigger.configurator.initial-scope') }}
|
||||
</h5>
|
||||
</b-card-header>
|
||||
<b-card-body
|
||||
class="p-0"
|
||||
>
|
||||
<b-table
|
||||
id="variable"
|
||||
fixed
|
||||
borderless
|
||||
head-row-variant="secondary"
|
||||
class="mb-4"
|
||||
:items="eventType.properties || []"
|
||||
:fields="scopeFields"
|
||||
>
|
||||
<template #cell(type)="{ item: v }">
|
||||
<var>{{ v.type }}</var>
|
||||
</template>
|
||||
</b-table>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import { VueSelect } from 'vue-select'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import { objectSearchMaker } from '../../lib/filter'
|
||||
const { CInputDateTime } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CInputDateTime,
|
||||
VueSelect,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
modules: [],
|
||||
|
||||
eventTypes: [],
|
||||
resourceTypes: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
resourceTypeOptions () {
|
||||
return this.resourceTypes
|
||||
},
|
||||
|
||||
eventTypeOptions () {
|
||||
return this.eventTypes.filter(({ resourceType }) => resourceType === this.item.triggers.resourceType)
|
||||
},
|
||||
|
||||
eventType () {
|
||||
return this.eventTypes.find(({ resourceType, eventType }) => resourceType === this.item.triggers.resourceType && eventType === this.item.triggers.eventType) || {}
|
||||
},
|
||||
|
||||
showConstraints () {
|
||||
if (this.item.triggers.resourceType && this.item.triggers.eventType) {
|
||||
return this.constraintNameTypes.length ? true : this.item.triggers.eventType !== 'onManual'
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
constraintFields () {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
thClass: 'pl-3 py-2 w-auto',
|
||||
tdClass: 'pr-0 text-truncate pointer',
|
||||
},
|
||||
{
|
||||
key: 'op',
|
||||
label: this.$t('steps:trigger.configurator.operator'),
|
||||
thClass: 'py-2 operator text-center',
|
||||
tdClass: 'pl-0 text-truncate text-center pointer',
|
||||
},
|
||||
{
|
||||
key: 'values',
|
||||
thClass: 'pr-3 py-2 w-auto text-center',
|
||||
tdClass: 'position-relative pointer text-center',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
scopeFields () {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
thClass: 'pl-3 py-2',
|
||||
tdClass: 'text-truncate',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
thClass: 'pr-3 py-2',
|
||||
tdClass: 'text-truncate',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
constraintNameTypes () {
|
||||
const constraints = this.eventType.constraints || []
|
||||
|
||||
return constraints.reduce((cons, { name }) => {
|
||||
if (!name.includes('*')) {
|
||||
cons.push({
|
||||
value: name,
|
||||
text: name.split('.').map(s => {
|
||||
return s[0].toUpperCase() + s.slice(1).toLowerCase()
|
||||
}).join(' '),
|
||||
})
|
||||
}
|
||||
|
||||
return cons
|
||||
}, [])
|
||||
},
|
||||
|
||||
constraintOperatorTypes () {
|
||||
return [
|
||||
{ value: '=', text: this.$t('steps:trigger.configurator.equal') },
|
||||
{ value: '!=', text: this.$t('steps:trigger.configurator.not-equal') },
|
||||
{ value: 'like', text: this.$t('steps:trigger.configurator.like') },
|
||||
{ value: 'not like', text: this.$t('steps:trigger.configurator.not-like') },
|
||||
|
||||
]
|
||||
},
|
||||
},
|
||||
|
||||
async created () {
|
||||
if (!this.item.triggers) {
|
||||
this.$set(this.item, 'triggers', {
|
||||
resourceType: null,
|
||||
eventType: null,
|
||||
constraints: [],
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
|
||||
await this.getEventTypes()
|
||||
},
|
||||
|
||||
methods: {
|
||||
resTypeFilter: objectSearchMaker('text'),
|
||||
evtTypeFilter: objectSearchMaker('eventType'),
|
||||
constrFilter: objectSearchMaker('text'),
|
||||
|
||||
async getEventTypes () {
|
||||
return this.$AutomationAPI.eventTypesList()
|
||||
.then(({ set }) => {
|
||||
this.eventTypes = set
|
||||
const resourceTypes = new Set(set.map(({ resourceType }) => resourceType))
|
||||
this.resourceTypes = [...resourceTypes].map(resourceType => {
|
||||
return {
|
||||
value: resourceType,
|
||||
text: resourceType.split(':').map(s => {
|
||||
return s[0].toUpperCase() + s.slice(1).toLowerCase()
|
||||
}).join(' '),
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('steps:trigger.configurator.failed-fetch-event-types')))
|
||||
},
|
||||
|
||||
addConstraint () {
|
||||
this.item.triggers.constraints.push({
|
||||
name: null,
|
||||
op: '=',
|
||||
values: [''],
|
||||
_showDetails: true,
|
||||
})
|
||||
|
||||
this.$root.$emit('change-detected')
|
||||
},
|
||||
|
||||
removeConstraint (index) {
|
||||
this.item.triggers.constraints.splice(index, 1)
|
||||
this.$root.$emit('change-detected')
|
||||
},
|
||||
|
||||
resourceChanged () {
|
||||
this.item.triggers.eventType = null
|
||||
this.item.triggers.constraints = []
|
||||
this.$root.$emit('change-detected')
|
||||
this.updateDefaultName()
|
||||
},
|
||||
|
||||
eventChanged () {
|
||||
this.item.triggers.constraints = []
|
||||
this.addConstraint()
|
||||
this.$root.$emit('change-detected')
|
||||
this.updateDefaultName()
|
||||
},
|
||||
|
||||
enabledChanged () {
|
||||
this.$root.$emit('trigger-updated', this.item.node)
|
||||
this.$root.$emit('change-detected')
|
||||
},
|
||||
|
||||
rowClass (item, type) {
|
||||
if (type === 'row') {
|
||||
return item._showDetails ? 'border-thick' : 'border-thick-transparent'
|
||||
} else if (type === 'row-details') {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
updateDefaultName () {
|
||||
let { resourceType, eventType } = this.item.triggers
|
||||
|
||||
if (resourceType) {
|
||||
eventType = eventType || ''
|
||||
let value = [resourceType.split(':').join(' '), eventType].filter(v => v).join(' - ')
|
||||
value = value.charAt(0).toUpperCase() + value.slice(1)
|
||||
this.$emit('update-default-value', { value, force: !this.item.node.value })
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.operator {
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
||||
176
client/web/workflow/src/components/Configurator/Workflow.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group
|
||||
:label="$t('general:label')"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="workflow.meta.name"
|
||||
data-test-id="input-label"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('general:handle')"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="workflow.handle"
|
||||
data-test-id="input-handle"
|
||||
:state="handleState"
|
||||
:placeholder="$t('workflow.placeholder-handle')"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
<b-form-invalid-feedback
|
||||
data-test-id="input-handle-invalid-state"
|
||||
:state="handleState"
|
||||
>
|
||||
{{ $t('workflow.invalid-handle-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('general:description')"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="workflow.meta.description"
|
||||
data-test-id="input-description"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('workflow.run-as')"
|
||||
:description="$t('workflow.not-setup-properly')"
|
||||
>
|
||||
<vue-select
|
||||
:options="user.options"
|
||||
data-test-id="select-run-as"
|
||||
:get-option-label="getOptionLabel"
|
||||
:value="user.value"
|
||||
@search="search"
|
||||
@input="updateRunAs"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="workflow.enabled"
|
||||
data-test-id="checkbox-enable-workflow"
|
||||
@change="$root.$emit('change-detected')"
|
||||
>
|
||||
{{ $t('general:enabled') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:description="$t('workflow.sub-workflow.description')"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="workflow.meta.subWorkflow"
|
||||
data-test-id="checkbox-sub-workflow"
|
||||
@change="$root.$emit('change-detected')"
|
||||
>
|
||||
{{ $t('workflow.sub-workflow.label') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { debounce } from 'lodash'
|
||||
import { VueSelect } from 'vue-select'
|
||||
import { handle } from '@cortezaproject/corteza-vue'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'configurator',
|
||||
},
|
||||
components: {
|
||||
VueSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
workflow: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
user: {
|
||||
options: [],
|
||||
value: undefined,
|
||||
|
||||
filter: {
|
||||
query: null,
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
handleState () {
|
||||
return handle.handleState(this.workflow.handle)
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
if (this.workflow.runAs) {
|
||||
this.fetchUsers()
|
||||
this.getUserByID()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
search: debounce(function (query) {
|
||||
if (query !== this.user.filter.query) {
|
||||
this.user.filter.query = query
|
||||
this.user.filter.page = 1
|
||||
}
|
||||
|
||||
if (query) {
|
||||
this.fetchUsers()
|
||||
}
|
||||
}, 300),
|
||||
|
||||
fetchUsers () {
|
||||
this.$SystemAPI.userList(this.user.filter)
|
||||
.then(({ set }) => {
|
||||
this.user.options = set.map(m => Object.freeze(m))
|
||||
})
|
||||
},
|
||||
|
||||
async getUserByID () {
|
||||
if (this.workflow.runAs !== '0') {
|
||||
this.$SystemAPI.userRead({ userID: this.workflow.runAs })
|
||||
.then(user => {
|
||||
this.user.value = user
|
||||
}).catch(() => {
|
||||
return {}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
updateRunAs (user) {
|
||||
if (user && user.userID) {
|
||||
this.user.value = user
|
||||
this.workflow.runAs = user.userID
|
||||
} else {
|
||||
this.user.value = null
|
||||
this.workflow.runAs = '0'
|
||||
}
|
||||
this.$root.$emit('change-detected')
|
||||
},
|
||||
|
||||
getOptionKey ({ userID }) {
|
||||
return userID
|
||||
},
|
||||
|
||||
getOptionLabel ({ userID, email, name, username }) {
|
||||
return name || username || email || `<@${userID}>`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
25
client/web/workflow/src/components/Configurator/base.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
edges: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
outEdges: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
|
||||
isSubworkflow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
72
client/web/workflow/src/components/Configurator/basic.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group
|
||||
:label="$t('general:label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="label"
|
||||
@input="$emit('update-value', $event)"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<!-- <b-form-group
|
||||
v-if="getSelectedItemConfigJSON"
|
||||
label="Config"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="getSelectedItemConfigJSON"
|
||||
rows="10"
|
||||
/>
|
||||
</b-form-group> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
// Ignores exclusiveGateway indexes (#n)
|
||||
label: {
|
||||
get () {
|
||||
if (this.getSourceType) {
|
||||
if (this.getSourceType === 'gatewayExclusive') {
|
||||
/* eslint-disable no-unused-vars */
|
||||
const [edgeID, ...rest] = this.item.node.value.split(' - ')
|
||||
return rest.join(' - ')
|
||||
}
|
||||
}
|
||||
|
||||
return this.item.node.value
|
||||
},
|
||||
|
||||
set (label) {
|
||||
if (this.getSourceType) {
|
||||
if (this.getSourceType === 'gatewayExclusive') {
|
||||
/* eslint-disable no-unused-vars */
|
||||
const [edgeID, ...rest] = this.item.node.value.split(' - ')
|
||||
const newLabel = [edgeID]
|
||||
if (label) {
|
||||
newLabel.push(label)
|
||||
}
|
||||
label = newLabel.join(' - ')
|
||||
}
|
||||
}
|
||||
|
||||
this.item.node.value = label
|
||||
},
|
||||
},
|
||||
|
||||
getSourceType () {
|
||||
const { source } = this.item.node
|
||||
if (source && source.style) {
|
||||
return source.style
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
89
client/web/workflow/src/components/Configurator/index.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<b-card
|
||||
class="flex-grow-1 border-bottom border-light rounded-0"
|
||||
>
|
||||
<b-card-header
|
||||
header-tag="header"
|
||||
class="bg-white p-0 mb-3"
|
||||
>
|
||||
<h5
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('general:general') }}
|
||||
</h5>
|
||||
</b-card-header>
|
||||
<b-card-body
|
||||
class="p-0"
|
||||
>
|
||||
<basic
|
||||
:item="item"
|
||||
@update-value="$emit('update-value', $event)"
|
||||
/>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<component
|
||||
:is="stepComponent"
|
||||
v-if="stepComponent"
|
||||
:item.sync="item"
|
||||
:edges.sync="edges"
|
||||
:out-edges="outEdges"
|
||||
:is-subworkflow="isSubworkflow"
|
||||
@update-default-value="updateDefaultName"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import base from './base'
|
||||
import basic from './basic'
|
||||
import * as Configurators from './loader'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
...Configurators,
|
||||
basic,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
collapse: {
|
||||
basic: true,
|
||||
configurator: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
stepComponent () {
|
||||
return Configurators[this.kind]
|
||||
},
|
||||
|
||||
kind () {
|
||||
const { kind } = this.item.config
|
||||
|
||||
if (kind === 'exec-workflow') {
|
||||
return 'ExecWorkflow'
|
||||
}
|
||||
|
||||
if (kind) {
|
||||
return kind.charAt(0).toUpperCase() + kind.slice(1)
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateDefaultName ({ value, force = false }) {
|
||||
if (force || this.item.config.defaultName || this.item.config.defaultName === undefined) {
|
||||
this.$emit('update-default-value', value)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
11
client/web/workflow/src/components/Configurator/loader.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export { default as Workflow } from './Workflow'
|
||||
export { default as Expressions } from './Expressions'
|
||||
export { default as Function } from './Function'
|
||||
export { default as Iterator } from './Iterator'
|
||||
export { default as Trigger } from './Trigger'
|
||||
export { default as Error } from './Error'
|
||||
export { default as Gateway } from './Gateway'
|
||||
export { default as Edge } from './Edge'
|
||||
export { default as Prompt } from './Prompt'
|
||||
export { default as Delay } from './Delay'
|
||||
export { default as ExecWorkflow } from './ExecWorkflow'
|
||||
75
client/web/workflow/src/components/Export.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<b-button
|
||||
variant="light"
|
||||
size="lg"
|
||||
@click="jsonExport(workflows)"
|
||||
>
|
||||
{{ $t('general:export') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
workflows: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
|
||||
fileName: {
|
||||
type: String,
|
||||
default: 'workflows-export',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async jsonExport (workflowID = []) {
|
||||
const triggers = {}
|
||||
let workflows = []
|
||||
|
||||
// Get workflow triggers
|
||||
await this.$AutomationAPI.triggerList({ workflowID, disabled: 1 })
|
||||
.then(({ set = [] }) => {
|
||||
set.forEach(({ workflowID, resourceType, eventType, constraints, enabled, stepID, meta }) => {
|
||||
if (!triggers[workflowID]) {
|
||||
triggers[workflowID] = []
|
||||
}
|
||||
|
||||
triggers[workflowID].push({
|
||||
resourceType,
|
||||
eventType,
|
||||
constraints,
|
||||
enabled,
|
||||
stepID,
|
||||
meta,
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:failed-fetch-triggers')))
|
||||
|
||||
// Get workflows, add related triggers
|
||||
await this.$AutomationAPI.workflowList({ workflowID, disabled: 1 })
|
||||
.then(({ set = [] }) => {
|
||||
workflows = set.map(({ workflowID, handle, enabled, keepSessions, steps, paths, meta }) => {
|
||||
return {
|
||||
handle,
|
||||
enabled,
|
||||
meta,
|
||||
keepSessions,
|
||||
steps,
|
||||
paths,
|
||||
triggers: triggers[workflowID],
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:failed-fetch-workflows')))
|
||||
|
||||
// Save file
|
||||
const blob = new Blob([JSON.stringify({ workflows }, null, 2)], { type: 'application/json' })
|
||||
saveAs(blob, `${this.fileName}.json`)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
120
client/web/workflow/src/components/ExpressionEditor.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div
|
||||
class="position-relative"
|
||||
>
|
||||
<ace-editor
|
||||
v-model="expressionValue"
|
||||
:lang="lang"
|
||||
:mode="lang"
|
||||
theme="chrome"
|
||||
width="100%"
|
||||
:height="height"
|
||||
:class="{ 'border': border }"
|
||||
v-on="$listeners"
|
||||
@init="editorInit"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
v-if="showPopout"
|
||||
variant="link"
|
||||
class="popout position-absolute px-2 py-1"
|
||||
@click="$emit('open')"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'expand-alt']"
|
||||
/>
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AceEditor from 'vue2-ace-editor'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AceEditor,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
lang: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
|
||||
height: {
|
||||
type: String,
|
||||
default: '80',
|
||||
},
|
||||
|
||||
showLineNumbers: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: '14px',
|
||||
},
|
||||
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
showPopout: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
expressionValue: {
|
||||
get () {
|
||||
return this.value
|
||||
},
|
||||
|
||||
set (value = '') {
|
||||
this.$emit('update:value', value)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
editorInit (editor) {
|
||||
require('brace/mode/text')
|
||||
require('brace/mode/javascript')
|
||||
require('brace/theme/chrome')
|
||||
|
||||
editor.setOptions({
|
||||
tabSize: 2,
|
||||
fontSize: this.fontSize,
|
||||
wrap: true,
|
||||
indentedSoftWrap: false,
|
||||
showLineNumbers: this.showLineNumbers,
|
||||
showGutter: this.showLineNumbers,
|
||||
displayIndentGuides: this.lang !== 'text',
|
||||
useWorker: false,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.border {
|
||||
background-color: #FFF;
|
||||
border: 2px solid #E4E9EF;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.popout {
|
||||
z-index: 7;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
144
client/web/workflow/src/components/ExpressionTable.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<b-table
|
||||
id="expression-table"
|
||||
fixed
|
||||
borderless
|
||||
hover
|
||||
head-row-variant="secondary"
|
||||
details-td-class="bg-white"
|
||||
class="mb-4"
|
||||
:items="items"
|
||||
:fields="fields"
|
||||
thead-tr-class="border-thick"
|
||||
:tbody-tr-class="rowClass"
|
||||
@row-clicked="item => $set(item, '_showDetails', !item._showDetails)"
|
||||
>
|
||||
<template #cell(target)="{ value }">
|
||||
<var>{{ value }}</var>
|
||||
</template>
|
||||
|
||||
<template #cell(type)="{ value }">
|
||||
<var>{{ value }}</var>
|
||||
</template>
|
||||
|
||||
<template #[`cell(${valueField})`]="{ value, item, index }">
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<div
|
||||
class="text-truncate"
|
||||
:class="{ 'w-75': item._showDetails}"
|
||||
>
|
||||
<samp>{{ value }}</samp>
|
||||
</div>
|
||||
|
||||
<b-button
|
||||
v-if="item._showDetails"
|
||||
variant="outline-danger"
|
||||
class="position-absolute trash border-0"
|
||||
@click="$emit('remove', index)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'trash-alt']"
|
||||
/>
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #row-details="{ item, index }">
|
||||
<div class="arrow-up" />
|
||||
|
||||
<b-card
|
||||
class="bg-light"
|
||||
body-class="px-4 pb-3"
|
||||
>
|
||||
<b-form-group
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="item.target"
|
||||
placeholder="Target"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
label-class="text-primary"
|
||||
:description="getTypeDescription(item.type)"
|
||||
>
|
||||
<vue-select
|
||||
v-model="item.type"
|
||||
:options="types"
|
||||
:clearable="false"
|
||||
:filter="varFilter"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
class="mb-0"
|
||||
>
|
||||
<expression-editor
|
||||
:value.sync="item[valueField]"
|
||||
lang="javascript"
|
||||
show-line-numbers
|
||||
@open="$emit('open-editor', index)"
|
||||
@input="$root.$emit('change-detected')"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-card>
|
||||
</template>
|
||||
</b-table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ExpressionEditor from './ExpressionEditor.vue'
|
||||
import { objectSearchMaker } from '../lib/filter'
|
||||
import { VueSelect } from 'vue-select'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExpressionEditor,
|
||||
VueSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
valueField: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
types: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
varFilter: objectSearchMaker('text'),
|
||||
|
||||
rowClass (item, type) {
|
||||
return item._showDetails && type === 'row' ? 'border-thick' : 'border-thick-transparent'
|
||||
},
|
||||
|
||||
getTypeDescription (type) {
|
||||
// This will be moved to backend field type information
|
||||
const typeDescriptions = {
|
||||
ID: 'Make sure to provide the ID in double quotes if you\'re using a literal value. Example "123"',
|
||||
}
|
||||
|
||||
return typeDescriptions[type]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
87
client/web/workflow/src/components/Help.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group
|
||||
label-class="sticky-top bg-white pb-0"
|
||||
class="p-2 mb-0"
|
||||
>
|
||||
<template #label>
|
||||
<h5
|
||||
class="d-flex align-items-center text-primary p-2 mb-0"
|
||||
>
|
||||
{{ $t('help:basic-controls-label') }}
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'mouse']"
|
||||
class="ml-2"
|
||||
/>
|
||||
</h5>
|
||||
</template>
|
||||
<b-table
|
||||
:items="basicControls"
|
||||
fixed
|
||||
thead-class="d-none"
|
||||
/>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
label-class="sticky-top bg-white pb-0"
|
||||
class="p-2 mb-0"
|
||||
>
|
||||
<template #label>
|
||||
<h5
|
||||
class="d-flex align-items-center text-primary pt-2 px-2 mb-0"
|
||||
>
|
||||
{{ $t('help:keyboard-shortcuts-label') }}
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'keyboard']"
|
||||
class="ml-2"
|
||||
/>
|
||||
</h5>
|
||||
</template>
|
||||
<small
|
||||
class="ml-2"
|
||||
>
|
||||
{{ $t('help:mac-users') }}
|
||||
</small>
|
||||
<b-table
|
||||
:items="keyboardShortcuts"
|
||||
fixed
|
||||
thead-class="d-none"
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
basicControls: [
|
||||
{ action: this.$t('help:basic-controls.action.select'), shortcut: this.$t('help:basic-controls.shortcut.left-click.label') },
|
||||
{ action: this.$t('help:basic-controls.action.select-multiple'), shortcut: this.$t('help:basic-controls.shortcut.left-click.drag') },
|
||||
{ action: this.$t('help:basic-controls.action.deselect'), shortcut: this.$t('help:basic-controls.shortcut.left-click.background') },
|
||||
{ action: this.$t('help:basic-controls.action.pan'), shortcut: this.$t('help:basic-controls.shortcut.right-click-drag') },
|
||||
{ action: this.$t('help:basic-controls.action.zoom-in/out'), shortcut: this.$t('help:basic-controls.shortcut.mouse-wheel') },
|
||||
],
|
||||
|
||||
keyboardShortcuts: [
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.select-all'), shortcut: 'Ctrl + A' },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.add-selection'), shortcut: `Ctrl + ${this.$t('help:keyboard-shortcuts.shortcut.left-mouse-button')}` },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.delete-selected-elements'), shortcut: 'Delete or Backspace' },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.reset-view-to-start-point'), shortcut: 'Ctrl + Space' },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.undo'), shortcut: 'Ctrl + Z' },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.redo'), shortcut: 'Ctrl + Shift + Z' },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.cut'), shortcut: 'Ctrl + X' },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.copy'), shortcut: 'Ctrl + C' },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.paste'), shortcut: 'Ctrl + V' },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.save'), shortcut: 'Ctrl + S' },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.nudge'), shortcut: `${this.$t('help:keyboard-shortcuts.shortcut.arrow-keys')} Shift ${this.$t('help:keyboard-shortcuts.shortcut.adjust-distance')}` },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.show-guides'), shortcut: `${this.$t('help:keyboard-shortcuts.shortcut.hold')} Alt` },
|
||||
{ action: this.$t('help:keyboard-shortcuts.action.show-help'), shortcut: 'Shift + ?' },
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
105
client/web/workflow/src/components/Import.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-button
|
||||
variant="light"
|
||||
size="lg"
|
||||
@click="show = true"
|
||||
>
|
||||
{{ $t('general:import.label') }}
|
||||
</b-button>
|
||||
|
||||
<b-modal
|
||||
v-model="show"
|
||||
size="lg"
|
||||
:title="$t('general:import.json')"
|
||||
ok-only
|
||||
class="d-none"
|
||||
@ok="$emit('import', workflows)"
|
||||
>
|
||||
<b-form-group
|
||||
:description="$t('general:import.reassign-run-as')"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-file
|
||||
:placeholder="$t('general:import.upload-files')"
|
||||
@change="fileUpload"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<template #modal-footer>
|
||||
<b-button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
:disabled="!workflows.length || processing"
|
||||
class="d-flex justify-content-center align-items-center"
|
||||
@click="$emit('import', workflows)"
|
||||
>
|
||||
<b-spinner
|
||||
v-if="processing"
|
||||
small
|
||||
type="grow"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-else
|
||||
>
|
||||
{{ $t('general:import.label') }}
|
||||
</span>
|
||||
</b-button>
|
||||
</template>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
show: false,
|
||||
workflows: [],
|
||||
processing: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fileUpload (e = {}) {
|
||||
const { files = [] } = (e.type === 'drop' ? e.dataTransfer : e.target) || {}
|
||||
|
||||
if (files[0]) {
|
||||
this.processing = true
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.readAsText(files[0])
|
||||
|
||||
reader.onload = (evt) => {
|
||||
try {
|
||||
const { workflows = [] } = JSON.parse(evt.target.result)
|
||||
this.workflows = workflows
|
||||
} catch (err) {
|
||||
err.message = this.$t('notification:failed-load-file')
|
||||
this.toastErrorHandler(this.$t('notification:general.warning'))(err)
|
||||
} finally {
|
||||
this.processing = false
|
||||
}
|
||||
}
|
||||
|
||||
reader.onerror = () => {
|
||||
this.toastErrorHandler(this.$t('notification:failed-load-file'))
|
||||
this.processing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
85
client/web/workflow/src/components/Tooltip.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<b-tooltip
|
||||
:id="kind"
|
||||
:target="kind"
|
||||
triggers="hover"
|
||||
placement="right"
|
||||
variant="light"
|
||||
custom-class="ml-2"
|
||||
boundary="window"
|
||||
noninteractive
|
||||
@hide="show = false"
|
||||
@show="show = true"
|
||||
>
|
||||
<transition name="fade">
|
||||
<b-card
|
||||
v-if="show"
|
||||
:img-src="img"
|
||||
:img-alt="kind"
|
||||
img-height="100px"
|
||||
img-top
|
||||
:title="title"
|
||||
class="text-left"
|
||||
>
|
||||
<b-card-text>
|
||||
{{ text }}
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
</transition>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
kind: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
img: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tooltip.b-tooltip-light > .tooltip-inner {
|
||||
padding: 0;
|
||||
max-width: 20rem;
|
||||
min-width: 20rem;
|
||||
background-color: transparent !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltip.b-tooltip-light.bs-tooltip-right .arrow::before {
|
||||
border-right-color: #2D2D2D !important;
|
||||
}
|
||||
|
||||
.tooltip.b-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
48
client/web/workflow/src/components/WorkflowEditor.c3.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { default as component } from './WorkflowEditor.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { checkbox } = components.C3.controls
|
||||
|
||||
const props = {
|
||||
workflowObject: {
|
||||
workflowID: '',
|
||||
handle: 'handle',
|
||||
meta: {
|
||||
name: 'Name',
|
||||
description: 'desc',
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
workflowTriggers: [],
|
||||
changeDetected: false,
|
||||
canCreate: true,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Workflow editor',
|
||||
group: ['Root components'],
|
||||
component,
|
||||
props,
|
||||
|
||||
controls: [
|
||||
checkbox('CanCreate', 'canCreate'),
|
||||
],
|
||||
|
||||
scenarios: [
|
||||
{
|
||||
label: 'Full form',
|
||||
props,
|
||||
},
|
||||
{
|
||||
label: 'Empty form',
|
||||
props: {
|
||||
...props,
|
||||
workflowObject: {
|
||||
...props.workflowObject,
|
||||
enabled: false,
|
||||
name: '',
|
||||
},
|
||||
canCreate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
2649
client/web/workflow/src/components/WorkflowEditor.vue
Normal file
61
client/web/workflow/src/components/faIcons.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faCheck,
|
||||
faTimes,
|
||||
faHome,
|
||||
faSave,
|
||||
faKeyboard,
|
||||
faMouse,
|
||||
faCog,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faChevronDown,
|
||||
faExclamationTriangle,
|
||||
faSearchPlus,
|
||||
faSearchMinus,
|
||||
faThumbtack,
|
||||
faGripHorizontal,
|
||||
faSearch,
|
||||
faExpandAlt,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faAngleDoubleLeft,
|
||||
faAngleRight,
|
||||
faAngleLeft,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import {
|
||||
faEdit,
|
||||
faTrashAlt,
|
||||
faQuestionCircle,
|
||||
faUser,
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faEdit,
|
||||
faTrashAlt,
|
||||
faCheck,
|
||||
faTimes,
|
||||
faHome,
|
||||
faSave,
|
||||
faKeyboard,
|
||||
faMouse,
|
||||
faCog,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faChevronDown,
|
||||
faExclamationTriangle,
|
||||
faQuestionCircle,
|
||||
faSearchPlus,
|
||||
faSearchMinus,
|
||||
faUser,
|
||||
faThumbtack,
|
||||
faSearch,
|
||||
faGripHorizontal,
|
||||
faExpandAlt,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faAngleDoubleLeft,
|
||||
faAngleRight,
|
||||
faAngleLeft,
|
||||
)
|
||||
11
client/web/workflow/src/components/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import './faIcons'
|
||||
import PortalVue from 'portal-vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.use(PortalVue)
|
||||
Vue.component('font-awesome-icon', FontAwesomeIcon)
|
||||
Vue.component('font-awesome-layers', FontAwesomeLayers)
|
||||
Vue.component('c-permissions-button', components.CPermissionsButton)
|
||||
Vue.component('c-input-confirm', components.CInputConfirm)
|
||||
8
client/web/workflow/src/config-check.js
Normal file
@@ -0,0 +1,8 @@
|
||||
[
|
||||
'CortezaAPI',
|
||||
].forEach((cfg) => {
|
||||
if (window[cfg] === undefined) {
|
||||
throw new Error(`Missing or invalid configuration.
|
||||
Make sure there is a public/config.js configuration file with window.${cfg} entry.`)
|
||||
}
|
||||
})
|
||||
6
client/web/workflow/src/console-splash.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable no-undef */
|
||||
const text = `%c${WEBAPP || 'Corteza Webapp'}, version: ${VERSION}, build time: ${BUILD_TIME}`
|
||||
const style = 'background-color: #1397CB; color: white; padding: 3px 10px; border: 1px solid black; font: Courier'
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log(text, style)
|
||||
42
client/web/workflow/src/dev-env.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import c3catalogue from './components/C3'
|
||||
import { components, i18n } from '@cortezaproject/corteza-vue'
|
||||
import './components'
|
||||
import './themes'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/c3',
|
||||
name: 'c3',
|
||||
component: components.C3.View,
|
||||
props: { catalogue: c3catalogue },
|
||||
},
|
||||
{ path: '*', redirect: { name: 'c3' } },
|
||||
]
|
||||
|
||||
Vue.use(Router)
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default new Vue({
|
||||
el: '#app',
|
||||
name: 'DevEnv',
|
||||
template: '<router-view/>',
|
||||
router: new Router({
|
||||
mode: 'history',
|
||||
routes,
|
||||
}),
|
||||
i18n: i18n(Vue,
|
||||
{ app: 'corteza-webapp-workflow' },
|
||||
'configurator',
|
||||
'editor',
|
||||
'help',
|
||||
'general',
|
||||
'navigation',
|
||||
'notification',
|
||||
'permissions',
|
||||
'configurator',
|
||||
'steps',
|
||||
),
|
||||
})
|
||||
6
client/web/workflow/src/filters/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import Vue from 'vue'
|
||||
import { filters } from '@cortezaproject/corteza-vue'
|
||||
|
||||
for (const n in filters) {
|
||||
Vue.filter(n, filters[n])
|
||||
}
|
||||
77
client/web/workflow/src/lib/codec.js
Normal file
@@ -0,0 +1,77 @@
|
||||
export function encodeGraph (model, vertices, edges) {
|
||||
const steps = []
|
||||
const paths = {}
|
||||
const triggers = []
|
||||
|
||||
Object.values(model.cells)
|
||||
.filter(cell => {
|
||||
return !!cell.vertex
|
||||
}).forEach(cell => {
|
||||
const triggerEdges = []
|
||||
|
||||
const defaultName = vertices[cell.id].config.defaultName || false
|
||||
|
||||
cell = {
|
||||
id: cell.id,
|
||||
value: cell.value,
|
||||
defaultName,
|
||||
xywh: [
|
||||
cell.geometry.x || 0,
|
||||
cell.geometry.y || 0,
|
||||
cell.geometry.width || 0,
|
||||
cell.geometry.height || 0,
|
||||
],
|
||||
parent: cell.parent.id,
|
||||
edges: (cell.edges || []).forEach(({ id, value, parent, source, target, geometry, style }) => {
|
||||
const edge = {
|
||||
...((edges[id] || {}).config || {}),
|
||||
parentID: source.id,
|
||||
childID: target.id,
|
||||
meta: {
|
||||
label: value || '',
|
||||
description: '',
|
||||
visual: {
|
||||
id,
|
||||
value,
|
||||
parent: parent.id,
|
||||
points: geometry.points,
|
||||
style,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if (vertices[source.id].triggers || vertices[target.id].triggers) {
|
||||
triggerEdges.push(edge)
|
||||
} else if (!paths[id]) {
|
||||
paths[id] = edge
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
if (vertices[cell.id].triggers) {
|
||||
cell.edges = triggerEdges
|
||||
triggers.push({
|
||||
...vertices[cell.id].triggers,
|
||||
stepID: (triggerEdges[0] || { childID: '0' }).childID,
|
||||
enabled: vertices[cell.id].triggers.enabled,
|
||||
constraints: vertices[cell.id].triggers.constraints,
|
||||
meta: {
|
||||
name: cell.value || '',
|
||||
description: '',
|
||||
visual: cell,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
steps.push({
|
||||
...vertices[cell.id].config,
|
||||
meta: {
|
||||
label: cell.value || '',
|
||||
description: '',
|
||||
visual: cell,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return { steps, paths: Object.values(paths), triggers }
|
||||
}
|
||||
146
client/web/workflow/src/lib/dry-run.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import { automation } from '@cortezaproject/corteza-js'
|
||||
|
||||
export async function encodeInput (initialScope, ComposeAPI, SystemAPI) {
|
||||
const ev = { args: {} }
|
||||
|
||||
// Types that can and must be fetched
|
||||
const {
|
||||
namespace,
|
||||
oldNamespace,
|
||||
module,
|
||||
oldModule,
|
||||
page,
|
||||
oldPage,
|
||||
record,
|
||||
oldRecord,
|
||||
user,
|
||||
oldUser,
|
||||
role,
|
||||
oldRole,
|
||||
application,
|
||||
oldApplication,
|
||||
} = initialScope
|
||||
|
||||
if (namespace && namespace.value) {
|
||||
await ComposeAPI.namespaceList({ slug: namespace.value })
|
||||
.then(({ set = [] }) => {
|
||||
const [ns] = set
|
||||
if (ns) {
|
||||
ev.args.namespace = { ...ns, resourceType: 'compose:namespace' }
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
if (oldNamespace && oldNamespace.value) {
|
||||
await ComposeAPI.namespaceList({ slug: oldNamespace.value })
|
||||
.then(({ set = [] }) => {
|
||||
const [ns] = set
|
||||
if (ns) {
|
||||
ev.args.oldNamespace = { ...ns, resourceType: 'compose:namespace' }
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
if (module && module.value && ev.args.namespace) {
|
||||
await ComposeAPI.moduleList({ namespaceID: ev.args.namespace.namespaceID, handle: module.value })
|
||||
.then(({ set = [] }) => {
|
||||
const [m] = set
|
||||
if (m) {
|
||||
ev.args.module = { ...m, resourceType: 'compose:module' }
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
if (oldModule && oldModule.value && ev.args.namespace) {
|
||||
await ComposeAPI.moduleList({ namespaceID: ev.args.namespace.namespaceID, handle: oldModule.value })
|
||||
.then(({ set = [] }) => {
|
||||
const [m] = set
|
||||
if (m) {
|
||||
ev.args.oldModule = { ...m, resourceType: 'compose:module' }
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
if (page && page.value && ev.args.namespace) {
|
||||
await ComposeAPI.pageRead({ pageID: page.value, namespaceID: ev.args.namespace.namespaceID })
|
||||
.then(p => {
|
||||
ev.args.page = { ...p }
|
||||
})
|
||||
}
|
||||
|
||||
if (oldPage && oldPage.value && ev.args.namespace) {
|
||||
await ComposeAPI.pageRead({ pageID: page.value, namespaceID: ev.args.namespace.namespaceID })
|
||||
.then(p => {
|
||||
ev.args.oldPage = { ...p }
|
||||
})
|
||||
}
|
||||
|
||||
if (record && record.value && ev.args.module && ev.args.namespace) {
|
||||
await ComposeAPI.recordRead({ recordID: record.value, moduleID: ev.args.module.moduleID, namespaceID: ev.args.namespace.namespaceID })
|
||||
.then(r => {
|
||||
ev.args.record = { ...r, resourceType: 'compose:record' }
|
||||
})
|
||||
}
|
||||
|
||||
if (oldRecord && oldRecord.value && ev.args.module && ev.args.namespace) {
|
||||
await ComposeAPI.recordRead({ recordID: oldRecord.value, moduleID: ev.args.module.moduleID, namespaceID: ev.args.namespace.namespaceID })
|
||||
.then(r => {
|
||||
ev.args.oldRecord = { ...r, resourceType: 'compose:record' }
|
||||
})
|
||||
}
|
||||
|
||||
if (user && user.value) {
|
||||
await SystemAPI.userRead({ userID: user.value })
|
||||
.then(u => {
|
||||
ev.args.user = { ...u, resourceType: 'User' }
|
||||
})
|
||||
}
|
||||
|
||||
if (oldUser && oldUser.value) {
|
||||
await SystemAPI.oldUserRead({ userID: oldUser.value })
|
||||
.then(u => {
|
||||
ev.args.oldUser = { ...u, resourceType: 'User' }
|
||||
})
|
||||
}
|
||||
|
||||
if (role && role.value) {
|
||||
await SystemAPI.roleRead({ roleID: role.value })
|
||||
.then(r => {
|
||||
ev.args.role = { ...r, resourceType: 'Role' }
|
||||
})
|
||||
}
|
||||
|
||||
if (oldRole && oldRole.value) {
|
||||
await SystemAPI.roleRead({ roleID: oldRole.value })
|
||||
.then(r => {
|
||||
ev.args.oldRole = { ...r, resourceType: 'Role' }
|
||||
})
|
||||
}
|
||||
|
||||
if (application && application.value) {
|
||||
await SystemAPI.applicationRead({ applicationID: application.value })
|
||||
.then(a => {
|
||||
ev.args.application = { ...a }
|
||||
})
|
||||
}
|
||||
|
||||
if (oldApplication && oldApplication.value) {
|
||||
await SystemAPI.applicationRead({ applicationID: oldApplication.value })
|
||||
.then(a => {
|
||||
ev.args.oldApplication = { ...a }
|
||||
})
|
||||
}
|
||||
|
||||
// Add rest to args
|
||||
Object.entries(initialScope).forEach(([key, value]) => {
|
||||
if (!ev.args[key]) {
|
||||
ev.args[key] = {}
|
||||
}
|
||||
})
|
||||
|
||||
return automation.Encode(ev.args)
|
||||
}
|
||||
13
client/web/workflow/src/lib/filter.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { filter } from '@cortezaproject/corteza-vue'
|
||||
|
||||
export function objectSearchMaker (field, ...fields) {
|
||||
return function (opts, search) {
|
||||
return opts.filter(o => filter.Assert(o, search, field, ...fields))
|
||||
}
|
||||
}
|
||||
|
||||
export function stringSearchMaker () {
|
||||
return function (opts, search) {
|
||||
return opts.filter(o => filter.Assert(o, search))
|
||||
}
|
||||
}
|
||||
178
client/web/workflow/src/lib/style.js
Normal file
@@ -0,0 +1,178 @@
|
||||
export function getStyleFromKind ({ kind = '', ref = '' }) {
|
||||
let kindRef = kind
|
||||
|
||||
if (['visual', 'gateway'].includes(kind)) {
|
||||
kindRef = `${kind}${ref ? (ref[0].toUpperCase() + ref.slice(1).toLowerCase()) : ''}`
|
||||
}
|
||||
|
||||
return kindToStyle[kindRef] || {}
|
||||
}
|
||||
|
||||
// The style property tells mxGraph what internal style to use for displaying the specific step
|
||||
const kindToStyle = {
|
||||
visualSwimlane: {
|
||||
width: 320,
|
||||
height: 160,
|
||||
icon: 'icons/swimlane.svg',
|
||||
style: 'swimlane',
|
||||
},
|
||||
|
||||
expressions: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/expressions.svg',
|
||||
style: 'expressions',
|
||||
},
|
||||
|
||||
function: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/function.svg',
|
||||
style: 'function',
|
||||
},
|
||||
|
||||
iterator: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/iterator.svg',
|
||||
style: 'iterator',
|
||||
},
|
||||
|
||||
'exec-workflow': {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/exec-workflow.svg',
|
||||
style: 'exec-workflow',
|
||||
},
|
||||
|
||||
break: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/break.svg',
|
||||
style: 'break',
|
||||
},
|
||||
|
||||
continue: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/continue.svg',
|
||||
style: 'continue',
|
||||
},
|
||||
|
||||
trigger: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/trigger.svg',
|
||||
style: 'trigger',
|
||||
},
|
||||
|
||||
'error-handler': {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/error-handler.svg',
|
||||
style: 'error-handler',
|
||||
},
|
||||
|
||||
error: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/error.svg',
|
||||
style: 'error',
|
||||
},
|
||||
|
||||
termination: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/termination.svg',
|
||||
style: 'termination',
|
||||
},
|
||||
|
||||
gatewayExcl: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/gateway-exclusive.svg',
|
||||
style: 'gatewayExclusive',
|
||||
},
|
||||
|
||||
gatewayIncl: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/gateway-inclusive.svg',
|
||||
style: 'gatewayInclusive',
|
||||
},
|
||||
|
||||
gatewayFork: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/gateway-parallel.svg',
|
||||
style: 'gatewayParallel',
|
||||
},
|
||||
|
||||
gatewayJoin: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/gateway-parallel.svg',
|
||||
style: 'gatewayParallel',
|
||||
},
|
||||
|
||||
prompt: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/prompt.svg',
|
||||
style: 'prompt',
|
||||
},
|
||||
|
||||
delay: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/delay.svg',
|
||||
style: 'delay',
|
||||
},
|
||||
|
||||
debug: {
|
||||
width: 200,
|
||||
height: 80,
|
||||
icon: 'icons/debug.svg',
|
||||
style: 'debug',
|
||||
},
|
||||
}
|
||||
|
||||
// When adding & or copy/pasting a new cell, this is used to determine the kind & ref
|
||||
export function getKindFromStyle (vertex) {
|
||||
const { style } = vertex
|
||||
if (!style) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const kind = style.split(';')[0]
|
||||
|
||||
if (kind.includes('gateway')) {
|
||||
if (gatewayKinds[kind]) {
|
||||
return gatewayKinds[kind]
|
||||
} else {
|
||||
// Determine if fork or join
|
||||
let inEdgeCount = 0
|
||||
let outEdgeCount = 0
|
||||
const edges = vertex.edges || []
|
||||
|
||||
edges.forEach(({ source, target }) => {
|
||||
if (source.id === vertex.id) {
|
||||
outEdgeCount++
|
||||
} else if (target.id === vertex.id) {
|
||||
inEdgeCount++
|
||||
}
|
||||
})
|
||||
|
||||
return { kind: 'gateway', ref: (inEdgeCount > outEdgeCount ? 'join' : 'fork') }
|
||||
}
|
||||
} else if (kind === 'swimlane') {
|
||||
return { kind: 'visual', ref: 'swimlane' }
|
||||
} else {
|
||||
return { kind }
|
||||
}
|
||||
}
|
||||
|
||||
const gatewayKinds = {
|
||||
gatewayExclusive: { kind: 'gateway', ref: 'excl' },
|
||||
gatewayInclusive: { kind: 'gateway', ref: 'incl' },
|
||||
}
|
||||
82
client/web/workflow/src/lib/toolbar.js
Normal file
@@ -0,0 +1,82 @@
|
||||
export default [
|
||||
{
|
||||
kind: 'visual',
|
||||
ref: 'swimlane',
|
||||
},
|
||||
{
|
||||
kind: 'hr',
|
||||
},
|
||||
{
|
||||
kind: 'trigger',
|
||||
ref: 'start',
|
||||
},
|
||||
{
|
||||
kind: 'termination',
|
||||
},
|
||||
{
|
||||
kind: 'hr',
|
||||
},
|
||||
{
|
||||
kind: 'expressions',
|
||||
},
|
||||
{
|
||||
kind: 'function',
|
||||
},
|
||||
{
|
||||
kind: 'exec-workflow',
|
||||
},
|
||||
{
|
||||
kind: 'hr',
|
||||
},
|
||||
{
|
||||
kind: 'iterator',
|
||||
},
|
||||
{
|
||||
kind: 'break',
|
||||
},
|
||||
{
|
||||
kind: 'continue',
|
||||
},
|
||||
{
|
||||
kind: 'hr',
|
||||
},
|
||||
{
|
||||
kind: 'error-handler',
|
||||
},
|
||||
{
|
||||
kind: 'error',
|
||||
},
|
||||
{
|
||||
kind: 'hr',
|
||||
},
|
||||
{
|
||||
kind: 'gateway',
|
||||
ref: 'excl',
|
||||
},
|
||||
{
|
||||
kind: 'gateway',
|
||||
ref: 'incl',
|
||||
},
|
||||
{
|
||||
kind: 'gateway',
|
||||
ref: 'fork',
|
||||
},
|
||||
{
|
||||
kind: 'hr',
|
||||
},
|
||||
{
|
||||
kind: 'prompt',
|
||||
},
|
||||
{
|
||||
kind: 'delay',
|
||||
},
|
||||
{
|
||||
kind: 'hr',
|
||||
},
|
||||
{
|
||||
kind: 'debug',
|
||||
},
|
||||
{
|
||||
kind: 'hr',
|
||||
},
|
||||
]
|
||||
4
client/web/workflow/src/main.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import app from './app'
|
||||
import './themes'
|
||||
|
||||
app()
|
||||
5
client/web/workflow/src/mixins/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import toast from './toast'
|
||||
|
||||
Vue.mixin(toast)
|
||||
197
client/web/workflow/src/mixins/listHelpers.js
Normal file
@@ -0,0 +1,197 @@
|
||||
import { debounce } from 'lodash'
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
/**
|
||||
* Placeholder, if component does not define own filter
|
||||
*/
|
||||
filter: {},
|
||||
|
||||
pagination: {
|
||||
limit: 10,
|
||||
pageCursor: undefined,
|
||||
prevPage: '',
|
||||
nextPage: '',
|
||||
total: 0,
|
||||
page: 1,
|
||||
},
|
||||
|
||||
// Used to save query when fetching for total if we came to the page with a pageCursor in URL
|
||||
tempQuery: undefined,
|
||||
|
||||
sorting: {},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* When fullPath for this component changes, we most likely should update our
|
||||
* filters.
|
||||
* @todo make this.filter reactive
|
||||
*/
|
||||
'$route.fullPath': {
|
||||
handler () {
|
||||
this.handleQueryParams()
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.handleQueryParams(true)
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions({
|
||||
incLoader: 'ui/incLoader',
|
||||
decLoader: 'ui/decLoader',
|
||||
}),
|
||||
|
||||
/**
|
||||
* Parses query params into list filtering params.
|
||||
* @param initial {Boolean} - used to determine it this is the initial fetch
|
||||
*/
|
||||
handleQueryParams (initial = false) {
|
||||
// Pagination
|
||||
let {
|
||||
limit = this.pagination.limit,
|
||||
pageCursor = this.pagination.pageCursor,
|
||||
prevPage = this.pagination.prevCursor,
|
||||
nextPage = this.pagination.nextCursor,
|
||||
total = this.pagination.total,
|
||||
page = this.pagination.page,
|
||||
...r1
|
||||
} = this.$route.query
|
||||
|
||||
limit = parseInt(limit)
|
||||
total = parseInt(total)
|
||||
page = parseInt(page)
|
||||
|
||||
// If we came to the page with a pageCursor in the URL
|
||||
if (initial && pageCursor) {
|
||||
this.tempQuery = this.$route.query
|
||||
// Fetch replace query to trigger fetch of total number of items
|
||||
this.$router.replace({ query: { ...this.$route.query, limit: 1, pageCursor: undefined } })
|
||||
return
|
||||
}
|
||||
|
||||
/// To prevent extra list fetch, check if pageCursor is defined (not first page)
|
||||
const refresh = this.$route.query.pageCursor !== this.pagination.pageCursor
|
||||
this.pagination = { limit, pageCursor, prevPage, nextPage, total, page }
|
||||
|
||||
// Sorting
|
||||
let { sortBy = this.sorting.sortBy, sortDesc = this.sorting.sortDesc, ...r2 } = r1
|
||||
|
||||
sortDesc = sortDesc === true || sortDesc === 'true'
|
||||
|
||||
// Reset pageCursor when sort changes, except on first fetch (so we use the pageCursor from url)
|
||||
if (!initial && (sortBy !== this.sorting.sortBy || sortDesc !== this.sorting.sortDesc)) {
|
||||
this.pagination.pageCursor = ''
|
||||
this.pagination.page = 1
|
||||
}
|
||||
this.sorting = { sortBy, sortDesc }
|
||||
|
||||
// Filtering
|
||||
// make sure filter fields are of the right type
|
||||
for (const key in r2) {
|
||||
if (typeof this.filter[key] === 'boolean') {
|
||||
r2[key] = r2[key] === 'true'
|
||||
}
|
||||
}
|
||||
|
||||
this.filter = { ...this.filter, ...r2 }
|
||||
|
||||
// Only refresh if pageCursor actually changed,
|
||||
if (refresh) {
|
||||
this.$root.$emit('bv::refresh::table', 'resource-list')
|
||||
}
|
||||
},
|
||||
|
||||
filterList: debounce(function () {
|
||||
// reset pagination when filtering changes
|
||||
//
|
||||
// we want to prevent situations with page is preset to a number that
|
||||
// exceeds the number of pages of a filtered results
|
||||
this.pagination.pageCursor = ''
|
||||
this.pagination.page = 1
|
||||
|
||||
// notify b-table about the change
|
||||
//
|
||||
// this effectively calls items()/procListResults()
|
||||
this.$root.$emit('bv::refresh::table', 'resource-list')
|
||||
}, 300),
|
||||
|
||||
encodeListParams () {
|
||||
const { sortBy, sortDesc } = this.sorting
|
||||
const { limit, pageCursor } = this.pagination
|
||||
|
||||
const sort = sortBy ? `${sortBy} ${sortDesc ? 'DESC' : 'ASC'}` : undefined
|
||||
|
||||
return {
|
||||
limit,
|
||||
sort: pageCursor ? undefined : sort,
|
||||
...this.filter,
|
||||
pageCursor,
|
||||
incTotal: !pageCursor || this.tempQuery,
|
||||
}
|
||||
},
|
||||
|
||||
encodeRouteParams () {
|
||||
const { limit, pageCursor, page } = this.pagination
|
||||
|
||||
return {
|
||||
query: {
|
||||
limit,
|
||||
...this.sorting,
|
||||
...this.filter,
|
||||
page,
|
||||
pageCursor,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param p {Promise}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
procListResults (p, updateQuery = true) {
|
||||
// Push new router/params to cause URL change
|
||||
//
|
||||
// We want this because in case when user refreshes or shares URL
|
||||
// he needs to land on the same page with the same parameters
|
||||
if (updateQuery && !this.tempQuery) {
|
||||
this.$router.replace(this.encodeRouteParams())
|
||||
}
|
||||
|
||||
return p.then(async ({ set, filter } = {}) => {
|
||||
if (filter.incTotal) {
|
||||
this.pagination.total = filter.total
|
||||
}
|
||||
|
||||
// This was a fetch of total number of items
|
||||
if (this.tempQuery) {
|
||||
const query = this.tempQuery
|
||||
this.tempQuery = undefined
|
||||
|
||||
this.$router.replace({ query })
|
||||
return []
|
||||
}
|
||||
|
||||
this.pagination.pageCursor = undefined
|
||||
this.pagination.nextPage = filter.nextPage
|
||||
this.pagination.prevPage = filter.prevPage
|
||||
|
||||
return set
|
||||
}).catch(this.toastErrorHandler(this.$t('notification:list.load.error')))
|
||||
.finally(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
})
|
||||
},
|
||||
|
||||
genericRowClass (item) {
|
||||
return { 'text-secondary': item && !!item.deletedAt }
|
||||
},
|
||||
},
|
||||
}
|
||||
38
client/web/workflow/src/mixins/toast.js
Normal file
@@ -0,0 +1,38 @@
|
||||
export default {
|
||||
methods: {
|
||||
toastSuccess (message, title = this.$t('notification:general.success')) {
|
||||
this.toast(message, { title, variant: 'success' })
|
||||
},
|
||||
|
||||
toastWarning (message, title = this.$t('notification:general.warning')) {
|
||||
this.toast(message, { title, variant: 'warning' })
|
||||
},
|
||||
|
||||
toastDanger (message, title = this.$t('notification:general.error')) {
|
||||
this.toast(message, { title, variant: 'danger' })
|
||||
},
|
||||
|
||||
toastInfo (message, title = this.$t('notification:general.info')) {
|
||||
this.toast(message, { title, variant: 'info' })
|
||||
},
|
||||
|
||||
toast (msg, opt = { variant: 'success' }) {
|
||||
this.$root.$bvToast.toast(msg, opt)
|
||||
},
|
||||
|
||||
toastErrorHandler (opt) {
|
||||
if (typeof opt === 'string') {
|
||||
opt = { prefix: opt }
|
||||
}
|
||||
|
||||
const { prefix, title } = opt
|
||||
|
||||
return (err = {}) => {
|
||||
/* eslint-disable no-console */
|
||||
console.error(err)
|
||||
const msg = err.message ? (prefix + ': ' + err.message) : prefix
|
||||
this.toastDanger(msg, title)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
28
client/web/workflow/src/plugins/index.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
|
||||
import { plugins } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.use(BootstrapVue, {
|
||||
BToast: {
|
||||
// see https://bootstrap-vue.org/docs/components/toast#comp-ref-b-toast-props
|
||||
autoHideDelay: 7000,
|
||||
toaster: 'b-toaster-bottom-right',
|
||||
},
|
||||
})
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
Vue.use(BootstrapVueIcons)
|
||||
|
||||
Vue.use(plugins.Auth(), { app: 'workflow' })
|
||||
|
||||
Vue.use(plugins.CortezaAPI('system'))
|
||||
Vue.use(plugins.CortezaAPI('compose'))
|
||||
Vue.use(plugins.CortezaAPI('automation'))
|
||||
Vue.use(plugins.Settings, { api: Vue.prototype.$SystemAPI })
|
||||
7
client/web/workflow/src/router.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Router from 'vue-router'
|
||||
import routes from './views/routes.js'
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
routes,
|
||||
})
|
||||
17
client/web/workflow/src/store/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import { store as cvStore } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
rbac: {
|
||||
namespaced: true,
|
||||
...cvStore.RBAC(Vue.prototype.$AutomationAPI),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default store
|
||||
272
client/web/workflow/src/themes/corteza-base/custom.scss
Normal file
@@ -0,0 +1,272 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
html {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline-color: $primary;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grab {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
thead th,
|
||||
legend,
|
||||
label,
|
||||
.btn {
|
||||
font-family: $font-medium;
|
||||
}
|
||||
|
||||
strong,
|
||||
b,
|
||||
.font-weight-bold {
|
||||
font-family: $font-semibold;
|
||||
}
|
||||
|
||||
th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vue-grid-layout {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.vue-grid-item {
|
||||
transition: none !important;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.v-select {
|
||||
min-width: min(100%, 15vw);
|
||||
|
||||
.vs__search {
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.vs__dropdown-toggle {
|
||||
padding: 0.375rem;
|
||||
padding-top: 0;
|
||||
border-width: 2px;
|
||||
border-color: $light;
|
||||
background-color: white;
|
||||
|
||||
.vs__selected {
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.vs__actions {
|
||||
padding-top: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.vs__clear,
|
||||
.vs__open-indicator {
|
||||
fill: $gray-900;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.vs__dropdown-menu {
|
||||
min-width: min(100%, 15vw);
|
||||
z-index: 1090;
|
||||
}
|
||||
|
||||
.loader {
|
||||
height: calc(100vh - 2 * #{$topbar-height});
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
.pending {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 30px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror.ProseMirror-focused {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// Pagination
|
||||
.page-item {
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// Rich text content styling
|
||||
.rt-content {
|
||||
&.editor {
|
||||
border-radius: 4px;
|
||||
border: 2px solid $light;
|
||||
}
|
||||
|
||||
.editor__content {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// blockquote
|
||||
blockquote {
|
||||
border-left: 3px solid rgba($black, 0.1);
|
||||
padding-left: 0.8rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// Code
|
||||
pre {
|
||||
background-color: #23241f;
|
||||
color: #f8f8f2;
|
||||
overflow: visible;
|
||||
white-space: pre-wrap;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
// todo list - https://github.com/scrumpy/tiptap/blob/master/examples/Components/Routes/TodoList/index.vue
|
||||
li[data-type="todo_item"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.todo-checkbox {
|
||||
border: 2px solid $black;
|
||||
height: 0.9em;
|
||||
width: 0.9em;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10px;
|
||||
margin-top: 0.3rem;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
border-radius: 0.2em;
|
||||
background-color: transparent;
|
||||
transition: 0.4s background;
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
flex: 1;
|
||||
|
||||
> p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> ul[data-type="todo_list"] {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
li[data-done="true"] {
|
||||
> .todo-content {
|
||||
> p {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
> .todo-checkbox {
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
li[data-done="false"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
fieldset.required {
|
||||
&.error {
|
||||
legend::before {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
label::after {
|
||||
content: "*";
|
||||
display: inline-block;
|
||||
color: $danger;
|
||||
width: 10px;
|
||||
height: 18px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// Over-ride Bootstrap clear search input icon
|
||||
input[type="search"]::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
background: url("data:image/svg+xml;charset=UTF-8,%3csvg viewPort='0 0 12 12' version='1.1' xmlns='http://www.w3.org/2000/svg'%3e%3cline x1='1' y1='11' x2='11' y2='1' stroke='black' stroke-width='2'/%3e%3cline x1='1' y1='1' x2='11' y2='11' stroke='black' stroke-width='2'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.b-toaster.b-toaster-bottom-right {
|
||||
bottom: 75px;
|
||||
}
|
||||
|
||||
.vs__spinner, .vs__spinner::after {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
.wrap-with-vertical-gutters {
|
||||
margin-top: -0.25rem;
|
||||
|
||||
> * {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Supporting CSS to improve print-to-PDF option
|
||||
@media print {
|
||||
@page {
|
||||
size: auto;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.block {
|
||||
break-inside: avoid;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||