3
0

Add files from webapp-workflow with client/web/workflow prefix

This commit is contained in:
Corteza Monorepo Migrator
2022-11-14 09:26:45 +01:00
parent 8cbd9274ad
commit 6564abcf36
116 changed files with 20700 additions and 0 deletions

View 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
View 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

View 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"]

View 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 $@

View File

@@ -0,0 +1,12 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
env: {
test: {
plugins: [
['istanbul', { useInlineSourceMaps: false }],
],
},
},
}

View File

@@ -0,0 +1,5 @@
files:
- source: /src/i18n/en
ignore:
- /src/i18n/en/index.js
translation: /src/i18n/%locale%

View 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

View 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;
}
}
}

View 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
View File

@@ -0,0 +1 @@
config.js

View 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';

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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>

View 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)
}

View File

@@ -0,0 +1,5 @@
import { default as WorkflowEditor } from './WorkflowEditor.c3'
export default {
WorkflowEditor,
}

View 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>

View File

@@ -0,0 +1,13 @@
<template>
<div>
{{ $t('configurator:edge') }}
</div>
</template>
<script>
import base from './base'
export default {
extends: base,
}
</script>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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'

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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,
},
},
],
}

File diff suppressed because it is too large Load Diff

View 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,
)

View 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)

View 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.`)
}
})

View 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)

View 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',
),
})

View 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])
}

View 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 }
}

View 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)
}

View 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))
}
}

View 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' },
}

View 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',
},
]

View File

@@ -0,0 +1,4 @@
import app from './app'
import './themes'
app()

View File

@@ -0,0 +1,5 @@
import Vue from 'vue'
import toast from './toast'
Vue.mixin(toast)

View 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 }
},
},
}

View 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)
}
},
},
}

View 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 })

View File

@@ -0,0 +1,7 @@
import Router from 'vue-router'
import routes from './views/routes.js'
export default new Router({
mode: 'history',
routes,
})

View 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

View 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%;
}
}

Some files were not shown because too many files have changed in this diff Show More