Adds workflow spec category dialog

This commit is contained in:
Aaron Louie 2020-03-16 16:10:34 -04:00
parent 721a73b5ff
commit 7c1dd01a4d
10 changed files with 302 additions and 16 deletions

6
package-lock.json generated
View File

@ -11734,9 +11734,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sartography-workflow-lib": {
"version": "0.0.58",
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.58.tgz",
"integrity": "sha512-y0TyRb0LiiLWo3uA9HV91ZIct2tIDoRBAUtwoTi2Kynj4eH0ui2Y4d/pllbC2GFKrfbmmw2NqMP8ZcsCnkuJgA=="
"version": "0.0.59",
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.59.tgz",
"integrity": "sha512-Aylrdco6fei7P4VAF+ZG+q3F3TJLkyobPmzGJDe+8vfZ5jlUJ+EffWKFV6LKCdLdkaV3oYRpo4voPnm1OvuRSA=="
},
"sass": {
"version": "1.23.3",

View File

@ -46,7 +46,7 @@
"dmn-js-properties-panel": "^0.3.4",
"file-saver": "^2.0.2",
"rxjs": "~6.5.4",
"sartography-workflow-lib": "^0.0.58",
"sartography-workflow-lib": "^0.0.59",
"tslib": "^1.10.0",
"uuid": "^7.0.0",
"zone.js": "~0.10.2"

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,70 @@
import {async, ComponentFixture, TestBed} 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';
import {MatIconModule} from '@angular/material/icon';
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 {mockWorkflowSpec0} from 'sartography-workflow-lib';
import {WorkflowSpecCategoryDialogData} from '../../_interfaces/dialog-data';
import {WorkflowSpecCategoryDialogComponent} from './workflow-spec-category-dialog.component';
describe('WorkflowSpecDialogComponent', () => {
let component: WorkflowSpecCategoryDialogComponent;
let fixture: ComponentFixture<WorkflowSpecCategoryDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
FormlyModule.forRoot(),
FormlyMaterialModule,
FormsModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
NoopAnimationsModule,
ReactiveFormsModule,
],
declarations: [WorkflowSpecCategoryDialogComponent],
providers: [
{
provide: MatDialogRef,
useValue: {
close: (dialogResult: any) => {
}
}
},
{provide: MAT_DIALOG_DATA, useValue: []},
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(WorkflowSpecCategoryDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should save data on submit', () => {
const closeSpy = spyOn(component.dialogRef, 'close').and.stub();
const expectedData: WorkflowSpecCategoryDialogData = mockWorkflowSpec0 as WorkflowSpecCategoryDialogData;
component.model = expectedData;
component.onSubmit();
expect(closeSpy).toHaveBeenCalledWith(expectedData);
});
it('should not change data on cancel', () => {
const closeSpy = spyOn(component.dialogRef, 'close').and.stub();
component.onNoClick();
expect(closeSpy).toHaveBeenCalledWith();
});
});

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/dialog-data';
import {toSnakeCase} from '../../_util/string-clean';
@Component({
selector: 'app-workflow-spec-category-dialog',
templateUrl: './workflow-spec-category-dialog.component.html',
styleUrls: ['./workflow-spec-category-dialog.component.scss']
})
export class WorkflowSpecCategoryDialogComponent {
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<WorkflowSpecCategoryDialogComponent>,
@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

@ -20,6 +20,12 @@ export interface WorkflowSpecDialogData {
description: string;
}
export interface WorkflowSpecCategoryDialogData {
id: string;
name: string;
display_name: string;
}
export interface DeleteFileDialogData {
confirm: boolean;
fileMeta: FileMeta;

View File

@ -26,6 +26,7 @@ import {DeleteWorkflowSpecDialogComponent} from './_dialogs/delete-workflow-spec
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 {EmailValidator, EmailValidatorMessage, ShowError} from './_forms/validators/formly.validator';
import {GetIconCodePipe} from './_pipes/get-icon-code.pipe';
@ -81,6 +82,7 @@ export class AppFormlyConfig {
OpenFileDialogComponent,
SignInComponent,
SignOutComponent,
WorkflowSpecCategoryDialogComponent,
WorkflowSpecDialogComponent,
WorkflowSpecListComponent,
HomeComponent,
@ -116,6 +118,7 @@ export class AppFormlyConfig {
FileMetaDialogComponent,
NewFileDialogComponent,
OpenFileDialogComponent,
WorkflowSpecCategoryDialogComponent,
WorkflowSpecDialogComponent,
],
providers: [

View File

@ -1,23 +1,30 @@
<div class="container">
<h1>Workflow Specifications</h1>
<button mat-flat-button color="primary" (click)="editWorkflowSpec()">
<mat-icon>library_add</mat-icon>
Add new workflow specification
</button>
<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="primary" (click)="addWorkflowSpecCategory()">
<mat-icon>library_add</mat-icon>
Add category
</button>
</div>
</div>
<div class="container">
<mat-card *ngFor="let wfs of workflowSpecs" class="mat-elevation-z0">
<div class="container" *ngFor="let cat of workflowSpecsByCategory">
<h2>{{cat.display_name}}</h2>
<mat-card *ngFor="let wfs of cat.workflow_specs" class="mat-elevation-z0">
<mat-card-header>
<mat-card-title>
<h1>
<h3>
{{wfs.display_name}}
<button mat-mini-fab color="primary" (click)="editWorkflowSpec(wfs)"><mat-icon>edit</mat-icon></button>
</h1>
</h3>
</mat-card-title>
<mat-card-subtitle>
<h2>{{wfs.name}}</h2>
<h3>{{wfs.id}}</h3>
<h4>{{wfs.name}}</h4>
<h5>{{wfs.id}}</h5>
</mat-card-subtitle>
<span fxFlex></span>
<button mat-icon-button title="Delete this workflow specification" color="warn" (click)="confirmDeleteWorkflowSpec(wfs)">

View File

@ -1,10 +1,22 @@
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 {ApiService, WorkflowSpec, WorkflowSpecCategory} from 'sartography-workflow-lib';
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 {DeleteWorkflowSpecDialogData, WorkflowSpecDialogData} from '../_interfaces/dialog-data';
import {
DeleteWorkflowSpecDialogData,
WorkflowSpecCategoryDialogData,
WorkflowSpecDialogData
} from '../_interfaces/dialog-data';
interface WorklflowSpecCategoryGroup {
id: number;
name: string;
display_name: string;
workflow_specs?: WorkflowSpec[];
}
@Component({
selector: 'app-workflow-spec-list',
@ -14,6 +26,8 @@ import {DeleteWorkflowSpecDialogData, WorkflowSpecDialogData} from '../_interfac
export class WorkflowSpecListComponent implements OnInit {
workflowSpecs: WorkflowSpec[] = [];
selectedSpec: WorkflowSpec;
selectedCat: WorkflowSpecCategory;
workflowSpecsByCategory: WorklflowSpecCategoryGroup[] = [];
constructor(
private api: ApiService,
@ -48,6 +62,27 @@ export class WorkflowSpecListComponent implements OnInit {
});
}
editWorkflowSpecCategory(selectedCat?: WorkflowSpecCategory) {
this.selectedCat = selectedCat;
// Open new filename/workflow spec dialog
const dialogRef = this.dialog.open(WorkflowSpecCategoryDialogComponent, {
height: '65vh',
width: '50vw',
data: {
id: this.selectedCat ? this.selectedCat.id : '',
name: this.selectedCat ? this.selectedCat.name || this.selectedCat.id : '',
display_name: this.selectedCat ? this.selectedCat.display_name : '',
},
});
dialogRef.afterClosed().subscribe((data: WorkflowSpecCategoryDialogData) => {
if (data && data.id && data.name && data.display_name) {
this._upsertWorkflowSpecCategory(data);
}
});
}
confirmDeleteWorkflowSpec(wfs: WorkflowSpec) {
const dialogRef = this.dialog.open(DeleteWorkflowSpecDialogComponent, {
data: {
@ -66,6 +101,33 @@ export class WorkflowSpecListComponent implements OnInit {
private _loadWorkflowSpecs() {
this.api.getWorkflowSpecList().subscribe(wfs => {
this.workflowSpecs = wfs;
this.workflowSpecs.forEach(wf => {
if (wf.workflow_spec_category) {
const cat = this.workflowSpecsByCategory.find(c => c.id === wf.workflow_spec_category_id);
if (cat) {
cat.workflow_specs.push(wf);
} else {
this.workflowSpecsByCategory.push({
id: wf.workflow_spec_category_id,
name: wf.workflow_spec_category.name,
display_name: wf.workflow_spec_category.display_name,
workflow_specs: [wf],
});
}
} else {
const cat = this.workflowSpecsByCategory.find(c => c.id === -1);
if (cat) {
cat.workflow_specs.push(wf);
} else {
this.workflowSpecsByCategory.push({
id: -1,
name: 'none',
display_name: 'No category',
workflow_specs: [wf],
});
}
}
});
});
}
@ -90,6 +152,26 @@ export class WorkflowSpecListComponent implements OnInit {
}
}
private _upsertWorkflowCategorySpecification(data: WorkflowSpecCategoryDialogData) {
if (data.id && data.name && data.display_name) {
// Save old workflow spec id, in case it's changed
const specId = this.selectedSpec ? this.selectedSpec.id : undefined;
const newCat: WorkflowSpecCategory = {
id: data.id,
name: data.name,
display_name: data.display_name,
};
if (specId) {
this._updateWorkflowSpecCategory(specId, newCat);
} else {
this._addWorkflowSpecCategory(newCat);
}
}
}
private _updateWorkflowSpec(specId: string, newSpec: WorkflowSpec) {
this.api.updateWorkflowSpecification(specId, newSpec).subscribe(spec => {
this._loadWorkflowSpecs();
@ -104,6 +186,20 @@ export class WorkflowSpecListComponent implements OnInit {
});
}
private _updateWorkflowSpecCategory(specId: string, newCat: WorkflowSpecCategory) {
this.api.updateWorkflowSpecCategory(specId, newCat).subscribe(spec => {
this._loadWorkflowSpecs();
this._displayMessage('Saved changes to workflow spec.');
});
}
private _addWorkflowSpecCategory(newCat: WorkflowSpecCategory) {
this.api.addWorkflowSpecCategory(newCat).subscribe(spec => {
this._loadWorkflowSpecs();
this._displayMessage('Saved new workflow spec.');
});
}
private _deleteWorkflowSpec(workflowSpec: WorkflowSpec) {
this.api.deleteWorkflowSpecification(workflowSpec.id).subscribe(() => {
this._loadWorkflowSpecs();
@ -114,5 +210,9 @@ export class WorkflowSpecListComponent implements OnInit {
private _displayMessage(message: string) {
this.snackBar.open(message, 'Ok', {duration: 3000});
}
addWorkflowSpecCategory() {
}
}