mirror of
https://github.com/sartography/cr-connect-bpmn.git
synced 2025-02-19 19:58:23 +00:00
Merge branch 'dev'
This commit is contained in:
commit
83f58df3f3
58
.eslintrc.json
Normal file
58
.eslintrc.json
Normal 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": {}
|
||||
}
|
||||
]
|
||||
}
|
41
.github/workflows/create-docker-action.yml
vendored
Normal file
41
.github/workflows/create-docker-action.yml
vendored
Normal 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 }}
|
15
.travis.yml
15
.travis.yml
@ -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
|
||||
|
13
Dockerfile
13
Dockerfile
@ -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
|
||||
|
16
README.md
16
README.md
@ -1,15 +1,17 @@
|
||||
# CR Connect BPMN Configurator
|
||||
|
||||
# sartography/cr-connect-bpmn
|
||||
[](https://travis-ci.com/sartography/cr-connect-bpmn)
|
||||
[](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).
|
||||
|
90
angular.json
90
angular.json
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
45
deploy.sh
45
deploy.sh
@ -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."
|
@ -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
0
docker/substitute-env-variables.sh
Executable file → Normal file
45
e2e/.eslintrc.json
Normal file
45
e2e/.eslintrc.json
Normal 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": {}
|
||||
}
|
||||
]
|
||||
}
|
@ -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');
|
||||
|
||||
|
@ -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
22317
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
156
package.json
156
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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>
|
@ -0,0 +1,3 @@
|
||||
mat-dialog-container {
|
||||
padding: 0px !important
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
27
src/app/_dialogs/confirm-dialog/confirm-dialog.component.ts
Normal file
27
src/app/_dialogs/confirm-dialog/confirm-dialog.component.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -27,4 +27,5 @@ export class DeleteFileDialogComponent {
|
||||
this.dialogRef.close(data);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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 = [
|
||||
{
|
||||
|
@ -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,
|
||||
|
@ -22,7 +22,7 @@ export class NewFileDialogComponent {
|
||||
}
|
||||
|
||||
onSubmit(fileType: FileType) {
|
||||
const data: NewFileDialogData = {fileType: fileType};
|
||||
const data: NewFileDialogData = {fileType};
|
||||
this.dialogRef.close(data);
|
||||
}
|
||||
|
||||
|
@ -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> </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> </span>
|
||||
|
@ -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', () => {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -1,3 +1,3 @@
|
||||
::ng-deep formly-field mat-form-field {
|
||||
margin-bottom: 40px;
|
||||
|
||||
}
|
||||
|
@ -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(() => {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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)
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,12 @@
|
||||
export interface BpmnError {
|
||||
warnings: BpmnWarning[];
|
||||
}
|
||||
|
||||
export interface BpmnWarning {
|
||||
message?: string;
|
||||
element?: any;
|
||||
property?: string;
|
||||
value?: string;
|
||||
context?: any;
|
||||
error?: Error;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
export interface ModelerConfig {
|
||||
additionalModules: any[];
|
||||
moddleExtensions: {
|
||||
camunda: any
|
||||
camunda: any;
|
||||
};
|
||||
}
|
||||
|
3
src/app/_util/diagram-type.ts
Normal file
3
src/app/_util/diagram-type.ts
Normal 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);
|
@ -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: [
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
|
@ -14,3 +14,7 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
.diagram-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -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 ';
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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: [
|
||||
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
|
11
src/app/library-list/library-list.component.html
Normal file
11
src/app/library-list/library-list.component.html
Normal 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>
|
||||
|
16
src/app/library-list/library-list.component.scss
Normal file
16
src/app/library-list/library-list.component.scss
Normal 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;
|
||||
}
|
55
src/app/library-list/library-list.component.spec.ts
Normal file
55
src/app/library-list/library-list.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
63
src/app/library-list/library-list.component.ts
Normal file
63
src/app/library-list/library-list.component.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
@ -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> </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> </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">
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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', () => {
|
||||
|
@ -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'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
<div class="full-height" fxLayout="column" fxLayoutAlign="center center">
|
||||
<h1>Protocol Builder Tester</h1>
|
||||
<p>(Coming soon)</p>
|
||||
</div>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
16
src/app/settings.service.spec.ts
Normal file
16
src/app/settings.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
24
src/app/settings.service.ts
Normal file
24
src/app/settings.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
14
src/app/settings/settings.component.html
Normal file
14
src/app/settings/settings.component.html
Normal 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>
|
39
src/app/settings/settings.component.spec.ts
Normal file
39
src/app/settings/settings.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
31
src/app/settings/settings.component.ts
Normal file
31
src/app/settings/settings.component.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
|
@ -48,3 +48,11 @@ mat-card {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.library-list{
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.expand{
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
@ -1,40 +0,0 @@
|
||||
|
||||
.dmn-icon-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-clear { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-decision { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-input { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-output { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-copy { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-undo { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-redo { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-setting { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-eraser { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-resize-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-file-code { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-business-knowledge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-knowledge-source { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-input-data { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-text-annotation { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-connection { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-connection-multi { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-drag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-lasso-tool { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-screw-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-bpmn-io { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-decision-table { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-literal-expression { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-cut { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-paste { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
@ -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 = ' '); }
|
||||
.dmn-icon-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-clear { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-decision { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-input { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-output { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-copy { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-undo { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-redo { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-setting { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-eraser { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-resize-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-file-code { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-business-knowledge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-knowledge-source { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-input-data { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-text-annotation { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-connection { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-connection-multi { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-drag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-lasso-tool { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-screw-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-bpmn-io { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-decision-table { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-literal-expression { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-cut { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.dmn-icon-paste { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
@ -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
Loading…
x
Reference in New Issue
Block a user