mirror of
https://github.com/sartography/cr-connect-bpmn.git
synced 2025-01-11 17:44:32 +00:00
Imports formly. Adds workflow spec dialog. Links buttons to modeler screen. Creates new DMN diagram.
This commit is contained in:
parent
7d2616dc6a
commit
094fa31fcf
19
package-lock.json
generated
19
package-lock.json
generated
@ -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",
|
||||
|
@ -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": {
|
||||
|
6
src/app/_interfaces/workflow-spec-dialog-data.ts
Normal file
6
src/app/_interfaces/workflow-spec-dialog-data.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface WorkflowSpecDialogData {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
description: string;
|
||||
}
|
18
src/app/_pipes/get-icon-code.pipe.spec.ts
Normal file
18
src/app/_pipes/get-icon-code.pipe.spec.ts
Normal 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());
|
||||
});
|
||||
});
|
19
src/app/_pipes/get-icon-code.pipe.ts
Normal file
19
src/app/_pipes/get-icon-code.pipe.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
5
src/app/_util/diagram-type.ts
Normal file
5
src/app/_util/diagram-type.ts
Normal 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);
|
||||
};
|
@ -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' },
|
||||
];
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
this.modeler.createDiagram((e, w) => this.onImport(e, w));
|
||||
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) {
|
||||
|
@ -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>
|
||||
|
@ -0,0 +1,4 @@
|
||||
::ng-deep mat-list-item:hover {
|
||||
background-color: #EEEEFF;
|
||||
cursor: pointer;
|
||||
}
|
@ -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);
|
||||
|
@ -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}`]);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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>
|
@ -0,0 +1,3 @@
|
||||
::ng-deep formly-field mat-form-field {
|
||||
margin-bottom: 40px;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user