Imports formly. Adds workflow spec dialog. Links buttons to modeler screen. Creates new DMN diagram.

This commit is contained in:
Aaron Louie 2020-01-27 17:32:05 -05:00
parent 7d2616dc6a
commit 094fa31fcf
22 changed files with 448 additions and 108 deletions

19
package-lock.json generated
View File

@ -3316,6 +3316,22 @@
}
}
},
"@ngx-formly/core": {
"version": "5.5.10",
"resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-5.5.10.tgz",
"integrity": "sha512-nKtO8zvztBjB6MMmszXZqb95gmG4xjJIMphzFqRN15PjCxNSXDDlYH/ljm7proGBmt1gMz/liwTP7Hf2TcwydA==",
"requires": {
"tslib": "^1.7.1"
}
},
"@ngx-formly/material": {
"version": "5.5.10",
"resolved": "https://registry.npmjs.org/@ngx-formly/material/-/material-5.5.10.tgz",
"integrity": "sha512-Fxdd1u7YS0eni9y0EUk+bJe5FFUmmeGX2jru15Fwp5NEeYEXGwWzfNqD0ROgKz6x0m/PLt49QKhr5teOWYn/yg==",
"requires": {
"tslib": "^1.9.0"
}
},
"@schematics/angular": {
"version": "8.3.23",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-8.3.23.tgz",
@ -13438,8 +13454,7 @@
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"validate-npm-package-license": {
"version": "3.0.4",

View File

@ -34,6 +34,8 @@
"@angular/platform-browser": "^8.2.14",
"@angular/platform-browser-dynamic": "^8.2.14",
"@angular/router": "^8.2.14",
"@ngx-formly/core": "^5.5.10",
"@ngx-formly/material": "^5.5.10",
"bpmn-js": "^3.5.0",
"bpmn-js-properties-panel": "^0.33.1",
"camunda-bpmn-moddle": "^4.3.0",
@ -48,6 +50,7 @@
"rxjs": "~6.5.4",
"sartography-workflow-lib": "^0.0.16",
"tslib": "^1.10.0",
"uuid": "^3.4.0",
"zone.js": "~0.9.1"
},
"devDependencies": {

View File

@ -0,0 +1,6 @@
export interface WorkflowSpecDialogData {
id: string;
name: string;
display_name: string;
description: string;
}

View File

@ -0,0 +1,18 @@
import {FileType} from 'sartography-workflow-lib';
import { GetIconCodePipe } from './get-icon-code.pipe';
describe('GetIconCodePipe', () => {
let pipe;
beforeEach(() => {
pipe = new GetIconCodePipe();
})
it('create an instance', () => {
expect(pipe).toBeTruthy()
});
it('should get an icon code for each file type', () => {
Object.values(FileType).forEach(ft => expect(pipe.transform(ft)).toBeTruthy());
});
});

View File

@ -0,0 +1,19 @@
import { Pipe, PipeTransform } from '@angular/core';
import {FileType} from 'sartography-workflow-lib';
@Pipe({
name: 'getIconCode'
})
export class GetIconCodePipe implements PipeTransform {
transform(value: FileType, ...args: any[]): any {
switch (value) {
case FileType.BPMN:
return 'account_tree';
case FileType.SVG:
return 'image';
case FileType.DMN:
return 'view_list';
}
}
}

View File

@ -0,0 +1,5 @@
import {FileType} from 'sartography-workflow-lib';
export const getDiagramTypeFromXml = (xml: string): FileType => {
return (xml && xml.includes('dmn.xsd') ? FileType.DMN : FileType.BPMN);
};

View File

@ -6,7 +6,8 @@ import {WorkflowSpecListComponent} from './workflow-spec-list/workflow-spec-list
const appRoutes: Routes = [
{ path: 'modeler', component: ModelerComponent },
{ path: 'modeler/:workflowSpecId', component: ModelerComponent },
{ path: 'modeler/:workflowSpecId/:fileMetaId', component: ModelerComponent },
{ path: 'workflow-specs', component: WorkflowSpecListComponent },
{ path: '', redirectTo: '/workflow-specs', pathMatch: 'full' },
];

View File

@ -16,15 +16,19 @@ 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 {FormlyMaterialModule} from '@ngx-formly/material';
import {AppEnvironment} from 'sartography-workflow-lib';
import {environment} from '../environments/environment';
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 {FileMetaDialogComponent} from './file-meta-dialog/file-meta-dialog.component';
import {ModelerComponent} from './modeler/modeler.component';
import {WorkflowSpecDialogComponent} from './workflow-spec-dialog/workflow-spec-dialog.component';
import {WorkflowSpecListComponent} from './workflow-spec-list/workflow-spec-list.component';
import { FileListComponent } from './file-list/file-list.component';
import { GetIconCodePipe } from './_pipes/get-icon-code.pipe';
class ThisEnvironment implements AppEnvironment {
production = environment.production;
@ -41,11 +45,19 @@ class ThisEnvironment implements AppEnvironment {
ModelerComponent,
WorkflowSpecListComponent,
FileListComponent,
WorkflowSpecDialogComponent,
GetIconCodePipe,
],
imports: [
BrowserAnimationsModule,
BrowserModule,
FlexLayoutModule,
FormlyMaterialModule,
FormlyModule.forRoot({
validationMessages: [
{name: 'required', message: 'This field is required'},
],
}),
FormsModule,
HttpClientModule,
MatButtonModule,
@ -64,7 +76,10 @@ class ThisEnvironment implements AppEnvironment {
MatListModule,
],
bootstrap: [AppComponent],
entryComponents: [FileMetaDialogComponent],
entryComponents: [
FileMetaDialogComponent,
WorkflowSpecDialogComponent
],
providers: [{provide: 'APP_ENVIRONMENT', useClass: ThisEnvironment}]
})
export class AppModule {

View File

@ -5,10 +5,13 @@ import BpmnModeler from 'bpmn-js/lib/Modeler';
import DmnModeler from 'dmn-js/lib/Modeler';
import * as fileSaver from 'file-saver';
import {ApiService, FileType} from 'sartography-workflow-lib';
import {DMN_DIAGRAM_EMPTY} from '../../testing/mocks/diagram.mocks';
import {BpmnWarning} from '../_interfaces/bpmn-warning';
import {ImportEvent} from '../_interfaces/import-event';
import {getDiagramTypeFromXml} from '../_util/diagram-type';
import {bpmnModelerConfig} from './bpmn-modeler-config';
import {dmnModelerConfig} from './dmn-modeler-config';
import {v4 as uuidv4} from 'uuid';
@Component({
selector: 'app-diagram',
@ -83,14 +86,20 @@ export class DiagramComponent implements ControlValueAccessor, AfterViewInit {
}
openDiagram(xml?: string, diagramType?: FileType) {
this.diagramType = diagramType || FileType.BPMN;
this.diagramType = diagramType || getDiagramTypeFromXml(xml);
this.xml = xml;
this.initializeModeler(diagramType);
return this.zone.run(() => {
if (xml) {
this.modeler.importXML(xml, (e, w) => this.onImport(e, w));
} else {
if (this.modeler.createDiagram) {
this.modeler.createDiagram((e, w) => this.onImport(e, w));
} else {
const r = 'REPLACE_ME';
const newXml = DMN_DIAGRAM_EMPTY.replace(/REPLACE_ME/gi, () => uuidv4().slice(0, 7));
this.modeler.importXML(newXml, (e, w) => this.onImport(e, w));
}
}
});
}
@ -99,7 +108,7 @@ export class DiagramComponent implements ControlValueAccessor, AfterViewInit {
this.saveDiagram();
this.modeler.saveSVG((err, svg) => {
const blob = new Blob([svg], {type: 'image/svg+xml'});
fileSaver.saveAs(blob, `BPMN Diagram - ${new Date().toISOString()}.svg`);
fileSaver.saveAs(blob, `${this.diagramType.toString().toUpperCase()} Diagram - ${new Date().toISOString()}.svg`);
});
}
@ -131,7 +140,7 @@ export class DiagramComponent implements ControlValueAccessor, AfterViewInit {
*/
loadUrl(url: string) {
this.api.getStringFromUrl(url).subscribe(xml => {
const diagramType = (xml.includes('dmn.xsd') ? FileType.DMN : FileType.BPMN);
const diagramType = getDiagramTypeFromXml(xml);
this.openDiagram(xml, diagramType);
}, error => this._handleErrors(error));
}
@ -181,7 +190,9 @@ export class DiagramComponent implements ControlValueAccessor, AfterViewInit {
moddleExtensions: dmnModelerConfig.moddleExtensions,
});
this.modeler.on('views.changed', event => console.log('DMN Modeler changed event:', event));
this.modeler.on('commandStack.changed', () => this.saveDiagram());
this.modeler.on('views.changed', event => this.saveDiagram());
this.modeler.on('import.done', ({error}) => {
if (!error) {

View File

@ -1,8 +1,9 @@
<mat-list>
<mat-list-item *ngFor="let fm of fileMetas" class="mat-elevation-z0">
<mat-icon mat-list-icon>{{getIconCode(fm.type)}}</mat-icon>
<mat-list-item *ngFor="let fm of fileMetas" (click)="editFile(fm.id)">
<mat-icon mat-list-icon>{{fm.type | getIconCode}}</mat-icon>
<h4 mat-line>{{fm.name}}</h4>
<p mat-line>{{fm.last_updated | date}}</p>
<button mat-icon-button color="warn" (click)="deleteFile(fm.id)"><mat-icon>delete</mat-icon></button>
<button mat-icon-button color="primary" (click)="editFile(fm.id)" class="mat-elevation-z0"><mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="deleteFile(fm.id)" class="mat-elevation-z0"><mat-icon>delete</mat-icon></button>
</mat-list-item>
</mat-list>

View File

@ -0,0 +1,4 @@
::ng-deep mat-list-item:hover {
background-color: #EEEEFF;
cursor: pointer;
}

View File

@ -57,10 +57,6 @@ describe('FileListComponent', () => {
expect(component).toBeTruthy();
});
it('should get an icon code for each file type', () => {
Object.values(FileType).forEach(ft => expect(component.getIconCode(ft)).toBeTruthy());
});
it('should delete a file', () => {
const loadFileMetasSpy = spyOn((component as any), 'loadFileMetas').and.stub();
component.deleteFile(mockFileMeta0.id);

View File

@ -1,4 +1,5 @@
import {Component, Input, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {ApiService, FileMeta, FileType, WorkflowSpec} from 'sartography-workflow-lib';
@Component({
@ -10,24 +11,16 @@ export class FileListComponent implements OnInit {
@Input() workflowSpec: WorkflowSpec;
fileMetas: FileMeta[];
constructor(private api: ApiService) {
constructor(
private api: ApiService,
private router: Router
) {
}
ngOnInit() {
this.loadFileMetas();
}
getIconCode(file_type: string) {
switch (file_type) {
case FileType.BPMN:
return 'account_tree';
case FileType.SVG:
return 'image';
case FileType.DMN:
return 'view_list';
}
}
deleteFile(fileMetaId: number) {
this.api.deleteFileMeta(fileMetaId).subscribe(() => this.loadFileMetas());
}
@ -35,4 +28,8 @@ export class FileListComponent implements OnInit {
private loadFileMetas() {
this.api.listBpmnFiles(this.workflowSpec.id).subscribe(fms => this.fileMetas = fms);
}
editFile(fileMetaId: number) {
this.router.navigate([`/modeler/${this.workflowSpec.id}/${fileMetaId}`]);
}
}

View File

@ -1,6 +1,19 @@
<mat-toolbar [ngClass]="{'expanded': expandToolbar}">
<mat-toolbar-row>
<button mat-button (click)="newDiagram()" title="Create new BPMN diagram"><mat-icon>insert_drive_file</mat-icon></button>
<button mat-button [matMenuTriggerFor]="newMenu" title="Open diagram">
<mat-icon>insert_drive_file</mat-icon>
<mat-icon>arrow_drop_down</mat-icon>
</button>
<mat-menu #newMenu="matMenu">
<button mat-button (click)="newDiagram(fileTypes.BPMN)">
<mat-icon>{{fileTypes.BPMN | getIconCode}}</mat-icon>
Create new BPMN diagram
</button>
<button mat-button (click)="newDiagram(fileTypes.DMN)">
<mat-icon>{{fileTypes.DMN | getIconCode}}</mat-icon>
Create new DMN diagram
</button>
</mat-menu>
<button mat-button [matMenuTriggerFor]="importMenu" title="Open diagram">
<mat-icon>folder</mat-icon>
@ -41,7 +54,7 @@
<mat-icon>arrow_drop_down</mat-icon>
</button>
<mat-menu #downloadMenu="matMenu">
<button mat-menu-item (click)="diagram.saveSVG()"><mat-icon>image</mat-icon> Download SVG Image</button>
<button mat-menu-item (click)="diagram.saveSVG()"><mat-icon>{{fileTypes.SVG | getIconCode}}</mat-icon> Download SVG Image</button>
<button mat-menu-item (click)="diagram.saveXML()"><mat-icon>code</mat-icon> Download XML File</button>
</mat-menu>
@ -62,7 +75,7 @@
<mat-icon matPrefix>folder_open</mat-icon>
<input matInput disabled [value]="getFileName()" type="text">
</mat-form-field>
<input hidden (change)="onFileSelected($event)" #fileInput type="file" id="file" accept=".bpmn,.xml,application/xml,text/xml">
<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>

View File

@ -2,10 +2,12 @@ import {DatePipe} from '@angular/common';
import {AfterViewInit, Component, ViewChild} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {MatSnackBar} from '@angular/material/snack-bar';
import {ActivatedRoute} from '@angular/router';
import {ApiService, FileMeta, FileType, WorkflowSpec} from 'sartography-workflow-lib';
import {BpmnWarning} from '../_interfaces/bpmn-warning';
import {FileMetaDialogData} from '../_interfaces/file-meta-dialog-data';
import {ImportEvent} from '../_interfaces/import-event';
import {getDiagramTypeFromXml} from '../_util/diagram-type';
import {DiagramComponent} from '../diagram/diagram.component';
import {FileMetaDialogComponent} from '../file-meta-dialog/file-meta-dialog.component';
@ -27,6 +29,7 @@ export class ModelerComponent implements AfterViewInit {
bpmnFiles: FileMeta[] = [];
diagramFileMeta: FileMeta;
fileName: string;
fileTypes = FileType;
private xml = '';
private draftXml = '';
@ViewChild(DiagramComponent, {static: false}) private diagramComponent: DiagramComponent;
@ -34,7 +37,8 @@ export class ModelerComponent implements AfterViewInit {
constructor(
private api: ApiService,
private snackBar: MatSnackBar,
public dialog: MatDialog
public dialog: MatDialog,
private route: ActivatedRoute,
) {
this.loadFilesFromDb();
}
@ -95,8 +99,9 @@ export class ModelerComponent implements AfterViewInit {
// Arrow function here preserves this context
onLoad = (event: ProgressEvent) => {
this.xml = (event.target as FileReader).result.toString();
this.diagramComponent.openDiagram(this.xml, this.diagramFileMeta.type);
}
const diagramType = this.diagramFileMeta ? this.diagramFileMeta.type : 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.
@ -128,34 +133,13 @@ export class ModelerComponent implements AfterViewInit {
this.onSubmitFileToOpen();
}
newDiagram() {
newDiagram(diagramType?: FileType) {
this.xml = '';
this.draftXml = '';
this.fileName = '';
this.workflowSpec = undefined;
this.diagramFileMeta = undefined;
this.diagramFile = undefined;
this.diagramComponent.openDiagram();
}
private loadFilesFromDb() {
this.api.getWorkflowSpecList().subscribe(wfs => {
this.workflowSpecs = wfs;
this.workflowSpecs.forEach(w => {
this.api.listBpmnFiles(w.id).subscribe(files => {
this.bpmnFiles = [];
files.forEach(f => {
this.api.getFileData(f.id).subscribe(d => {
if ((f.type === FileType.BPMN) || (f.type === FileType.DMN)) {
f.content_type = 'text/xml';
f.file = new File([d], f.name, {type: f.content_type});
this.bpmnFiles.push(f);
}
});
});
});
});
});
this.diagramComponent.openDiagram(undefined, diagramType);
}
editFileMeta() {
@ -178,6 +162,79 @@ export class ModelerComponent implements AfterViewInit {
});
}
getWorkflowSpec(workflow_spec_id: string): WorkflowSpec {
return this.workflowSpecs.find(wfs => workflow_spec_id === wfs.id);
}
getFileMetaDisplayString(fileMeta: FileMeta) {
const spec = this.getWorkflowSpec(fileMeta.workflow_spec_id);
if (spec) {
const specName = spec.id + ' - ' + spec.name + ' - ' + spec.display_name;
const lastUpdated = new DatePipe('en-us').transform(fileMeta.last_updated);
return `${specName} (${fileMeta.name}) - v${fileMeta.version} (${lastUpdated})`;
} else {
return 'Loading...';
}
}
getFileMetaTooltipText(fileMeta: FileMeta) {
const spec = this.getWorkflowSpec(fileMeta.workflow_spec_id);
if (spec) {
const lastUpdated = new DatePipe('en-us').transform(fileMeta.last_updated);
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.version}
`;
} else {
return 'Loading...';
}
}
private loadFilesFromDb() {
this.route.paramMap.subscribe(paramMap => {
const workflowSpecId = paramMap.get('workflowSpecId');
const fileMetaId = parseInt(paramMap.get('fileMetaId'), 10);
console.log('workflowSpecId', workflowSpecId);
console.log('fileMetaId', fileMetaId);
this.api.getWorkflowSpecList().subscribe(wfs => {
this.workflowSpecs = wfs;
this.workflowSpecs.forEach(w => {
if (w.id === workflowSpecId) {
this.workflowSpec = w;
}
this.api.listBpmnFiles(w.id).subscribe(files => {
this.bpmnFiles = [];
files.forEach(f => {
this.api.getFileData(f.id).subscribe(d => {
if ((f.type === FileType.BPMN) || (f.type === FileType.DMN)) {
f.content_type = 'text/xml';
f.file = new File([d], f.name, {type: f.content_type});
this.bpmnFiles.push(f);
if (f.id === fileMetaId) {
this.diagramFileMeta = f;
this.diagramFile = f.file;
this.onSubmitFileToOpen();
}
}
});
});
});
});
});
});
}
private _upsertSpecAndFileMeta(data: FileMetaDialogData) {
if (data.fileName && data.workflowSpecId) {
this.xml = this.draftXml;
@ -229,46 +286,12 @@ export class ModelerComponent implements AfterViewInit {
}
}
getWorkflowSpec(workflow_spec_id: string): WorkflowSpec {
return this.workflowSpecs.find(wfs => workflow_spec_id === wfs.id);
}
getFileMetaDisplayString(fileMeta: FileMeta) {
const spec = this.getWorkflowSpec(fileMeta.workflow_spec_id);
if (spec) {
const specName = spec.id + ' - ' + spec.name + ' - ' + spec.display_name;
const lastUpdated = new DatePipe('en-us').transform(fileMeta.last_updated);
return `${specName} (${fileMeta.name}) - v${fileMeta.version} (${lastUpdated})`;
} else {
return 'Loading...';
}
}
getFileMetaTooltipText(fileMeta: FileMeta) {
const spec = this.getWorkflowSpec(fileMeta.workflow_spec_id);
if (spec) {
const lastUpdated = new DatePipe('en-us').transform(fileMeta.last_updated);
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.version}
`;
} else {
return 'Loading...';
}
}
private isXmlFile(file: File) {
return file.type === 'text/xml' ||
file.type === 'application/xml' ||
file.name.slice(-5) === '.bpmn' ||
file.name.slice(-4) === '.xml';
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() {

View File

@ -0,0 +1,14 @@
<div mat-dialog-title>
<h1>Workflow Specification</h1>
</div>
<div mat-dialog-content>
<form [formGroup]="form">
<formly-form [model]="model" [fields]="fields" [options]="options" [form]="form"></formly-form>
</form>
</div>
<div mat-dialog-actions>
<button [disabled]="form.invalid" (click)="onSubmit()" color="primary" mat-flat-button>Save</button>
<button (click)="onNoClick()" mat-flat-button>Cancel</button>
</div>

View File

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

View File

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

View File

@ -0,0 +1,83 @@
import {Component, Inject} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {FormlyFieldConfig, FormlyFormOptions} from '@ngx-formly/core';
import {v4 as uuidv4} from 'uuid';
import {WorkflowSpecDialogData} from '../_interfaces/workflow-spec-dialog-data';
import {toSnakeCase} from '../_util/string-clean';
@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[] = [
{
key: 'id',
type: 'input',
defaultValue: this.data.id || uuidv4(),
templateOptions: {
label: 'ID',
placeholder: 'UUID of workflow specification',
description: 'This is an autogenerated unique ID and is not editable.',
required: true,
disabled: true,
},
},
{
key: 'name',
type: 'input',
defaultValue: this.data.name,
templateOptions: {
label: 'Name',
placeholder: 'Name of workflow specification',
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,
},
},
{
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,
},
},
{
key: 'description',
type: 'textarea',
defaultValue: this.data.description,
templateOptions: {
label: 'Description',
placeholder: 'Description of workflow specification',
description: 'Write a few sentences explaining to users why this workflow exists and what it should be used for.',
required: true,
},
},
];
constructor(
public dialogRef: MatDialogRef<WorkflowSpecDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: WorkflowSpecDialogData,
) {
}
onNoClick() {
this.dialogRef.close();
}
onSubmit() {
this.model.name = toSnakeCase(this.model.name);
this.dialogRef.close(this.model);
}
}

View File

@ -1,7 +1,7 @@
<div class="container">
<h1>Workflow Specifications</h1>
<button mat-flat-button color="primary" (click)="newWorkflowSpec()">
<button mat-flat-button color="primary" (click)="editWorkflowSpec()">
<mat-icon>library_add</mat-icon>
Add new workflow specification
</button>
@ -10,7 +10,10 @@
<mat-card *ngFor="let wfs of workflowSpecs" class="mat-elevation-z0">
<mat-card-header>
<mat-card-title>
<h1>{{wfs.id}}</h1>
<h1>
{{wfs.id}}
<button mat-mini-fab color="primary" (click)="editWorkflowSpec(wfs)"><mat-icon>edit</mat-icon></button>
</h1>
</mat-card-title>
<mat-card-subtitle>
<h2>{{wfs.name}}</h2>
@ -26,13 +29,13 @@
<app-file-list [workflowSpec]="wfs"></app-file-list>
</mat-card-content>
<mat-card-actions>
<button mat-button>
<button mat-button [routerLink]="['/modeler/' + wfs.id]" [queryParams]="{action: 'newFile'}">
<mat-icon>note_add</mat-icon>
Add new file
Add new BPMN or DMN file
</button>
<button mat-button>
<button mat-button [routerLink]="['/modeler/' + wfs.id]" [queryParams]="{action: 'openFile'}">
<mat-icon>cloud_upload</mat-icon>
Upload file
Upload BPMN or DMN file
</button>
</mat-card-actions>
</mat-card>

View File

@ -1,5 +1,9 @@
import {Component, OnInit} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {MatSnackBar} from '@angular/material/snack-bar';
import {ApiService, WorkflowSpec} from 'sartography-workflow-lib';
import {WorkflowSpecDialogData} from '../_interfaces/workflow-spec-dialog-data';
import {WorkflowSpecDialogComponent} from '../workflow-spec-dialog/workflow-spec-dialog.component';
@Component({
selector: 'app-workflow-spec-list',
@ -8,24 +12,78 @@ import {ApiService, WorkflowSpec} from 'sartography-workflow-lib';
})
export class WorkflowSpecListComponent implements OnInit {
workflowSpecs: WorkflowSpec[] = [];
selectedSpec: WorkflowSpec;
constructor(private api: ApiService) {
constructor(
private api: ApiService,
private snackBar: MatSnackBar,
public dialog: MatDialog
) {
this.loadWorkflowSpecs();
}
ngOnInit() {
}
newWorkflowSpec() {
}
deleteWorkflowSpec(specId: string) {
this.api.deleteWorkflowSpecification(specId).subscribe(() => this.loadWorkflowSpecs());
}
editWorkflowSpec(selectedSpec?: WorkflowSpec) {
this.selectedSpec = selectedSpec;
// Open new filename/workflow spec dialog
const dialogRef = this.dialog.open(WorkflowSpecDialogComponent, {
height: '65vh',
width: '50vw',
data: {
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 : '',
},
});
dialogRef.afterClosed().subscribe((data: WorkflowSpecDialogData) => {
if (data && data.id && data.name && data.display_name && data.description) {
this._upsertSpecAndFileMeta(data);
}
});
}
private loadWorkflowSpecs() {
this.api.getWorkflowSpecList().subscribe(wfs => {
this.workflowSpecs = wfs;
});
}
private _upsertSpecAndFileMeta(data: WorkflowSpecDialogData) {
if (data.id && data.name && data.display_name && data.description) {
// Save old workflow spec id, if user wants to change it
const specId = this.selectedSpec ? this.selectedSpec.id : undefined;
const newSpec: WorkflowSpec = {
id: data.id,
name: data.name,
display_name: data.display_name,
description: data.description,
};
if (specId) {
// Update existing workflow spec and file
this.api.updateWorkflowSpecification(specId, newSpec).subscribe(spec => {
this.snackBar.open('Saved changes to workflow spec.', 'Ok', {duration: 3000});
this.loadWorkflowSpecs();
});
} else {
// Add new workflow spec and file
this.api.addWorkflowSpecification(newSpec).subscribe(spec => {
this.snackBar.open('Saved new workflow spec.', 'Ok', {duration: 3000});
this.loadWorkflowSpecs();
});
}
}
}
}

View File

@ -30,6 +30,33 @@ export const BPMN_DIAGRAM_WITH_WARNINGS = `
</definitions>
`;
export const DMN_DIAGRAM_EMPTY = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions
xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd"
xmlns:biodi="http://bpmn.io/schema/dmn/biodi/1.0"
id="Definitions_REPLACE_ME"
name="DRD"
namespace="http://camunda.org/schema/1.0/dmn"
>
<decision id="Decision_REPLACE_ME" name="Decision 1">
<extensionElements>
<biodi:bounds x="157" y="81" width="180" height="80" />
</extensionElements>
<decisionTable id="decisionTable_1">
<input id="input_1">
<inputExpression id="inputExpression_1" typeRef="string">
<text></text>
</inputExpression>
</input>
<output id="output_1" typeRef="string" />
</decisionTable>
</decision>
</definitions>
`;
export const DMN_DIAGRAM = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions