WIP: Moves util methods to sartography-workflow-lib. Attempts to get file dialog working.

This commit is contained in:
Aaron Louie 2020-03-20 16:49:17 -04:00
parent ced9a852e8
commit afca7cece8
17 changed files with 123 additions and 146 deletions

6
package-lock.json generated
View File

@ -12171,9 +12171,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"sartography-workflow-lib": { "sartography-workflow-lib": {
"version": "0.0.68", "version": "0.0.72",
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.68.tgz", "resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.72.tgz",
"integrity": "sha512-W5wJ7YIqDu/0ul1AZNazCUNzmxGNTbLd2i/nArYNgf0WwJO2j+zQsxd41TJVjHLp4dHpR/UsnCXGH/cC0Pz23w==" "integrity": "sha512-l6153jMOkKPgi2dOODOdTSHdPyyZpYjuylkPt38ct4XYaaF+kiT3GadhZli3sxg8KRkYfiaJ5krZRgl3adNu7A=="
}, },
"sass": { "sass": {
"version": "1.23.3", "version": "1.23.3",

View File

@ -49,7 +49,7 @@
"ngx-file-drop": "^8.0.8", "ngx-file-drop": "^8.0.8",
"ngx-markdown": "^9.0.0", "ngx-markdown": "^9.0.0",
"rxjs": "~6.5.4", "rxjs": "~6.5.4",
"sartography-workflow-lib": "^0.0.68", "sartography-workflow-lib": "^0.0.72",
"tslib": "^1.11.1", "tslib": "^1.11.1",
"uuid": "^7.0.2", "uuid": "^7.0.2",
"zone.js": "^0.10.3" "zone.js": "^0.10.3"

View File

@ -2,9 +2,8 @@ import {Component, Inject} from '@angular/core';
import {FormGroup} from '@angular/forms'; import {FormGroup} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {FormlyFieldConfig, FormlyFormOptions} from '@ngx-formly/core'; import {FormlyFieldConfig, FormlyFormOptions} from '@ngx-formly/core';
import {FileType} from 'sartography-workflow-lib'; import {cleanUpFilename, FileType} from 'sartography-workflow-lib';
import {FileMetaDialogData} from '../../_interfaces/dialog-data'; import {FileMetaDialogData} from '../../_interfaces/dialog-data';
import {cleanUpFilename} from '../../_util/string-clean';
@Component({ @Component({
selector: 'app-new-file-dialog', selector: 'app-new-file-dialog',
@ -21,55 +20,47 @@ export class FileMetaDialogComponent {
public dialogRef: MatDialogRef<FileMetaDialogComponent>, public dialogRef: MatDialogRef<FileMetaDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: FileMetaDialogData @Inject(MAT_DIALOG_DATA) public data: FileMetaDialogData
) { ) {
const fileTypeOptions = Object.entries(FileType).map(ft => { const fileTypeOptions = Object.entries(FileType).map(ft => {
return { return {
label: ft[0], label: ft[0],
value: ft[1] value: ft[1]
}; };
}); });
this.fields = [ this.fields = [
{ {
key: 'id', key: 'fileName',
type: 'input', type: 'input',
defaultValue: this.data.id, defaultValue: this.data.fileName,
templateOptions: { templateOptions: {
hide: true, label: 'File Name',
}, placeholder: 'Name of file',
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: 'fileName', {
type: 'input', key: 'fileType',
defaultValue: this.data.fileName, type: 'select',
templateOptions: { defaultValue: this.data.fileType,
label: 'File Name', templateOptions: {
placeholder: 'Name of workflow specification', label: 'File Type',
description: 'Enter a name, in lowercase letters, separated by underscores, that is easy for you to remember.' + placeholder: 'Extension of file',
'It will be converted to all_lowercase_with_underscores when you save.', required: true,
required: true, options: fileTypeOptions,
},
}, },
{ },
key: 'fileType', {
type: 'select', key: 'file',
defaultValue: this.data.fileType, type: 'file',
templateOptions: { defaultValue: this.data.file,
label: 'File Type', templateOptions: {
placeholder: 'Extension of file', label: 'File',
required: true, required: true,
options: fileTypeOptions,
},
}, },
{ }
key: 'file', ];
type: 'file',
defaultValue: this.data.file,
templateOptions: {
label: 'File',
required: true,
},
}
];
} }
onNoClick() { onNoClick() {

View File

@ -1,8 +1,6 @@
import { Component, OnInit } from '@angular/core'; import {Component} from '@angular/core';
import {MatDialogRef} from '@angular/material/dialog'; import {MatDialogRef} from '@angular/material/dialog';
import {ApiService} from 'sartography-workflow-lib'; import {ApiService, cleanUpFilename, getDiagramTypeFromXml} from 'sartography-workflow-lib';
import {getDiagramTypeFromXml} from '../../_util/diagram-type';
import {cleanUpFilename} from '../../_util/string-clean';
@Component({ @Component({
selector: 'app-open-file-dialog', selector: 'app-open-file-dialog',

View File

@ -2,8 +2,8 @@ import {Component, Inject} from '@angular/core';
import {FormGroup} from '@angular/forms'; import {FormGroup} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {FormlyFieldConfig, FormlyFormOptions} from '@ngx-formly/core'; import {FormlyFieldConfig, FormlyFormOptions} from '@ngx-formly/core';
import {toSnakeCase} from 'sartography-workflow-lib';
import {WorkflowSpecDialogData} from '../../_interfaces/dialog-data'; import {WorkflowSpecDialogData} from '../../_interfaces/dialog-data';
import {toSnakeCase} from '../../_util/string-clean';
@Component({ @Component({
selector: 'app-workflow-spec-category-dialog', selector: 'app-workflow-spec-category-dialog',

View File

@ -2,10 +2,9 @@ import {Component, Inject} from '@angular/core';
import {FormGroup} from '@angular/forms'; import {FormGroup} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {FormlyFieldConfig, FormlyFormOptions, FormlyTemplateOptions} from '@ngx-formly/core'; import {FormlyFieldConfig, FormlyFormOptions, FormlyTemplateOptions} from '@ngx-formly/core';
import {ApiService} from 'sartography-workflow-lib'; import {ApiService, toSnakeCase} from 'sartography-workflow-lib';
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
import {WorkflowSpecDialogData} from '../../_interfaces/dialog-data'; import {WorkflowSpecDialogData} from '../../_interfaces/dialog-data';
import {toSnakeCase} from '../../_util/string-clean';
@Component({ @Component({
selector: 'app-workflow-spec-dialog', selector: 'app-workflow-spec-dialog',

View File

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

View File

@ -1,3 +0,0 @@
export const isNumberDefined = (n: number): boolean => {
return (typeof n === 'number') && isFinite(n) && !isNaN(n);
};

View File

@ -1,20 +0,0 @@
import {cleanUpFilename, toSnakeCase, trimString} from './string-clean';
describe('String Cleaning Utilities', () => {
const afterTrimming = `I'm tired of wasting letters when punctuation will do, period. -Steve Martin`;
const beforeTrimming = ` 📌📍🏁 <>?:"{}[] ${afterTrimming} !@#$%^& ✌️👍👆 `;
it('converts a string to snake case', () => {
expect(toSnakeCase(beforeTrimming)).toEqual('i_m_tired_of_wasting_letters_when_punctuation_will_do_period_steve_martin');
});
it('cleans up a file name and replaces or adds the extension', () => {
expect(cleanUpFilename(beforeTrimming, 'bpmn')).toEqual(`I'm tired of wasting letters when punctuation will do, period.bpmn`);
expect(cleanUpFilename(' no extension ', 'bpmn')).toEqual('no extension.bpmn');
});
it('trims non-word characters from a string', () => {
expect(trimString(beforeTrimming)).toEqual(afterTrimming);
});
});

View File

@ -1,25 +0,0 @@
import {FileType} from 'sartography-workflow-lib';
export const trimString = (str: string): string => {
return !str ? '' : String(str).replace(/^\W+|\W+$/gi, '');
};
export const toSnakeCase = (str: string): string => {
str = trimString(str);
return !str ? '' : String(str)
.replace(/\W+/gi, '_')
.toLowerCase();
};
export const cleanUpFilename = (str: string, extension: FileType|string): string => {
const arr = trimString(str).split('.');
// Add file extension, if necessary
if (arr.length < 2) {
arr.push(extension);
} else {
(arr[arr.length - 1]) = extension;
}
return arr.join('.');
};

View File

@ -5,11 +5,16 @@ import {ControlValueAccessor} from '@angular/forms';
import BpmnModeler from 'bpmn-js/lib/Modeler'; import BpmnModeler from 'bpmn-js/lib/Modeler';
import DmnModeler from 'dmn-js/lib/Modeler'; import DmnModeler from 'dmn-js/lib/Modeler';
import * as fileSaver from 'file-saver'; import * as fileSaver from 'file-saver';
import {ApiService, BPMN_DIAGRAM_DEFAULT, DMN_DIAGRAM_DEFAULT, FileType} from 'sartography-workflow-lib'; import {
ApiService,
BPMN_DIAGRAM_DEFAULT,
DMN_DIAGRAM_DEFAULT,
FileType,
getDiagramTypeFromXml
} from 'sartography-workflow-lib';
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
import {BpmnWarning} from '../_interfaces/bpmn-warning'; import {BpmnWarning} from '../_interfaces/bpmn-warning';
import {ImportEvent} from '../_interfaces/import-event'; import {ImportEvent} from '../_interfaces/import-event';
import {getDiagramTypeFromXml} from '../_util/diagram-type';
import {bpmnModelerConfig} from './bpmn-modeler-config'; import {bpmnModelerConfig} from './bpmn-modeler-config';
import {dmnModelerConfig} from './dmn-modeler-config'; import {dmnModelerConfig} from './dmn-modeler-config';

View File

@ -27,3 +27,17 @@
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
</div> </div>
<div class="file-list-actions">
<button [queryParams]="{action: 'newFile'}" [routerLink]="['/modeler/' + workflowSpec.id]" mat-button>
<mat-icon>note_add</mat-icon>
Add new BPMN or DMN file
</button>
<button [queryParams]="{action: 'openFile'}" [routerLink]="['/modeler/' + workflowSpec.id]" mat-button>
<mat-icon>cloud_upload</mat-icon>
Upload BPMN or DMN file
</button>
<button (click)="editFile()" mat-button>
<mat-icon>cloud_upload</mat-icon>
Upload Template file
</button>
</div>

View File

@ -1,8 +1,8 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {MatDialog} from '@angular/material/dialog'; import {MatDialog} from '@angular/material/dialog';
import {MatSnackBar} from '@angular/material/snack-bar'; import {MatSnackBar} from '@angular/material/snack-bar';
import {Router} from '@angular/router'; import {ActivatedRoute, Params, Router} from '@angular/router';
import {ApiService, FileMeta, FileType, WorkflowSpec} from 'sartography-workflow-lib'; import {ApiService, FileMeta, FileType, isNumberDefined, WorkflowSpec} from 'sartography-workflow-lib';
import {DeleteFileDialogComponent} from '../_dialogs/delete-file-dialog/delete-file-dialog.component'; import {DeleteFileDialogComponent} from '../_dialogs/delete-file-dialog/delete-file-dialog.component';
import {FileMetaDialogComponent} from '../_dialogs/file-meta-dialog/file-meta-dialog.component'; import {FileMetaDialogComponent} from '../_dialogs/file-meta-dialog/file-meta-dialog.component';
import {DeleteFileDialogData, FileMetaDialogData} from '../_interfaces/dialog-data'; import {DeleteFileDialogData, FileMetaDialogData} from '../_interfaces/dialog-data';
@ -20,6 +20,7 @@ export class FileListComponent implements OnInit {
constructor( constructor(
private api: ApiService, private api: ApiService,
public dialog: MatDialog, public dialog: MatDialog,
private route: ActivatedRoute,
private router: Router, private router: Router,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
) { ) {
@ -29,8 +30,8 @@ export class FileListComponent implements OnInit {
this._loadFileMetas(); this._loadFileMetas();
} }
editFile(fileMeta: FileMeta) { editFile(fileMeta?: FileMeta) {
if (fileMeta.type === FileType.BPMN ||fileMeta.type === FileType.DMN) { if (fileMeta && ((fileMeta.type === FileType.BPMN) || (fileMeta.type === FileType.DMN))) {
this.router.navigate([`/modeler/${this.workflowSpec.id}/${fileMeta.id}`]); this.router.navigate([`/modeler/${this.workflowSpec.id}/${fileMeta.id}`]);
} else { } else {
// Show edit file meta dialog // Show edit file meta dialog
@ -39,11 +40,30 @@ export class FileListComponent implements OnInit {
} }
editFileMeta(fm: FileMeta) { editFileMeta(fm: FileMeta) {
// Set route query string
this.router.navigate([], {
relativeTo: this.route,
fragment: this.workflowSpec.id,
queryParams: {
workflow_spec_id: this.workflowSpec.id
},
queryParamsHandling: 'merge'
}).finally(() => {
// Get file data
if (fm && isNumberDefined(fm.id)) {
this.api.getFileData(fm.id).subscribe(fileData => this._openFileDialog(fm, fileData));
} else {
this._openFileDialog();
}
});
}
private _openFileDialog(fm?: FileMeta, fileData?: Blob) {
const dialogRef = this.dialog.open(FileMetaDialogComponent, { const dialogRef = this.dialog.open(FileMetaDialogComponent, {
data: { data: {
fileName: fm.name, fileName: fm ? fm.name : undefined,
fileType: fm.type, fileType: fm ? fm.type : undefined,
file: fm.file, file: fileData ? fileData : undefined,
} }
}); });
@ -61,6 +81,7 @@ export class FileListComponent implements OnInit {
}); });
} }
}); });
} }
confirmDelete(fm: FileMeta) { confirmDelete(fm: FileMeta) {
@ -114,4 +135,5 @@ export class FileListComponent implements OnInit {
this.api.getFileData(fm.id).subscribe((fd: File) => fm.file = fd); this.api.getFileData(fm.id).subscribe((fd: File) => fm.file = fd);
}); });
} }
} }

View File

@ -3,16 +3,21 @@ import {AfterViewInit, Component, ViewChild} from '@angular/core';
import {MatDialog} from '@angular/material/dialog'; import {MatDialog} from '@angular/material/dialog';
import {MatSnackBar} from '@angular/material/snack-bar'; import {MatSnackBar} from '@angular/material/snack-bar';
import {ActivatedRoute, Params, Router} from '@angular/router'; import {ActivatedRoute, Params, Router} from '@angular/router';
import {ApiService, FileMeta, FileType, WorkflowSpec} from 'sartography-workflow-lib'; import {
import {BpmnWarning} from '../_interfaces/bpmn-warning'; ApiService,
import {FileMetaDialogData, NewFileDialogData, OpenFileDialogData} from '../_interfaces/dialog-data'; FileMeta,
import {ImportEvent} from '../_interfaces/import-event'; FileType,
import {getDiagramTypeFromXml} from '../_util/diagram-type'; getDiagramTypeFromXml,
import {isNumberDefined} from '../_util/is-number-defined'; isNumberDefined,
import {DiagramComponent} from '../diagram/diagram.component'; WorkflowSpec
} from 'sartography-workflow-lib';
import {FileMetaDialogComponent} from '../_dialogs/file-meta-dialog/file-meta-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 {NewFileDialogComponent} from '../_dialogs/new-file-dialog/new-file-dialog.component';
import {OpenFileDialogComponent} from '../_dialogs/open-file-dialog/open-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 {ImportEvent} from '../_interfaces/import-event';
import {DiagramComponent} from '../diagram/diagram.component';
@Component({ @Component({
selector: 'app-modeler', selector: 'app-modeler',
@ -115,7 +120,7 @@ export class ModelerComponent implements AfterViewInit {
this.xml = (event.target as FileReader).result.toString(); this.xml = (event.target as FileReader).result.toString();
const diagramType = getDiagramTypeFromXml(this.xml); const diagramType = getDiagramTypeFromXml(this.xml);
this.diagramComponent.openDiagram(this.xml, diagramType); this.diagramComponent.openDiagram(this.xml, diagramType);
} };
readFile(file: File) { readFile(file: File) {
// FileReader must be instantiated this way so unit test can spy on it. // FileReader must be instantiated this way so unit test can spy on it.
@ -157,8 +162,7 @@ export class ModelerComponent implements AfterViewInit {
} }
openFileDialog() { openFileDialog() {
const dialogRef = this.dialog.open(OpenFileDialogComponent, { const dialogRef = this.dialog.open(OpenFileDialogComponent, {});
});
dialogRef.afterClosed().subscribe((data: OpenFileDialogData) => { dialogRef.afterClosed().subscribe((data: OpenFileDialogData) => {
if (data && data.file) { if (data && data.file) {
@ -169,8 +173,7 @@ export class ModelerComponent implements AfterViewInit {
} }
newFileDialog() { newFileDialog() {
const dialogRef = this.dialog.open(NewFileDialogComponent, { const dialogRef = this.dialog.open(NewFileDialogComponent, {});
});
dialogRef.afterClosed().subscribe((data: NewFileDialogData) => { dialogRef.afterClosed().subscribe((data: NewFileDialogData) => {
if (data && data.fileType) { if (data && data.fileType) {
@ -185,6 +188,7 @@ export class ModelerComponent implements AfterViewInit {
data: { data: {
fileName: this.diagramFile ? this.diagramFile.name : this.fileName || '', fileName: this.diagramFile ? this.diagramFile.name : this.fileName || '',
fileType: this.diagramType || getDiagramTypeFromXml(this.xml), fileType: this.diagramType || getDiagramTypeFromXml(this.xml),
file: this.diagramFile || undefined,
}, },
}); });

View File

@ -1,4 +1,4 @@
<mat-card class="mat-elevation-z0"> <mat-card class="mat-elevation-z0" [id]="workflowSpec.id">
<mat-card-header> <mat-card-header>
<mat-card-title fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="10px"> <mat-card-title fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="10px">
<h3>{{workflowSpec.display_name}}</h3> <h3>{{workflowSpec.display_name}}</h3>
@ -18,13 +18,5 @@
<app-file-list [workflowSpec]="workflowSpec"></app-file-list> <app-file-list [workflowSpec]="workflowSpec"></app-file-list>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button [queryParams]="{action: 'newFile'}" [routerLink]="['/modeler/' + workflowSpec.id]" mat-button>
<mat-icon>note_add</mat-icon>
Add new BPMN or DMN file
</button>
<button [queryParams]="{action: 'openFile'}" [routerLink]="['/modeler/' + workflowSpec.id]" mat-button>
<mat-icon>cloud_upload</mat-icon>
Upload BPMN or DMN file
</button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>

View File

@ -1,5 +1,7 @@
import {Component, Input, OnInit, TemplateRef} from '@angular/core'; import {Component, Input, OnInit, TemplateRef} from '@angular/core';
import {WorkflowSpec} from 'sartography-workflow-lib'; import {MatDialog} from '@angular/material/dialog';
import {MatSnackBar} from '@angular/material/snack-bar';
import {ApiService, WorkflowSpec} from 'sartography-workflow-lib';
@Component({ @Component({
selector: 'app-workflow-spec-card', selector: 'app-workflow-spec-card',
@ -16,4 +18,8 @@ export class WorkflowSpecCardComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
} }
openFileDialog() {
}
} }

View File

@ -1,7 +1,7 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {MatDialog} from '@angular/material/dialog'; import {MatDialog} from '@angular/material/dialog';
import {MatSnackBar} from '@angular/material/snack-bar'; import {MatSnackBar} from '@angular/material/snack-bar';
import {ApiService, WorkflowSpec, WorkflowSpecCategory} from 'sartography-workflow-lib'; import {ApiService, isNumberDefined, WorkflowSpec, WorkflowSpecCategory} from 'sartography-workflow-lib';
import {DeleteWorkflowSpecCategoryDialogComponent} from '../_dialogs/delete-workflow-spec-category-dialog/delete-workflow-spec-category-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 {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 {WorkflowSpecCategoryDialogComponent} from '../_dialogs/workflow-spec-category-dialog/workflow-spec-category-dialog.component';
@ -12,7 +12,6 @@ import {
WorkflowSpecCategoryDialogData, WorkflowSpecCategoryDialogData,
WorkflowSpecDialogData WorkflowSpecDialogData
} from '../_interfaces/dialog-data'; } from '../_interfaces/dialog-data';
import {isNumberDefined} from '../_util/is-number-defined';
interface WorklflowSpecCategoryGroup { interface WorklflowSpecCategoryGroup {
id: number; id: number;
@ -138,13 +137,13 @@ export class WorkflowSpecListComponent implements OnInit {
} }
private _loadWorkflowSpecs() { private _loadWorkflowSpecs() {
this.api.getWorkflowSpecList().subscribe(wfs => { this.api.getWorkflowSpecList().subscribe(wfs => {
this.workflowSpecs = wfs; this.workflowSpecs = wfs;
this.workflowSpecsByCategory.forEach(cat => { this.workflowSpecsByCategory.forEach(cat => {
cat.workflow_specs = this.workflowSpecs.filter(wf => wf.workflow_spec_category_id === cat.id); cat.workflow_specs = this.workflowSpecs.filter(wf => wf.workflow_spec_category_id === cat.id);
}); });
}); });
} }
private _upsertWorkflowSpecification(data: WorkflowSpecDialogData) { private _upsertWorkflowSpecification(data: WorkflowSpecDialogData) {
if (data.id && data.name && data.display_name && data.description) { if (data.id && data.name && data.display_name && data.description) {