Merge branch 'dev'

This commit is contained in:
Dan 2022-01-04 15:31:41 -05:00
commit 83f58df3f3
132 changed files with 13355 additions and 15746 deletions

58
.eslintrc.json Normal file
View File

@ -0,0 +1,58 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "objectLiteralProperty",
"format": ["camelCase", "snake_case"],
"leadingUnderscore": "allow",
"trailingUnderscore": "allow"
}
],
"no-underscore-dangle": "off",
"arrow-body-style": "warn"
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

View File

@ -0,0 +1,41 @@
name: Create and publish a Docker image
on:
push:
branches: ['master']
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Log in to the Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -5,15 +5,19 @@ dist: bionic
language: node_js
node_js:
- 12
- 14
services:
- docker
- xvfb
before_install:
- dpkg --compare-versions `npm -v` ge 7.10 || npm i -g npm@^7.10
- npm --version
- |
if [[ $TRAVIS_BRANCH =~ ^(dev|testing|demo|training|staging|rrt\/.*)$ ]]; then
if [[ $TRAVIS_BRANCH =~ ^(feature\/.*)$ ]]; then
export E2E_TAG="dev"
elif [[ $TRAVIS_BRANCH =~ ^(dev|testing|demo|training|staging|rrt\/.*)$ ]]; then
export E2E_TAG="${TRAVIS_BRANCH//\//_}"
else
export E2E_TAG="latest"
@ -38,13 +42,6 @@ env:
script:
- npm run ci
deploy:
provider: script
script: bash ./deploy.sh sartography/cr-connect-bpmn
on:
all_branches: true
condition: $TRAVIS_BRANCH =~ ^(dev|testing|demo|training|staging|master|rrt\/.*)$
notifications:
email:
on_success: change

View File

@ -1,15 +1,18 @@
### STAGE 1: Build ###
FROM sartography/cr-connect-angular-base AS builder
FROM quay.io/sartography/node:latest AS builder
RUN mkdir /app
WORKDIR /app
ADD package.json /app/
ADD package-lock.json /app/
COPY . /app/
ARG build_config=prod
RUN npm install && \
npm run build:$build_config
### STAGE 2: Run ###
FROM nginx:alpine
RUN set -x && apk add --update --no-cache bash libintl gettext curl
FROM quay.io/sartography/nginx:alpine
RUN set -x && apk add --update --no-cache bash libintl gettext curl``
COPY --from=builder /app/dist/* /etc/nginx/html/
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf

View File

@ -1,15 +1,17 @@
# CR Connect BPMN Configurator
# sartography/cr-connect-bpmn
[![Build Status](https://travis-ci.com/sartography/cr-connect-bpmn.svg?branch=master)](https://travis-ci.com/sartography/cr-connect-bpmn)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=sartography_cr-connect-bpmn&metric=coverage)](https://sonarcloud.io/dashboard?id=sartography_cr-connect-bpmn)
# CR Connect BPMN Configurator
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.1.1.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
You can also user `npm run start:dev` to get a dev server with lazy loading. This makes development much more efficient
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
@ -26,6 +28,14 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
One way to check for coverage:
Install lcov (in ubuntu: sudo apt-get install lcov)
run `ng test --no-watch --code-coverage` to generate a coverage directory, with an lcov file in it
run `genhtml coverage/lcov.info -o coverage/html` to generate an html doc that looks at coverage (index.html)
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@ -16,25 +16,62 @@
"customWebpackConfig": {
"path": "./webpack.config.js"
},
"allowedCommonJsDependencies": [
"lodash",
"ids",
"bpmn-js-properties-panel",
"diagram-js-code-editor"
],
"outputPath": "dist/cr-connect-bpmn",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"preserveSymlinks": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"node_modules/bpmn-js/dist/assets/diagram-js.css",
"node_modules/bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css",
"node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css",
"node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css",
"node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn.css",
"node_modules/bpmn-js/dist/assets/diagram-js.css",
"node_modules/dmn-js-properties-panel/dist/assets/dmn-js-properties-panel.css",
"node_modules/diagram-js-minimap/assets/diagram-js-minimap.css",
"node_modules/dmn-js/dist/assets/diagram-js.css",
"node_modules/dmn-js/dist/assets/dmn-font/css/dmn-codes.css",
"node_modules/dmn-js/dist/assets/dmn-font/css/dmn-embedded.css",
"node_modules/dmn-js/dist/assets/dmn-font/css/dmn.css",
"node_modules/dmn-js/dist/assets/dmn-js-decision-table-controls.css",
"node_modules/dmn-js/dist/assets/dmn-js-decision-table.css",
"node_modules/dmn-js/dist/assets/dmn-js-drd.css",
"node_modules/dmn-js/dist/assets/dmn-js-literal-expression.css",
"node_modules/dmn-js/dist/assets/dmn-js-shared.css",
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"production": {
"optimization": true,
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
@ -52,7 +89,14 @@
]
},
"staging": {
"optimization": true,
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
@ -70,7 +114,14 @@
]
},
"test": {
"optimization": true,
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
@ -98,15 +149,16 @@
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"port": 5002,
"customWebpackConfig": {
"path": "./webpack.config.js"
},
"browserTarget": "cr-connect-bpmn:build"
},
"configurations": {
"production": {
"browserTarget": "cr-connect-bpmn:build:production"
},
"development": {
"browserTarget": "cr-connect-bpmn:build:development"
}
}
},
"extract-i18n": {
@ -143,14 +195,11 @@
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"builder": "@angular-eslint/builder:lint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
@ -174,11 +223,11 @@
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"builder": "@angular-eslint/builder:lint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
"lintFilePatterns": [
"e2e//**/*.ts",
"e2e//**/*.html"
]
}
}
@ -192,6 +241,7 @@
}
},
"cli": {
"analytics": false
"analytics": false,
"defaultCollection": "@angular-eslint/schematics"
}
}
}

View File

@ -1,45 +0,0 @@
#!/bin/bash
#########################################################################
# Builds the Docker image for the current git branch on Travis CI and
# publishes it to Docker Hub.
#
# Parameters:
# $1: Docker Hub repository to publish to
#
# Required environment variables (place in Settings menu on Travis CI):
# $DOCKER_USERNAME: Docker Hub username
# $DOCKER_TOKEN: Docker Hub access token
#########################################################################
echo 'Building Docker image...'
DOCKER_REPO="$1"
function branch_to_tag () {
if [ "$1" == "master" ]; then echo "latest"; else echo "$1" ; fi
}
function branch_to_deploy_group() {
if [[ $1 =~ ^(rrt\/.*)$ ]]; then echo "rrt"; else echo "crconnect" ; fi
}
DOCKER_TAG=$(branch_to_tag "$TRAVIS_BRANCH")
DEPLOY_GROUP=$(branch_to_deploy_group "$TRAVIS_BRANCH")
if [ "$DEPLOY_GROUP" == "rrt" ]; then
IFS='/' read -ra ARR <<< "$TRAVIS_BRANCH" # Split branch on '/' character
DOCKER_TAG=$(branch_to_tag "rrt_${ARR[1]}")
fi
echo "DOCKER_REPO = $DOCKER_REPO"
echo "DOCKER_TAG = $DOCKER_TAG"
echo "$DOCKER_TOKEN" | docker login -u "$DOCKER_USERNAME" --password-stdin || exit 1
docker build -f Dockerfile -t "$DOCKER_REPO:$DOCKER_TAG" . || exit 1
# Push Docker image to Docker Hub
echo "Publishing to Docker Hub..."
docker push "$DOCKER_REPO" || exit 1
echo "Done."

View File

@ -2,7 +2,7 @@ version: "3.3"
services:
db:
container_name: db
image: sartography/cr-connect-db:$E2E_TAG
image: quay.io/sartography/cr-connect-db:$E2E_TAG
ports:
- "5432:5432"
environment:
@ -19,7 +19,7 @@ services:
container_name: pb
depends_on:
- db
image: sartography/protocol-builder-mock:$E2E_TAG
image: quay.io/sartography/protocol-builder-mock:$E2E_TAG
environment:
- APPLICATION_ROOT=/
- CORS_ALLOW_ORIGINS=localhost:5000,backend:5000,localhost:5002,bpmn:5002,localhost:4200,frontend:4200
@ -39,7 +39,7 @@ services:
depends_on:
- db
- pb
image: sartography/cr-connect-workflow:$E2E_TAG
image: quay.io/sartography/cr-connect-workflow:$E2E_TAG
environment:
- APPLICATION_ROOT=/
- CORS_ALLOW_ORIGINS=localhost:5002,bpmn:5002,localhost:4200,frontend:4200
@ -67,7 +67,7 @@ services:
# depends_on:
# - db
# - backend
# image: sartography/cr-connect-bpmn:dev
# image: quay.io/sartography/cr-connect-bpmn:dev
# environment:
# - API_URL=http://localhost:5000/api/v1.0
# - BASE_HREF=/bpmn/
@ -83,7 +83,7 @@ services:
# depends_on:
# - db
# - backend
# image: sartography/cr-connect-frontend:dev
# image: quay.io/sartography/cr-connect-frontend:dev
# environment:
# - API_URL=http://localhost:5000/api/v1.0
# - BASE_HREF=/app/

0
docker/substitute-env-variables.sh Executable file → Normal file
View File

45
e2e/.eslintrc.json Normal file
View File

@ -0,0 +1,45 @@
{
"ignorePatterns": [
"!**/*"
], "overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/component-selector": [
"error",
{
"prefix": "app",
"style": "kebab-case",
"type": "element"
}
],
"@angular-eslint/directive-selector": [
"error",
{
"prefix": "app",
"style": "camelCase",
"type": "attribute"
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

View File

@ -15,15 +15,6 @@ describe('workspace-project App', () => {
expect(page.getElements('app-file-list').count()).toBeGreaterThan(0);
});
it('should display diagram', async () => {
const el = await page.getElement('app-file-list mat-list-item');
const specId = await el.getAttribute('data-workflow-spec-id');
const fileMetaId = await el.getAttribute('data-file-meta-id');
const expectedRoute = `/modeler/${specId}/${fileMetaId}`;
page.clickAndExpectRoute('app-file-list mat-list-item h4', expectedRoute);
expect(page.getElements('.diagram-container').count()).toBeGreaterThan(0);
});
it('should show dialog to open a diagram file');

View File

@ -31,9 +31,7 @@ export class AppPage {
}
focus(selector: string) {
return browser.controlFlow().execute(() => {
return browser.executeScript('arguments[0].focus()', this.getElement(selector).getWebElement());
});
return browser.controlFlow().execute(() => browser.executeScript('arguments[0].focus()', this.getElement(selector).getWebElement()));
}
getElement(selector: string): ElementFinder {
@ -49,9 +47,7 @@ export class AppPage {
}
getNumTabs() {
return browser.getAllWindowHandles().then(wh => {
return wh.length;
});
return browser.getAllWindowHandles().then(wh => wh.length);
}
async getRoute() {
@ -78,13 +74,11 @@ export class AppPage {
}
switchFocusToTab(tabIndex: number) {
return browser.getAllWindowHandles().then(wh => {
return wh.forEach((h, i) => {
return browser.getAllWindowHandles().then(wh => wh.forEach((h, i) => {
if (i === tabIndex) {
return browser.switchTo().window(h);
}
});
});
}));
}
waitFor(t: number) {

22317
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,17 @@
{
"name": "cr-connect-bpmn",
"version": "0.0.0",
"engines": {
"npm": "7.20.3"
},
"scripts": {
"ng": "ng",
"start": "ng serve",
"start:dev": "ng serve --configuration development",
"build": "ng build",
"build:prod": "ng build --configuration=production --prod --base-href=__REPLACE_ME_WITH_BASE_HREF__ --deploy-url=__REPLACE_ME_WITH_DEPLOY_URL__",
"build:staging": "ng build --configuration=staging --prod --base-href=__REPLACE_ME_WITH_BASE_HREF__ --deploy-url=__REPLACE_ME_WITH_DEPLOY_URL__",
"build:prod": "ng build --configuration=production --configuration production --base-href=__REPLACE_ME_WITH_BASE_HREF__ --deploy-url=__REPLACE_ME_WITH_DEPLOY_URL__",
"build:staging": "ng build --configuration=staging --configuration production --base-href=__REPLACE_ME_WITH_BASE_HREF__ --deploy-url=__REPLACE_ME_WITH_DEPLOY_URL__",
"build:dev": "ng build --configuration=development",
"build:test": "ng build --configuration=test",
"test": "ng test",
"test:coverage": "ng test --codeCoverage=true --watch=false --browsers=ChromeHeadless",
@ -18,75 +23,96 @@
"backend:start": "cd docker && docker-compose up -d --force-recreate && cd ..",
"backend": "npm run backend:stop && npm run backend:build && npm run backend:start",
"env": "chmod +x ./docker/substitute-env-variables.sh && ./docker/substitute-env-variables.sh src/index.html PRODUCTION,API_URL,IRB_URL,HOME_ROUTE,BASE_HREF,DEPLOY_URL,PORT0,GOOGLE_ANALYTICS_KEY,SENTRY_KEY,TITLE",
"ci": "npm run lint && npm run test:coverage && sonar-scanner && npm run env && npm run backend && npm run e2e"
"ci": "npm run lint && npm run test:coverage && sonar-scanner"
},
"private": true,
"dependencies": {
"@angular/animations": "^9.0.7",
"@angular/cdk": "^9.2.1",
"@angular/common": "^9.0.7",
"@angular/compiler": "^9.0.7",
"@angular/core": "^9.0.7",
"@angular/flex-layout": "^9.0.0-beta.29",
"@angular/forms": "^9.0.7",
"@angular/material": "^9.1.3",
"@angular/platform-browser": "^9.0.7",
"@angular/platform-browser-dynamic": "^9.0.7",
"@angular/router": "^9.0.7",
"@ngx-formly/core": "^5.5.15",
"@ngx-formly/material": "^5.5.15",
"@sentry/browser": "^5.16.1",
"@yellowspot/ng-truncate": "^1.5.1",
"bpmn-js": "^6.4.2",
"bpmn-js-properties-panel": "^0.33.2",
"camunda-bpmn-moddle": "^4.4.0",
"camunda-dmn-moddle": "^1.0.0",
"diagram-js": "^6.4.1",
"diagram-js-minimap": "^2.0.3",
"dmn-js": "^7.5.0",
"dmn-js-properties-panel": "^0.3.5",
"dmn-moddle": "^6.0.0",
"file-saver": "^2.0.2",
"ngx-device-detector": "^1.4.1",
"ngx-file-drop": "^8.0.8",
"ngx-markdown": "^9.0.0",
"rxjs": "~6.5.4",
"sartography-workflow-lib": "0.0.266",
"tslib": "^1.11.1",
"uuid": "^7.0.2",
"zone.js": "^0.10.3"
"@angular/animations": "^12.2.1",
"@angular/cdk": "^12.2.1",
"@angular/common": "^12.2.1",
"@angular/compiler": "^12.2.1",
"@angular/core": "^12.2.1",
"@angular/flex-layout": "^12.0.0-beta.34",
"@angular/forms": "^12.2.1",
"@angular/material": "^12.2.1",
"@angular/platform-browser": "^12.2.1",
"@angular/platform-browser-dynamic": "^12.2.1",
"@angular/router": "^12.2.1",
"@bpmn-io/dmn-migrate": "^0.4.3",
"@ngx-formly/core": "^5.10.22",
"@ngx-formly/material": "^5.10.22",
"@sentry/browser": "^5.30.0",
"@yellowspot/ng-truncate": "^1.7.0",
"bpmn-js": "^8.7.1",
"bpmn-js-properties-panel": "^0.44.0",
"camunda-bpmn-moddle": "^5.1.2",
"camunda-dmn-moddle": "^1.1.0",
"diagram-js": "^7.3.0",
"diagram-js-code-editor": "^1.1.85",
"diagram-js-minimap": "^2.0.4",
"dmn-js": "^11.0.1",
"dmn-js-properties-panel": "^0.6.1",
"dmn-js-shared": "^11.0.0",
"dmn-moddle": "^10.0.0",
"file-saver": "^2.0.5",
"fs-extra": "^10.0.0",
"lodash": "^4.5.0",
"ngx-device-detector": "^2.1.1",
"ngx-file-drop": "^11.1.0",
"ngx-highlightjs": "^4.1.4",
"ngx-markdown": "^12.0.1",
"protractor": "^7.0.0",
"rxjs": "^6.5.3",
"sartography-workflow-lib": "0.0.586",
"tslib": "^2.3.0",
"uuid": "^8.3.2",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^9.0.0",
"@angular-devkit/build-angular": "^0.900.7",
"@angular/cli": "^9.0.7",
"@angular/compiler-cli": "^9.0.7",
"@angular/language-service": "^9.0.7",
"@babel/core": "^7.8.7",
"@babel/preset-env": "^7.8.7",
"@babel/preset-react": "^7.8.3",
"@types/jasmine": "^3.5.9",
"@types/jasminewd2": "^2.0.8",
"@types/node": "^12.12.30",
"babel-loader": "^8.0.6",
"codelyzer": "^5.1.2",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "^4.4.1",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "^2.1.1",
"karma-jasmine": "~3.1.1",
"karma-jasmine-html-reporter": "^1.5.2",
"mockdate": "^2.0.5",
"@angular-builders/custom-webpack": "^12.1.0",
"@angular-devkit/build-angular": "^12.2.1",
"@angular-devkit/build-webpack": "^0.1202.1",
"@angular-eslint/builder": "12.3.1",
"@angular-eslint/eslint-plugin": "12.3.1",
"@angular-eslint/eslint-plugin-template": "12.3.1",
"@angular-eslint/schematics": "12.3.1",
"@angular-eslint/template-parser": "12.3.1",
"@angular/cli": "^12.2.1",
"@angular/compiler-cli": "^12.2.1",
"@angular/language-service": "^12.2.1",
"@babel/compat-data": "^7.15.0",
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@types/jasmine": "^3.8.2",
"@types/jasminewd2": "^2.0.10",
"@types/node": "^16.4.13",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "4.28.2",
"babel-loader": "^8.2.2",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.24.0",
"eslint-plugin-jsdoc": "^36.0.7",
"eslint-plugin-prefer-arrow": "^1.2.3",
"jasmine-core": "^3.8.0",
"jasmine-spec-reporter": "^7.0.0",
"karma": "^6.3.4",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage-istanbul-reporter": "^3.0.3",
"karma-jasmine": "^4.0.1",
"karma-jasmine-html-reporter": "^1.7.0",
"lodash.isequal": "^4.5.0",
"mockdate": "^3.0.5",
"moddle-xml": "^9.0.5",
"postcss": "^8.3.6",
"postcss-loader": "^6.1.1",
"postcss-short": "^5.0.0",
"protractor": "^5.4.3",
"puppeteer": "^2.1.1",
"puppeteer": "^10.2.0",
"sonar-scanner": "^3.1.0",
"ts-node": "~8.6.2",
"tslint": "~6.0.0",
"typescript": "~3.7.5",
"webpack": "^4.42.0",
"webpack-dev-server": "^3.10.3",
"webpack-notifier": "^1.8.0"
"ts-node": "^10.2.0",
"typescript": "^4.3.5",
"webpack": "^5.50.0",
"webpack-dev-server": "^3.11.2",
"webpack-notifier": "^1.13.0"
}
}

View File

@ -1,25 +1,26 @@
@use '~@angular/material' as mat;
@import 'config';
@import '../node_modules/@angular/material/theming';
// Define a custom typography config that overrides the font-family
$custom-typography: mat-typography-config(
$custom-typography: mat.define-typography-config(
$font-family: $body-font-family,
$display-4: mat-typography-level(3.75rem, 1.0925, 700, $body-font-family),
$display-3: mat-typography-level(2.4917rem, 1.1037, 700, $body-font-family),
$display-2: mat-typography-level(2.2148rem, 1.1287, 700, $body-font-family),
$display-1: mat-typography-level(1.9688rem, 1.1429, 700, $body-font-family),
$headline: mat-typography-level(1.7500rem, 1.1429, 700, $body-font-family), // h1
$title: mat-typography-level(1.5625rem, 1.1200, 500, $body-font-family), // h2
$subheading-2: mat-typography-level(1.3750rem, 1.0000, 700, $heading-font-family), // h3
$subheading-1: mat-typography-level(1.2500rem, 1.1000, 700, $body-font-family), // h4
$body-2: mat-typography-level(1.1250rem, 1.1111, 500, $body-font-family), // h5
$body-1: mat-typography-level(1.0000rem, 1.1250, 500, $body-font-family), // p
$caption: mat-typography-level(0.8750rem, 0.7143, 500, $body-font-family), // small
$button: mat-typography-level(1.0000rem, 1.1250, 500, $body-font-family),
$input: mat-typography-level(1.0000rem, 1.1250, 500, $body-font-family)
$display-4: mat.define-typography-level(3.75rem, 1.0925, 700, $body-font-family),
$display-3: mat.define-typography-level(2.4917rem, 1.1037, 700, $body-font-family),
$display-2: mat.define-typography-level(2.2148rem, 1.1287, 700, $body-font-family),
$display-1: mat.define-typography-level(1.9688rem, 1.1429, 700, $body-font-family),
$headline: mat.define-typography-level(1.7500rem, 1.1429, 700, $body-font-family), // h1
$title: mat.define-typography-level(1.5625rem, 1.1200, 500, $body-font-family), // h2
$subheading-2: mat.define-typography-level(1.3750rem, 1.0000, 700, $heading-font-family), // h3
$subheading-1: mat.define-typography-level(1.2500rem, 1.1000, 700, $body-font-family), // h4
$body-2: mat.define-typography-level(1.1250rem, 1.1111, 500, $body-font-family), // h5
$body-1: mat.define-typography-level(1.0000rem, 1.1250, 500, $body-font-family), // p
$caption: mat.define-typography-level(0.8750rem, 0.7143, 500, $body-font-family), // small
$button: mat.define-typography-level(1.0000rem, 1.1250, 500, $body-font-family),
$input: mat.define-typography-level(1.0000rem, 1.1250, 500, $body-font-family)
);
$mat-blue: (
mat.$blue-palette: (
50: #f1f5f7,
100: #b3c1d3,
200: $brand-primary-tint-3,
@ -52,7 +53,7 @@ $mat-blue: (
)
);
$mat-orange: (
mat.$orange-palette: (
50: #fceee0,
100: $brand-accent-tint-3,
200: $brand-accent-tint-2,
@ -85,8 +86,8 @@ $mat-orange: (
)
);
$cr-connect-primary: mat-palette($mat-blue);
$cr-connect-accent: mat-palette($mat-orange);
$cr-connect-warn: mat-palette($mat-red);
$cr-connect-theme: mat-light-theme($cr-connect-primary, $cr-connect-accent, $cr-connect-warn);
$cr-connect-primary: mat.define-palette(mat.$blue-palette);
$cr-connect-accent: mat.define-palette(mat.$orange-palette);
$cr-connect-warn: mat.define-palette(mat.$red-palette);
$cr-connect-theme: mat.define-light-theme($cr-connect-primary, $cr-connect-accent, $cr-connect-warn);

View File

@ -0,0 +1,12 @@
<mat-card fxLayoutGap>
<p mat-dialog-title>{{data.title}}</p>
<p display-2>{{data.message}}</p>
<div mat-dialog-actions>
<button mat-button (click)="onDismiss()">No</button>
<button mat-raised-button color="primary" (click)="onConfirm()">Yes</button>
</div>
</mat-card>

View File

@ -0,0 +1,3 @@
mat-dialog-container {
padding: 0px !important
}

View File

@ -0,0 +1,40 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { ConfirmDialogComponent } from './confirm-dialog.component';
describe('ConfirmDialogComponent', () => {
let component: ConfirmDialogComponent;
let fixture: ComponentFixture<ConfirmDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ConfirmDialogComponent ],
imports : [MatDialogModule],
providers: [
{
provide: MatDialogRef,
useValue: {
close: (dialogResult: any) => {
}
}
},
{provide: MAT_DIALOG_DATA, useValue: {
confirm: false,
}},
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ConfirmDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,27 @@
import { Component, OnInit, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {ConfirmDialogData} from '../../_interfaces/dialog-data';
@Component({
selector: 'app-confirm-dialog',
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss']
})
export class ConfirmDialogComponent {
constructor(
public dialogRef: MatDialogRef<ConfirmDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ConfirmDialogData
) {
}
onConfirm(): void {
// Close the dialog, return true
this.dialogRef.close(true);
}
onDismiss(): void {
// Close the dialog, return false
this.dialogRef.close(false);
}
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
@ -11,7 +11,7 @@ describe('DeleteFileDialogComponent', () => {
let component: DeleteFileDialogComponent;
let fixture: ComponentFixture<DeleteFileDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,

View File

@ -27,4 +27,5 @@ export class DeleteFileDialogComponent {
this.dialogRef.close(data);
}
}

View File

@ -1,5 +1,5 @@
<div mat-dialog-title *ngIf="data && data.category">
Delete category <strong>{{data.category.name}}</strong>?
Delete category <strong>{{data.category.display_name}}</strong>?
<button mat-icon-button mat-dialog-close=""><mat-icon>close</mat-icon></button>
</div>
<div mat-dialog-content>

View File

@ -1,4 +1,4 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
@ -11,7 +11,7 @@ describe('DeleteFileDialogComponent', () => {
let component: DeleteWorkflowSpecCategoryDialogComponent;
let fixture: ComponentFixture<DeleteWorkflowSpecCategoryDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,

View File

@ -1,5 +1,5 @@
<div mat-dialog-title *ngIf="data && data.workflowSpec">
Delete workflow specification <strong>{{data.workflowSpec.name}}</strong>?
Delete workflow specification <strong>{{data.workflowSpec.id}}</strong>?
<button mat-icon-button mat-dialog-close=""><mat-icon>close</mat-icon></button>
</div>
<div mat-dialog-content>

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
@ -11,7 +11,7 @@ describe('DeleteFileDialogComponent', () => {
let component: DeleteWorkflowSpecDialogComponent;
let fixture: ComponentFixture<DeleteWorkflowSpecDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,

View File

@ -1,4 +1,4 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import {MatFormFieldModule} from '@angular/material/form-field';
@ -6,16 +6,16 @@ import {MatInputModule} from '@angular/material/input';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {FormlyModule} from '@ngx-formly/core';
import {FormlyMaterialModule} from '@ngx-formly/material';
import createClone from 'rfdc';
import {FileType} from 'sartography-workflow-lib';
import {FileMetaDialogData} from '../../_interfaces/dialog-data';
import {FileMetaDialogComponent} from './file-meta-dialog.component';
import { cloneDeep } from 'lodash';
describe('EditFileMetaDialogComponent', () => {
let component: FileMetaDialogComponent;
let fixture: ComponentFixture<FileMetaDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [FileMetaDialogComponent],
imports: [
@ -86,7 +86,7 @@ describe('EditFileMetaDialogComponent', () => {
component.model = dataBefore;
component.onSubmit();
const expectedData: FileMetaDialogData = createClone()(dataBefore);
const expectedData: FileMetaDialogData = cloneDeep(dataBefore);
expectedData.fileName = 'green_eggs.v1-2020-01-01.XML.bpmn';
expect(closeSpy).toHaveBeenCalledWith(expectedData);
});

View File

@ -20,12 +20,10 @@ export class FileMetaDialogComponent {
public dialogRef: MatDialogRef<FileMetaDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: FileMetaDialogData,
) {
const fileTypeOptions = Object.entries(FileType).map(ft => {
return {
const fileTypeOptions = Object.entries(FileType).map(ft => ({
label: ft[0],
value: ft[1]
};
});
}));
this.fields = [
{

View File

@ -1,4 +1,4 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
@ -12,7 +12,7 @@ describe('NewFileDialogComponent', () => {
let component: NewFileDialogComponent;
let fixture: ComponentFixture<NewFileDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,

View File

@ -22,7 +22,7 @@ export class NewFileDialogComponent {
}
onSubmit(fileType: FileType) {
const data: NewFileDialogData = {fileType: fileType};
const data: NewFileDialogData = {fileType};
this.dialogRef.close(data);
}

View File

@ -2,6 +2,7 @@
<ng-container *ngIf="!mode">Where is your file?</ng-container>
<ng-container *ngIf="mode === 'local'">Upload a local {{fileTypesString()}} file</ng-container>
<ng-container *ngIf="mode === 'remote'">Open a {{fileTypesString()}} file from URL</ng-container>
<ng-container *ngIf="mode === 'reference'">Upload a new reference file ({{fileTypes}})</ng-container>
<span fxFlex></span>
<button mat-icon-button mat-dialog-close=""><mat-icon>close</mat-icon></button>
</div>
@ -28,6 +29,17 @@
<button (click)="cancel()" mat-flat-button>Cancel</button>
</div>
<div *ngIf="mode === 'reference'">
<mat-form-field (click)="fileInput.click()">
<span matPrefix><mat-icon>folder_open</mat-icon> &nbsp;</span>
<input [value]="getFileName()" disabled matInput type="text">
</mat-form-field>
<input #fileInput (change)="onFileSelected($event)" accept="{{fileExtensions()}}" hidden id="file"
type="file">
<button (click)="onSubmit()" [disabled]="!data.file" color="primary" mat-flat-button>Upload File</button>
<button (click)="cancel()" mat-flat-button>Cancel</button>
</div>
<div *ngIf="mode === 'remote'">
<mat-form-field>
<span matPrefix><mat-icon>link</mat-icon> &nbsp;</span>

View File

@ -1,6 +1,6 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import {MatFormFieldModule} from '@angular/material/form-field';
@ -9,7 +9,7 @@ import {MatInputModule} from '@angular/material/input';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {ApiService, MockEnvironment, mockFileMeta0} from 'sartography-workflow-lib';
import {ApiService, MockEnvironment, mockFile0, mockFileMeta0} from 'sartography-workflow-lib';
import {OpenFileDialogData} from '../../_interfaces/dialog-data';
import { OpenFileDialogComponent } from './open-file-dialog.component';
@ -20,7 +20,7 @@ describe('OpenFileDialogComponent', () => {
let fixture: ComponentFixture<OpenFileDialogComponent>;
const mockRouter = {navigate: jasmine.createSpy('navigate')};
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
@ -78,14 +78,14 @@ describe('OpenFileDialogComponent', () => {
it('should save data on submit', () => {
const closeSpy = spyOn(component.dialogRef, 'close').and.stub();
component.data.file = mockFileMeta0.file;
component.data.file = mockFile0;
component.onSubmit();
expect(closeSpy).toHaveBeenCalledWith(component.data);
});
it('should not change data on cancel', () => {
const closeSpy = spyOn(component.dialogRef, 'close').and.stub();
const expectedData: OpenFileDialogData = { file: mockFileMeta0.file };
const expectedData: OpenFileDialogData = { file: mockFile0 };
component.data.file = expectedData.file;
component.onNoClick();
@ -111,14 +111,14 @@ describe('OpenFileDialogComponent', () => {
component.data.file = undefined;
expect(component.getFileName()).toEqual('Click to select a file');
component.data.file = mockFileMeta0.file;
expect(component.getFileName()).toEqual(mockFileMeta0.file.name);
component.data.file = mockFile0;
expect(component.getFileName()).toEqual(mockFile0.name);
});
it('should get a file from the file input field event', () => {
const event = {target: {files: [mockFileMeta0.file]}};
const event = {target: {files: [mockFile0]}};
(component as any).onFileSelected(event);
expect(component.data.file).toEqual(mockFileMeta0.file);
expect(component.data.file).toEqual(mockFile0);
});
it('should determine if a string is a valid URL', () => {

View File

@ -1,7 +1,8 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {ApiService, cleanUpFilename, FileType, getDiagramTypeFromXml} from 'sartography-workflow-lib';
import {ApiService, cleanUpFilename, FileType} from 'sartography-workflow-lib';
import {OpenFileDialogData} from '../../_interfaces/dialog-data';
import { getDiagramTypeFromXml } from '../../_util/diagram-type';
@Component({
selector: 'app-open-file-dialog',
@ -56,7 +57,7 @@ export class OpenFileDialogComponent {
}
isValidUrl() {
// tslint:disable-next-line:max-line-length
// eslint-disable-next-line max-len
const re = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/i;
return re.test(this.url);
}

View File

@ -1,4 +1,4 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatFormFieldModule} from '@angular/material/form-field';
@ -15,7 +15,7 @@ describe('WorkflowSpecCategoryDialogComponent', () => {
let component: WorkflowSpecCategoryDialogComponent;
let fixture: ComponentFixture<WorkflowSpecCategoryDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,

View File

@ -3,7 +3,7 @@ import {FormGroup} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {FormlyFieldConfig, FormlyFormOptions} from '@ngx-formly/core';
import {toSnakeCase} from 'sartography-workflow-lib';
import {WorkflowSpecCategoryDialogData, WorkflowSpecDialogData} from '../../_interfaces/dialog-data';
import {WorkflowSpecCategoryDialogData} from '../../_interfaces/dialog-data';
@Component({
selector: 'app-workflow-spec-category-dialog',
@ -25,18 +25,7 @@ export class WorkflowSpecCategoryDialogComponent {
placeholder: 'ID of workflow spec category',
required: true,
},
},
{
key: 'name',
type: 'input',
defaultValue: this.data.name,
templateOptions: {
label: 'Name',
placeholder: 'Name of workflow spec category',
description: 'Enter a name, in lowercase letters, separated by underscores, that is easy for you to remember.' +
'It will be converted to all_lowercase_with_underscores when you save.',
required: true,
},
hideExpression: true,
},
{
key: 'display_name',
@ -51,17 +40,15 @@ export class WorkflowSpecCategoryDialogComponent {
},
},
{
key: 'display_order',
type: 'input',
defaultValue: this.data.display_order,
key: 'admin',
type: 'checkbox',
defaultValue: this.data.admin ? this.data.admin : false,
templateOptions: {
type: 'number',
label: 'Display Order',
placeholder: 'Order in which category will be displayed',
description: 'Sort order that the category should appear in. Lower numbers will appear first.',
required: true,
},
},
label: 'Admin Category',
description: 'Should this category only be shown to Admins?',
indeterminate: false,
}
}
];
constructor(
@ -75,6 +62,7 @@ export class WorkflowSpecCategoryDialogComponent {
}
onSubmit() {
console.log('data is ', this.model);
this.model.name = toSnakeCase(this.model.name);
this.dialogRef.close(this.model);
}

View File

@ -1,7 +1,11 @@
<div mat-dialog-title>
<div *ngIf="!this.data.library" mat-dialog-title>
<h1>Workflow Specification</h1>
</div>
<div *ngIf="this.data.library" mat-dialog-title>
<h1>Library Specification</h1>
</div>
<div mat-dialog-content>
<form [formGroup]="form">
<formly-form [model]="model" [fields]="fields" [options]="options" [form]="form"></formly-form>

View File

@ -1,3 +1,3 @@
::ng-deep formly-field mat-form-field {
margin-bottom: 40px;
}

View File

@ -1,6 +1,6 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatFormFieldModule} from '@angular/material/form-field';
@ -11,7 +11,13 @@ import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {FormlyModule} from '@ngx-formly/core';
import {FormlyMaterialModule} from '@ngx-formly/material';
import {ApiService, MockEnvironment, mockWorkflowSpec0, mockWorkflowSpecCategories} from 'sartography-workflow-lib';
import {
ApiService,
MockEnvironment,
mockWorkflowSpec0,
mockWorkflowSpecCategories,
mockWorkflowSpecs
} from 'sartography-workflow-lib';
import {WorkflowSpecDialogData} from '../../_interfaces/dialog-data';
import {WorkflowSpecDialogComponent} from './workflow-spec-dialog.component';
@ -22,7 +28,7 @@ describe('WorkflowSpecDialogComponent', () => {
let fixture: ComponentFixture<WorkflowSpecDialogComponent>;
const mockRouter = {navigate: jasmine.createSpy('navigate')};
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
@ -74,6 +80,12 @@ describe('WorkflowSpecDialogComponent', () => {
expect(catReq.request.method).toEqual('GET');
catReq.flush(mockWorkflowSpecCategories);
expect(component.categories.length).toBeGreaterThan(0);
const specReq = httpMock.expectOne('apiRoot/workflow-specification');
expect(specReq.request.method).toEqual('GET');
specReq.flush(mockWorkflowSpecs);
expect(component.specs.length).toBeGreaterThan(0);
});
afterEach(() => {

View File

@ -1,22 +1,24 @@
import {Component, Inject} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {FormControl, FormGroup, ValidationErrors} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {FormlyFieldConfig, FormlyFormOptions, FormlyTemplateOptions} from '@ngx-formly/core';
import {ApiService, toSnakeCase} from 'sartography-workflow-lib';
import {v4 as uuidv4} from 'uuid';
import {WorkflowSpecDialogData} from '../../_interfaces/dialog-data';
import {of} from "rxjs";
@Component({
selector: 'app-workflow-spec-dialog',
templateUrl: './workflow-spec-dialog.component.html',
styleUrls: ['./workflow-spec-dialog.component.scss']
})
export class WorkflowSpecDialogComponent {
form: FormGroup = new FormGroup({});
model: any = {};
options: FormlyFormOptions = {};
fields: FormlyFieldConfig[] = [];
categories: any;
specs: any;
constructor(
private api: ApiService,
@ -24,36 +26,80 @@ export class WorkflowSpecDialogComponent {
@Inject(MAT_DIALOG_DATA) public data: WorkflowSpecDialogData,
) {
this.api.getWorkflowSpecCategoryList().subscribe(cats => {
this.categories = cats.map(c => {
return {
this.categories = cats.map(c => ({
value: c.id,
label: c.display_name,
};
});
}));
this.api.getWorkflowSpecList().subscribe(wfs => {
this.specs = wfs.map(w => w.id);
this.fields = [
{
key: 'id',
key: 'display_name',
type: 'input',
defaultValue: this.data.id || uuidv4(),
defaultValue: this.data.display_name,
templateOptions: {
label: 'ID',
placeholder: 'UUID of workflow specification',
description: 'This is an autogenerated unique ID and is not editable.',
label: 'Display Name',
placeholder: 'Title of the workflow specification',
description: 'This is a human-readable title for the workflow specification,' +
'which should be easy for others to read and remember.',
required: true,
disabled: true,
modelOptions:
{
updateOn: 'blur',
}
},
},
{
key: 'name',
key: 'id',
type: 'input',
defaultValue: this.data.name,
defaultValue: this.data.id,
templateOptions: {
label: 'Name',
label: 'ID',
placeholder: 'Name of workflow specification',
description: 'Enter a name, in lowercase letters, separated by underscores, that is easy for you to remember.' +
description: 'Enter a name to identify this spec. It cannot be changed later.' +
'It will be converted to all_lowercase_with_underscores when you save.',
required: true,
disabled: this.data.id !== '',
help: 'This must be in a universal format for XML standards. ' +
'It can only contain letters, numbers, and underscores. ' +
'It should not start with a digit.',
modelOptions:
{
updateOn: 'focus',
}
},
expressionProperties: {
'model.id': (m, formState, field) => {
if (!m.id && field.focus) {
m.id = m.display_name.replace(/ /g,"_").toLowerCase();
field.formControl.markAsDirty();
return m.id;
} else {
return m.id;
}
},
'templateOptions.change': (m, formState, field)=> {
if (field.focus) {
field.formControl.updateValueAndValidity();
}
},
},
asyncValidators: {
uniqueID: {
expression: (control: FormControl) => {
return of(this.specs.indexOf(control.value) === -1);
},
message: 'This ID name is already taken.',
},
},
validators: {
formatter: {
expression: (c) => !c.value || /^[A-Za-z_][A-Za-z0-9]*(?:_[A-Za-z0-9]+)*$/.test(c.value),
message: (error, field: FormlyFieldConfig) => `"${field.formControl.value}" is not in a valid format.`,
},
},
},
{
@ -66,18 +112,7 @@ export class WorkflowSpecDialogComponent {
required: true,
options: this.categories
},
},
{
key: 'display_name',
type: 'input',
defaultValue: this.data.display_name,
templateOptions: {
label: 'Display Name',
placeholder: 'Title of the workflow specification',
description: 'This is a human-readable title for the workflow specification,' +
'which should be easy for others to read and remember.',
required: true,
},
hideExpression: this.data.library,
},
{
key: 'description',
@ -89,24 +124,24 @@ export class WorkflowSpecDialogComponent {
description: 'Write a few sentences explaining to users why this workflow exists and what it should be used for.',
required: true,
},
},
{
key: 'standalone',
defaultValue: this.data.standalone ? this.data.standalone : false,
hideExpression: true,
},
{
key: 'display_order',
type: 'input',
defaultValue: this.data.display_order,
templateOptions: {
type: 'number',
label: 'Display Order',
placeholder: 'Order in which spec will be displayed',
description: 'Sort order that the spec should appear in within its category. Lower numbers will appear first.',
required: true,
},
key: 'library',
defaultValue: this.data.library ? this.data.library : false,
hideExpression: true,
},
];
});
});
}
onNoClick() {
console.log('form model : ', this.model);
this.dialogRef.close();
}

View File

@ -1,4 +1,4 @@
// tslint:disable-next-line:max-line-length
// eslint-disable-next-line max-len
export const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;
export default EMAIL_REGEX;

View File

@ -1,9 +1,9 @@
import { AbstractControl, ValidationErrors } from '@angular/forms';
import EMAIL_REGEX from './email.regex';
export function ValidateEmail(control: AbstractControl): ValidationErrors {
export const ValidateEmail = (control: AbstractControl): ValidationErrors => {
if (!EMAIL_REGEX.test(control.value) && control.value && control.value !== '') {
const error: ValidationErrors = { email: true };
return error;
}
}
};

View File

@ -4,31 +4,19 @@ import EMAIL_REGEX from './email.regex';
import PHONE_REGEX from './phone.regex';
import URL_REGEX from './url.regex';
export function EmailValidator(control: FormControl): ValidationErrors {
return !control.value || EMAIL_REGEX.test(control.value) ? null : {email: true};
}
export const EmailValidator = (control: FormControl): ValidationErrors => !control.value || EMAIL_REGEX.test(control.value) ? null : {email: true};
export function EmailValidatorMessage(err, field: FormlyFieldConfig) {
return `"${field.formControl.value}" is not a valid email address`;
}
export const EmailValidatorMessage = (err, field: FormlyFieldConfig) => `"${field.formControl.value}" is not a valid email address`;
export function UrlValidator(control: FormControl): ValidationErrors {
return !control.value || URL_REGEX.test(control.value) ? null : {url: true};
}
export const UrlValidator = (control: FormControl): ValidationErrors => !control.value || URL_REGEX.test(control.value) ? null : {url: true};
export function UrlValidatorMessage(err, field: FormlyFieldConfig) {
return `We cannot save "${field.formControl.value}". Please provide the full path, including http:// or https://`;
}
export const UrlValidatorMessage = (err, field: FormlyFieldConfig) => `We cannot save "${field.formControl.value}". Please provide the full path, including http:// or https://`;
export function PhoneValidator(control: FormControl): ValidationErrors {
return !control.value || PHONE_REGEX.test(control.value) ? null : {phone: true};
}
export const PhoneValidator = (control: FormControl): ValidationErrors => !control.value || PHONE_REGEX.test(control.value) ? null : {phone: true};
export function PhoneValidatorMessage(err, field: FormlyFieldConfig) {
return `"${field.formControl.value}" is not a valid phone number`;
}
export const PhoneValidatorMessage = (err, field: FormlyFieldConfig) => `"${field.formControl.value}" is not a valid phone number`;
export function MulticheckboxValidator(control: FormControl): ValidationErrors {
export const MulticheckboxValidator = (control: FormControl): ValidationErrors => {
if (control.value) {
for (const key in control.value) {
if (control.value[key] === true) {
@ -37,26 +25,18 @@ export function MulticheckboxValidator(control: FormControl): ValidationErrors {
}
}
return {required: true};
}
};
export function MulticheckboxValidatorMessage(err, field: FormlyFieldConfig) {
return 'At least one of these checkboxes must be selected.';
}
export const MulticheckboxValidatorMessage = (err, field: FormlyFieldConfig) => 'At least one of these checkboxes must be selected.';
export function MinValidationMessage(err, field) {
return `This value should be more than ${field.templateOptions.min}`;
}
export const MinValidationMessage = (err, field) => `This value should be more than ${field.templateOptions.min}`;
export function MaxValidationMessage(err, field) {
return `This value should be less than ${field.templateOptions.max}`;
}
export const MaxValidationMessage = (err, field) => `This value should be less than ${field.templateOptions.max}`;
export function ShowError(field: FieldType) {
return field.formControl &&
field.formControl.invalid &&
(
field.formControl.dirty ||
(field.options && field.options.parentForm && field.options.parentForm.submitted) ||
(field.field && field.field.validation && field.field.validation.show)
);
}
export const ShowError = (field: FieldType) => field.formControl &&
field.formControl.invalid &&
(
field.formControl.dirty ||
(field.options && field.options.parentForm && field.options.parentForm.submitted) ||
(field.field && field.field.validation && field.field.validation.show)
);

View File

@ -1,4 +1,4 @@
// tslint:disable-next-line:max-line-length
// eslint-disable-next-line max-len
export const PHONE_REGEX = /^(?:\+?1\s*(?:[.-]\s*)?)?(?:\(\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\s*\)|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\s*(?:[.-]\s*)?([2-9]1[02-9]|[2-9][02-9]1|[2-9][02-9]{2})\s*(?:[.-]\s*)?([0-9]{4})(?:\s*(?:#|x\.?|ext\.?|extension)\s*(\d+))?$/i;
export default PHONE_REGEX;

View File

@ -1,6 +1,6 @@
import { AbstractControl, ValidationErrors } from '@angular/forms';
export function ValidateUrl(control: AbstractControl): ValidationErrors {
export const ValidateUrl = (control: AbstractControl): ValidationErrors => {
const urlRegEx = new RegExp(
'^' +
@ -28,4 +28,4 @@ export function ValidateUrl(control: AbstractControl): ValidationErrors {
const error: ValidationErrors = { url: true };
return error;
}
}
};

View File

@ -1,7 +1,12 @@
export interface BpmnError {
warnings: BpmnWarning[];
}
export interface BpmnWarning {
message?: string;
element?: any;
property?: string;
value?: string;
context?: any;
error?: Error;
}

View File

@ -20,18 +20,19 @@ export interface OpenFileDialogData {
export interface WorkflowSpecDialogData {
id: string;
name: string;
display_name: string;
description: string;
category_id: number;
display_order: number;
standalone: boolean;
library: boolean;
}
export interface WorkflowSpecCategoryDialogData {
id: number;
name: string;
display_name: string;
display_order?: number;
admin: boolean;
}
export interface DeleteFileDialogData {
@ -39,6 +40,13 @@ export interface DeleteFileDialogData {
fileMeta: FileMeta;
}
export interface ConfirmDialogData {
title: string;
message: string;
}
export interface DeleteWorkflowSpecDialogData {
confirm: boolean;
workflowSpec: WorkflowSpec;

View File

@ -1,6 +1,6 @@
export interface ModelerConfig {
additionalModules: any[];
moddleExtensions: {
camunda: any
camunda: any;
};
}

View File

@ -0,0 +1,3 @@
import { FileType } from 'sartography-workflow-lib';
export const getDiagramTypeFromXml = (xml: string): FileType =>(xml && /dmn\.xsd|dmndi:DMNDiagram/.test(xml) ? FileType.DMN : FileType.BPMN);

View File

@ -1,12 +1,13 @@
import {APP_BASE_HREF} from '@angular/common';
import {Injectable, NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ExtraOptions, RouterModule, Routes} from '@angular/router';
import {AppEnvironment, SessionRedirectComponent} from 'sartography-workflow-lib';
import {environment} from '../environments/environment.runtime';
import {HomeComponent} from './home/home.component';
import {ModelerComponent} from './modeler/modeler.component';
import {ProtocolBuilderComponent} from './protocol-builder/protocol-builder.component';
import {ReferenceFilesComponent} from './reference-files/reference-files.component';
import {WorkflowSpecListComponent} from './workflow-spec-list/workflow-spec-list.component';
import {SettingsComponent} from './settings/settings.component';
@Injectable()
export class ThisEnvironment implements AppEnvironment {
@ -30,8 +31,8 @@ const routes: Routes = [
component: HomeComponent
},
{
path: 'pb',
component: ProtocolBuilderComponent
path: 'home/:spec',
component: WorkflowSpecListComponent
},
{
path: 'reffiles',
@ -48,6 +49,10 @@ const routes: Routes = [
{
path: 'session',
component: SessionRedirectComponent
},
{
path: 'settings',
component: SettingsComponent
}
];
@ -55,10 +60,11 @@ const routes: Routes = [
declarations: [],
imports: [
RouterModule.forRoot(routes, {
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
scrollOffset: [0, 84],
})
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
scrollOffset: [0, 84],
relativeLinkResolution: 'legacy'
})
],
exports: [RouterModule],
providers: [

View File

@ -1,7 +1,7 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClient} from '@angular/common/http';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {MatIconModule} from '@angular/material/icon';
import {FakeMatIconRegistry} from '@angular/material/icon/testing';
import {MatMenuModule} from '@angular/material/menu';
@ -17,7 +17,7 @@ describe('AppComponent', () => {
const mockEnvironment = new MockEnvironment();
const mockTitle = `'Once,' said the Mock Title at last, with a deep sigh, 'I was a real Title.'`;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,

View File

@ -1,55 +1,63 @@
import {APP_BASE_HREF, PlatformLocation} from '@angular/common';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {Injectable, NgModule} from '@angular/core';
import {FlexLayoutModule} from '@angular/flex-layout';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatBottomSheetModule} from '@angular/material/bottom-sheet';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatDialogModule} from '@angular/material/dialog';
import {MatDividerModule} from '@angular/material/divider';
import {MAT_FORM_FIELD_DEFAULT_OPTIONS} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatListModule} from '@angular/material/list';
import {MatMenuModule} from '@angular/material/menu';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatTabsModule} from '@angular/material/tabs';
import {MatToolbarModule} from '@angular/material/toolbar';
import {MatTooltipModule} from '@angular/material/tooltip';
import {BrowserModule} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {FormlyModule} from '@ngx-formly/core';
import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { Injectable, NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormlyModule } from '@ngx-formly/core';
import {
AppEnvironment,
AuthInterceptor,
ErrorInterceptor,
SartographyFormsModule,
SartographyPipesModule,
SartographyWorkflowLibModule
SartographyWorkflowLibModule,
} from 'sartography-workflow-lib';
import {environment} from '../environments/environment.runtime';
import {DeleteFileDialogComponent} from './_dialogs/delete-file-dialog/delete-file-dialog.component';
import {DeleteWorkflowSpecCategoryDialogComponent} from './_dialogs/delete-workflow-spec-category-dialog/delete-workflow-spec-category-dialog.component';
import {DeleteWorkflowSpecDialogComponent} from './_dialogs/delete-workflow-spec-dialog/delete-workflow-spec-dialog.component';
import {FileMetaDialogComponent} from './_dialogs/file-meta-dialog/file-meta-dialog.component';
import {NewFileDialogComponent} from './_dialogs/new-file-dialog/new-file-dialog.component';
import {OpenFileDialogComponent} from './_dialogs/open-file-dialog/open-file-dialog.component';
import {WorkflowSpecCategoryDialogComponent} from './_dialogs/workflow-spec-category-dialog/workflow-spec-category-dialog.component';
import {WorkflowSpecDialogComponent} from './_dialogs/workflow-spec-dialog/workflow-spec-dialog.component';
import {GetIconCodePipe} from './_pipes/get-icon-code.pipe';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {DiagramComponent} from './diagram/diagram.component';
import {FileListComponent} from './file-list/file-list.component';
import {FooterComponent} from './footer/footer.component';
import {HomeComponent} from './home/home.component';
import {ModelerComponent} from './modeler/modeler.component';
import {NavbarComponent} from './navbar/navbar.component';
import {ProtocolBuilderComponent} from './protocol-builder/protocol-builder.component';
import {ReferenceFilesComponent} from './reference-files/reference-files.component';
import {WorkflowSpecCardComponent} from './workflow-spec-card/workflow-spec-card.component';
import {WorkflowSpecListComponent} from './workflow-spec-list/workflow-spec-list.component';
import { environment } from '../environments/environment.runtime';
import { DeleteFileDialogComponent } from './_dialogs/delete-file-dialog/delete-file-dialog.component';
import {
DeleteWorkflowSpecCategoryDialogComponent
} from './_dialogs/delete-workflow-spec-category-dialog/delete-workflow-spec-category-dialog.component';
import { DeleteWorkflowSpecDialogComponent } from './_dialogs/delete-workflow-spec-dialog/delete-workflow-spec-dialog.component';
import { FileMetaDialogComponent } from './_dialogs/file-meta-dialog/file-meta-dialog.component';
import { NewFileDialogComponent } from './_dialogs/new-file-dialog/new-file-dialog.component';
import { OpenFileDialogComponent } from './_dialogs/open-file-dialog/open-file-dialog.component';
import { WorkflowSpecCategoryDialogComponent } from './_dialogs/workflow-spec-category-dialog/workflow-spec-category-dialog.component';
import { WorkflowSpecDialogComponent } from './_dialogs/workflow-spec-dialog/workflow-spec-dialog.component';
import { GetIconCodePipe } from './_pipes/get-icon-code.pipe';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { DiagramComponent } from './diagram/diagram.component';
import { FileListComponent } from './file-list/file-list.component';
import { FooterComponent } from './footer/footer.component';
import { HomeComponent } from './home/home.component';
import { ModelerComponent } from './modeler/modeler.component';
import { NavbarComponent } from './navbar/navbar.component';
import { ReferenceFilesComponent } from './reference-files/reference-files.component';
import { WorkflowSpecCardComponent } from './workflow-spec-card/workflow-spec-card.component';
import { WorkflowSpecListComponent } from './workflow-spec-list/workflow-spec-list.component';
import { MatSidenavModule } from '@angular/material/sidenav';
import { ConfirmDialogComponent } from './_dialogs/confirm-dialog/confirm-dialog.component';
import { MatExpansionModule } from '@angular/material/expansion';
import { SettingsComponent } from './settings/settings.component';
import { MatSelectModule } from '@angular/material/select';
import { LibraryListComponent } from './library-list/library-list.component';
import { MatCheckboxModule } from '@angular/material/checkbox';
@Injectable()
export class ThisEnvironment implements AppEnvironment {
@ -73,9 +81,7 @@ export class ThisEnvironment implements AppEnvironment {
* @param platformLocation an Angular service used to interact with a browser's URL
* @return a string instance of the `<base href="" />` value from `index.html`
*/
export function getBaseHref(platformLocation: PlatformLocation): string {
return platformLocation.getBaseHrefFromDOM();
}
export const getBaseHref = (platformLocation: PlatformLocation): string => platformLocation.getBaseHrefFromDOM();
@NgModule({
declarations: [
@ -88,6 +94,7 @@ export function getBaseHref(platformLocation: PlatformLocation): string {
FileMetaDialogComponent,
FooterComponent,
GetIconCodePipe,
LibraryListComponent,
ModelerComponent,
NavbarComponent,
NewFileDialogComponent,
@ -97,8 +104,9 @@ export function getBaseHref(platformLocation: PlatformLocation): string {
WorkflowSpecListComponent,
HomeComponent,
WorkflowSpecCardComponent,
ProtocolBuilderComponent,
ReferenceFilesComponent,
ConfirmDialogComponent,
SettingsComponent,
],
imports: [
BrowserAnimationsModule,
@ -120,11 +128,16 @@ export function getBaseHref(platformLocation: PlatformLocation): string {
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatSelectModule,
ReactiveFormsModule,
SartographyFormsModule,
SartographyPipesModule,
SartographyWorkflowLibModule,
AppRoutingModule, // <-- This line MUST be last (https://angular.io/guide/router#module-import-order-matters)
AppRoutingModule,
MatSidenavModule,
MatExpansionModule,
MatCheckboxModule,
// <-- This line MUST be last (https://angular.io/guide/router#module-import-order-matters)
],
bootstrap: [AppComponent],
entryComponents: [
@ -142,8 +155,9 @@ export function getBaseHref(platformLocation: PlatformLocation): string {
{provide: 'APP_ENVIRONMENT', useClass: ThisEnvironment},
{provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true},
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
{provide: APP_BASE_HREF, useFactory: getBaseHref, deps: [PlatformLocation]}
]
{provide: APP_BASE_HREF, useFactory: getBaseHref, deps: [PlatformLocation]},
],
})
export class AppModule {
}

View File

@ -1,16 +1,18 @@
import propertiesPanelModule from 'bpmn-js-properties-panel';
import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda';
import * as bpmnModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda.json';
import * as camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda.json';
import minimapModule from 'diagram-js-minimap';
import codeModule from 'diagram-js-code-editor';
import {ModelerConfig} from '../_interfaces/modeler-config';
export const bpmnModelerConfig: ModelerConfig = {
additionalModules: [
propertiesProviderModule,
propertiesPanelModule,
propertiesProviderModule,
propertiesPanelModule,
minimapModule,
codeModule,
],
moddleExtensions: {
camunda: bpmnModdleDescriptor['default']
camunda: camundaModdleDescriptor
}
};

View File

@ -14,3 +14,7 @@
font-size: 24px;
}
}
.diagram-container {
width: 100%;
height: 100%;
}

View File

@ -1,27 +1,43 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {DebugNode} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MatIconModule} from '@angular/material/icon';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import { APP_BASE_HREF } from '@angular/common';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DebugNode } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { MatIconModule } from '@angular/material/icon';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import * as FileSaver from 'file-saver';
import {ApiService, BPMN_DIAGRAM_DEFAULT, FileType, MockEnvironment} from 'sartography-workflow-lib';
import {
ApiService,
BPMN_DIAGRAM_DEFAULT,
DMN_DIAGRAM_DEFAULT,
FileType,
MockEnvironment,
} from 'sartography-workflow-lib';
import {
BPMN_DIAGRAM,
BPMN_DIAGRAM_WITH_WARNINGS,
DMN_DIAGRAM,
DMN_DIAGRAM_WITH_WARNINGS
DMN_DIAGRAM, DMN_DIAGRAM_EMPTY,
DMN_DIAGRAM_WITH_WARNINGS,
} from '../../testing/mocks/diagram.mocks';
import {DiagramComponent} from './diagram.component';
import { DiagramComponent } from './diagram.component';
describe('DiagramComponent', () => {
let httpMock: HttpTestingController;
let fixture: ComponentFixture<DiagramComponent>;
let component: DebugNode['componentInstance'];
const mockRouter = {navigate: jasmine.createSpy('navigate')};
const testSaveAs = async (fileType: FileType, xml: string, callMethod: string, filename: string) => {
const saveDiagramSpy = spyOn(component, 'saveDiagram').and.callThrough();
const fileSaveAsSpy = spyOn(FileSaver, 'saveAs').and.stub();
component.fileName = filename + '.' + fileType;
component.diagramType = fileType;
component.xml = xml;
await component[callMethod]();
await expect(saveDiagramSpy).toHaveBeenCalled();
await expect(fileSaveAsSpy).toHaveBeenCalledWith(jasmine.any(Blob), jasmine.stringMatching(filename));
};
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
@ -34,7 +50,7 @@ describe('DiagramComponent', () => {
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment},
{provide: APP_BASE_HREF, useValue: ''},
{provide: Router, useValue: mockRouter},
]
],
});
httpMock = TestBed.inject(HttpTestingController);
@ -58,7 +74,7 @@ describe('DiagramComponent', () => {
component.importDone.subscribe(result => {
expect(result).toEqual({
type: 'success',
warnings: []
warnings: [],
});
done();
});
@ -73,7 +89,7 @@ describe('DiagramComponent', () => {
component.importDone.subscribe(result => {
expect(result).toEqual({
type: 'success',
warnings: []
warnings: [],
});
done();
});
@ -85,12 +101,17 @@ describe('DiagramComponent', () => {
it('should expose BPMN import warnings', (done) => {
const diagramURL = 'some-url';
component.importDone.subscribe(result => {
expect(result.type).toEqual('success');
expect(result.warnings.length).toEqual(1);
expect(result.warnings[0].message).toContain('unparsable content <process> detected');
done();
});
component.importDone.subscribe(
result => {
expect(result.type).toEqual('error');
expect(result.error).toBeDefined();
expect(result.error.warnings).toBeDefined();
expect(result.error.warnings.length).toEqual(1);
expect(result.error.warnings[0].message).toContain('unparsable content <process> detected');
done();
},
error => {
});
component.loadUrl(diagramURL);
const request = httpMock.expectOne({url: diagramURL, method: 'GET'});
@ -100,9 +121,9 @@ describe('DiagramComponent', () => {
it('should expose DMN import warnings', (done) => {
const diagramURL = 'some-url';
component.importDone.subscribe(result => {
expect(result.type).toEqual('success');
expect(result.warnings.length).toEqual(1);
expect(result.warnings[0].message).toContain('unparsable content <decision> detected');
expect(result.type).toEqual('error');
expect(result.error.warnings.length).toEqual(1);
expect(result.error.warnings[0].message).toContain('unparsable content <decision> detected');
done();
});
component.loadUrl(diagramURL);
@ -123,20 +144,24 @@ describe('DiagramComponent', () => {
const request = httpMock.expectOne({url: diagramURL, method: 'GET'});
request.flush('Not Found', {
status: 404,
statusText: 'FOO'
statusText: 'FOO',
});
});
it('should save diagram as SVG', () => {
const fileSaverSpy = spyOn(FileSaver, 'saveAs').and.stub();
component.saveSVG();
expect(fileSaverSpy).toHaveBeenCalled();
it('should save BPMN diagram as SVG', async () => {
await testSaveAs(FileType.BPMN, BPMN_DIAGRAM, 'saveSVG', 'some_bpmn_name');
});
it('should save diagram as XML', () => {
const fileSaverSpy = spyOn(FileSaver, 'saveAs').and.stub();
component.saveXML();
expect(fileSaverSpy).toHaveBeenCalled();
it('should save BPMN diagram as XML', async () => {
await testSaveAs(FileType.BPMN, BPMN_DIAGRAM, 'saveXML', 'some_bpmn_name');
});
it('should save DMN diagram as SVG', async () => {
await testSaveAs(FileType.DMN, DMN_DIAGRAM, 'saveSVG', 'some_dmn_name');
});
it('should save DMN diagram as XML', async () => {
await testSaveAs(FileType.DMN, DMN_DIAGRAM, 'saveXML', 'some_dmn_name');
});
it('should insert date into filename', () => {
@ -152,61 +177,89 @@ describe('DiagramComponent', () => {
it('should create a new diagram', () => {
const initializeModelerSpy = spyOn(component, 'initializeModeler').and.stub();
const importXMLSpy = spyOn(component.modeler, 'importXML').and.stub();
const importXMLSpy = spyOn(component.modeler, 'importXML').and.callThrough();
spyOn(component, 'getRandomString').and.returnValue('REPLACE_ME');
component.openDiagram();
expect(initializeModelerSpy).toHaveBeenCalledWith(undefined);
expect(importXMLSpy).toHaveBeenCalledWith(BPMN_DIAGRAM_DEFAULT, jasmine.any(Function));
expect(importXMLSpy).toHaveBeenCalledWith(BPMN_DIAGRAM_DEFAULT);
});
it('should open an existing BPMN diagram from XML', () => {
const initializeBPMNModelerSpy = spyOn(component, 'initializeBPMNModeler').and.stub();
const importXMLSpy = spyOn(component.modeler, 'importXML').and.stub();
const importXMLSpy = spyOn(component.modeler, 'importXML').and.callThrough();
component.openDiagram(BPMN_DIAGRAM, FileType.BPMN);
expect(initializeBPMNModelerSpy).toHaveBeenCalled();
expect(importXMLSpy).toHaveBeenCalled();
});
it('should open an existing DMN diagram from XML', () => {
it('should open an existing DMN diagram from XML', async () => {
const initializeDMNModelerSpy = spyOn(component, 'initializeDMNModeler').and.stub();
const importXMLSpy = spyOn(component.modeler, 'importXML').and.stub();
component.openDiagram(DMN_DIAGRAM, FileType.DMN);
const importXMLSpy = spyOn(component.modeler, 'importXML').and.callThrough();
await component.openDiagram(DMN_DIAGRAM, FileType.DMN);
expect(initializeDMNModelerSpy).toHaveBeenCalled();
expect(importXMLSpy).toHaveBeenCalled();
});
it('should fail to open BPMN diagram', (done) => {
component.openDiagram('INVALID BPMN XML', FileType.BPMN);
component.importDone.subscribe(result => {
expect(result.type).toEqual('error');
expect(result.error.message).toContain('unparsable content INVALID BPMN XML detected');
done();
});
component.importDone.subscribe(
result => {
expect(result.type).toEqual('error');
expect(result.error.message).toContain('unparsable content INVALID BPMN XML detected');
done();
},
error => {
});
});
it('should fail to open DMN diagram', (done) => {
component.openDiagram('INVALID DMN XML', FileType.DMN);
component.importDone.subscribe(result => {
expect(result.type).toEqual('error');
expect(result.error.message).toContain('unparsable content INVALID DMN XML detected');
done();
});
component.importDone.subscribe(
result => {
expect(result.type).toEqual('error');
expect(result.error.message).toContain('unparsable content INVALID DMN XML detected');
done();
},
error => {
console.error('importDone > subscribe > error', error);
});
});
it('should edit diagram', () => {
it('should edit BPMN diagram', () => {
const initializeModelerSpy = spyOn(component, 'initializeModeler').and.stub();
const onChangeSpy = spyOn(component, 'onChange').and.stub();
const importXMLSpy = spyOn(component.modeler, 'importXML').and.stub();
const importXMLSpy = spyOn(component.modeler, 'importXML').and.callThrough();
spyOn(component, 'getRandomString').and.returnValue('REPLACE_ME');
component.openDiagram();
expect(initializeModelerSpy).toHaveBeenCalledWith(undefined);
expect(importXMLSpy).toHaveBeenCalledWith(BPMN_DIAGRAM_DEFAULT, jasmine.any(Function));
expect(importXMLSpy).toHaveBeenCalledWith(BPMN_DIAGRAM_DEFAULT);
component.writeValue(BPMN_DIAGRAM);
expect(initializeModelerSpy).toHaveBeenCalledWith(undefined);
expect(onChangeSpy).toHaveBeenCalled();
});
it('should edit DMN diagram', async () => {
fixture.detectChanges();
await fixture.whenRenderingDone();
const initializeModelerSpy = spyOn(component, 'initializeModeler').and.stub();
const onChangeSpy = spyOn(component, 'onChange').and.stub();
const dmnMigrateSpy = spyOn((component as any), 'convertDMN').and.returnValue(DMN_DIAGRAM_EMPTY);
const importXMLSpy = spyOn(component.modeler, 'importXML').and.callThrough();
spyOn(component, 'getRandomString').and.returnValue('REPLACE_ME');
await component.openDiagram(DMN_DIAGRAM_DEFAULT, FileType.DMN);
expect(initializeModelerSpy).toHaveBeenCalledWith(FileType.DMN);
expect(dmnMigrateSpy).toHaveBeenCalledOnceWith(DMN_DIAGRAM_DEFAULT);
expect(importXMLSpy).toHaveBeenCalledWith(DMN_DIAGRAM_EMPTY);
initializeModelerSpy.calls.reset();
component.writeValue(DMN_DIAGRAM);
expect(initializeModelerSpy).toHaveBeenCalledWith(undefined);
expect(onChangeSpy).toHaveBeenCalled();
});
it('should register onChange function', () => {
const fn = (s: string) => s.toLowerCase().trim();
const input = ' TRIMMED AND LOWERCASED ';

View File

@ -1,39 +1,55 @@
import {formatDate} from '@angular/common';
import {HttpErrorResponse} from '@angular/common/http';
import {AfterViewInit, Component, ElementRef, EventEmitter, Input, NgZone, Output, ViewChild} from '@angular/core';
import {ControlValueAccessor} from '@angular/forms';
import { formatDate } from '@angular/common';
import {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
Input,
NgZone,
OnChanges,
Output,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import BpmnModeler from 'bpmn-js/lib/Modeler';
import DmnModeler from 'dmn-js/lib/Modeler';
import * as fileSaver from 'file-saver';
import {
ApiService,
BPMN_DIAGRAM_DEFAULT,
CameltoSnakeCase,
DMN_DIAGRAM_DEFAULT,
FileType,
getDiagramTypeFromXml
} from 'sartography-workflow-lib';
import {v4 as uuidv4} from 'uuid';
import {BpmnWarning} from '../_interfaces/bpmn-warning';
import {ImportEvent} from '../_interfaces/import-event';
import {bpmnModelerConfig} from './bpmn-modeler-config';
import {dmnModelerConfig} from './dmn-modeler-config';
import { v4 as uuidv4 } from 'uuid';
import { BpmnError, BpmnWarning } from '../_interfaces/bpmn-warning';
import { ImportEvent } from '../_interfaces/import-event';
import { bpmnModelerConfig } from './bpmn-modeler-config';
import { dmnModelerConfig } from './dmn-modeler-config';
import { getDiagramTypeFromXml } from '../_util/diagram-type';
import isEqual from 'lodash.isequal';
import { migrateDiagram } from '@bpmn-io/dmn-migrate';
@Component({
selector: 'app-diagram',
templateUrl: 'diagram.component.html',
styleUrls: ['diagram.component.scss'],
})
export class DiagramComponent implements ControlValueAccessor, AfterViewInit {
@Input() fileName: string;
export class DiagramComponent implements ControlValueAccessor, AfterViewInit, OnChanges {
@ViewChild('containerRef', {static: true}) containerRef: ElementRef;
@ViewChild('propertiesRef', {static: true}) propertiesRef: ElementRef;
@Output() private importDone: EventEmitter<ImportEvent> = new EventEmitter();
@Input() fileName: string;
@Input() validation_data: { [key: string]: any } = {};
@Input() validation_state: string;
@Output() validationStart: EventEmitter<string> = new EventEmitter();
@Output() importDone: EventEmitter<ImportEvent> = new EventEmitter();
public eventBus;
private diagramType: FileType;
private modeler: BpmnModeler | DmnModeler;
private xml = '';
private svg;
private disabled = false;
// Hack so we can spy on this function
private _formatDate = formatDate;
@ -57,19 +73,41 @@ export class DiagramComponent implements ControlValueAccessor, AfterViewInit {
}
onChange(newValue: string, svgValue: string) {
console.log('DiagramComponent default onChange');
}
ngOnChanges(changes: SimpleChanges) {
if (changes.validation_data) {
this.validation_data = changes.validation_data.currentValue;
if (this.modeler) {
if (this.validation_data.task_data) {
this.modeler.get('eventBus').fire('editor.objects.response', {objects: this.validation_data.task_data});
}
}
}
if (changes.validation_state) {
this.validation_state = changes.validation_state.currentValue;
if (this.modeler) {
const resp = {
state: this.validation_state, line_number: undefined,
};
if (this.validation_data.line_number) {
resp.line_number = this.validation_data.line_number;
}
this.modeler.get('eventBus').fire('editor.validation.response', resp);
}
}
}
onTouched() {
}
initializeModeler(diagramType: FileType) {
initializeModeler(diagramType: FileType): DmnModeler | BpmnModeler {
this.clearElements();
if (diagramType === FileType.DMN) {
this.initializeDMNModeler();
return this.initializeDMNModeler() as DmnModeler;
} else {
this.initializeBPMNModeler();
return this.initializeBPMNModeler() as BpmnModeler;
}
}
@ -103,54 +141,88 @@ export class DiagramComponent implements ControlValueAccessor, AfterViewInit {
openDiagram(xml?: string, diagramType?: FileType) {
this.diagramType = diagramType || getDiagramTypeFromXml(xml);
this.xml = xml;
this.initializeModeler(diagramType);
const modeler = this.initializeModeler(diagramType);
return this.zone.run(async () => {
const isDMN = diagramType === FileType.DMN;
return this.zone.run(() => {
if (!xml) {
const defaultXml = diagramType === FileType.DMN ? DMN_DIAGRAM_DEFAULT : BPMN_DIAGRAM_DEFAULT;
xml = defaultXml.replace(/REPLACE_ME/gi, () => this.getRandomString(7));
const defaultXml = isDMN ? DMN_DIAGRAM_DEFAULT : BPMN_DIAGRAM_DEFAULT;
const randomString = this.getRandomString(7);
xml = defaultXml.replace(/REPLACE_ME/gi, () => randomString);
}
this.modeler.importXML(xml, (e, w) => this.onImport(e, w));
// Convert any DMN 1.1 or 1.2 DMN to v1.3
const convertedXML = isDMN ? await this.convertDMN(xml) : xml;
this.modeler.importXML(convertedXML).then(
(e, w) => this.onImport(e, w || e && e.warnings),
e => this.onImport(e, e && e.warnings),
);
});
}
saveSVG() {
this.saveDiagram();
this.modeler.saveSVG((err, svg) => {
const blob = new Blob([svg], {type: 'image/svg+xml'});
fileSaver.saveAs(blob, `${this.diagramType.toString().toUpperCase()} Diagram - ${new Date().toISOString()}.svg`);
});
async saveSVG() {
await this.saveDiagram();
const {svg} = await this.modeler.saveSVG();
const blob = new Blob([svg], {type: 'image/svg+xml'});
fileSaver.saveAs(blob, this.insertDateIntoFileName());
}
saveDiagram() {
async saveDiagram() {
if (this.modeler && this.modeler.saveSVG) {
this.modeler.saveSVG((svgErr, svg) => {
this.svg = svg;
this.modeler.saveXML({format: true}, (xmlErr, xml) => {
this.xml = xml;
this.writeValue(xml);
});
});
} else {
this.modeler.saveXML({format: true}, (xmlErr, xml) => {
this.xml = xml;
this.writeValue(xml);
});
const {svg} = await this.modeler.saveSVG();
this.svg = svg;
}
const {xml} = await this.modeler.saveXML({format: true});
this.xml = xml;
this.writeValue(xml);
}
saveXML() {
this.saveDiagram();
this.modeler.saveXML({format: true}, (err, xml) => {
const blob = new Blob([xml], {type: 'text/xml'});
fileSaver.saveAs(blob, this.insertDateIntoFileName());
});
async saveXML() {
await this.saveDiagram();
const {xml} = await this.modeler.saveXML({format: true});
const blob = new Blob([xml], {type: 'text/xml'});
fileSaver.saveAs(blob, this.insertDateIntoFileName());
}
onImport(err?: HttpErrorResponse, warnings?: BpmnWarning[]) {
if (err) {
this._handleErrors(err);
onImport(err?: Error | BpmnError, warnings?: BpmnWarning[]) {
warnings = warnings || [];
// SUCCESS (no errors or warnings)
// -----------------------------------------
// err = {warnings: []}
// warnings = []
// -----------------------------------------
// err = undefined
// warnings = []
// -----------------------------------------
// ERROR
// -----------------------------------------
// err = {warnings: [{message: '...', error: {stack '...', message: '...'}}]}
// warnings = [{message: '...', error: {stack '...', message: '...'}}]
// -----------------------------------------
// SUCCESS WITH WARNINGS
// -----------------------------------------
// err = {warnings: [{message: '...', error: {}}]
// warnings = [{message: '...', error: {}}]
// -----------------------------------------
// err = undefined
// warnings = [{message: '...', error: {}}]
// -----------------------------------------
const isSuccess = (!err || isEqual(err, {warnings: []})) && isEqual(warnings, []);
const isError = !isSuccess && err && (
err instanceof Error ||
err.warnings && err.warnings.some(w => w.error && !isEqual(w.error, {}))
);
if (isSuccess && !isError) {
this._handleSuccess();
} else if (isError) {
const errors = err || warnings.filter(w => !!w.error);
this._handleErrors(errors);
} else {
this._handleWarnings(warnings);
}
@ -160,27 +232,38 @@ export class DiagramComponent implements ControlValueAccessor, AfterViewInit {
* Load diagram from URL and emit completion event
*/
loadUrl(url: string) {
this.api.getStringFromUrl(url).subscribe(xml => {
const diagramType = getDiagramTypeFromXml(xml);
this.openDiagram(xml, diagramType);
}, error => this._handleErrors(error));
this.api.getStringFromUrl(url).subscribe(
xml => {
const diagramType = getDiagramTypeFromXml(xml);
this.openDiagram(xml, diagramType);
},
error => this._handleErrors(error),
);
}
private _handleSuccess() {
this.importDone.emit({
type: 'success',
warnings: [],
});
}
private _handleWarnings(warnings: BpmnWarning[]) {
this.importDone.emit({
type: 'success',
warnings: warnings
warnings: warnings || [],
});
}
private _handleErrors(err) {
this.importDone.emit({
type: 'error',
error: err
error: err,
});
}
private initializeBPMNModeler() {
private initializeBPMNModeler(): BpmnModeler {
this.diagramType = FileType.BPMN;
this.modeler = new BpmnModeler({
container: this.containerRef.nativeElement,
propertiesPanel: {
@ -202,9 +285,27 @@ export class DiagramComponent implements ControlValueAccessor, AfterViewInit {
});
}
});
const eventBus = this.modeler.get('eventBus');
eventBus.on('editor.scripts.request', () => {
this.api.listScripts().subscribe((data) => {
data.forEach(element => {
element.name = CameltoSnakeCase(element.name);
});
this.modeler.get('eventBus').fire('editor.scripts.response', {scripts: data});
});
});
eventBus.on('editor.validation.request', (request) => {
this.validationStart.emit(request.task_name);
});
return this.modeler as BpmnModeler;
}
private initializeDMNModeler() {
private initializeDMNModeler(): DmnModeler {
this.diagramType = FileType.DMN;
this.modeler = new DmnModeler({
container: this.containerRef.nativeElement,
drd: {
@ -244,6 +345,8 @@ export class DiagramComponent implements ControlValueAccessor, AfterViewInit {
});
}
});
return this.modeler as DmnModeler;
}
private clearElements() {
@ -271,4 +374,8 @@ export class DiagramComponent implements ControlValueAccessor, AfterViewInit {
return `${this.fileName}_${dateString}.${this.diagramType}`;
}
}
private async convertDMN(xml: string) {
return await migrateDiagram(xml);
}
}

View File

@ -1,7 +1,7 @@
import * as camundaModdleDescriptor from 'camunda-dmn-moddle/resources/camunda.json';
import propertiesPanelModule from 'dmn-js-properties-panel';
import drdAdapterModule from 'dmn-js-properties-panel/lib/adapter/drd';
import propertiesProviderModule from 'dmn-js-properties-panel/lib/provider/camunda';
import * as camundaModdleDescriptor from 'camunda-dmn-moddle/resources/camunda.json';
import {ModelerConfig} from '../_interfaces/modeler-config';
export const dmnModelerConfig: ModelerConfig = {

View File

@ -17,7 +17,9 @@
</button>
</ng-container>
<h4 (click)="editFile(fm)" mat-line>{{fm.name}}</h4>
<p (click)="editFile(fm)" mat-line>{{fm.last_updated | date}}</p>
<p (click)="editFile(fm)" mat-line> Updated on {{fm.last_modified | date:'medium'}}
<span *ngIf="fm.user_uid"> by {{fm.user_uid}}</span>
</p>
<button (click)="downloadFile(fm)" class="mat-elevation-z0" color="primary" mat-icon-button>
<mat-icon>save_alt</mat-icon>
</button>

View File

@ -1,7 +1,7 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpHeaders} from '@angular/common/http';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {MatListModule} from '@angular/material/list';
@ -9,15 +9,15 @@ import {MatSnackBarModule} from '@angular/material/snack-bar';
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {RouterTestingModule} from '@angular/router/testing';
import createClone from 'rfdc';
import { cloneDeep } from 'lodash';
import {of} from 'rxjs';
import {
ApiService,
FileMeta,
FileType,
MockEnvironment,
MockEnvironment, mockFile0,
mockFileMeta0,
mockFileMetas,
mockFileMetas, mockFiles,
mockWorkflowSpec0
} from 'sartography-workflow-lib';
import {DeleteFileDialogComponent} from '../_dialogs/delete-file-dialog/delete-file-dialog.component';
@ -30,8 +30,11 @@ describe('FileListComponent', () => {
let httpMock: HttpTestingController;
let component: FileListComponent;
let fixture: ComponentFixture<FileListComponent>;
const timeString = '2020-01-23T12:34:12.345Z';
const timeCode = new Date(timeString).getTime();
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
@ -77,31 +80,12 @@ describe('FileListComponent', () => {
component.workflowSpec = mockWorkflowSpec0;
fixture.detectChanges();
const justFiles: File[] = [];
const fmsNoFiles: FileMeta[] = mockFileMetas.map(fm => {
justFiles.push(fm.file);
delete fm['file'];
return fm;
});
expect(justFiles.length).toEqual(mockFileMetas.length);
expect(fmsNoFiles.every(fm => !fm.file)).toEqual(true);
expect(justFiles.every(f => !!f.name)).toEqual(true);
const fmsReq = httpMock.expectOne(`apiRoot/file?workflow_spec_id=${mockWorkflowSpec0.id}`);
expect(fmsReq.request.method).toEqual('GET');
fmsReq.flush(fmsNoFiles);
fmsReq.flush(mockFileMetas);
expect(component.fileMetas.length).toBeGreaterThan(0);
fmsNoFiles.forEach((fm, i) => {
const fReq = httpMock.expectOne(`apiRoot/file/${fm.id}/data`);
const mockHeaders = new HttpHeaders()
.append('last-modified', justFiles[i].lastModified.toString())
.append('content-type', justFiles[i].type);
fReq.flush(new ArrayBuffer(8), {headers: mockHeaders});
expect(fReq.request.method).toEqual('GET');
expect(component.fileMetas[i].file).toBeTruthy();
});
});
afterEach(() => {
@ -163,7 +147,7 @@ describe('FileListComponent', () => {
expect(routerNavigateSpy).toHaveBeenCalledWith([`/modeler/${mockWorkflowSpec0.id}/${mockFileMeta0.id}`]);
routerNavigateSpy.calls.reset();
const mockDmnMeta = createClone()(mockFileMeta0);
const mockDmnMeta = cloneDeep(mockFileMeta0);
mockDmnMeta.type = FileType.DMN;
component.editFile(mockDmnMeta);
expect(routerNavigateSpy).toHaveBeenCalledWith([`/modeler/${mockWorkflowSpec0.id}/${mockDmnMeta.id}`]);
@ -173,7 +157,7 @@ describe('FileListComponent', () => {
const routerNavigateSpy = spyOn((component as any).router, 'navigate');
const editFileMetaSpy = spyOn(component, 'editFileMeta');
component.workflowSpec = mockWorkflowSpec0;
const mockDocMeta = createClone()(mockFileMeta0);
const mockDocMeta = cloneDeep(mockFileMeta0);
mockDocMeta.type = FileType.DOCX;
component.editFile(mockDocMeta);
expect(routerNavigateSpy).not.toHaveBeenCalled();
@ -189,13 +173,13 @@ describe('FileListComponent', () => {
it('should open file metadata dialog', () => {
const _openFileDialogSpy = spyOn((component as any), '_openFileDialog').and.stub();
component.workflowSpec = mockWorkflowSpec0;
const mockDocMeta: FileMeta = createClone()(mockFileMeta0);
const mockDocMeta: FileMeta = cloneDeep(mockFileMeta0);
mockDocMeta.type = FileType.DOCX;
component.editFileMeta(mockDocMeta);
const expectedFile = new File([], mockDocMeta.name, {
type: mockDocMeta.content_type,
lastModified: mockDocMeta.file.lastModified
lastModified: timeCode
});
const fReq = httpMock.expectOne(`apiRoot/file/${mockDocMeta.id}/data`);
@ -215,7 +199,7 @@ describe('FileListComponent', () => {
it('should upload new file from file dialog', () => {
const openDialogSpy = spyOn(component.dialog, 'open')
.and.returnValue({afterClosed: () => of({file: mockFileMeta0.file})} as any);
.and.returnValue({afterClosed: () => of({file: mockFile0})} as any);
const _loadFileMetasSpy = spyOn((component as any), '_loadFileMetas').and.stub();
component.workflowSpec = mockWorkflowSpec0;
@ -230,11 +214,11 @@ describe('FileListComponent', () => {
it('should update existing file from file dialog', () => {
const openDialogSpy = spyOn(component.dialog, 'open')
.and.returnValue({afterClosed: () => of({fileMetaId: mockFileMeta0.id, file: mockFileMeta0.file})} as any);
.and.returnValue({afterClosed: () => of({fileMetaId: mockFileMeta0.id, file: mockFile0})} as any);
const _loadFileMetasSpy = spyOn((component as any), '_loadFileMetas').and.stub();
component.workflowSpec = mockWorkflowSpec0;
(component as any)._openFileDialog(mockFileMeta0, mockFileMeta0.file);
(component as any)._openFileDialog(mockFileMeta0, mockFile0);
const updateReq = httpMock.expectOne(`apiRoot/file/${mockFileMeta0.id}/data`);
expect(updateReq.request.method).toEqual('PUT');
updateReq.flush(mockFileMeta0);
@ -251,7 +235,6 @@ describe('FileListComponent', () => {
expect(updateFileMetaSpy).toHaveBeenCalledTimes(mockFileMetas.length);
expect(component.fileMetas.length).toEqual(mockFileMetas.length);
expect(component.fileMetas.every(fm => !!fm.file)).toEqual(true);
expect(component.fileMetas.reduce((sum, fm) => fm.primary ? sum + 1 : sum, 0)).toEqual(1);
expect(_loadFileMetasSpy).toHaveBeenCalled();
});

View File

@ -1,27 +1,28 @@
import {Component, Input, OnInit} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {MatSnackBar} from '@angular/material/snack-bar';
import {ActivatedRoute, Router} from '@angular/router';
import { Component, Input, OnChanges, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router';
import {
ApiService,
FileMeta,
FileParams,
FileType,
getFileType,
isNumberDefined, newFileFromResponse,
WorkflowSpec
isNumberDefined,
newFileFromResponse,
WorkflowSpec,
} from 'sartography-workflow-lib';
import {DeleteFileDialogComponent} from '../_dialogs/delete-file-dialog/delete-file-dialog.component';
import {OpenFileDialogComponent} from '../_dialogs/open-file-dialog/open-file-dialog.component';
import {DeleteFileDialogData, OpenFileDialogData} from '../_interfaces/dialog-data';
import { DeleteFileDialogComponent } from '../_dialogs/delete-file-dialog/delete-file-dialog.component';
import { OpenFileDialogComponent } from '../_dialogs/open-file-dialog/open-file-dialog.component';
import { DeleteFileDialogData, OpenFileDialogData } from '../_interfaces/dialog-data';
import * as fileSaver from 'file-saver';
@Component({
selector: 'app-file-list',
templateUrl: './file-list.component.html',
styleUrls: ['./file-list.component.scss']
styleUrls: ['./file-list.component.scss'],
})
export class FileListComponent implements OnInit {
export class FileListComponent implements OnInit, OnChanges {
@Input() workflowSpec: WorkflowSpec;
fileMetas: FileMeta[];
fileType = FileType;
@ -39,6 +40,10 @@ export class FileListComponent implements OnInit {
this._loadFileMetas();
}
ngOnChanges() {
this._loadFileMetas();
}
editFile(fileMeta?: FileMeta) {
if (fileMeta && ((fileMeta.type === FileType.BPMN) || (fileMeta.type === FileType.DMN))) {
this.router.navigate([`/modeler/${this.workflowSpec.id}/${fileMeta.id}`]);
@ -64,7 +69,7 @@ export class FileListComponent implements OnInit {
data: {
confirm: false,
fileMeta: fm,
}
},
});
dialogRef.afterClosed().subscribe((data: DeleteFileDialogData) => {
@ -77,6 +82,7 @@ export class FileListComponent implements OnInit {
makePrimary(fmPrimary: FileMeta) {
if (fmPrimary.type === FileType.BPMN) {
let numUpdated = 0;
// Fixme: This buisness rule does not belong here.
this.fileMetas.forEach(fm => {
fm.primary = (fmPrimary.id === fm.id);
this.api.updateFileMeta(fm).subscribe(() => {
@ -91,12 +97,19 @@ export class FileListComponent implements OnInit {
}
}
downloadFile(fm: FileMeta) {
this.api.getFileData(fm.id).subscribe(response => {
const blob = new Blob([response.body], {type: fm.content_type});
fileSaver.saveAs(blob, fm.name);
});
}
private _openFileDialog(fm?: FileMeta, file?: File) {
const dialogData: OpenFileDialogData = {
fileMetaId: fm ? fm.id : undefined,
file: file,
file,
mode: 'local',
fileTypes: [FileType.DOC, FileType.DOCX, FileType.XLSX, FileType.XLS],
fileTypes: [FileType.DOC, FileType.DOCX, FileType.XLSX],
};
const dialogRef = this.dialog.open(OpenFileDialogComponent, {data: dialogData});
@ -107,13 +120,12 @@ export class FileListComponent implements OnInit {
content_type: data.file.type,
name: data.file.name,
type: getFileType(data.file),
file: data.file,
workflow_spec_id: this.workflowSpec.id,
};
if (isNumberDefined(data.fileMetaId)) {
// Update existing file
this.api.updateFileData(newFileMeta).subscribe(() => {
this.api.updateFileData(newFileMeta, data.file).subscribe(() => {
this._loadFileMetas();
});
} else {
@ -122,13 +134,12 @@ export class FileListComponent implements OnInit {
workflow_spec_id: this.workflowSpec.id,
};
this.api.addFileMeta(fileParams, newFileMeta).subscribe(dbFm => {
this.api.addFile(fileParams, newFileMeta, data.file).subscribe(dbFm => {
this._loadFileMetas();
});
}
}
});
}
private _deleteFile(fileMeta: FileMeta) {
@ -141,22 +152,6 @@ export class FileListComponent implements OnInit {
private _loadFileMetas() {
this.api.getFileMetas({workflow_spec_id: this.workflowSpec.id}).subscribe(fms => {
this.fileMetas = fms.sort((a, b) => (a.name > b.name) ? 1 : -1);
this._loadFileData();
});
}
private _loadFileData() {
this.fileMetas.forEach(fm => {
this.api.getFileData(fm.id).subscribe(response => {
fm.file = newFileFromResponse(fm, response);
});
});
}
downloadFile(fm: FileMeta) {
this.api.getFileData(fm.id).subscribe(response => {
const blob = new Blob([response.body], {type: fm.content_type});
fileSaver.saveAs(blob, fm.name);
});
}
}

View File

@ -1,5 +1,5 @@
import {APP_BASE_HREF} from '@angular/common';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {MockEnvironment} from 'sartography-workflow-lib';
import {FooterComponent} from './footer.component';
@ -7,7 +7,7 @@ describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [FooterComponent],
providers: [

View File

@ -6,7 +6,7 @@ import {AppEnvironment} from 'sartography-workflow-lib';
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss']
})
export class FooterComponent implements OnInit {
export class FooterComponent {
title: string;
@ -14,7 +14,4 @@ export class FooterComponent implements OnInit {
this.title = environment.title;
}
ngOnInit() {
}
}

View File

@ -2,7 +2,7 @@ import {APP_BASE_HREF} from '@angular/common';
import {HttpClient} from '@angular/common/http';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {Component} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {ApiService, MockEnvironment} from 'sartography-workflow-lib';
@ -29,7 +29,7 @@ describe('HomeComponent', () => {
let fixture: ComponentFixture<HomeComponent>;
const mockRouter = {navigate: jasmine.createSpy('navigate')};
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
HomeComponent,

View File

@ -0,0 +1,11 @@
<div class="file-list">
<mat-list>
<mat-list-item
*ngFor="let fm of getCurrentItems()"
>
<mat-checkbox (click)="updateItem(fm,isChecked(fm))" [checked]="isChecked(fm)">{{fm.display_name}}</mat-checkbox>
</mat-list-item>
</mat-list>
</div>

View File

@ -0,0 +1,16 @@
@import "../../config";
::ng-deep mat-list-item {
border-left: 4px solid $brand-gray-light;
&:hover {
border-left: 4px solid $brand-primary;
background-color: $brand-primary-tint-4;
cursor: pointer;
}
}
.make-primary {
padding-right: 4rem;
margin-right: 4rem;
}

View File

@ -0,0 +1,55 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {MatIconModule} from '@angular/material/icon';
import {MatMenuModule} from '@angular/material/menu';
import {ApiService, MockEnvironment, mockWorkflowSpec0, mockWorkflowSpec1, WorkflowSpec} from 'sartography-workflow-lib';
import {LibraryListComponent} from './library-list.component';
describe('LibraryListComponent', () => {
let component: LibraryListComponent;
let fixture: ComponentFixture<LibraryListComponent>;
let httpMock: HttpTestingController;
let libraries: WorkflowSpec[];
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
LibraryListComponent
],
imports: [
HttpClientTestingModule,
MatIconModule,
MatMenuModule,
// RouterTestingModule,
],
providers: [
ApiService,
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment},
{provide: APP_BASE_HREF, useValue: ''},
],
})
.compileComponents();
}));
beforeEach(() => {
localStorage.setItem('token', 'some_token');
httpMock = TestBed.inject(HttpTestingController);
fixture = TestBed.createComponent(LibraryListComponent);
component = fixture.componentInstance;
libraries = [mockWorkflowSpec0, mockWorkflowSpec1];
libraries[0].library = true;
libraries[1].library = true;
fixture.detectChanges();
const uReq = httpMock.expectOne('apiRoot/workflow-specification?libraries=true');
expect(uReq.request.method).toEqual('GET');
uReq.flush(libraries);
expect(component.workflowLibraries).toEqual(libraries);
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,63 @@
import {Component, Input, OnChanges, OnInit} from '@angular/core';
import {
ApiService,
WorkflowSpec
} from 'sartography-workflow-lib';
@Component({
selector: 'app-library-list',
templateUrl: './library-list.component.html',
styleUrls: ['./library-list.component.scss']
})
export class LibraryListComponent implements OnInit, OnChanges {
@Input() workflowSpecId: string;
@Input() showAll: boolean;
workflowLibraries: WorkflowSpec[];
constructor(
private api: ApiService,
) {
this.showAll = false;
this.workflowLibraries =[];
}
ngOnInit() {
this._loadWorkflowLibraries();
}
ngOnChanges() {
this._loadWorkflowLibraries();
}
isChecked(libraryspec): boolean {
let checked = false;
for (const item of libraryspec.parents) {
checked = checked || (item.id === this.workflowSpecId);
}
return checked;
}
getCurrentItems(){
return this.workflowLibraries.filter((item)=> this.isChecked(item) || this.showAll)
}
updateItem(library: WorkflowSpec , checked: boolean) {
if (checked) {
this.api.deleteWorkflowLibrary(this.workflowSpecId, library.id).subscribe(() => {
this._loadWorkflowLibraries();
});
} else {
this.api.addWorkflowLibrary(this.workflowSpecId, library.id).subscribe(() => {
this._loadWorkflowLibraries();
});
}
}
private _loadWorkflowLibraries() {
this.api.getWorkflowSpecificationLibraries().subscribe(wfs => {
this.workflowLibraries = wfs;
});
}
}

View File

@ -1,11 +1,11 @@
<mat-toolbar [ngClass]="{'expanded': expandToolbar}">
<mat-toolbar-row *ngIf="workflowSpec">
<a mat-button [routerLink]="['/']">
<a mat-button (click)="checkSaved()">
<mat-icon>arrow_back</mat-icon>
Back
</a>
{{workflowSpec.display_name}}
({{workflowSpec.name}})
({{workflowSpec.id}})
</mat-toolbar-row>
<mat-toolbar-row>
<button #newMenuTrigger="matMenuTrigger" mat-button [matMenuTriggerFor]="newMenu" title="Open diagram">
@ -28,15 +28,20 @@
<mat-icon>arrow_drop_down</mat-icon>
</button>
<mat-menu #importMenu="matMenu">
<button mat-menu-item (click)="openMethod = 'db'" [matMenuTriggerFor]="dbMenu" title="Open diagram from database">
<button
mat-menu-item
[disabled]="bpmnFilesNoSelf.length === 0"
(click)="openMethod = 'db'"
[matMenuTriggerFor]="dbMenu"
title="Open diagram from database">
<mat-icon>cloud</mat-icon>
Open previously saved...
Open related BPMN/DMN File ...
</button>
<mat-menu #dbMenu="matMenu">
<a
mat-menu-item
*ngFor="let bf of bpmnFiles"
[routerLink]="['/modeler', workflowSpec.id, bf.id]"
*ngFor="let bf of bpmnFilesNoSelf"
(click)="checkChangeBPMN(bf)"
[matTooltip]="getFileMetaTooltipText(bf)"
matTooltipClass="tooltip-text"
matTooltipPosition="right"
@ -45,18 +50,15 @@
</a>
</mat-menu>
<button mat-menu-item (click)="openMethod = 'file'; expandToolbar = true">
<button mat-menu-item (click)="fileInput.click()">
<mat-icon>code</mat-icon>
Open from XML File
</button>
<button mat-menu-item (click)="openMethod = 'url'; expandToolbar = true">
<mat-icon>link</mat-icon>
Open from URL
</button>
</mat-menu>
<button mat-button (click)="saveChanges()" [disabled]="!hasChanged()"><mat-icon>save</mat-icon></button>
<button mat-button (click)="validate()" [disabled]="hasChanged()"><mat-icon>verified_user</mat-icon></button>
<button mat-button [matMenuTriggerFor]="downloadMenu" title="Download diagram">
<mat-icon>save_alt</mat-icon>
<mat-icon>arrow_drop_down</mat-icon>
@ -71,40 +73,11 @@
{{getFileName()}}
</button>
</mat-toolbar-row>
<mat-toolbar-row *ngIf="expandToolbar">
<ng-container *ngIf="!openMethod">
<mat-form-field>
<input matInput [(ngModel)]="fileName" placeholder="File name" type="text">
</mat-form-field>
</ng-container>
<ng-container *ngIf="openMethod === 'file'">
<mat-form-field (click)="fileInput.click()">
<span matPrefix><mat-icon>folder_open</mat-icon> &nbsp;</span>
<input matInput disabled [value]="getFileName()" type="text">
</mat-form-field>
<input hidden (change)="onFileSelected($event)" #fileInput type="file" id="file" accept=".bpmn,.dmn,.xml,application/xml,text/xml">
</ng-container>
<ng-container *ngIf="openMethod === 'url'">
<mat-form-field>
<span matPrefix><mat-icon>link</mat-icon> &nbsp;</span>
<input name="diagramUrl" [(ngModel)]="diagramUrl" matInput placeholder="Open diagram from URL" type="text">
</mat-form-field>
</ng-container>
<button
mat-flat-button
(click)="onSubmitFileToOpen()"
[disabled]="!diagramFile"
color="primary"
id="open_file_button"
>Open file <mat-icon>arrow_forward</mat-icon></button>
<span fxFlex></span>
<button mat-icon-button (click)="expandToolbar = false"><mat-icon>close</mat-icon></button>
</mat-toolbar-row>
</mat-toolbar>
<div fxLayout="column">
<div class="diagram-parent">
<app-diagram #diagram (importDone)="handleImported($event)" [fileName]="getFileName()"></app-diagram>
<app-diagram #diagram (importDone)="handleImported($event)" [fileName]="getFileName()" [validation_state]="validationState" [validation_data]="validationData" (validationStart)="partially_validate($event)" ></app-diagram>
<div *ngIf="importError" class="import-error">
<strong>Failed to render diagram: </strong>
@ -113,3 +86,4 @@
</div>
</div>
<input hidden (change)="onFileSelected($event)" #fileInput type="file" id="file" accept=".bpmn,.dmn,.xml,.xlsx,application/xml,text/xml">

View File

@ -1,43 +1,44 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpErrorResponse, HttpHeaders, HttpResponse} from '@angular/common/http';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {DebugNode} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MAT_BOTTOM_SHEET_DATA, MatBottomSheetModule, MatBottomSheetRef} from '@angular/material/bottom-sheet';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatListModule} from '@angular/material/list';
import {MatMenuModule} from '@angular/material/menu';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatToolbarModule} from '@angular/material/toolbar';
import {MatTooltipModule} from '@angular/material/tooltip';
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {ActivatedRoute, convertToParamMap} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {of} from 'rxjs';
import { APP_BASE_HREF, DatePipe } from '@angular/common';
import { HttpErrorResponse, HttpHeaders, HttpResponse } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DebugNode } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MAT_BOTTOM_SHEET_DATA, MatBottomSheetModule, MatBottomSheetRef } from '@angular/material/bottom-sheet';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import {
ApiService,
FileMeta,
FileType,
MockEnvironment,
mockFile0,
mockFileMeta0,
mockFileMetas,
mockWorkflowSpec0,
mockWorkflowSpecs
mockWorkflowSpecs,
} from 'sartography-workflow-lib';
import {BPMN_DIAGRAM, BPMN_DIAGRAM_EMPTY, BPMN_DIAGRAM_WITH_WARNINGS} from '../../testing/mocks/diagram.mocks';
import {FileMetaDialogComponent} from '../_dialogs/file-meta-dialog/file-meta-dialog.component';
import {NewFileDialogComponent} from '../_dialogs/new-file-dialog/new-file-dialog.component';
import {OpenFileDialogComponent} from '../_dialogs/open-file-dialog/open-file-dialog.component';
import {BpmnWarning} from '../_interfaces/bpmn-warning';
import {FileMetaDialogData, NewFileDialogData, OpenFileDialogData} from '../_interfaces/dialog-data';
import {GetIconCodePipe} from '../_pipes/get-icon-code.pipe';
import {DiagramComponent} from '../diagram/diagram.component';
import {ModelerComponent} from './modeler.component';
import { BPMN_DIAGRAM, BPMN_DIAGRAM_EMPTY } from '../../testing/mocks/diagram.mocks';
import { FileMetaDialogComponent } from '../_dialogs/file-meta-dialog/file-meta-dialog.component';
import { NewFileDialogComponent } from '../_dialogs/new-file-dialog/new-file-dialog.component';
import { OpenFileDialogComponent } from '../_dialogs/open-file-dialog/open-file-dialog.component';
import { BpmnWarning } from '../_interfaces/bpmn-warning';
import { FileMetaDialogData, NewFileDialogData, OpenFileDialogData } from '../_interfaces/dialog-data';
import { GetIconCodePipe } from '../_pipes/get-icon-code.pipe';
import { DiagramComponent } from '../diagram/diagram.component';
import { ModelerComponent } from './modeler.component';
describe('ModelerComponent', () => {
@ -45,7 +46,7 @@ describe('ModelerComponent', () => {
let component: DebugNode['componentInstance'];
let httpMock: HttpTestingController;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
DiagramComponent,
@ -81,8 +82,8 @@ describe('ModelerComponent', () => {
provide: MatDialogRef,
useValue: {
close: (dialogResult: any) => {
}
}
},
},
},
{provide: MAT_DIALOG_DATA, useValue: []},
{
@ -90,29 +91,29 @@ describe('ModelerComponent', () => {
useValue: {
dismiss: () => {
},
}
},
},
{provide: MAT_BOTTOM_SHEET_DATA, useValue: []},
{
provide: ActivatedRoute, useValue: {
queryParams: of(convertToParamMap({
action: ''
action: '',
})),
paramMap: of(convertToParamMap({
workflowSpecId: mockWorkflowSpec0.id,
fileMetaId: `${mockFileMeta0.id}`
}))
}
}
]
fileMetaId: `${mockFileMeta0.id}`,
})),
},
},
],
}).overrideModule(BrowserDynamicTestingModule, {
set: {
entryComponents: [
FileMetaDialogComponent,
NewFileDialogComponent,
OpenFileDialogComponent,
]
}
],
},
}).compileComponents();
}));
@ -132,14 +133,9 @@ describe('ModelerComponent', () => {
expect(req.request.method).toEqual('GET');
req.flush(mockFileMetas);
mockFileMetas.forEach((fm, i) => {
const fmReq = httpMock.expectOne(`apiRoot/file/${fm.id}/data`);
const mockHeaders = new HttpHeaders()
.append('last-modified', mockFileMetas[i].file.lastModified.toString())
.append('content-type', mockFileMetas[i].content_type);
expect(fmReq.request.method).toEqual('GET');
fmReq.flush(new ArrayBuffer(8), {headers: mockHeaders});
});
const fmReq = httpMock.expectOne(`apiRoot/file/${mockFileMeta0.id}/data`);
});
afterEach(() => {
@ -162,7 +158,7 @@ describe('ModelerComponent', () => {
component.handleImported({
type: 'error',
error
error,
});
expect(component.importError).toEqual(error);
@ -170,37 +166,18 @@ describe('ModelerComponent', () => {
it('sets warning messages', () => {
const warnings: BpmnWarning[] = [{
message: 'WARNING'
message: 'WARNING',
}];
component.handleImported({
type: 'success',
error: null,
warnings: warnings,
warnings,
});
expect(component.importWarnings).toEqual(warnings);
});
it('loads a diagram from URL', () => {
component.diagramUrl = 'some-url';
component.openMethod = 'url';
component.onSubmitFileToOpen();
const sReq = httpMock.expectOne(component.diagramUrl);
expect(sReq.request.method).toEqual('GET');
sReq.flush(BPMN_DIAGRAM);
});
it('loads a diagram from URL with warnings', () => {
component.diagramUrl = 'some-url';
component.openMethod = 'url';
component.onSubmitFileToOpen();
const sReq = httpMock.expectOne(component.diagramUrl);
expect(sReq.request.method).toEqual('GET');
sReq.flush(BPMN_DIAGRAM_WITH_WARNINGS);
});
it('loads a diagram from File', () => {
const readFileSpy = spyOn(component, 'readFile').and.stub();
@ -215,7 +192,7 @@ describe('ModelerComponent', () => {
const mockFileReader = {
target: {result: BPMN_DIAGRAM},
readAsText: (blob) => {
}
},
};
spyOn((window as any), 'FileReader').and.returnValue(mockFileReader);
spyOn(mockFileReader, 'readAsText').and.callFake((blob) => {
@ -236,7 +213,7 @@ describe('ModelerComponent', () => {
component.onSubmitFileToOpen();
const expectedParams = {
type: 'error',
error: new Error('Wrong file type. Please choose a BPMN XML file.')
error: new Error('Wrong file type. Please choose a BPMN XML file.'),
};
expect(handleImportedSpy).toHaveBeenCalledWith(expectedParams);
});
@ -244,8 +221,8 @@ describe('ModelerComponent', () => {
it('should get the diagram file name', () => {
expect(component.getFileName()).toEqual(mockFileMeta0.name);
const filename = 'expected_file_name.jpg';
component.diagramFile = new File([], filename, {type: 'image/jpeg'});
const filename = 'one-fish.bpmn';
component.diagramFileMeta.name = filename;
expect(component.getFileName()).toEqual(filename);
});
@ -310,7 +287,7 @@ describe('ModelerComponent', () => {
component.diagramComponent.writeValue(BPMN_DIAGRAM_EMPTY.replace(/REPLACE_ME/g, 'cream_colored_ponies'));
component.saveFileChanges();
expect(updateFileDataSpy).toHaveBeenCalledWith(mockFileMeta0);
expect(updateFileDataSpy).toHaveBeenCalledWith(mockFileMeta0, mockFile0);
expect(snackBarOpenSpy).toHaveBeenCalled();
});
@ -337,12 +314,11 @@ describe('ModelerComponent', () => {
const updateFileMetaSpy = spyOn(component.api, 'updateFileMeta')
.and.returnValue(of(mockFileMeta0));
const updateFileDataSpy = spyOn(component.api, 'updateFileData')
.and.returnValue(of(mockFileMeta0.file));
.and.returnValue(of(mockFile0));
const loadFilesFromDbSpy = spyOn(component, 'loadFilesFromDb').and.stub();
const snackBarSpy = spyOn(component.snackBar, 'open').and.stub();
const noDateOrVersion: FileMeta = {
content_type: mockFileMeta0.content_type,
file: mockFileMeta0.file,
id: mockFileMeta0.id,
name: mockFileMeta0.name,
type: mockFileMeta0.type,
@ -353,7 +329,7 @@ describe('ModelerComponent', () => {
component._upsertFileMeta(data);
expect(component.xml).toEqual(newXml);
expect(updateFileMetaSpy).toHaveBeenCalledWith(noDateOrVersion);
expect(updateFileDataSpy).toHaveBeenCalledWith(noDateOrVersion);
expect(updateFileDataSpy).toHaveBeenCalledWith(noDateOrVersion, mockFile0);
expect(loadFilesFromDbSpy).toHaveBeenCalled();
expect(snackBarSpy).toHaveBeenCalled();
});
@ -368,13 +344,12 @@ describe('ModelerComponent', () => {
const noDateOrVersion: FileMeta = {
id: undefined,
content_type: mockFileMeta0.content_type,
file: mockFileMeta0.file,
name: mockFileMeta0.name,
type: mockFileMeta0.type,
workflow_spec_id: mockFileMeta0.workflow_spec_id,
};
const addFileMetaSpy = spyOn(component.api, 'addFileMeta')
const addFileMetaSpy = spyOn(component.api, 'addFile')
.and.returnValue(of(mockFileMeta0));
const loadFilesFromDbSpy = spyOn(component, 'loadFilesFromDb').and.stub();
const routerNavigateSpy = spyOn(component.router, 'navigate').and.stub();
@ -386,7 +361,7 @@ describe('ModelerComponent', () => {
component.draftXml = newXml;
component._upsertFileMeta(data);
expect(component.xml).toEqual(newXml);
expect(addFileMetaSpy).toHaveBeenCalledWith({workflow_spec_id: mockWorkflowSpec0.id}, noDateOrVersion);
expect(addFileMetaSpy).toHaveBeenCalledWith({workflow_spec_id: mockWorkflowSpec0.id}, noDateOrVersion, mockFile0);
expect(loadFilesFromDbSpy).not.toHaveBeenCalled();
expect(routerNavigateSpy).toHaveBeenCalled();
expect(snackBarSpy).toHaveBeenCalled();
@ -394,7 +369,7 @@ describe('ModelerComponent', () => {
it('should load files from the database', () => {
const mockHeaders = new HttpHeaders()
.append('last-modified', mockFileMeta0.file.lastModified.toString())
.append('last-modified', mockFileMeta0.last_modified.toString())
.append('content-type', mockFileMeta0.content_type);
const mockResponse = new HttpResponse<ArrayBuffer>({
body: new ArrayBuffer(8),
@ -413,9 +388,6 @@ describe('ModelerComponent', () => {
expect(component.workflowSpec).toEqual(mockWorkflowSpec0);
expect(getFileMetasSpy).toHaveBeenCalledWith({workflow_spec_id: mockWorkflowSpec0.id});
mockFileMetas.forEach(fm => {
expect(getFileDataSpy).toHaveBeenCalledWith(fm.id);
});
expect(component.bpmnFiles.length).toEqual(mockFileMetas.length);
});
@ -423,8 +395,8 @@ describe('ModelerComponent', () => {
it('should load a database file', () => {
const onSubmitFileToOpenSpy = spyOn(component, 'onSubmitFileToOpen').and.stub();
component.workflowSpecs = mockWorkflowSpecs;
component.loadDbFile(mockFileMeta0);
expect(component.diagramFile).toEqual(mockFileMeta0.file);
component.loadDbFile(mockFileMeta0, mockFile0);
expect(component.diagramFile).toEqual(mockFile0);
expect(component.diagramFileMeta).toEqual(mockFileMeta0);
expect(component.workflowSpec).toEqual(mockWorkflowSpec0);
expect(onSubmitFileToOpenSpy).toHaveBeenCalled();
@ -434,7 +406,7 @@ describe('ModelerComponent', () => {
component.xml = BPMN_DIAGRAM_EMPTY.replace(/REPLACE_ME/g, 'sleigh_bells');
component.draftXml = BPMN_DIAGRAM_EMPTY.replace(/REPLACE_ME/g, 'schnitzel_with_noodles');
component.diagramFileMeta = mockFileMeta0;
component.diagramFile = mockFileMeta0.file;
component.diagramFile = mockFile0;
component.workflowSpec = mockWorkflowSpec0;
component.newDiagram();
@ -449,36 +421,27 @@ describe('ModelerComponent', () => {
it('should get a file metadata display string', () => {
expect(component.getFileMetaDisplayString(undefined)).toEqual('Loading...');
const expectedString = 'one-fish.bpmn - v1.0 (Jan 23, 2020)';
const file = new File([], 'one-fish.bpmn', {
type: 'text/xml',
lastModified: new Date('2020-01-23T12:34:12.345Z').getTime(),
});
mockFileMeta0.file = file;
const expectedString = 'one-fish.bpmn';
mockFileMeta0.type = FileType.BPMN;
mockFileMeta0.last_modified = '2020-01-23T12:34:12.345Z';
expect(component.getFileMetaDisplayString(mockFileMeta0)).toEqual(expectedString);
});
it('should get file metadata tooltip text', () => {
component.workflowSpec = undefined;
expect(component.getFileMetaTooltipText(mockFileMeta0)).toEqual('Loading...');
mockFileMeta0.type = FileType.BPMN;
mockFileMeta0.last_modified = '2020-01-23T12:34:12.345Z';
const lastUpdated = new DatePipe('en-us').transform(mockFileMeta0.last_modified, 'medium');
component.workflowSpec = mockWorkflowSpec0;
const expectedString = `
Workflow spec ID: all_things
Workflow name: all_things
Display name: Everything
Description: Do all the things
File name: one-fish.bpmn
Last updated: Jan 23, 2020
Last updated: ${lastUpdated}
Version: 1.0
`;
const file = new File([], 'one-fish.bpmn', {
type: 'text/xml',
lastModified: new Date('2020-01-23T12:34:12.345Z').getTime(),
});
mockFileMeta0.file = file;
mockFileMeta0.type = FileType.BPMN;
mockFileMeta0.last_modified = '2020-01-23T12:34:12.345Z';
expect(component.getFileMetaTooltipText(mockFileMeta0)).toEqual(expectedString);
});
@ -497,14 +460,15 @@ describe('ModelerComponent', () => {
it('should display open file dialog', () => {
const data: OpenFileDialogData = {
file: mockFileMeta0.file
file: mockFile0,
};
const expectedFile = new File([], mockFileMeta0.name, {type: mockFileMeta0.content_type});
const event = {target: {files: [expectedFile]}};
const onSubmitFileToOpenSpy = spyOn(component, 'onSubmitFileToOpen').and.stub();
const openDialogSpy = spyOn(component.dialog, 'open')
.and.returnValue({afterClosed: () => of(data)});
component.openFileDialog();
expect(openDialogSpy).toHaveBeenCalled();
expect(component.requestFileClick).toBeTrue();
component.onFileSelected(event);
expect(component.diagramFile).toEqual(data.file);
expect(onSubmitFileToOpenSpy).toHaveBeenCalled();
});

View File

@ -1,5 +1,5 @@
import {DatePipe} from '@angular/common';
import {AfterViewInit, Component, ViewChild} from '@angular/core';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {MatBottomSheet} from '@angular/material/bottom-sheet';
import {MatDialog} from '@angular/material/dialog';
import {MatSnackBar} from '@angular/material/snack-bar';
@ -9,25 +9,27 @@ import {
ApiService,
FileMeta,
FileType,
getDiagramTypeFromXml,
isNumberDefined,
newFileFromResponse,
WorkflowSpec
WorkflowSpec,
} from 'sartography-workflow-lib';
import {FileMetaDialogComponent} from '../_dialogs/file-meta-dialog/file-meta-dialog.component';
import {NewFileDialogComponent} from '../_dialogs/new-file-dialog/new-file-dialog.component';
import {OpenFileDialogComponent} from '../_dialogs/open-file-dialog/open-file-dialog.component';
import {ConfirmDialogComponent} from '../_dialogs/confirm-dialog/confirm-dialog.component';
import {BpmnWarning} from '../_interfaces/bpmn-warning';
import {FileMetaDialogData, NewFileDialogData, OpenFileDialogData} from '../_interfaces/dialog-data';
import {FileMetaDialogData, NewFileDialogData} from '../_interfaces/dialog-data';
import {ImportEvent} from '../_interfaces/import-event';
import {DiagramComponent} from '../diagram/diagram.component';
import {SettingsService} from '../settings.service';
import {getDiagramTypeFromXml} from '../_util/diagram-type';
@Component({
selector: 'app-modeler',
templateUrl: './modeler.component.html',
styleUrls: ['./modeler.component.scss']
styleUrls: ['./modeler.component.scss'],
})
export class ModelerComponent implements AfterViewInit {
@ViewChild('fileInput', {static: true}) fileInput: ElementRef;
title = 'bpmn-js-angular';
diagramUrl = 'https://cdn.staticaly.com/gh/bpmn-io/bpmn-js-examples/dfceecba/starter/diagram.bpmn';
importError?: Error;
@ -40,13 +42,17 @@ export class ModelerComponent implements AfterViewInit {
diagramFileMeta: FileMeta;
fileName: string;
fileTypes = FileType;
validationState: string;
validationData: { [key: string]: any } = {};
@ViewChild(DiagramComponent) private diagramComponent: DiagramComponent;
private xml = '';
private draftXml = '';
private svg = '';
@ViewChild(DiagramComponent) private diagramComponent: DiagramComponent;
private diagramType: FileType;
private workflowSpecId: string;
private fileMetaId: number;
private isNew = false;
private requestFileClick = false;
constructor(
private api: ApiService,
@ -55,6 +61,7 @@ export class ModelerComponent implements AfterViewInit {
public dialog: MatDialog,
private route: ActivatedRoute,
private router: Router,
private settingsService: SettingsService,
) {
this.route.queryParams.subscribe(q => {
this._handleAction(q);
@ -67,19 +74,40 @@ export class ModelerComponent implements AfterViewInit {
});
}
get bpmnFilesNoSelf(): FileMeta[] {
return this.bpmnFiles.filter(f => f.id !== this.fileMetaId);
}
static isXmlFile(file: File) {
return file.type.toLowerCase() === 'text/xml' ||
file.type.toLowerCase() === 'application/xml' ||
file.name.slice(-5).toLowerCase() === '.bpmn' ||
file.name.slice(-4).toLowerCase() === '.dmn' ||
file.name.slice(-4).toLowerCase() === '.xml';
}
ngAfterViewInit(): void {
this.diagramComponent.registerOnChange((newXmlValue: string, newSvgValue: string) => {
console.log('ModelerComponent > DiagramComponent > onChange');
this.draftXml = newXmlValue;
if (this.draftXml !== newXmlValue + ' ') {
// When we initialize a new dmn, the component registers a change even if nothing
// changes. So . . . we check to make sure it *really* changed before updating the draftXml.
this.draftXml = newXmlValue;
}
this.svg = newSvgValue;
});
if (this.requestFileClick) {
this.fileInput.nativeElement.click();
this.requestFileClick = false;
}
}
handleImported(event: ImportEvent) {
const {
type,
error,
warnings
warnings,
} = event;
if (type === 'success') {
@ -92,34 +120,95 @@ export class ModelerComponent implements AfterViewInit {
this.importError = error;
this.importWarnings = warnings;
this.draftXml = this.xml;
// if this is a new file then we force a change to the file
if (this.isNew) {
this.draftXml = this.xml + ' ';
this.isNew = false;
} else {
this.draftXml = this.xml;
}
}
onSubmitFileToOpen() {
this.expandToolbar = false;
if (this.openMethod === 'url') {
this.diagramComponent.loadUrl(this.diagramUrl);
} else {
if (this.diagramFile && this.isXmlFile(this.diagramFile)) {
/** If it is a spreadsheet, create a DMN from it */
if (this.diagramFile && this.diagramFile.type.toLowerCase() === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
this.api.createDMNFromSS(this.diagramFile).subscribe(file => {
let fileMeta = {
id: 0,
content_type: 'text/xml',
name: 'new_dmn',
type: FileType.DMN,
}
this.diagramFile = newFileFromResponse(fileMeta, file);
console.log(this.diagramFile);
console.log(file);
this.readFile(this.diagramFile);
} else {
this.handleImported({
type: 'error',
error: new Error('Wrong file type. Please choose a BPMN XML file.')
});
}
});
}
else if (this.diagramFile && ModelerComponent.isXmlFile(this.diagramFile)) {
this.readFile(this.diagramFile);
} else {
this.handleImported({
type: 'error',
error: new Error('Wrong file type. Please choose a BPMN XML file.'),
});
}
this.openMethod = undefined;
}
getFileName() {
return this.diagramFile ? this.diagramFile.name : this.fileName || 'No file selected';
// return this.diagramFile ? this.diagramFile.name : this.fileName || 'No file selected';
return this.diagramFileMeta ? this.diagramFileMeta.name : this.fileName || 'No file selected';
}
checkSaved() {
if (this.hasChanged()) {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
maxWidth: '300px',
data: {
title: 'Unsaved Changes!',
message: 'Are you sure you want to abandon changes?',
},
});
dialogRef.afterClosed().subscribe(dialogResult => {
if (dialogResult) {
this.router.navigate(['/home', this.workflowSpec.id]);
}
});
} else {
this.router.navigate(['/home', this.workflowSpec.id]);
}
}
checkChangeBPMN(b: FileMeta) {
if (this.hasChanged()) {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
maxWidth: '300px',
data: {
title: 'Unsaved Changes!',
message: 'Are you sure you want to abandon changes?',
},
});
dialogRef.afterClosed().subscribe(dialogResult => {
if (dialogResult) {
this.router.navigate(['/modeler', this.workflowSpecId, b.id]);
}
});
} else {
this.router.navigate(['/modeler', this.workflowSpecId, b.id])
}
}
onFileSelected($event: Event) {
this.diagramFile = ($event.target as HTMLFormElement).files[0];
this.onSubmitFileToOpen();
this.isNew = true;
}
// Arrow function here preserves this context
@ -127,7 +216,7 @@ export class ModelerComponent implements AfterViewInit {
this.xml = (event.target as FileReader).result.toString();
const diagramType = getDiagramTypeFromXml(this.xml);
this.diagramComponent.openDiagram(this.xml, diagramType);
}
};
readFile(file: File) {
// FileReader must be instantiated this way so unit test can spy on it.
@ -148,12 +237,46 @@ export class ModelerComponent implements AfterViewInit {
}
}
hasChanged(): boolean {
return this.xml !== this.draftXml;
partially_validate(until_task: string) {
this.saveChanges();
const study_id = this.settingsService.getStudyIdForValidation();
this.validationState = 'unknown';
this.validationData = {testing_only: {a: 1, b: 'b', c: false, e: [], d: undefined}, real_fields: undefined};
this.api.validateWorkflowSpecification(this.diagramFileMeta.workflow_spec_id, until_task, study_id).subscribe(apiErrors => {
if (apiErrors && apiErrors.length === 1) {
if (apiErrors[0].code === 'validation_break') {
this.validationData = apiErrors[0];
this.validationState = 'passing';
} else {
this.validationData = apiErrors[0];
this.validationState = 'failing';
}
}
if (apiErrors.length > 0 && this.validationState !== 'passing') {
this.bottomSheet.open(ApiErrorsComponent, {data: {apiErrors}});
}
});
}
loadDbFile(bf: FileMeta) {
this.diagramFile = bf.file;
validate() {
this.saveChanges();
const studyId = this.settingsService.getStudyIdForValidation();
this.api.validateWorkflowSpecification(this.diagramFileMeta.workflow_spec_id, '', studyId).subscribe(apiErrors => {
if (apiErrors && apiErrors.length > 0) {
this.bottomSheet.open(ApiErrorsComponent, {data: {apiErrors}});
} else {
this.snackBar.open('Workflow specification is valid!', 'Ok', {duration: 5000});
}
});
}
hasChanged(): boolean {
return (this.xml !== this.draftXml) || this.isNew;
}
loadDbFile(bf: FileMeta, f: File) {
this.diagramFile = f;
this.diagramFileMeta = bf;
this.onSubmitFileToOpen();
}
@ -170,18 +293,14 @@ export class ModelerComponent implements AfterViewInit {
}
openFileDialog() {
const dialogData: OpenFileDialogData = {
file: undefined,
fileTypes: [FileType.DMN, FileType.BPMN],
};
const dialogRef = this.dialog.open(OpenFileDialogComponent, {data: dialogData});
// NB - Aaron said that doing this may be problematic.
// When we are handling the action in the constructor, the component hasn't been
// Rendered yet. I needed to call fileInput.click() after the component has rendered.
dialogRef.afterClosed().subscribe((data: OpenFileDialogData) => {
if (data && data.file) {
this.diagramFile = data.file;
this.onSubmitFileToOpen();
}
});
// In order to make this work, I check for the requestFileClick variable in ngAfterViewInit
// and then click it. I couldn't see any other way to make this do what I wanted to do
// it *appears* to work fine.
this.requestFileClick = true;
}
newFileDialog() {
@ -213,10 +332,7 @@ export class ModelerComponent implements AfterViewInit {
getFileMetaDisplayString(fileMeta: FileMeta) {
if (fileMeta) {
const lastUpdated = new DatePipe('en-us').transform(fileMeta.file.lastModified);
return fileMeta.name +
(fileMeta.latest_version ? ` - v${fileMeta.latest_version}` : '') +
(lastUpdated ? ` (${lastUpdated})` : '');
return fileMeta.name;
} else {
return 'Loading...';
}
@ -226,13 +342,9 @@ export class ModelerComponent implements AfterViewInit {
const spec = this.workflowSpec;
if (spec) {
const lastUpdatedDate = new Date(fileMeta.file.lastModified);
const lastUpdated = new DatePipe('en-us').transform(lastUpdatedDate);
const lastUpdatedDate = new Date(fileMeta.last_modified);
const lastUpdated = new DatePipe('en-us').transform(lastUpdatedDate, 'medium');
return `
Workflow spec ID: ${spec.id}
Workflow name: ${spec.name}
Display name: ${spec.display_name}
Description: ${spec.description}
File name: ${fileMeta.name}
Last updated: ${lastUpdated}
Version: ${fileMeta.latest_version}
@ -248,19 +360,18 @@ export class ModelerComponent implements AfterViewInit {
this.api.getFileMetas({workflow_spec_id: wfs.id}).subscribe(files => {
this.bpmnFiles = [];
files.forEach(f => {
this.api.getFileData(f.id).subscribe(response => {
if ((f.type === FileType.BPMN) || (f.type === FileType.DMN)) {
f.content_type = 'text/xml';
f.file = newFileFromResponse(f, response);
this.bpmnFiles.push(f);
if ((f.type === FileType.BPMN) || (f.type === FileType.DMN)) {
f.content_type = 'text/xml';
this.bpmnFiles.push(f);
if (f.id === this.fileMetaId) {
this.diagramFileMeta = f;
this.diagramFile = f.file;
if (f.id === this.fileMetaId) {
this.diagramFileMeta = f;
this.api.getFileData(f.id).subscribe(response => {
this.diagramFile = newFileFromResponse(f, response);
this.onSubmitFileToOpen();
}
});
}
});
}
});
});
});
@ -276,13 +387,14 @@ export class ModelerComponent implements AfterViewInit {
content_type: 'text/xml',
name: data.fileName,
type: fileType,
file: new File([this.xml], data.fileName, {type: 'text/xml'}),
workflow_spec_id: this.workflowSpec.id,
};
this.diagramFile = new File([this.xml], data.fileName, {type: 'text/xml'});
if (this.workflowSpec && isNumberDefined(fileMetaId)) {
// Update existing file meta
this.api.updateFileData(this.diagramFileMeta).subscribe(() => {
this.api.updateFileData(this.diagramFileMeta, this.diagramFile).subscribe(() => {
this.api.updateFileMeta(this.diagramFileMeta).subscribe(() => {
this.loadFilesFromDb();
this.snackBar.open(`Saved changes to file ${this.diagramFileMeta.name}.`, 'Ok', {duration: 5000});
@ -290,32 +402,24 @@ export class ModelerComponent implements AfterViewInit {
});
} else {
// Add new file meta
this.api.addFileMeta({workflow_spec_id: this.workflowSpec.id}, this.diagramFileMeta).subscribe(fileMeta => {
this.api.addFile({workflow_spec_id: this.workflowSpec.id}, this.diagramFileMeta, this.diagramFile).subscribe(fileMeta => {
this.router.navigate(['/modeler', this.workflowSpec.id, fileMeta.id]);
this.snackBar.open(`Saved new file ${fileMeta.name} to workflow spec ${this.workflowSpec.name}.`, 'Ok', {duration: 5000});
this.snackBar.open(`Saved new file ${fileMeta.name} to workflow spec ${this.workflowSpec.display_name}.`, 'Ok', {duration: 5000});
}, () => {
// if this fails, we make sure that the file is treated as still new,
// and we make the user re-enter the file details as they weren't actually saved.
this.isNew = true;
this.diagramFileMeta = undefined;
});
}
}
}
private isXmlFile(file: File) {
return file.type.toLowerCase() === 'text/xml' ||
file.type.toLowerCase() === 'application/xml' ||
file.name.slice(-5).toLowerCase() === '.bpmn' ||
file.name.slice(-4).toLowerCase() === '.dmn' ||
file.name.slice(-4).toLowerCase() === '.xml';
}
private saveFileChanges() {
this.xml = this.draftXml;
this.diagramFileMeta.file = new File([this.xml], this.diagramFileMeta.name, {type: 'text/xml'});
this.diagramFile = new File([this.xml], this.diagramFileMeta.name, {type: 'text/xml'});
if (this.svg && this.svg !== '') {
const svgFile = new File([this.svg], this.diagramFileMeta.name, {type: 'text/xml'});
// this.api.updateFileData();
}
this.api.updateFileData(this.diagramFileMeta).subscribe(newFileMeta => {
this.api.updateFileData(this.diagramFileMeta, this.diagramFile).subscribe(newFileMeta => {
this.diagramFileMeta = newFileMeta;
this.snackBar.open(`Saved changes to file metadata ${this.diagramFileMeta.name}.`, 'Ok', {duration: 5000});
});
@ -330,4 +434,5 @@ export class ModelerComponent implements AfterViewInit {
}
}
}
}

View File

@ -1,11 +1,11 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {MatIconModule} from '@angular/material/icon';
import {MatMenuModule} from '@angular/material/menu';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {ApiService, MockEnvironment, mockUser} from 'sartography-workflow-lib';
import {ApiService, MockEnvironment, mockUser0} from 'sartography-workflow-lib';
import { NavbarComponent } from './navbar.component';
@ -15,7 +15,7 @@ describe('NavbarComponent', () => {
let httpMock: HttpTestingController;
const mockRouter = {navigate: jasmine.createSpy('navigate')};
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
NavbarComponent
@ -48,8 +48,8 @@ describe('NavbarComponent', () => {
const uReq = httpMock.expectOne('apiRoot/user');
expect(uReq.request.method).toEqual('GET');
uReq.flush(mockUser);
expect(component.user).toEqual(mockUser);
uReq.flush(mockUser0);
expect(component.user).toEqual(mockUser0);
});
it('should create', () => {

View File

@ -52,20 +52,14 @@ export class NavbarComponent {
}
private _loadNavLinks() {
const displayName = this.user.display_name || this.user.first_name || this.user.last_name;
const displayName = this.user.ldap_info.display_name;
this.navLinks = [
{path: '/home', id: 'nav_home', label: 'Configurator'},
{path: '/pb', id: 'nav_pb', label: 'Protocol Builder Tester'},
{path: '/reffiles', id: 'nav_reffiles', label: 'Reference Files'},
{path: '/help', id: 'nav_help', label: 'Help'},
{path: '/settings', id: 'settings', label: 'Settings'},
{
id: 'nav_account', label: `${displayName} (${this.user.email_address})`,
icon: 'account_circle',
links: [
{path: '/profile', id: 'nav_profile', label: 'Profile', icon: 'person'},
{path: '/notifications', id: 'nav_notifications', label: 'Notifications', icon: 'notifications'},
{path: '/sign-out', id: 'nav_sign_out', label: 'Sign out', icon: 'exit_to_app'},
]
id: 'nav_account', label: `${displayName} (${this.user.ldap_info.email_address})`,
icon: 'account_circle'
}
];
}

View File

@ -1,4 +0,0 @@
<div class="full-height" fxLayout="column" fxLayoutAlign="center center">
<h1>Protocol Builder Tester</h1>
<p>(Coming soon)</p>
</div>

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProtocolBuilderComponent } from './protocol-builder.component';
describe('ProtocolBuilderComponentComponent', () => {
let component: ProtocolBuilderComponent;
let fixture: ComponentFixture<ProtocolBuilderComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ProtocolBuilderComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProtocolBuilderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,15 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-protocol-builder',
templateUrl: './protocol-builder.component.html',
styleUrls: ['./protocol-builder.component.scss']
})
export class ProtocolBuilderComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -1,17 +1,29 @@
<div class="container" fxLayout="column" fxLayoutGap="40px">
<h1>Reference Files</h1>
<div class="top-bar" fxLayout="row" fxLayoutGap="75px">
<h1>Reference Files</h1>
<button mat-button color="accent" (click)="addNewReferenceFile()">
<mat-icon>file_upload</mat-icon> Add new Reference File
</button>
</div>
<div *ngFor="let refFile of referenceFiles" fxLayout="row" fxLayoutGap="40px">
<mat-card class="mat-elevation-z0">
<mat-card-header>
<mat-card-header fxLayout="row" fxLayoutAlign="space-between center">
<mat-card-title>
<h2>
<mat-icon>{{refFile.type | getIconCode}}</mat-icon>
{{refFile.name}}
{{refFile.name}}
</h2>
</mat-card-title>
<div class="trashcan" *ngIf="(refFile.name !== 'documents.xlsx') && (refFile.name !== 'investigators.xlsx')">
<button mat-button (click)="deleteFile(refFile.id, refFile.name)" matTooltip="Delete reference file '{{ refFile.name }}'">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-card-header>
<mat-card-content>
<div>Last Modified: {{refFile.last_modified | date:'medium'}}</div>
<div *ngIf="refFile.user_uid">By: {{refFile.user_uid}}</div>
<hr>
</mat-card-content>
<mat-card-footer>

View File

@ -1,6 +1,15 @@
@import "../../config";
.top-bar {
padding-top: 1em;
margin-bottom: 1em;
}
mat-card {
padding: 2em;
border: 1px solid $brand-gray;
}
.trashcan {
color: darkorange;
}

View File

@ -1,7 +1,7 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpHeaders} from '@angular/common/http';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {MatSnackBarModule} from '@angular/material/snack-bar';
@ -10,9 +10,9 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import * as FileSaver from 'file-saver';
import createClone from 'rfdc';
import { cloneDeep } from 'lodash';
import {of} from 'rxjs';
import {ApiService, FileMeta, FileType, MockEnvironment, mockFileMetaReference0} from 'sartography-workflow-lib';
import {ApiService, FileMeta, FileType, MockEnvironment, mockFileMetaReference0, mockFileReference0} from 'sartography-workflow-lib';
import {OpenFileDialogComponent} from '../_dialogs/open-file-dialog/open-file-dialog.component';
import {ReferenceFilesComponent} from './reference-files.component';
@ -23,12 +23,15 @@ describe('ReferenceFilesComponent', () => {
const mockRouter = {navigate: jasmine.createSpy('navigate')};
// Mock file and response headers
const mockDocMeta: FileMeta = createClone()(mockFileMetaReference0);
const mockDocMeta: FileMeta = cloneDeep(mockFileMetaReference0);
mockDocMeta.type = FileType.XLSX;
const timeString = '2020-01-23T12:34:12.345Z';
const timeCode = new Date(timeString).getTime();
const expectedFile = new File([], mockDocMeta.name, {
type: mockDocMeta.content_type,
lastModified: mockDocMeta.file.lastModified
lastModified: timeCode
});
const mockHeaders = new HttpHeaders()
@ -37,7 +40,7 @@ describe('ReferenceFilesComponent', () => {
const mockArrayBuffer = new ArrayBuffer(8);
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
@ -101,7 +104,7 @@ describe('ReferenceFilesComponent', () => {
.and.returnValue({
afterClosed: () => of({
fileMetaId: mockFileMetaReference0.id,
file: mockFileMetaReference0.file
file: mockFileReference0
})
} as any);
const _loadReferenceFilesSpy = spyOn((component as any), '_loadReferenceFiles').and.stub();

View File

@ -1,8 +1,8 @@
import {Component, OnInit} from '@angular/core';
import {Component} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {MatSnackBar} from '@angular/material/snack-bar';
import * as fileSaver from 'file-saver';
import {ApiService, FileMeta, FileType} from 'sartography-workflow-lib';
import {ApiService, FileMeta, FileParams, FileType, getFileType, isNumberDefined} from 'sartography-workflow-lib';
import {OpenFileDialogComponent} from '../_dialogs/open-file-dialog/open-file-dialog.component';
import {OpenFileDialogData} from '../_interfaces/dialog-data';
@ -11,7 +11,7 @@ import {OpenFileDialogData} from '../_interfaces/dialog-data';
templateUrl: './reference-files.component.html',
styleUrls: ['./reference-files.component.scss']
})
export class ReferenceFilesComponent implements OnInit {
export class ReferenceFilesComponent {
referenceFiles: FileMeta[];
constructor(
@ -22,9 +22,6 @@ export class ReferenceFilesComponent implements OnInit {
this._loadReferenceFiles();
}
ngOnInit(): void {
}
_loadReferenceFiles() {
this.apiService.listReferenceFiles().subscribe(f => this.referenceFiles = f);
}
@ -59,4 +56,36 @@ export class ReferenceFilesComponent implements OnInit {
fileSaver.saveAs(blob, fm.name);
});
}
addNewReferenceFile(fm?: FileMeta, file?: File) {
const dialogData: OpenFileDialogData = {
fileMetaId: fm ? fm.id : undefined,
file,
mode: 'reference',
fileTypes: [FileType.DOC, FileType.DOCX, FileType.XLSX, FileType.XLS],
};
const dialogRef = this.dialog.open(OpenFileDialogComponent, {data: dialogData});
dialogRef.afterClosed().subscribe((data: OpenFileDialogData) => {
if (data && data.file) {
const newFileMeta: FileMeta = {
content_type: data.file.type,
name: data.file.name,
type: getFileType(data.file),
is_reference: true,
};
this.apiService.addReferenceFile(newFileMeta, data.file).subscribe(refs => {
this.snackBar.open(`Added new file ${newFileMeta.name}.`, 'Ok', {duration: 3000});
this._loadReferenceFiles();
});
}
});
}
deleteFile(id: number, name: string) {
this.apiService.deleteFileMeta(id).subscribe(f => {
this.snackBar.open(`Deleted reference file ${name}.`, 'Ok', {duration: 3000});
this._loadReferenceFiles();
});
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SettingsService } from './settings.service';
describe('SettingsService', () => {
let service: SettingsService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SettingsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class SettingsService {
private studyIdKey = 'study_id';
constructor() { }
setStudyIdForValidation(id: number) {
localStorage.setItem(this.studyIdKey, id.toString());
}
getStudyIdForValidation() {
const value = localStorage.getItem(this.studyIdKey);
if (value) {
return parseInt(value, 10);
} else {
return null;
}
}
}

View File

@ -0,0 +1,14 @@
<div class="container" fxLayout="column" fxLayoutGap="40px">
<h1>Settings</h1>
<h4>Validation Settings</h4>
<mat-form-field appearance="fill" *ngIf="studies">
<mat-label>Select Study</mat-label>
<mat-hint>Here you can specify a default study to use when performing validation.</mat-hint>
<mat-select matNativeControl required [(value)]="selectedStudyId" (selectionChange)="selectStudy($event.value)">
<mat-option *ngFor="let s of studies" [value]="s.id">
{{s.id}} - {{s.title}}
</mat-option>
</mat-select>
</mat-form-field>
</div>

View File

@ -0,0 +1,39 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
import {HttpClient} from '@angular/common/http';
import {FakeMatIconRegistry} from '@angular/material/icon/testing';
import {ApiService, MockEnvironment} from 'sartography-workflow-lib';
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule} from '@angular/common/http/testing';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
const mockEnvironment = new MockEnvironment();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ SettingsComponent ],
imports: [
HttpClientTestingModule,
],
providers: [
HttpClient,
ApiService,
{provide: 'APP_ENVIRONMENT', useValue: mockEnvironment},
{provide: APP_BASE_HREF, useValue: ''},
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,31 @@
import {Component, Input, OnInit, Output} from '@angular/core';
import {Study} from 'sartography-workflow-lib/lib/types/study';
import {ApiService} from 'sartography-workflow-lib';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent implements OnInit {
studies: Study[] = [];
selectedStudyId: number;
constructor(private apiService: ApiService, private settingsService: SettingsService) { }
ngOnInit(): void {
this.selectedStudyId = this.settingsService.getStudyIdForValidation();
this.apiService.getStudies().subscribe(s => {
this.studies = s;
});
}
selectStudy(studyId: number) {
console.log('The study is ', studyId);
this.settingsService.setStudyIdForValidation(studyId);
}
}

View File

@ -15,14 +15,19 @@
</mat-card-title>
</mat-card-header>
<mat-card-content>
<dl gdAreas="field value" gdColumns="8ch auto">
<dt>ID</dt><dd>{{workflowSpec.id}}</dd>
<dt>Name</dt><dd>{{workflowSpec.name}}</dd>
<dl gdAreas="field value" gdColumns="10ch auto">
<dt>Name</dt><dd>{{workflowSpec.id}}</dd>
<dt>Description</dt><dd>{{workflowSpec.description}}</dd>
</dl>
<h4>Workflow Spec Files</h4>
<app-file-list [workflowSpec]="workflowSpec"></app-file-list>
<div *ngIf="!workflowSpec.library">
<h4 class="library-list">
<mat-icon (click)="expandToggle()" class="expand" *ngIf="!showAll">chevron_right</mat-icon>
<mat-icon (click)="expandToggle()" class="expand" *ngIf="showAll">expand_more</mat-icon>
Included Libraries</h4>
<app-library-list [showAll]="showAll" [workflowSpecId]="workflowSpec.id"></app-library-list>
</div>
</mat-card-content>
<mat-card-actions>
</mat-card-actions>

View File

@ -48,3 +48,11 @@ mat-card {
}
}
.library-list{
margin-top: 2rem;
}
.expand{
margin-right: 5px;
}

View File

@ -1,6 +1,6 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {MatCardModule} from '@angular/material/card';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
@ -16,7 +16,7 @@ describe('WorkflowSpecCardComponent', () => {
let component: WorkflowSpecCardComponent;
let fixture: ComponentFixture<WorkflowSpecCardComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
WorkflowSpecCardComponent,

View File

@ -1,20 +1,20 @@
import {Component, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
import {ApiService, WorkflowSpec} from 'sartography-workflow-lib';
import {Component, Input, TemplateRef} from '@angular/core';
import { WorkflowSpec} from 'sartography-workflow-lib';
@Component({
selector: 'app-workflow-spec-card',
templateUrl: './workflow-spec-card.component.html',
styleUrls: ['./workflow-spec-card.component.scss']
})
export class WorkflowSpecCardComponent implements OnInit {
export class WorkflowSpecCardComponent {
@Input() workflowSpec: WorkflowSpec;
@Input() actionButtons: TemplateRef<any>;
showAll: boolean;
constructor(
private api: ApiService
) {
}
ngOnInit(): void {
expandToggle() {
this.showAll = !this.showAll;
}
}

View File

@ -1,96 +1,182 @@
<div class="container mat-typography" fxLayout="column" fxLayoutGap="10px">
<h1>Workflow Specifications</h1>
<div fxLayout="row" fxLayoutGap="10px">
<button mat-flat-button color="primary" (click)="editWorkflowSpec()">
<mat-icon>library_add</mat-icon>
Add new workflow specification
</button>
<button mat-flat-button color="accent" (click)="editWorkflowSpecCategory()">
<mat-icon>post_add</mat-icon>
Add category
</button>
<div class="workflow-specs" fxLayout="column" fxLayoutGap="10px">
<div class="buttons" fxLayout="row" fxLayoutGap="20px">
<div>
<h1 style="margin-top: 16px">Workflow Specifications</h1>
</div>
<mat-form-field appearance="outline" style="padding:0">
<label><input matInput type="search" placeholder="Search Workflows" fxLayoutAlign="start" style="margin-top: 0;" class="form-control" [formControl]="searchField"></label>
</mat-form-field>
</div>
<ng-container *ngFor="let cat of workflowSpecsByCategory">
<ng-container *ngIf="!(cat.id === null && cat.workflow_specs.length === 0)">
<div class="category" fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="start center">
<h2>{{cat.display_name}} ({{cat.name}})</h2>
<div class="category-actions" fxLayout="row" fxLayoutGap="10px" *ngIf="cat.id !== null">
<button mat-mini-fab color="primary" (click)="editWorkflowSpecCategory(cat)">
<mat-icon>edit</mat-icon>
</button>
<button
*ngIf="cat.display_order > 0"
mat-mini-fab
title="Move up"
color="primary"
(click)="editCategoryDisplayOrder(cat.id, -1, workflowSpecsByCategory)"
>
<mat-icon>arrow_upward</mat-icon>
</button>
<button
*ngIf="cat.display_order < workflowSpecsByCategory.length - 2"
mat-mini-fab
title="Move down"
color="primary"
(click)="editCategoryDisplayOrder(cat.id, 1, workflowSpecsByCategory)"
>
<mat-icon>arrow_downward</mat-icon>
</button>
<button mat-icon-button title="Delete this category" color="warn" (click)="confirmDeleteWorkflowSpecCategory(cat)">
<mat-icon>delete</mat-icon>
<mat-drawer-container class="example-container" autosize>
<mat-drawer #drawer class="example-sidenav" mode="side" opened="true">
<ng-container *ngIf="masterStatusSpec">
<div class="category-top">
<h4>Master Specification</h4>
<mat-list>
<mat-list-item class="workflow-spec" fxLayout="row">
<span class="spec_menu_item" (click)="selectSpec(masterStatusSpec)">{{masterStatusSpec.display_name}}</span>
</mat-list-item>
</mat-list>
</div>
</ng-container>
<mat-divider></mat-divider>
<mat-divider></mat-divider>
<ng-container>
<div class="category" fxLayout="row">
<h4>Library Specs</h4>
<button mat-fab class="custom-fab" id="add_spec" title="Add new Library" color="primary" (click)="editWorkflowSpec('library')" fxLayoutAlign="auto">
<mat-icon class="custom-icon">library_add</mat-icon>
</button>
</div>
</div>
<div *ngFor="let wfs of cat.workflow_specs" class="workflow-spec">
<ng-container *ngTemplateOutlet="workflowSpecCard; context: {wfs: wfs, cat: cat}"></ng-container>
</div>
<div *ngIf="cat.workflow_specs.length === 0">No workflow specs in this category</div>
</ng-container>
</ng-container>
<mat-divider></mat-divider>
<ng-container *ngIf="masterStatusSpec">
<h1>Master Status Specification</h1>
<ng-container *ngTemplateOutlet="workflowSpecCard; context: {wfs: masterStatusSpec, cat: null}"></ng-container>
</ng-container>
</div>
<mat-divider></mat-divider>
<mat-accordion class="example-headers-align">
<mat-expansion-panel [expanded]="library_toggle" (opened)="libraryToggle(true)">
<mat-expansion-panel-header>
<mat-panel-title>
<h4>All Libraries</h4>
</mat-panel-title>
</mat-expansion-panel-header>
<mat-list>
<mat-list-item *ngFor="let wfs of workflowLibraries" class="workflow-spec" fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="start center">
<div>
<span [ngClass]="{'library_item':true, 'spec_menu_item':true, 'library-selected': selectedSpec && wfs.id === selectedSpec.id}" (click)="selectSpec(wfs)">{{wfs.display_name}}</span>
</div>
</mat-list-item>
</mat-list>
</mat-expansion-panel>
</mat-accordion>
</ng-container>
<ng-template #workflowSpecCard let-wfs="wfs" let-cat="cat">
<app-workflow-spec-card
[workflowSpec]="wfs"
[actionButtons]="actionButtons"
(workflowUpdated)="onWorkflowUpdated($event)"
></app-workflow-spec-card>
<ng-template #actionButtons>
<div class="workflow-spec-actions">
<button mat-mini-fab title="Check for errors in this workflow specification" color="accent" (click)="validateWorkflowSpec(wfs)">
<mat-icon>verified_user</mat-icon>
</button>
<button
*ngIf="cat && cat.workflow_specs.length > 0 && wfs.display_order !== 0"
mat-mini-fab
title="Move up"
color="primary"
(click)="editSpecDisplayOrder(wfs.id, -1, cat.workflow_specs)"
>
<mat-icon>arrow_upward</mat-icon>
</button>
<button
*ngIf="cat && cat.workflow_specs.length > 0 && (wfs.display_order < cat.workflow_specs.length - 1)"
mat-mini-fab
title="Move down"
color="primary"
(click)="editSpecDisplayOrder(wfs.id, 1, cat.workflow_specs)"
>
<mat-icon>arrow_downward</mat-icon>
</button>
<button mat-mini-fab title="Edit this workflow specification" color="primary" (click)="editWorkflowSpec(wfs)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button title="Delete this workflow specification" color="warn" (click)="confirmDeleteWorkflowSpec(wfs)">
<mat-icon>delete</mat-icon>
</button>
<mat-divider></mat-divider>
<ng-container>
<div class="category" fxLayout="row">
<h4 style="margin-right: 25px">Study Specs</h4>
<div fxLayout="row" fxLayoutGap="5px">
<button id="add_category" title="Add new Category" mat-fab class="custom-fab" color="accent" (click)="editWorkflowSpecCategory()" fxLayoutAlign="stretch">
<mat-icon class="custom-icon">post_add</mat-icon>
</button>
<button id="add_library" title="Add new Study Spec" mat-fab class="custom-fab" color="primary" (click)="editWorkflowSpec('study')" fxLayoutAlign="start">
<mat-icon class="custom-icon">library_add</mat-icon>
</button>
</div>
</div>
<mat-divider></mat-divider>
<mat-accordion class="example-headers-align" multi="false">
<ng-container *ngFor="let cat of workflowSpecsByCategory; let j = index">
<div *ngIf="!(searchField.value && cat.workflow_specs.length === 0)">
<ng-container *ngIf="!(cat.id === null && cat.workflow_specs.length === 0)">
<mat-expansion-panel hideToggle (opened)="selectCat(cat); libraryToggle(false)" [expanded]="isSelected(cat) && !library_toggle" >
<mat-expansion-panel-header>
<mat-panel-description>
<div *ngIf="cat.admin" style="color: darkorange">
{{cat.display_name}}
</div>
<div *ngIf="!cat.admin">
{{cat.display_name}}
</div>
</mat-panel-description>
<button mat-mini-fab color="primary" style="box-shadow: none">
{{cat.workflow_specs.length}}
</button>
</mat-expansion-panel-header>
<mat-list>
<mat-list-item *ngFor="let wfs of cat.workflow_specs; let i = index" class="workflow-spec" fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="start center">
<span [ngClass]="{'spec_menu_item':true, 'spec-selected': selectedSpec && wfs.id === selectedSpec.id}" (click)="selectSpec(wfs)">{{wfs.display_name}}</span>
<span class="spec-actions" fxLayout="row" fxLayoutGap="10px" *ngIf="cat.id !== null && cat">
<button
*ngIf="i!==0 && cat.workflow_specs.length > 0"
mat-icon-button
title="Move up"
color="primary"
(click)="editSpecDisplayOrder(cat, wfs.id, 'up')"
>
<mat-icon>arrow_upward</mat-icon>
</button>
<button
*ngIf="i!==cat.workflow_specs.length-1 && cat.workflow_specs.length > 0"
mat-icon-button
title="Move down"
color="primary"
(click)="editSpecDisplayOrder(cat, wfs.id, 'down')"
>
<mat-icon>arrow_downward</mat-icon>
</button>
</span>
<!--
<ng-container *ngTemplateOutlet="workflowSpecCard; context: {wfs: wfs, cat: cat}"></ng-container>
-->
</mat-list-item>
</mat-list>
<div *ngIf="cat.workflow_specs.length === 0">No workflow specs in this category</div>
<mat-action-row>
<button mat-mini-fab color="primary" (click)="editWorkflowSpecCategory(cat)">
<mat-icon>edit</mat-icon>
</button>
<button
*ngIf="j!==0"
mat-mini-fab
title="Move up"
color="primary"
(click)="editCategoryDisplayOrder(cat.id, 'up')"
>
<mat-icon>arrow_upward</mat-icon>
</button>
<button
*ngIf="j!== workflowSpecsByCategory.length-1"
mat-mini-fab
title="Move down"
color="primary"
(click)="editCategoryDisplayOrder(cat.id, 'down')"
>
<mat-icon>arrow_downward</mat-icon>
</button>
<button mat-icon-button title="Delete this category" color="warn"
(click)="confirmDeleteWorkflowSpecCategory(cat)">
<mat-icon>delete</mat-icon>
</button>
</mat-action-row>
</mat-expansion-panel>
</ng-container>
</div>
</ng-container>
</mat-accordion>
</ng-container>
</mat-drawer>
<div class="content">
<ng-container *ngIf="selectedSpec">
<ng-container *ngTemplateOutlet="workflowSpecCard; context: {wfs: selectedSpec, cat: null}"></ng-container>
</ng-container>
</div>
</ng-template>
</ng-template>
</mat-drawer-container>
<ng-template #workflowSpecCard let-wfs="wfs" let-cat="cat">
<app-workflow-spec-card
[workflowSpec]="wfs"
[actionButtons]="actionButtons"
></app-workflow-spec-card>
<ng-template #actionButtons>
<div class="workflow-spec-actions">
<button mat-mini-fab title="Check for errors in this workflow specification" color="accent"
(click)="validateWorkflowSpec(wfs)">
<mat-icon>verified_user</mat-icon>
</button>
<button mat-mini-fab title="Edit this workflow specification" color="primary" (click)="editWorkflowSpec(this.selectedSpec.library ? 'library' : 'study', wfs)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button title="Delete this workflow specification" color="warn"
(click)="confirmDeleteWorkflowSpec(wfs)">
<mat-icon>delete</mat-icon>
</button>
</div>
</ng-template>
</ng-template>
</div>

View File

@ -1,12 +1,37 @@
.container {
@import "../../config";
::ng-deep mat-form-field.mat-form-field-appearance-outline .mat-form-field-wrapper {
margin: 0;
max-height: 40px;
}
.workflow-specs {
padding: 3rem;
}
.category {
padding: 1rem 1rem 1rem 0;
.buttons {
height: 60px;
}
h2 {
margin: 1rem 1rem 1rem 0;
.custom-fab {
width: 45px;
height: 45px;
}
.custom-icon {
margin-left: 10px;
margin-top: -15px;
}
.content {
min-height: 600px;
}
.category-top {
padding: 0 1rem 1rem 0;
h4 {
margin: 1rem 1rem 0 0;
}
.category-actions {
@ -22,8 +47,76 @@
opacity: 1;
}
}
}
.category {
padding: 2rem 1rem .75rem 0;
h4 {
margin: 5px 1rem 0 0;
}
.category-actions {
opacity: 0;
&:hover {
opacity: 1;
}
}
&:hover {
.category-actions {
opacity: 1;
}
}
}
.mat-list-base .mat-list-item, .mat-list-base .mat-list-option {
height: auto;
}
.workflow-spec {
border-left: 4px solid $brand-gray-light;
font-size: 1em;
cursor: pointer;
&:hover {
border-left: 4px solid $brand-primary;
background-color: $brand-primary-tint-4;
cursor: pointer;
}
.library_item {
padding: 10px;
}
.spec_menu_item {
flex-flow: row;
display: inline-block;
width: 300px;
margin-left: 20px;
}
.spec-selected {
font-weight: bold;
}
.spec-actions {
width: 100px;
opacity: 0;
}
&:hover {
.spec-actions {
opacity: 1;
}
}
}
.library-selected {
font-weight: bold;
}
.workflow-spec-actions {
button {
margin-right: 1em;

View File

@ -1,24 +1,24 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
import {MAT_BOTTOM_SHEET_DATA, MatBottomSheetModule, MatBottomSheetRef} from '@angular/material/bottom-sheet';
import {MatCardModule} from '@angular/material/card';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {MatListModule} from '@angular/material/list';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {RouterTestingModule} from '@angular/router/testing';
import createClone from 'rfdc';
import { cloneDeep } from 'lodash';
import {of} from 'rxjs';
import {
ApiErrorsComponent,
ApiService,
MockEnvironment,
MockEnvironment, mockWorkflowMeta1,
mockWorkflowSpec0,
mockWorkflowSpec1,
mockWorkflowSpec2,
mockWorkflowSpec2, mockWorkflowSpec3,
mockWorkflowSpecCategories,
mockWorkflowSpecCategory0,
mockWorkflowSpecCategory1,
@ -36,14 +36,38 @@ import {
} from '../_interfaces/dialog-data';
import {GetIconCodePipe} from '../_pipes/get-icon-code.pipe';
import {FileListComponent} from '../file-list/file-list.component';
import {WorkflowSpecListComponent} from './workflow-spec-list.component';
import {WorkflowSpecCategoryGroup, WorkflowSpecListComponent} from './workflow-spec-list.component';
export class MdDialogMock {
// When the component calls this.dialog.open(...) we'll return an object
// with an afterClosed method that allows to subscribe to the dialog result observable.
open() {
return {
afterClosed: () => of([
{}
])
};
}
}
const librarySpec0: WorkflowSpec = {
id: 'one_thing',
display_name: 'One thing',
description: 'Do just one thing',
category_id: 2,
library: true,
category: mockWorkflowSpecCategory2,
display_order: 2,
};
describe('WorkflowSpecListComponent', () => {
let httpMock: HttpTestingController;
let component: WorkflowSpecListComponent;
let fixture: ComponentFixture<WorkflowSpecListComponent>;
let dialog: MatDialog;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
@ -68,11 +92,7 @@ describe('WorkflowSpecListComponent', () => {
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment},
{provide: APP_BASE_HREF, useValue: ''},
{
provide: MatDialogRef,
useValue: {
close: (dialogResult: any) => {
},
}
provide: MatDialogRef, useClass: MdDialogMock,
},
{provide: MAT_DIALOG_DATA, useValue: []},
{
@ -100,15 +120,23 @@ describe('WorkflowSpecListComponent', () => {
fixture = TestBed.createComponent(WorkflowSpecListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
dialog = TestBed.inject(MatDialog);
const catReq = httpMock.expectOne('apiRoot/workflow-specification-category');
expect(catReq.request.method).toEqual('GET');
catReq.flush(mockWorkflowSpecCategories);
expect(component.categories.length).toBeGreaterThan(0);
const specReq = httpMock.expectOne('apiRoot/workflow-specification');
const specReq2 = httpMock.expectOne('apiRoot/workflow-specification?libraries=true');
expect(specReq2.request.method).toEqual('GET');
specReq2.flush([librarySpec0]);
fixture.detectChanges();
expect(component.workflowLibraries.length).toBeGreaterThan(0);
const specReq = httpMock.expectOne('apiRoot/workflow-specification');
expect(specReq.request.method).toEqual('GET');
specReq.flush(mockWorkflowSpecs);
fixture.detectChanges();
expect(component.workflowSpecs.length).toBeGreaterThan(0);
});
@ -124,24 +152,27 @@ describe('WorkflowSpecListComponent', () => {
it('should show a metadata dialog when editing a workflow spec', () => {
let mockSpecData: WorkflowSpecDialogData = {
id: '',
name: '',
display_name: '',
description: '',
category_id: 0,
display_order: 0,
standalone: false,
library: false
};
const _upsertWorkflowSpecificationSpy = spyOn((component as any), '_upsertWorkflowSpecification')
.and.stub();
const openDialogSpy = spyOn(component.dialog, 'open')
.and.returnValue({afterClosed: () => of(mockSpecData)} as any);
component.editWorkflowSpec(mockWorkflowSpec0);
component.selectedSpec = mockWorkflowSpec1;
component.selectedSpec.parents = [];
component.selectedSpec.libraries = [];
component.editWorkflowSpec('study');
expect(openDialogSpy).toHaveBeenCalled();
expect(_upsertWorkflowSpecificationSpy).not.toHaveBeenCalled();
mockSpecData = mockWorkflowSpec0 as WorkflowSpecDialogData;
component.editWorkflowSpec(mockWorkflowSpec0);
component.editWorkflowSpec('study', mockWorkflowSpec0);
expect(openDialogSpy).toHaveBeenCalled();
expect(_upsertWorkflowSpecificationSpy).toHaveBeenCalled();
});
@ -151,7 +182,7 @@ describe('WorkflowSpecListComponent', () => {
const _updateWorkflowSpecSpy = spyOn((component as any), '_updateWorkflowSpec').and.stub();
component.selectedSpec = undefined;
(component as any)._upsertWorkflowSpecification(mockWorkflowSpec1 as WorkflowSpecDialogData);
(component as any)._upsertWorkflowSpecification(true, mockWorkflowSpec1 as WorkflowSpecDialogData);
expect(_addWorkflowSpecSpy).toHaveBeenCalled();
expect(_updateWorkflowSpecSpy).not.toHaveBeenCalled();
@ -159,15 +190,16 @@ describe('WorkflowSpecListComponent', () => {
_updateWorkflowSpecSpy.calls.reset();
component.selectedSpec = mockWorkflowSpec0;
const modifiedData: WorkflowSpecDialogData = createClone({circles: true})(mockWorkflowSpec0);
const modifiedData: WorkflowSpec = cloneDeep(mockWorkflowSpec0);
modifiedData.display_name = 'Modified';
(component as any)._upsertWorkflowSpecification(modifiedData);
(component as any)._upsertWorkflowSpecification(false, modifiedData);
expect(_addWorkflowSpecSpy).not.toHaveBeenCalled();
expect(_updateWorkflowSpecSpy).toHaveBeenCalled();
});
it('should add a workflow spec', () => {
const _loadWorkflowSpecsSpy = spyOn((component as any), '_loadWorkflowSpecs').and.stub();
const _loadWorkflowLibrariesSpy = spyOn((component as any), '_loadWorkflowLibraries').and.stub();
const _displayMessageSpy = spyOn((component as any), '_displayMessage').and.stub();
(component as any)._addWorkflowSpec(mockWorkflowSpec0);
const wfsReq = httpMock.expectOne(`apiRoot/workflow-specification`);
@ -175,6 +207,7 @@ describe('WorkflowSpecListComponent', () => {
wfsReq.flush(mockWorkflowSpec0);
expect(_loadWorkflowSpecsSpy).toHaveBeenCalled();
expect(_loadWorkflowLibrariesSpy).toHaveBeenCalled();
expect(_displayMessageSpy).toHaveBeenCalled();
});
@ -186,6 +219,11 @@ describe('WorkflowSpecListComponent', () => {
expect(wfsReq.request.method).toEqual('PUT');
wfsReq.flush(mockWorkflowSpec0);
const wfsReq2 = httpMock.expectOne(`apiRoot/workflow-specification?libraries=true`);
expect(wfsReq2.request.method).toEqual('GET');
wfsReq2.flush([librarySpec0]);
expect(_loadWorkflowSpecsSpy).toHaveBeenCalled();
expect(_displayMessageSpy).toHaveBeenCalled();
});
@ -212,19 +250,29 @@ describe('WorkflowSpecListComponent', () => {
it('should delete a workflow spec', () => {
const loadWorkflowSpecsSpy = spyOn((component as any), '_loadWorkflowSpecs').and.stub();
const _loadWorkflowLibrariesSpy = spyOn((component as any), '_loadWorkflowLibraries').and.stub();
(component as any)._deleteWorkflowSpec(mockWorkflowSpec0);
const wfsReq = httpMock.expectOne(`apiRoot/workflow-specification/${mockWorkflowSpec0.id}`);
expect(wfsReq.request.method).toEqual('DELETE');
wfsReq.flush(null);
expect(loadWorkflowSpecsSpy).toHaveBeenCalled();
expect(_loadWorkflowLibrariesSpy).toHaveBeenCalled();
});
it('should set a library spec as the selected spec', () => {
const _loadWorkflowLibrariesSpy = spyOn((component as any), '_loadWorkflowLibraries').and.stub();
(component as any)._loadWorkflowLibraries(mockWorkflowSpec3)
component.selectedSpec = mockWorkflowSpec3;
expect(_loadWorkflowLibrariesSpy).toHaveBeenCalled()
expect(component.selectedSpec).toEqual(mockWorkflowSpec3)
})
it('should show a metadata dialog when editing a workflow spec category', () => {
let mockCatData: WorkflowSpecCategoryDialogData = {
id: null,
name: '',
display_name: '',
admin: null,
};
const _upsertWorkflowSpecCategorySpy = spyOn((component as any), '_upsertWorkflowSpecCategory')
@ -247,6 +295,7 @@ describe('WorkflowSpecListComponent', () => {
const _updateWorkflowSpecCategorySpy = spyOn((component as any), '_updateWorkflowSpecCategory').and.stub();
component.selectedCat = undefined;
mockWorkflowSpecCategory1.id = null;
(component as any)._upsertWorkflowSpecCategory(mockWorkflowSpecCategory1 as WorkflowSpecCategoryDialogData);
expect(_addWorkflowSpecCategorySpy).toHaveBeenCalled();
expect(_updateWorkflowSpecCategorySpy).not.toHaveBeenCalled();
@ -255,7 +304,7 @@ describe('WorkflowSpecListComponent', () => {
_updateWorkflowSpecCategorySpy.calls.reset();
component.selectedCat = mockWorkflowSpecCategory0;
const modifiedData: WorkflowSpecCategoryDialogData = createClone({circles: true})(mockWorkflowSpecCategory0);
const modifiedData: WorkflowSpecCategoryDialogData = cloneDeep(mockWorkflowSpecCategory0);
modifiedData.display_name = 'Modified';
(component as any)._upsertWorkflowSpecCategory(modifiedData);
expect(_addWorkflowSpecCategorySpy).not.toHaveBeenCalled();
@ -342,6 +391,10 @@ describe('WorkflowSpecListComponent', () => {
task_name: 'task_random_num',
file_name: 'random.bpmn',
tag: 'bpmn:definitions',
task_data: {},
line_number: 12,
offset: 0,
error_line: 'x != y'
};
invalidReq.flush([mockError]);
expect(bottomSheetSpy).toHaveBeenCalled();
@ -349,114 +402,49 @@ describe('WorkflowSpecListComponent', () => {
});
it('should edit category display order', () => {
const _reorderSpy = spyOn((component as any), '_reorder').and.stub();
const _updateCatDisplayOrdersSpy = spyOn((component as any), '_updateCatDisplayOrders').and.stub();
it('should update a single category display order', () => {
mockWorkflowSpecCategory1.id = 5;
component.editCategoryDisplayOrder(2, -1, mockWorkflowSpecCategories);
expect(_reorderSpy).toHaveBeenCalled();
expect(_updateCatDisplayOrdersSpy).toHaveBeenCalled();
// Intermittently, Jasmine does not find the array prototype function, causing errors.
// This defines the 'find' function in case it doesn't find it.
if (typeof Array.prototype.find !== 'function') {
Array.prototype.find = function(iterator) {
let list = Object(this);
let length = list.length >>> 0;
let thisArg = arguments[1];
let value;
for (let i = 0; i < length; i++) {
value = list[i];
if (iterator.call(thisArg, value, i, list)) {
return value;
}
}
return undefined;
};
}
(component as any).editCategoryDisplayOrder(mockWorkflowSpecCategory1.id, 'down');
let results = { param: 'direction', value: 'down' };
const req = httpMock.expectOne(`apiRoot/workflow-specification-category/${mockWorkflowSpecCategory1.id}/reorder?${results.param}=${results.value}`);
expect(req.request.method).toEqual('PUT');
req.flush(mockWorkflowSpecCategory1);
});
it('should edit workflow spec display order', () => {
const _reorderSpy = spyOn((component as any), '_reorder').and.stub();
const _updateSpecDisplayOrdersSpy = spyOn((component as any), '_updateSpecDisplayOrders').and.stub();
component.editSpecDisplayOrder('few_things', -1, mockWorkflowSpecs);
expect(_reorderSpy).toHaveBeenCalled();
expect(_updateSpecDisplayOrdersSpy).toHaveBeenCalled();
it('should update a single spec display order', () => {
let wfs_group: WorkflowSpecCategoryGroup[] = [];
mockWorkflowSpecCategory1.workflows.push(mockWorkflowMeta1);
wfs_group.push(mockWorkflowSpecCategory1);
(component as any).editSpecDisplayOrder(wfs_group[0], mockWorkflowSpec1.id, 'down');
let results = { param: 'direction', value: 'down' };
const req = httpMock.expectOne(`apiRoot/workflow-specification/${mockWorkflowSpec1.id}/reorder?${results.param}=${results.value}`);
expect(req.request.method).toEqual('PUT');
req.flush(mockWorkflowSpecCategory1);
});
it('should reorder categories', () => {
const snackBarSpy = spyOn((component as any).snackBar, 'open').and.stub();
const moveUpSpy = spyOn(component, 'moveUp').and.callThrough();
const moveDownSpy = spyOn(component, 'moveDown').and.callThrough();
const expectedCatsAfter = [mockWorkflowSpecCategory1, mockWorkflowSpecCategory0, mockWorkflowSpecCategory2];
expect((component as any)._reorder(99, 1, mockWorkflowSpecCategories)).toEqual([]);
expect(snackBarSpy).toHaveBeenCalled();
expect(moveUpSpy).not.toHaveBeenCalled();
expect(moveDownSpy).not.toHaveBeenCalled();
snackBarSpy.calls.reset();
moveUpSpy.calls.reset();
moveDownSpy.calls.reset();
expect((component as any)._reorder(1, -1, mockWorkflowSpecCategories)).toEqual(expectedCatsAfter);
expect(snackBarSpy).not.toHaveBeenCalled();
expect(moveUpSpy).toHaveBeenCalled();
expect(moveDownSpy).not.toHaveBeenCalled();
snackBarSpy.calls.reset();
moveUpSpy.calls.reset();
moveDownSpy.calls.reset();
expect((component as any)._reorder(0, 1, mockWorkflowSpecCategories)).toEqual(expectedCatsAfter);
expect(snackBarSpy).not.toHaveBeenCalled();
expect(moveUpSpy).not.toHaveBeenCalled();
expect(moveDownSpy).toHaveBeenCalled();
});
it('should reorder specs', () => {
const snackBarSpy = spyOn((component as any).snackBar, 'open').and.stub();
const moveUpSpy = spyOn(component, 'moveUp').and.callThrough();
const moveDownSpy = spyOn(component, 'moveDown').and.callThrough();
const specsAfter = [
mockWorkflowSpec1,
mockWorkflowSpec0,
mockWorkflowSpec2,
];
expect((component as any)._reorder('nonexistent_id', 1, mockWorkflowSpecs)).toEqual([]);
expect(snackBarSpy).toHaveBeenCalled();
expect(moveUpSpy).not.toHaveBeenCalled();
expect(moveDownSpy).not.toHaveBeenCalled();
snackBarSpy.calls.reset();
moveUpSpy.calls.reset();
moveDownSpy.calls.reset();
expect((component as any)._reorder(mockWorkflowSpec1.id, -1, mockWorkflowSpecs)).toEqual(specsAfter);
expect(snackBarSpy).not.toHaveBeenCalled();
expect(moveUpSpy).toHaveBeenCalled();
expect(moveDownSpy).not.toHaveBeenCalled();
snackBarSpy.calls.reset();
moveUpSpy.calls.reset();
moveDownSpy.calls.reset();
expect((component as any)._reorder(mockWorkflowSpec0.id, 1, mockWorkflowSpecs)).toEqual(specsAfter);
expect(snackBarSpy).not.toHaveBeenCalled();
expect(moveUpSpy).not.toHaveBeenCalled();
expect(moveDownSpy).toHaveBeenCalled();
});
it('should update all category display orders', () => {
const _loadWorkflowSpecCategoriesSpy = spyOn((component as any), '_loadWorkflowSpecCategories').and.stub();
(component as any)._updateCatDisplayOrders(mockWorkflowSpecCategories);
mockWorkflowSpecCategories.forEach((spec, i) => {
const req = httpMock.expectOne(`apiRoot/workflow-specification-category/${spec.id}`);
expect(req.request.method).toEqual('PUT');
req.flush(mockWorkflowSpecCategories[i]);
});
expect(_loadWorkflowSpecCategoriesSpy).toHaveBeenCalled();
});
it('should update all spec display orders', () => {
const _loadWorkflowSpecCategoriesSpy = spyOn((component as any), '_loadWorkflowSpecCategories').and.stub();
(component as any)._updateSpecDisplayOrders(mockWorkflowSpecs);
mockWorkflowSpecs.forEach((spec, i) => {
const req = httpMock.expectOne(`apiRoot/workflow-specification/${spec.id}`);
expect(req.request.method).toEqual('PUT');
req.flush(mockWorkflowSpecs[i]);
});
expect(_loadWorkflowSpecCategoriesSpy).toHaveBeenCalled();
});
it('should load master workflow spec', () => {
const mockMasterSpec: WorkflowSpec = {
id: 'master_status_spec',
name: 'master_status_spec',
display_name: 'master_status_spec',
description: 'master_status_spec',
is_master_spec: true,
@ -464,7 +452,7 @@ describe('WorkflowSpecListComponent', () => {
category_id: null,
};
(component as any)._loadWorkflowSpecs();
const allSpecs = createClone({circles: true})(mockWorkflowSpecs);
const allSpecs = cloneDeep(mockWorkflowSpecs);
allSpecs.push(mockMasterSpec);
const req = httpMock.expectOne(`apiRoot/workflow-specification`);
@ -473,10 +461,42 @@ describe('WorkflowSpecListComponent', () => {
expect(component.workflowSpecs).toEqual(allSpecs);
expect(component.workflowSpecsByCategory).toBeTruthy();
component.workflowSpecsByCategory.forEach(cat => {
expect(cat.workflow_specs).not.toContain(mockMasterSpec);
});
expect(component.masterStatusSpec).toEqual(mockMasterSpec);
});
it('should call editWorkflowSpec, open Dialog & call _upsertWorkflowSpecification when Edit button is clicked', fakeAsync(() => {
spyOn(dialog, 'open').and.callThrough();
const button = fixture.debugElement.nativeElement.querySelector('#add_spec');
button.click();
httpMock.expectOne(`apiRoot/workflow-specification-category`);
expect(dialog.open).toHaveBeenCalled();
}
));
it('should not delete a library if it is being used', () => {
const badWorkflowSpec = cloneDeep(mockWorkflowSpec0);
badWorkflowSpec.parents=[
{ id: 1234,
display_name: 'test parent',
}]
badWorkflowSpec.library=true;
const mockConfirmDeleteData: DeleteWorkflowSpecDialogData = {
confirm: false,
workflowSpec: badWorkflowSpec
};
const _deleteWorkflowSpecSpy = spyOn((component as any), '_deleteWorkflowSpec').and.stub();
const openDialogSpy = spyOn(component.dialog, 'open')
.and.returnValue({afterClosed: () => of(mockConfirmDeleteData)} as any);
const snackBarSpy = spyOn((component as any).snackBar, 'open').and.stub();
mockConfirmDeleteData.confirm = true;
component.confirmDeleteWorkflowSpec(badWorkflowSpec);
expect(openDialogSpy).toHaveBeenCalled();
expect(_deleteWorkflowSpecSpy).not.toHaveBeenCalled();
expect(snackBarSpy).toHaveBeenCalled();
});
});

View File

@ -1,84 +1,125 @@
import {Component, OnInit} from '@angular/core';
import {MatBottomSheet} from '@angular/material/bottom-sheet';
import {MatDialog} from '@angular/material/dialog';
import {MatSnackBar} from '@angular/material/snack-bar';
import createClone from 'rfdc';
import { Component, OnInit } from '@angular/core';
import { MatBottomSheet } from '@angular/material/bottom-sheet';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { cloneDeep } from 'lodash';
import {
ApiErrorsComponent,
ApiService,
isNumberDefined,
moveArrayElementDown,
moveArrayElementUp,
// moveArrayElementDown,
// moveArrayElementUp,
WorkflowSpec,
WorkflowSpecCategory
WorkflowSpecCategory,
} from 'sartography-workflow-lib';
import {DeleteWorkflowSpecCategoryDialogComponent} from '../_dialogs/delete-workflow-spec-category-dialog/delete-workflow-spec-category-dialog.component';
import {DeleteWorkflowSpecDialogComponent} from '../_dialogs/delete-workflow-spec-dialog/delete-workflow-spec-dialog.component';
import {WorkflowSpecCategoryDialogComponent} from '../_dialogs/workflow-spec-category-dialog/workflow-spec-category-dialog.component';
import {WorkflowSpecDialogComponent} from '../_dialogs/workflow-spec-dialog/workflow-spec-dialog.component';
import {
DeleteWorkflowSpecCategoryDialogComponent
} from '../_dialogs/delete-workflow-spec-category-dialog/delete-workflow-spec-category-dialog.component';
import { DeleteWorkflowSpecDialogComponent } from '../_dialogs/delete-workflow-spec-dialog/delete-workflow-spec-dialog.component';
import { WorkflowSpecCategoryDialogComponent } from '../_dialogs/workflow-spec-category-dialog/workflow-spec-category-dialog.component';
import { WorkflowSpecDialogComponent } from '../_dialogs/workflow-spec-dialog/workflow-spec-dialog.component';
import {
DeleteWorkflowSpecCategoryDialogData,
DeleteWorkflowSpecDialogData,
WorkflowSpecCategoryDialogData,
WorkflowSpecDialogData
WorkflowSpecDialogData,
} from '../_interfaces/dialog-data';
import {ApiErrorsComponent} from 'sartography-workflow-lib';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
import { environment } from '../../environments/environment.runtime';
import { FormControl } from '@angular/forms';
import { SettingsService } from '../settings.service';
export interface WorkflowSpecCategoryGroup {
id: number;
name: string;
id?: number;
display_name: string;
workflow_specs?: WorkflowSpec[];
display_order: number;
admin: boolean,
}
@Component({
selector: 'app-workflow-spec-list',
templateUrl: './workflow-spec-list.component.html',
styleUrls: ['./workflow-spec-list.component.scss']
styleUrls: ['./workflow-spec-list.component.scss'],
})
export class WorkflowSpecListComponent implements OnInit {
workflowSpecs: WorkflowSpec[] = [];
workflowLibraries: WorkflowSpec[] = [];
selectedSpec: WorkflowSpec;
masterStatusSpec: WorkflowSpec;
selectedCat: WorkflowSpecCategory;
workflowSpecsByCategory: WorkflowSpecCategoryGroup[] = [];
categories: WorkflowSpecCategory[];
moveUp = moveArrayElementUp;
moveDown = moveArrayElementDown;
searchField: FormControl;
library_toggle: boolean;
constructor(
private api: ApiService,
private snackBar: MatSnackBar,
private bottomSheet: MatBottomSheet,
public dialog: MatDialog
public dialog: MatDialog,
private route: ActivatedRoute,
private location: Location,
private settingsService: SettingsService,
) {
this._loadWorkflowSpecCategories();
}
ngOnInit() {
this.route.paramMap.subscribe(paramMap => {
if (paramMap.has('spec')) {
this._loadWorkflowSpecCategories(paramMap.get('spec'));
} else {
this._loadWorkflowSpecCategories();
}
});
this.searchField = new FormControl();
this.searchField.valueChanges.subscribe(value => {
this._loadWorkflowSpecs(null, value);
});
}
validateWorkflowSpec(wfs: WorkflowSpec) {
this.api.validateWorkflowSpecification(wfs.id).subscribe(apiErrors => {
const studyId = this.settingsService.getStudyIdForValidation();
this.api.validateWorkflowSpecification(wfs.id, '', studyId).subscribe(apiErrors => {
if (apiErrors && apiErrors.length > 0) {
this.bottomSheet.open(ApiErrorsComponent, {data: {apiErrors: apiErrors}});
this.bottomSheet.open(ApiErrorsComponent, {data: {apiErrors}});
} else {
this.snackBar.open('Workflow specification is valid!', 'Ok', {duration: 5000});
}
});
}
editWorkflowSpec(selectedSpec?: WorkflowSpec) {
selectCat(selectedCat?: WorkflowSpecCategoryGroup) {
this.selectedCat = selectedCat;
}
isSelected(cat: WorkflowSpecCategoryGroup) {
return this.selectedCat && this.selectedCat.id === cat.id;
}
libraryToggle(t: boolean) {
this.library_toggle = t;
}
selectSpec(selectedSpec?: WorkflowSpec) {
this.selectedSpec = selectedSpec;
const hasDisplayOrder = this.selectedSpec && isNumberDefined(this.selectedSpec.display_order);
this.location.replaceState(environment.homeRoute + '/' + selectedSpec.id);
}
editWorkflowSpec(state: String, selectedSpec?: WorkflowSpec) {
const hasDisplayOrder = selectedSpec && isNumberDefined(selectedSpec.display_order);
const dialogData: WorkflowSpecDialogData = {
id: this.selectedSpec ? this.selectedSpec.id : '',
name: this.selectedSpec ? this.selectedSpec.name || this.selectedSpec.id : '',
display_name: this.selectedSpec ? this.selectedSpec.display_name : '',
description: this.selectedSpec ? this.selectedSpec.description : '',
category_id: this.selectedSpec ? this.selectedSpec.category_id : null,
display_order: hasDisplayOrder ? this.selectedSpec.display_order : 0,
id: selectedSpec ? selectedSpec.id : '',
display_name: selectedSpec ? selectedSpec.display_name : '',
description: selectedSpec ? selectedSpec.description : '',
category_id: selectedSpec ? selectedSpec.category_id : null,
display_order: hasDisplayOrder ? selectedSpec.display_order : 0,
standalone: selectedSpec ? selectedSpec.standalone : null,
library: selectedSpec ? selectedSpec.library : (state === 'library' ? true : null),
};
// Open new filename/workflow spec dialog
@ -89,12 +130,18 @@ export class WorkflowSpecListComponent implements OnInit {
});
dialogRef.afterClosed().subscribe((data: WorkflowSpecDialogData) => {
if (data && data.id && data.name && data.display_name && data.description) {
this._upsertWorkflowSpecification(data);
}
if (data && data.id && data.display_name && data.description) {
data.id = this.toLowercaseId(data.id);
this._upsertWorkflowSpecification(selectedSpec == null, data);
}
});
}
// Helper function to convert strings to valid ID's.
toLowercaseId(id: String) {
return id.replace(/ /g,"_").toLowerCase();
}
editWorkflowSpecCategory(selectedCat?: WorkflowSpecCategoryGroup) {
this.selectedCat = selectedCat;
@ -104,25 +151,24 @@ export class WorkflowSpecListComponent implements OnInit {
width: '50vw',
data: {
id: this.selectedCat ? this.selectedCat.id : null,
name: this.selectedCat ? this.selectedCat.name || this.selectedCat.id : '',
display_name: this.selectedCat ? this.selectedCat.display_name : '',
display_order: this.selectedCat ? this.selectedCat.display_order : null,
admin: this.selectedCat ? this.selectedCat.admin : null,
},
});
dialogRef.afterClosed().subscribe((data: WorkflowSpecCategoryDialogData) => {
if (data && isNumberDefined(data.id) && data.name && data.display_name) {
if (data && data.display_name) {
this._upsertWorkflowSpecCategory(data);
}
});
}
confirmDeleteWorkflowSpecCategory(cat: WorkflowSpecCategory) {
confirmDeleteWorkflowSpecCategory(cat: WorkflowSpecCategoryGroup) {
const dialogRef = this.dialog.open(DeleteWorkflowSpecCategoryDialogComponent, {
data: {
confirm: false,
category: cat,
}
},
});
dialogRef.afterClosed().subscribe((data: DeleteWorkflowSpecCategoryDialogData) => {
@ -132,111 +178,167 @@ export class WorkflowSpecListComponent implements OnInit {
});
}
canDeleteWorkflowSpec(wfs){
if ((wfs.parents.length > 0) && (wfs.library)){
let message = '';
for (let p of wfs.parents) {
message += p.display_name + ', ';
}
message = message.replace(/,\s*$/, "");
this.snackBar.open('The Library ' + '\'' + wfs.display_name + '\'' +
' is still being referenced by these workflows: ' + message, 'Ok');
return false;
}
return true;
}
confirmDeleteWorkflowSpec(wfs: WorkflowSpec) {
const dialogRef = this.dialog.open(DeleteWorkflowSpecDialogComponent, {
data: {
confirm: false,
workflowSpec: wfs,
}
},
});
dialogRef.afterClosed().subscribe((data: DeleteWorkflowSpecDialogData) => {
if (data && data.confirm && data.workflowSpec) {
if (data && data.confirm && data.workflowSpec && this.canDeleteWorkflowSpec(data.workflowSpec)) {
this._deleteWorkflowSpec(data.workflowSpec);
if (typeof this.masterStatusSpec !== 'undefined') {
this.selectSpec(this.masterStatusSpec);
}
}
});
}
editCategoryDisplayOrder(catId: number, direction: number, cats: WorkflowSpecCategoryGroup[]) {
const reorderedCats = this._reorder(catId, direction, cats) as WorkflowSpecCategoryGroup[];
this._updateCatDisplayOrders(reorderedCats);
}
editSpecDisplayOrder(specId: string, direction: number, specs: WorkflowSpec[]) {
const reorderedSpecs = this._reorder(specId, direction, specs) as WorkflowSpec[];
this._updateSpecDisplayOrders(reorderedSpecs);
}
sortByDisplayOrder = (a, b) => (a.display_order < b.display_order) ? -1 : 1;
private _loadWorkflowSpecCategories() {
this.api.getWorkflowSpecCategoryList().subscribe(cats => {
this.categories = cats.sort(this.sortByDisplayOrder);
// Add a container for specs without a category
this.workflowSpecsByCategory = [{
id: null,
name: 'none',
display_name: 'No category',
workflow_specs: [],
display_order: -1, // Display it at the top
}];
this.categories.forEach((cat, i) => {
this.workflowSpecsByCategory.push(cat);
this.workflowSpecsByCategory[i + 1].workflow_specs = [];
});
this._loadWorkflowSpecs();
editCategoryDisplayOrder(catId: number, direction: string) {
this.api.reorderWorkflowCategory(catId, direction).subscribe(cat_change => {
this.workflowSpecsByCategory = this.workflowSpecsByCategory.map(cat => {
let new_cat = (cat_change.find(i2 => i2.id === cat.id));
cat.display_order = new_cat.display_order;
return cat;
});
this.workflowSpecsByCategory.sort((x,y) => x.display_order - y.display_order);
});
}
private _loadWorkflowSpecs() {
editSpecDisplayOrder(cat: WorkflowSpecCategoryGroup, specId: string, direction: string) {
this.api.reorderWorkflowSpecification(specId, direction).subscribe(wfs => {
cat.workflow_specs= wfs;
});
}
private _loadWorkflowSpecCategories(selectedSpecName: string = null) {
this.api.getWorkflowSpecCategoryList().subscribe(cats => {
this.categories = cats;
// Clear out this object before re-filling it
this.workflowSpecsByCategory = [];
this.categories.forEach((cat, i) => {
this.workflowSpecsByCategory.push(cat);
this.workflowSpecsByCategory[i].workflow_specs = [];
});
this._loadWorkflowSpecs(selectedSpecName);
this._loadWorkflowLibraries(selectedSpecName);
});
}
private _loadWorkflowLibraries(selectedSpecName: string = null) {
this.api.getWorkflowSpecificationLibraries().subscribe(wfs => {
this.workflowLibraries = wfs;
// If selected spec is a library, set it.
if (selectedSpecName) {
wfs.forEach(ws => {
if (selectedSpecName && selectedSpecName === ws.id) {
this.selectedSpec = ws;
this.library_toggle = true;
}
});
} else {
this.selectedSpec = this.masterStatusSpec;
}
});
}
private _loadWorkflowSpecs(selectedSpecName: string = null, searchSpecName: string = null) {
this.api.getWorkflowSpecList().subscribe(wfs => {
this.workflowSpecs = wfs;
// Populate categories with their specs
this.workflowSpecsByCategory.forEach(cat => {
cat.workflow_specs = this.workflowSpecs
.filter(wf => {
if (wf.is_master_spec) {
this.masterStatusSpec = wf;
if (searchSpecName) {
return (wf.category_id === cat.id) && wf.display_name.toLowerCase().includes(searchSpecName.toLowerCase());
} else {
return wf.category_id === cat.id;
}
})
.sort(this.sortByDisplayOrder);
cat.workflow_specs.sort((x,y) => x.display_order - y.display_order);
});
// Set master spec
wfs.forEach(wf => {
if (wf.is_master_spec){
this.masterStatusSpec = wf;
}
});
// Set the selected workflow to something sensible.
if (!selectedSpecName && this.selectedSpec) {
selectedSpecName = this.selectedSpec.id;
}
if (selectedSpecName) {
this.workflowSpecs.forEach(ws => {
if (selectedSpecName && selectedSpecName === ws.id) {
this.selectedSpec = ws;
this.selectedCat = this.selectedSpec.category;
}
});
}
});
}
private _upsertWorkflowSpecification(data: WorkflowSpecDialogData) {
if (data.id && data.name && data.display_name && data.description) {
// Save old workflow spec id, in case it's changed
const specId = this.selectedSpec ? this.selectedSpec.id : undefined;
private _upsertWorkflowSpecification(isNew: boolean, data: WorkflowSpecDialogData) {
if (data.id && data.display_name && data.description) {
const newSpec: WorkflowSpec = {
id: data.id,
name: data.name,
display_name: data.display_name,
description: data.description,
category_id: data.category_id,
display_order: data.display_order,
standalone: data.standalone,
library: data.library,
};
if (specId) {
this._updateWorkflowSpec(specId, newSpec);
} else {
if (isNew) {
this._addWorkflowSpec(newSpec);
this.selectSpec(newSpec);
} else {
this._updateWorkflowSpec(data.id, newSpec);
}
}
}
private _upsertWorkflowSpecCategory(data: WorkflowSpecCategoryDialogData) {
if (isNumberDefined(data.id) && data.name && data.display_name) {
// Save old workflow spec id, in case it's changed
const catId = this.selectedCat ? this.selectedCat.id : undefined;
const newCat: WorkflowSpecCategory = {
id: data.id,
name: data.name,
display_name: data.display_name,
display_order: data.display_order,
if (data.display_name) {
if (isNumberDefined(data.id)) {
const newCat: WorkflowSpecCategory = {
id: data.id,
display_name: data.display_name,
display_order: data.display_order,
admin: data.admin,
};
if (isNumberDefined(catId)) {
this._updateWorkflowSpecCategory(catId, newCat);
this._updateWorkflowSpecCategory(data.id, newCat);
} else {
const newCat: WorkflowSpecCategory = {
display_name: data.display_name,
display_order: data.display_order,
admin: data.admin,
};
this._addWorkflowSpecCategory(newCat);
}
}
@ -244,6 +346,7 @@ export class WorkflowSpecListComponent implements OnInit {
private _updateWorkflowSpec(specId: string, newSpec: WorkflowSpec) {
this.api.updateWorkflowSpecification(specId, newSpec).subscribe(_ => {
this._loadWorkflowLibraries();
this._loadWorkflowSpecs();
this._displayMessage('Saved changes to workflow spec.');
});
@ -251,7 +354,8 @@ export class WorkflowSpecListComponent implements OnInit {
private _addWorkflowSpec(newSpec: WorkflowSpec) {
this.api.addWorkflowSpecification(newSpec).subscribe(_ => {
this._loadWorkflowSpecs();
this._loadWorkflowLibraries(newSpec.id);
this._loadWorkflowSpecs(newSpec.id);
this._displayMessage('Saved new workflow spec.');
});
}
@ -259,7 +363,8 @@ export class WorkflowSpecListComponent implements OnInit {
private _deleteWorkflowSpec(workflowSpec: WorkflowSpec) {
this.api.deleteWorkflowSpecification(workflowSpec.id).subscribe(() => {
this._loadWorkflowSpecs();
this._displayMessage(`Deleted workflow spec ${workflowSpec.name}.`);
this._loadWorkflowLibraries();
this._displayMessage(`Deleted workflow spec ${workflowSpec.id}.`);
});
}
@ -280,7 +385,7 @@ export class WorkflowSpecListComponent implements OnInit {
private _deleteWorkflowSpecCategory(workflowSpecCategory: WorkflowSpecCategory) {
this.api.deleteWorkflowSpecCategory(workflowSpecCategory.id).subscribe(() => {
this._loadWorkflowSpecCategories();
this._displayMessage(`Deleted workflow spec category ${workflowSpecCategory.name}.`);
this._displayMessage(`Deleted workflow spec category ${workflowSpecCategory.display_name}.`);
});
}
@ -288,57 +393,5 @@ export class WorkflowSpecListComponent implements OnInit {
this.snackBar.open(message, 'Ok', {duration: 3000});
}
private _reorder(
id: number|string, direction: number,
list: Array<WorkflowSpecCategoryGroup|WorkflowSpec>
): Array<WorkflowSpecCategoryGroup|WorkflowSpec> {
const listClone = createClone({circles: true})(list);
const reorderedList = listClone.filter(item => item.id !== null && item.id !== undefined);
const i = reorderedList.findIndex(spec => spec.id === id);
if (i !== -1) {
if (direction === 1) {
this.moveDown(reorderedList, i);
} else if (direction === -1) {
this.moveUp(reorderedList, i);
}
return reorderedList;
} else {
this.snackBar.open('Item not found. Reload the page and try again.');
return [];
}
}
private _updateCatDisplayOrders(cats: WorkflowSpecCategory[]) {
let numUpdated = 0;
cats.forEach((cat, j) => {
if (isNumberDefined(cat.id)) {
const newCat: WorkflowSpecCategoryGroup = createClone({circles: true})(cat);
delete newCat.workflow_specs;
newCat.display_order = j;
this.api.updateWorkflowSpecCategory(cat.id, newCat as WorkflowSpecCategory).subscribe(updatedCat => {
numUpdated++;
if (numUpdated === cats.length) {
this._loadWorkflowSpecCategories();
}
});
}
});
}
private _updateSpecDisplayOrders(specs: WorkflowSpec[]) {
let numUpdated = 0;
specs.forEach((spec, j) => {
const newSpec = createClone({circles: true})(spec);
newSpec.display_order = j;
this.api.updateWorkflowSpecification(newSpec.id, newSpec).subscribe(() => {
numUpdated++;
if (numUpdated === specs.length) {
this._loadWorkflowSpecCategories();
}
});
});
}
}

View File

@ -1,60 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_06g9dcb" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<bpmn:process id="Process_1giz8il" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0myefwb</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0myefwb" sourceRef="StartEvent_1" targetRef="StepOne" />
<bpmn:sequenceFlow id="SequenceFlow_00p5po6" sourceRef="StepOne" targetRef="StepTwo" />
<bpmn:endEvent id="EndEvent_1gsujvg">
<bpmn:incoming>SequenceFlow_0huye14</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0huye14" sourceRef="StepTwo" targetRef="EndEvent_1gsujvg" />
<bpmn:userTask id="StepOne" name="Step 1" camunda:formKey="StepOneForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="color" label="What is your favorite color?" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0myefwb</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_00p5po6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="StepTwo" name="Step 2" camunda:formKey="StepTwoForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="capital" label="What is the capital of Assyria?" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_00p5po6</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0huye14</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1giz8il">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0myefwb_di" bpmnElement="SequenceFlow_0myefwb">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_00p5po6_di" bpmnElement="SequenceFlow_00p5po6">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_1gsujvg_di" bpmnElement="EndEvent_1gsujvg">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0huye14_di" bpmnElement="SequenceFlow_0huye14">
<di:waypoint x="530" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_1xakn8i_di" bpmnElement="StepOne">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0fltcd6_di" bpmnElement="StepTwo">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,732 +0,0 @@
/**
* outline styles
*/
.djs-outline {
fill: none;
visibility: hidden;
}
.djs-element.hover .djs-outline,
.djs-element.selected .djs-outline {
visibility: visible;
shape-rendering: crispEdges;
stroke-dasharray: 3,3;
}
.djs-element.selected .djs-outline {
stroke: #8888FF;
stroke-width: 1px;
}
.djs-element.hover .djs-outline {
stroke: #FF8888;
stroke-width: 1px;
}
.djs-shape.connect-ok .djs-visual > :nth-child(1) {
fill: #DCFECC /* light-green */ !important;
}
.djs-shape.connect-not-ok .djs-visual > :nth-child(1),
.djs-shape.drop-not-ok .djs-visual > :nth-child(1) {
fill: #f9dee5 /* light-red */ !important;
}
.djs-shape.new-parent .djs-visual > :nth-child(1) {
fill: #F7F9FF !important;
}
svg.drop-not-ok {
background: #f9dee5 /* light-red */ !important;
}
svg.new-parent {
background: #F7F9FF /* light-blue */ !important;
}
.djs-connection.connect-ok .djs-visual > :nth-child(1),
.djs-connection.drop-ok .djs-visual > :nth-child(1) {
stroke: #90DD5F /* light-green */ !important;
}
.djs-connection.connect-not-ok .djs-visual > :nth-child(1),
.djs-connection.drop-not-ok .djs-visual > :nth-child(1) {
stroke: #E56283 /* light-red */ !important;
}
.drop-not-ok,
.connect-not-ok {
cursor: not-allowed;
}
.djs-element.attach-ok .djs-visual > :nth-child(1) {
stroke-width: 5px !important;
stroke: rgba(255, 116, 0, 0.7) !important;
}
.djs-frame.connect-not-ok .djs-visual > :nth-child(1),
.djs-frame.drop-not-ok .djs-visual > :nth-child(1) {
stroke-width: 3px !important;
stroke: #E56283 /* light-red */ !important;
fill: none !important;
}
/**
* Selection box style
*
*/
.djs-lasso-overlay {
fill: rgb(255, 116, 0);
fill-opacity: 0.1;
stroke-dasharray: 5 1 3 1;
stroke: rgb(255, 116, 0);
shape-rendering: crispEdges;
pointer-events: none;
}
/**
* Resize styles
*/
.djs-resize-overlay {
fill: none;
stroke-dasharray: 5 1 3 1;
stroke: rgb(255, 116, 0);
pointer-events: none;
}
.djs-resizer-hit {
fill: none;
pointer-events: all;
}
.djs-resizer-visual {
fill: white;
stroke-width: 1px;
stroke: #BBB;
shape-rendering: geometricprecision;
}
.djs-resizer:hover .djs-resizer-visual {
stroke: #555;
}
.djs-cursor-resize-ns,
.djs-resizer-n,
.djs-resizer-s {
cursor: ns-resize;
}
.djs-cursor-resize-ew,
.djs-resizer-e,
.djs-resizer-w {
cursor: ew-resize;
}
.djs-cursor-resize-nwse,
.djs-resizer-nw,
.djs-resizer-se {
cursor: nwse-resize;
}
.djs-cursor-resize-nesw,
.djs-resizer-ne,
.djs-resizer-sw {
cursor: nesw-resize;
}
.djs-shape.djs-resizing > .djs-outline {
visibility: hidden !important;
}
.djs-shape.djs-resizing > .djs-resizer {
visibility: hidden;
}
.djs-dragger > .djs-resizer {
visibility: hidden;
}
/**
* drag styles
*/
.djs-dragger * {
fill: none !important;
stroke: rgb(255, 116, 0) !important;
}
.djs-dragger tspan,
.djs-dragger text {
fill: rgb(255, 116, 0) !important;
stroke: none !important;
}
marker.djs-dragger circle,
marker.djs-dragger path,
marker.djs-dragger polygon,
marker.djs-dragger polyline,
marker.djs-dragger rect {
fill: rgb(255, 116, 0) !important;
stroke: none !important;
}
marker.djs-dragger text,
marker.djs-dragger tspan {
fill: none !important;
stroke: rgb(255, 116, 0) !important;
}
.djs-dragging {
opacity: 0.3;
}
.djs-dragging,
.djs-dragging > * {
pointer-events: none !important;
}
.djs-dragging .djs-context-pad,
.djs-dragging .djs-outline {
display: none !important;
}
/**
* no pointer events for visual
*/
.djs-visual,
.djs-outline {
pointer-events: none;
}
.djs-element.attach-ok .djs-hit {
stroke-width: 60px !important;
}
/**
* all pointer events for hit shape
*/
.djs-element > .djs-hit-all {
pointer-events: all;
}
.djs-element > .djs-hit-stroke,
.djs-element > .djs-hit-click-stroke {
pointer-events: stroke;
}
/**
* all pointer events for hit shape
*/
.djs-drag-active .djs-element > .djs-hit-click-stroke {
pointer-events: all;
}
/**
* shape / connection basic styles
*/
.djs-connection .djs-visual {
stroke-width: 2px;
fill: none;
}
.djs-cursor-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.djs-cursor-grabbing {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
.djs-cursor-crosshair {
cursor: crosshair;
}
.djs-cursor-move {
cursor: move;
}
.djs-cursor-resize-ns {
cursor: ns-resize;
}
.djs-cursor-resize-ew {
cursor: ew-resize;
}
/**
* snapping
*/
.djs-snap-line {
stroke: rgb(255, 195, 66);
stroke: rgba(255, 195, 66, 0.50);
stroke-linecap: round;
stroke-width: 2px;
pointer-events: none;
}
/**
* snapping
*/
.djs-crosshair {
stroke: #555;
stroke-linecap: round;
stroke-width: 1px;
pointer-events: none;
shape-rendering: crispEdges;
stroke-dasharray: 5, 5;
}
/**
* palette
*/
.djs-palette {
position: absolute;
left: 20px;
top: 20px;
box-sizing: border-box;
width: 48px;
}
.djs-palette .separator {
margin: 0 5px;
padding-top: 5px;
border: none;
border-bottom: solid 1px #DDD;
clear: both;
}
.djs-palette .entry:before {
vertical-align: text-bottom;
}
.djs-palette .djs-palette-toggle {
cursor: pointer;
}
.djs-palette .entry,
.djs-palette .djs-palette-toggle {
color: #333;
font-size: 30px;
text-align: center;
}
.djs-palette .entry {
float: left;
}
.djs-palette .entry img {
max-width: 100%;
}
.djs-palette .djs-palette-entries:after {
content: '';
display: table;
clear: both;
}
.djs-palette .djs-palette-toggle:hover {
background: #666;
}
.djs-palette .entry:hover {
color: rgb(255, 116, 0);
}
.djs-palette .highlighted-entry {
color: rgb(255, 116, 0) !important;
}
.djs-palette .entry,
.djs-palette .djs-palette-toggle {
width: 46px;
height: 46px;
line-height: 46px;
cursor: default;
}
/**
* Palette open / two-column layout is controlled via
* classes on the palette. Events to hook into palette
* changed life-cycle are available in addition.
*/
.djs-palette.two-column.open {
width: 94px;
}
.djs-palette:not(.open) .djs-palette-entries {
display: none;
}
.djs-palette:not(.open) {
overflow: hidden;
}
.djs-palette.open .djs-palette-toggle {
display: none;
}
/**
* context-pad
*/
.djs-overlay-context-pad {
width: 72px;
}
.djs-context-pad {
position: absolute;
display: none;
pointer-events: none;
}
.djs-context-pad .entry {
width: 22px;
height: 22px;
text-align: center;
display: inline-block;
font-size: 22px;
margin: 0 2px 2px 0;
border-radius: 3px;
cursor: default;
background-color: #FEFEFE;
box-shadow: 0 0 2px 1px #FEFEFE;
pointer-events: all;
}
.djs-context-pad .entry:before {
vertical-align: top;
}
.djs-context-pad .entry:hover {
background: rgb(255, 252, 176);
}
.djs-context-pad.open {
display: block;
}
/**
* popup styles
*/
.djs-popup .entry {
line-height: 20px;
white-space: nowrap;
cursor: default;
}
/* larger font for prefixed icons */
.djs-popup .entry:before {
vertical-align: middle;
font-size: 20px;
}
.djs-popup .entry > span {
vertical-align: middle;
font-size: 14px;
}
.djs-popup .entry:hover,
.djs-popup .entry.active:hover {
background: rgb(255, 252, 176);
}
.djs-popup .entry.disabled {
background: inherit;
}
.djs-popup .djs-popup-header .entry {
display: inline-block;
padding: 2px 3px 2px 3px;
border: solid 1px transparent;
border-radius: 3px;
}
.djs-popup .djs-popup-header .entry.active {
color: rgb(255, 116, 0);
border: solid 1px rgb(255, 116, 0);
background-color: #F6F6F6;
}
.djs-popup-body .entry {
padding: 4px 10px 4px 5px;
}
.djs-popup-body .entry > span {
margin-left: 5px;
}
.djs-popup-body {
background-color: #FEFEFE;
}
.djs-popup-header {
border-bottom: 1px solid #DDD;
}
.djs-popup-header .entry {
margin: 1px;
margin-left: 3px;
}
.djs-popup-header .entry:last-child {
margin-right: 3px;
}
/**
* popup / palette styles
*/
.djs-popup, .djs-palette {
background: #FAFAFA;
border: solid 1px #CCC;
border-radius: 2px;
}
/**
* touch
*/
.djs-shape,
.djs-connection {
touch-action: none;
}
.djs-segment-dragger,
.djs-bendpoint {
display: none;
}
/**
* bendpoints
*/
.djs-segment-dragger .djs-visual {
fill: rgba(255, 255, 121, 0.2);
stroke-width: 1px;
stroke-opacity: 1;
stroke: rgba(255, 255, 121, 0.3);
}
.djs-bendpoint .djs-visual {
fill: rgba(255, 255, 121, 0.8);
stroke-width: 1px;
stroke-opacity: 0.5;
stroke: black;
}
.djs-segment-dragger:hover,
.djs-bendpoints.hover .djs-segment-dragger,
.djs-bendpoints.selected .djs-segment-dragger,
.djs-bendpoint:hover,
.djs-bendpoints.hover .djs-bendpoint,
.djs-bendpoints.selected .djs-bendpoint {
display: block;
}
.djs-drag-active .djs-bendpoints * {
display: none;
}
.djs-bendpoints:not(.hover) .floating {
display: none;
}
.djs-segment-dragger:hover .djs-visual,
.djs-segment-dragger.djs-dragging .djs-visual,
.djs-bendpoint:hover .djs-visual,
.djs-bendpoint.floating .djs-visual {
fill: yellow;
stroke-opacity: 0.5;
stroke: black;
}
.djs-bendpoint.floating .djs-hit {
pointer-events: none;
}
.djs-segment-dragger .djs-hit,
.djs-bendpoint .djs-hit {
pointer-events: all;
fill: none;
}
.djs-segment-dragger.horizontal .djs-hit {
cursor: ns-resize;
}
.djs-segment-dragger.vertical .djs-hit {
cursor: ew-resize;
}
.djs-segment-dragger.djs-dragging .djs-hit {
pointer-events: none;
}
.djs-updating,
.djs-updating > * {
pointer-events: none !important;
}
.djs-updating .djs-context-pad,
.djs-updating .djs-outline,
.djs-updating .djs-bendpoint,
.connect-ok .djs-bendpoint,
.connect-not-ok .djs-bendpoint,
.drop-ok .djs-bendpoint,
.drop-not-ok .djs-bendpoint {
display: none !important;
}
.djs-segment-dragger.djs-dragging,
.djs-bendpoint.djs-dragging {
display: block;
opacity: 1.0;
}
.djs-segment-dragger.djs-dragging .djs-visual,
.djs-bendpoint.djs-dragging .djs-visual {
fill: yellow;
stroke-opacity: 0.5;
}
/**
* tooltips
*/
.djs-tooltip-error {
font-size: 11px;
line-height: 18px;
text-align: left;
padding: 5px;
opacity: 0.7;
}
.djs-tooltip-error > * {
width: 160px;
background: rgb(252, 236, 240);
color: rgb(158, 76, 76);
padding: 3px 7px;
border-radius: 5px;
border-left: solid 5px rgb(174, 73, 73);
}
.djs-tooltip-error:hover {
opacity: 1;
}
/**
* search pad
*/
.djs-search-container {
position: absolute;
top: 20px;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
width: 25%;
min-width: 300px;
max-width: 400px;
z-index: 10;
font-size: 1.05em;
opacity: 0.9;
background: #FAFAFA;
border: solid 1px #CCC;
border-radius: 2px;
}
.djs-search-container:not(.open) {
display: none;
}
.djs-search-input input {
font-size: 1.05em;
width: 100%;
padding: 6px 10px;
border: 1px solid #ccc;
}
.djs-search-input input:focus {
outline: none;
border-color: #52B415;
}
.djs-search-results {
position: relative;
overflow-y: auto;
max-height: 200px;
}
.djs-search-results:hover {
/*background: #fffdd7;*/
cursor: pointer;
}
.djs-search-result {
width: 100%;
padding: 6px 10px;
background: white;
border-bottom: solid 1px #AAA;
border-radius: 1px;
}
.djs-search-highlight {
color: black;
}
.djs-search-result-primary {
margin: 0 0 10px;
}
.djs-search-result-secondary {
font-family: monospace;
margin: 0;
}
.djs-search-result:hover {
background: #fdffd6;
}
.djs-search-result-selected {
background: #fffcb0;
}
.djs-search-result-selected:hover {
background: #f7f388;
}
.djs-search-overlay {
background: yellow;
opacity: 0.3;
}
/**
* hidden styles
*/
.djs-element-hidden,
.djs-element-hidden .djs-hit,
.djs-element-hidden .djs-outline,
.djs-label-hidden .djs-label {
display: none !important;
}

View File

@ -1,85 +0,0 @@
/*
Animation example, for spinners
*/
.animate-spin {
-moz-animation: spin 2s infinite linear;
-o-animation: spin 2s infinite linear;
-webkit-animation: spin 2s infinite linear;
animation: spin 2s infinite linear;
display: inline-block;
}
@-moz-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-webkit-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-o-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-ms-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}

View File

@ -1,40 +0,0 @@
.dmn-icon-up:before { content: '\e800'; } /* '' */
.dmn-icon-down:before { content: '\e801'; } /* '' */
.dmn-icon-clear:before { content: '\e802'; } /* '' */
.dmn-icon-plus:before { content: '\e803'; } /* '' */
.dmn-icon-minus:before { content: '\e804'; } /* '' */
.dmn-icon-info:before { content: '\e805'; } /* '' */
.dmn-icon-left:before { content: '\e806'; } /* '' */
.dmn-icon-decision:before { content: '\e807'; } /* '' */
.dmn-icon-right:before { content: '\e808'; } /* '' */
.dmn-icon-input:before { content: '\e809'; } /* '' */
.dmn-icon-output:before { content: '\e80a'; } /* '' */
.dmn-icon-copy:before { content: '\e80b'; } /* '' */
.dmn-icon-keyboard:before { content: '\e80c'; } /* '' */
.dmn-icon-undo:before { content: '\e80d'; } /* '' */
.dmn-icon-redo:before { content: '\e80e'; } /* '' */
.dmn-icon-menu:before { content: '\e80f'; } /* '' */
.dmn-icon-setting:before { content: '\e810'; } /* '' */
.dmn-icon-wrench:before { content: '\e811'; } /* '' */
.dmn-icon-eraser:before { content: '\e812'; } /* '' */
.dmn-icon-attention:before { content: '\e813'; } /* '' */
.dmn-icon-resize-big:before { content: '\e814'; } /* '' */
.dmn-icon-resize-small:before { content: '\e815'; } /* '' */
.dmn-icon-file-code:before { content: '\e816'; } /* '' */
.dmn-icon-business-knowledge:before { content: '\e817'; } /* '' */
.dmn-icon-knowledge-source:before { content: '\e818'; } /* '' */
.dmn-icon-input-data:before { content: '\e819'; } /* '' */
.dmn-icon-text-annotation:before { content: '\e81a'; } /* '' */
.dmn-icon-connection:before { content: '\e81b'; } /* '' */
.dmn-icon-connection-multi:before { content: '\e81c'; } /* '' */
.dmn-icon-drag:before { content: '\e81d'; } /* '' */
.dmn-icon-lasso-tool:before { content: '\e81e'; } /* '' */
.dmn-icon-screw-wrench:before { content: '\e81f'; } /* '' */
.dmn-icon-trash:before { content: '\e820'; } /* '' */
.dmn-icon-bpmn-io:before { content: '\e821'; } /* '' */
.dmn-icon-decision-table:before { content: '\e822'; } /* '' */
.dmn-icon-literal-expression:before { content: '\e823'; } /* '' */
.dmn-icon-edit:before { content: '\e824'; } /* '' */
.dmn-icon-cut:before { content: '\e825'; } /* '' */
.dmn-icon-paste:before { content: '\f0ea'; } /* '' */

File diff suppressed because one or more lines are too long

View File

@ -1,40 +0,0 @@
.dmn-icon-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.dmn-icon-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.dmn-icon-clear { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.dmn-icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.dmn-icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.dmn-icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
.dmn-icon-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.dmn-icon-decision { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.dmn-icon-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.dmn-icon-input { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
.dmn-icon-output { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
.dmn-icon-copy { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
.dmn-icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80c;&nbsp;'); }
.dmn-icon-undo { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
.dmn-icon-redo { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
.dmn-icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
.dmn-icon-setting { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.dmn-icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
.dmn-icon-eraser { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
.dmn-icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
.dmn-icon-resize-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
.dmn-icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
.dmn-icon-file-code { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
.dmn-icon-business-knowledge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
.dmn-icon-knowledge-source { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
.dmn-icon-input-data { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
.dmn-icon-text-annotation { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
.dmn-icon-connection { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
.dmn-icon-connection-multi { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81c;&nbsp;'); }
.dmn-icon-drag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81d;&nbsp;'); }
.dmn-icon-lasso-tool { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81e;&nbsp;'); }
.dmn-icon-screw-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81f;&nbsp;'); }
.dmn-icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe820;&nbsp;'); }
.dmn-icon-bpmn-io { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe821;&nbsp;'); }
.dmn-icon-decision-table { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe822;&nbsp;'); }
.dmn-icon-literal-expression { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe823;&nbsp;'); }
.dmn-icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe824;&nbsp;'); }
.dmn-icon-cut { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe825;&nbsp;'); }
.dmn-icon-paste { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ea;&nbsp;'); }

View File

@ -1,51 +0,0 @@
[class^="dmn-icon-"], [class*=" dmn-icon-"] {
font-family: 'dmn';
font-style: normal;
font-weight: normal;
/* fix buttons height */
line-height: 1em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
}
.dmn-icon-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.dmn-icon-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.dmn-icon-clear { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.dmn-icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.dmn-icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.dmn-icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
.dmn-icon-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.dmn-icon-decision { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.dmn-icon-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.dmn-icon-input { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
.dmn-icon-output { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
.dmn-icon-copy { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
.dmn-icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80c;&nbsp;'); }
.dmn-icon-undo { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
.dmn-icon-redo { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
.dmn-icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
.dmn-icon-setting { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.dmn-icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
.dmn-icon-eraser { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
.dmn-icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
.dmn-icon-resize-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
.dmn-icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
.dmn-icon-file-code { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
.dmn-icon-business-knowledge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
.dmn-icon-knowledge-source { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
.dmn-icon-input-data { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
.dmn-icon-text-annotation { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
.dmn-icon-connection { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
.dmn-icon-connection-multi { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81c;&nbsp;'); }
.dmn-icon-drag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81d;&nbsp;'); }
.dmn-icon-lasso-tool { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81e;&nbsp;'); }
.dmn-icon-screw-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81f;&nbsp;'); }
.dmn-icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe820;&nbsp;'); }
.dmn-icon-bpmn-io { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe821;&nbsp;'); }
.dmn-icon-decision-table { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe822;&nbsp;'); }
.dmn-icon-literal-expression { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe823;&nbsp;'); }
.dmn-icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe824;&nbsp;'); }
.dmn-icon-cut { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe825;&nbsp;'); }
.dmn-icon-paste { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ea;&nbsp;'); }

View File

@ -1,91 +0,0 @@
@font-face {
font-family: 'dmn';
src: url('../font/dmn.eot?37326370');
src: url('../font/dmn.eot?37326370#iefix') format('embedded-opentype'),
url('../font/dmn.woff2?37326370') format('woff2'),
url('../font/dmn.woff?37326370') format('woff'),
url('../font/dmn.ttf?37326370') format('truetype'),
url('../font/dmn.svg?37326370#dmn') format('svg');
font-weight: normal;
font-style: normal;
}
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
/*
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'dmn';
src: url('../font/dmn.svg?37326370#dmn') format('svg');
}
}
*/
[class^="dmn-icon-"]:before, [class*=" dmn-icon-"]:before {
font-family: "dmn";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Font smoothing. That was taken from TWBS */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.dmn-icon-up:before { content: '\e800'; } /* '' */
.dmn-icon-down:before { content: '\e801'; } /* '' */
.dmn-icon-clear:before { content: '\e802'; } /* '' */
.dmn-icon-plus:before { content: '\e803'; } /* '' */
.dmn-icon-minus:before { content: '\e804'; } /* '' */
.dmn-icon-info:before { content: '\e805'; } /* '' */
.dmn-icon-left:before { content: '\e806'; } /* '' */
.dmn-icon-decision:before { content: '\e807'; } /* '' */
.dmn-icon-right:before { content: '\e808'; } /* '' */
.dmn-icon-input:before { content: '\e809'; } /* '' */
.dmn-icon-output:before { content: '\e80a'; } /* '' */
.dmn-icon-copy:before { content: '\e80b'; } /* '' */
.dmn-icon-keyboard:before { content: '\e80c'; } /* '' */
.dmn-icon-undo:before { content: '\e80d'; } /* '' */
.dmn-icon-redo:before { content: '\e80e'; } /* '' */
.dmn-icon-menu:before { content: '\e80f'; } /* '' */
.dmn-icon-setting:before { content: '\e810'; } /* '' */
.dmn-icon-wrench:before { content: '\e811'; } /* '' */
.dmn-icon-eraser:before { content: '\e812'; } /* '' */
.dmn-icon-attention:before { content: '\e813'; } /* '' */
.dmn-icon-resize-big:before { content: '\e814'; } /* '' */
.dmn-icon-resize-small:before { content: '\e815'; } /* '' */
.dmn-icon-file-code:before { content: '\e816'; } /* '' */
.dmn-icon-business-knowledge:before { content: '\e817'; } /* '' */
.dmn-icon-knowledge-source:before { content: '\e818'; } /* '' */
.dmn-icon-input-data:before { content: '\e819'; } /* '' */
.dmn-icon-text-annotation:before { content: '\e81a'; } /* '' */
.dmn-icon-connection:before { content: '\e81b'; } /* '' */
.dmn-icon-connection-multi:before { content: '\e81c'; } /* '' */
.dmn-icon-drag:before { content: '\e81d'; } /* '' */
.dmn-icon-lasso-tool:before { content: '\e81e'; } /* '' */
.dmn-icon-screw-wrench:before { content: '\e81f'; } /* '' */
.dmn-icon-trash:before { content: '\e820'; } /* '' */
.dmn-icon-bpmn-io:before { content: '\e821'; } /* '' */
.dmn-icon-decision-table:before { content: '\e822'; } /* '' */
.dmn-icon-literal-expression:before { content: '\e823'; } /* '' */
.dmn-icon-edit:before { content: '\e824'; } /* '' */
.dmn-icon-cut:before { content: '\e825'; } /* '' */
.dmn-icon-paste:before { content: '\f0ea'; } /* '' */

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