Adds delete workflow spec category dialog. Shows edit/delete buttons on hover..

This commit is contained in:
Aaron Louie 2020-03-18 11:33:12 -04:00
parent f261183732
commit d0f4916667
20 changed files with 491 additions and 159 deletions

6
package-lock.json generated
View File

@ -11734,9 +11734,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"sartography-workflow-lib": { "sartography-workflow-lib": {
"version": "0.0.62", "version": "0.0.64",
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.62.tgz", "resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.64.tgz",
"integrity": "sha512-Ml5D75HPvxFzeT+0OYKx0YVdkDSywBGuLUpXx981k0yPnOUHWqhDNumWY7jVfr3AsPxvxPXhZYrMTTshOx/1fw==" "integrity": "sha512-5tkoXjbELAhJRmVg6vc63o+B4M8WUWdpvS9oq8imODlEv8Oby4A4mDK/4g1h2daFgIvkC+99iqhiTTrD85gX8w=="
}, },
"sass": { "sass": {
"version": "1.23.3", "version": "1.23.3",

View File

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

View File

@ -0,0 +1,16 @@
<div mat-dialog-title *ngIf="data && data.workflowSpec">
Delete workflow specification <strong>{{data.workflowSpec.name}}</strong>?
<button mat-icon-button mat-dialog-close=""><mat-icon>close</mat-icon></button>
</div>
<div mat-dialog-content>
<div class="select-mode" fxLayoutAlign="center center" fxLayout="row" fxLayoutGap="20px">
<button (click)="onSubmit()" color="warn" mat-flat-button>
<mat-icon>check</mat-icon>
Yes
</button>
<button (click)="onNoClick()" color="primary" mat-flat-button>
<mat-icon>close</mat-icon>
No
</button>
</div>
</div>

View File

@ -0,0 +1,9 @@
.select-mode {
width: 100%;
height: 100%;
::ng-deep button {
padding: 1em 2em;
margin: 1em;
}
}

View File

@ -0,0 +1,60 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {mockWorkflowSpecCategory0} from 'sartography-workflow-lib';
import {DeleteWorkflowSpecCategoryDialogData} from '../../_interfaces/dialog-data';
import {DeleteWorkflowSpecCategoryDialogComponent} from './delete-workflow-spec-category-dialog.component';
describe('DeleteFileDialogComponent', () => {
let component: DeleteWorkflowSpecCategoryDialogComponent;
let fixture: ComponentFixture<DeleteWorkflowSpecCategoryDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
MatDialogModule,
MatIconModule,
NoopAnimationsModule,
],
declarations: [DeleteWorkflowSpecCategoryDialogComponent],
providers: [
{
provide: MatDialogRef,
useValue: {
close: (dialogResult: any) => {
}
}
},
{provide: MAT_DIALOG_DATA, useValue: []},
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DeleteWorkflowSpecCategoryDialogComponent);
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: DeleteWorkflowSpecCategoryDialogData = {confirm: true, category: mockWorkflowSpecCategory0};
component.data.category = mockWorkflowSpecCategory0;
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,30 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {DeleteWorkflowSpecCategoryDialogData} from '../../_interfaces/dialog-data';
@Component({
selector: 'app-delete-workflow-spec-dialog',
templateUrl: './delete-workflow-spec-category-dialog.component.html',
styleUrls: ['./delete-workflow-spec-category-dialog.component.scss']
})
export class DeleteWorkflowSpecCategoryDialogComponent {
constructor(
public dialogRef: MatDialogRef<DeleteWorkflowSpecCategoryDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DeleteWorkflowSpecCategoryDialogData
) {
}
onNoClick() {
this.dialogRef.close();
}
onSubmit() {
const data: DeleteWorkflowSpecCategoryDialogData = {
confirm: true,
category: this.data.category,
};
this.dialogRef.close(data);
}
}

View File

@ -7,7 +7,7 @@ import {MatInputModule} from '@angular/material/input';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {FormlyModule} from '@ngx-formly/core'; import {FormlyModule} from '@ngx-formly/core';
import {FormlyMaterialModule} from '@ngx-formly/material'; import {FormlyMaterialModule} from '@ngx-formly/material';
import {mockWorkflowSpec0} from 'sartography-workflow-lib'; import {mockWorkflowSpecCategory0} from 'sartography-workflow-lib';
import {WorkflowSpecCategoryDialogData} from '../../_interfaces/dialog-data'; import {WorkflowSpecCategoryDialogData} from '../../_interfaces/dialog-data';
import {WorkflowSpecCategoryDialogComponent} from './workflow-spec-category-dialog.component'; import {WorkflowSpecCategoryDialogComponent} from './workflow-spec-category-dialog.component';
@ -56,7 +56,7 @@ describe('WorkflowSpecDialogComponent', () => {
it('should save data on submit', () => { it('should save data on submit', () => {
const closeSpy = spyOn(component.dialogRef, 'close').and.stub(); const closeSpy = spyOn(component.dialogRef, 'close').and.stub();
const expectedData: WorkflowSpecCategoryDialogData = mockWorkflowSpec0 as WorkflowSpecCategoryDialogData; const expectedData: WorkflowSpecCategoryDialogData = mockWorkflowSpecCategory0 as WorkflowSpecCategoryDialogData;
component.model = expectedData; component.model = expectedData;
component.onSubmit(); component.onSubmit();
expect(closeSpy).toHaveBeenCalledWith(expectedData); expect(closeSpy).toHaveBeenCalledWith(expectedData);

View File

@ -1,7 +1,8 @@
import {Component, Inject} from '@angular/core'; 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, FormlyTemplateOptions} from '@ngx-formly/core';
import {ApiService} 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'; import {toSnakeCase} from '../../_util/string-clean';
@ -15,60 +16,83 @@ export class WorkflowSpecDialogComponent {
form: FormGroup = new FormGroup({}); form: FormGroup = new FormGroup({});
model: any = {}; model: any = {};
options: FormlyFormOptions = {}; options: FormlyFormOptions = {};
fields: FormlyFieldConfig[] = [ fields: FormlyFieldConfig[] = [];
{ categories: any;
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( constructor(
private api: ApiService,
public dialogRef: MatDialogRef<WorkflowSpecDialogComponent>, public dialogRef: MatDialogRef<WorkflowSpecDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: WorkflowSpecDialogData, @Inject(MAT_DIALOG_DATA) public data: WorkflowSpecDialogData,
) { ) {
this.api.getWorkflowSpecCategoryList().subscribe(cats => {
this.categories = cats.map(c => {
return {
value: c.id,
label: c.display_name,
};
});
this.fields = [
{
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: 'workflow_spec_category_id',
type: 'select',
defaultValue: this.data.name,
templateOptions: {
label: 'Category',
placeholder: 'Category of workflow specification',
required: true,
options: this.categories
},
},
{
key: 'display_name',
type: 'input',
defaultValue: this.data.display_name,
templateOptions: {
label: 'Display Name',
placeholder: 'Title of the workflow specification',
description: 'This is a human-readable title for the workflow specification,' +
'which should be easy for others to read and remember.',
required: true,
},
},
{
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,
},
},
];
});
} }
onNoClick() { onNoClick() {

View File

@ -1,4 +1,4 @@
import {FileMeta, FileType, WorkflowSpec} from 'sartography-workflow-lib'; import {FileMeta, FileType, WorkflowSpec, WorkflowSpecCategory} from 'sartography-workflow-lib';
export interface FileMetaDialogData { export interface FileMetaDialogData {
fileName: string; fileName: string;
@ -18,6 +18,7 @@ export interface WorkflowSpecDialogData {
name: string; name: string;
display_name: string; display_name: string;
description: string; description: string;
workflow_spec_category_id: number;
} }
export interface WorkflowSpecCategoryDialogData { export interface WorkflowSpecCategoryDialogData {
@ -35,3 +36,8 @@ export interface DeleteWorkflowSpecDialogData {
confirm: boolean; confirm: boolean;
workflowSpec: WorkflowSpec; workflowSpec: WorkflowSpec;
} }
export interface DeleteWorkflowSpecCategoryDialogData {
confirm: boolean;
category: WorkflowSpecCategory;
}

View File

@ -22,6 +22,7 @@ import {FormlyMaterialModule} from '@ngx-formly/material';
import {AppEnvironment, AuthInterceptor, SartographyWorkflowLibModule} from 'sartography-workflow-lib'; import {AppEnvironment, AuthInterceptor, SartographyWorkflowLibModule} from 'sartography-workflow-lib';
import {environment} from '../environments/environment'; import {environment} from '../environments/environment';
import {DeleteFileDialogComponent} from './_dialogs/delete-file-dialog/delete-file-dialog.component'; import {DeleteFileDialogComponent} from './_dialogs/delete-file-dialog/delete-file-dialog.component';
import {DeleteWorkflowSpecCategoryDialogComponent} from './_dialogs/delete-workflow-spec-category-dialog/delete-workflow-spec-category-dialog.component';
import {DeleteWorkflowSpecDialogComponent} from './_dialogs/delete-workflow-spec-dialog/delete-workflow-spec-dialog.component'; import {DeleteWorkflowSpecDialogComponent} from './_dialogs/delete-workflow-spec-dialog/delete-workflow-spec-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 {NewFileDialogComponent} from './_dialogs/new-file-dialog/new-file-dialog.component'; import {NewFileDialogComponent} from './_dialogs/new-file-dialog/new-file-dialog.component';
@ -41,6 +42,7 @@ import {NavbarComponent} from './navbar/navbar.component';
import {SignInComponent} from './sign-in/sign-in.component'; import {SignInComponent} from './sign-in/sign-in.component';
import {SignOutComponent} from './sign-out/sign-out.component'; import {SignOutComponent} from './sign-out/sign-out.component';
import {WorkflowSpecListComponent} from './workflow-spec-list/workflow-spec-list.component'; import {WorkflowSpecListComponent} from './workflow-spec-list/workflow-spec-list.component';
import { WorkflowSpecCardComponent } from './workflow-spec-card/workflow-spec-card.component';
@Injectable() @Injectable()
export class ThisEnvironment implements AppEnvironment { export class ThisEnvironment implements AppEnvironment {
@ -71,6 +73,7 @@ export class AppFormlyConfig {
AppComponent, AppComponent,
DeleteFileDialogComponent, DeleteFileDialogComponent,
DeleteWorkflowSpecDialogComponent, DeleteWorkflowSpecDialogComponent,
DeleteWorkflowSpecCategoryDialogComponent,
DiagramComponent, DiagramComponent,
FileListComponent, FileListComponent,
FileMetaDialogComponent, FileMetaDialogComponent,
@ -86,6 +89,7 @@ export class AppFormlyConfig {
WorkflowSpecDialogComponent, WorkflowSpecDialogComponent,
WorkflowSpecListComponent, WorkflowSpecListComponent,
HomeComponent, HomeComponent,
WorkflowSpecCardComponent,
], ],
imports: [ imports: [
BrowserAnimationsModule, BrowserAnimationsModule,
@ -115,6 +119,7 @@ export class AppFormlyConfig {
entryComponents: [ entryComponents: [
DeleteFileDialogComponent, DeleteFileDialogComponent,
DeleteWorkflowSpecDialogComponent, DeleteWorkflowSpecDialogComponent,
DeleteWorkflowSpecCategoryDialogComponent,
FileMetaDialogComponent, FileMetaDialogComponent,
NewFileDialogComponent, NewFileDialogComponent,
OpenFileDialogComponent, OpenFileDialogComponent,

View File

@ -1,23 +1,29 @@
<mat-list> <div class="file-list">
<mat-list-item <mat-list>
*ngFor="let fm of fileMetas" <mat-list-item
[attr.data-workflow-spec-id]="workflowSpec.id" *ngFor="let fm of fileMetas"
[attr.data-file-meta-id]="fm.id" [attr.data-file-meta-id]="fm.id"
> [attr.data-workflow-spec-id]="workflowSpec.id"
<mat-icon mat-list-icon (click)="editFile(fm.id)">{{fm.type | getIconCode}}</mat-icon> >
<ng-container *ngIf="fm.type === fileType.BPMN"> <mat-icon (click)="editFile(fm.id)" mat-list-icon>{{fm.type | getIconCode}}</mat-icon>
<button mat-flat-button *ngIf="!fm.primary" (click)="makePrimary(fm)"> <ng-container *ngIf="fm.type === fileType.BPMN">
<mat-icon>radio_button_unchecked</mat-icon> <button (click)="makePrimary(fm)" *ngIf="!fm.primary" mat-flat-button>
Make primary process <mat-icon>radio_button_unchecked</mat-icon>
Make primary process
</button>
<button *ngIf="fm.primary" mat-flat-button>
<mat-icon>radio_button_checked</mat-icon>
Primary process
</button>
</ng-container>
<h4 (click)="editFile(fm.id)" mat-line>{{fm.name}}</h4>
<p (click)="editFile(fm.id)" mat-line>{{fm.last_updated | date}}</p>
<button (click)="editFile(fm.id)" class="mat-elevation-z0" color="primary" mat-icon-button>
<mat-icon>edit</mat-icon>
</button> </button>
<button mat-flat-button *ngIf="fm.primary"> <button (click)="confirmDelete(fm)" class="mat-elevation-z0" color="warn" mat-icon-button>
<mat-icon>radio_button_checked</mat-icon> <mat-icon>delete</mat-icon>
Primary process
</button> </button>
</ng-container> </mat-list-item>
<h4 mat-line (click)="editFile(fm.id)">{{fm.name}}</h4> </mat-list>
<p mat-line (click)="editFile(fm.id)">{{fm.last_updated | date}}</p> </div>
<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)="confirmDelete(fm)" class="mat-elevation-z0"><mat-icon>delete</mat-icon></button>
</mat-list-item>
</mat-list>

View File

@ -1,4 +1,11 @@
::ng-deep mat-list-item:hover { @import "../../config";
background-color: #EEEEFF;
cursor: pointer; ::ng-deep mat-list-item {
border-left: 4px solid $brand-gray-light;
&:hover {
border-left: 4px solid $brand-primary;
background-color: $brand-primary-tint-4;
cursor: pointer;
}
} }

View File

@ -0,0 +1,30 @@
<mat-card class="mat-elevation-z0">
<mat-card-header>
<mat-card-title fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="10px">
<h3>{{workflowSpec.display_name}}</h3>
<div class="action-buttons">
<ng-container *ngTemplateOutlet="actionButtons"></ng-container>
</div>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<dl gdAreas="field value" gdColumns="8ch auto">
<dt>ID</dt><dd>{{workflowSpec.id}}</dd>
<dt>Name</dt><dd>{{workflowSpec.name}}</dd>
<dt>Description</dt><dd>{{workflowSpec.description}}</dd>
</dl>
<h4>Workflow Spec Files</h4>
<app-file-list [workflowSpec]="workflowSpec"></app-file-list>
</mat-card-content>
<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>

View File

@ -0,0 +1,28 @@
mat-card {
margin-bottom: 1em;
border: 1px solid #CCCCCC;
mat-card-title {
h3 {
margin: 2rem 0 2rem 0;
}
.action-buttons {
opacity: 0;
}
&:hover {
.action-buttons {
opacity: 1;
}
}
}
mat-card-content {
padding-left: 16px;
dl { margin: 0 0 3rem 0; }
dt { font-weight: bold; }
}
}

View File

@ -0,0 +1,60 @@
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MatCardModule} from '@angular/material/card';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {MatListModule} from '@angular/material/list';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {RouterTestingModule} from '@angular/router/testing';
import {ApiService, MockEnvironment, mockWorkflowSpec0} from 'sartography-workflow-lib';
import {GetIconCodePipe} from '../_pipes/get-icon-code.pipe';
import {FileListComponent} from '../file-list/file-list.component';
import {WorkflowSpecCardComponent} from './workflow-spec-card.component';
describe('WorkflowSpecCardComponent', () => {
let component: WorkflowSpecCardComponent;
let fixture: ComponentFixture<WorkflowSpecCardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
WorkflowSpecCardComponent,
FileListComponent,
GetIconCodePipe,
],
imports: [
HttpClientTestingModule,
MatCardModule,
MatDialogModule,
MatIconModule,
MatListModule,
MatSnackBarModule,
RouterTestingModule,
],
providers: [
ApiService,
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment},
{
provide: MatDialogRef,
useValue: {
close: (dialogResult: any) => {
},
}
},
{provide: MAT_DIALOG_DATA, useValue: []},
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(WorkflowSpecCardComponent);
component = fixture.componentInstance;
component.workflowSpec = mockWorkflowSpec0;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,19 @@
import {Component, Input, OnInit, TemplateRef} from '@angular/core';
import {WorkflowSpec} from 'sartography-workflow-lib';
@Component({
selector: 'app-workflow-spec-card',
templateUrl: './workflow-spec-card.component.html',
styleUrls: ['./workflow-spec-card.component.scss']
})
export class WorkflowSpecCardComponent implements OnInit {
@Input() workflowSpec: WorkflowSpec;
@Input() actionButtons: TemplateRef<any>;
constructor() {
}
ngOnInit(): void {
}
}

View File

@ -1,50 +1,45 @@
<div class="container"> <div class="container mat-typography" fxLayout="column" fxLayoutGap="10px">
<h1>Workflow Specifications</h1> <h1>Workflow Specifications</h1>
<div fxLayout="row" fxLayoutGap="10px"> <div fxLayout="row" fxLayoutGap="10px">
<button mat-flat-button color="primary" (click)="editWorkflowSpec()"> <button mat-flat-button color="primary" (click)="editWorkflowSpec()">
<mat-icon>library_add</mat-icon> <mat-icon>library_add</mat-icon>
Add new workflow specification Add new workflow specification
</button> </button>
<button mat-flat-button color="primary" (click)="editWorkflowSpecCategory()"> <button mat-flat-button color="accent" (click)="editWorkflowSpecCategory()">
<mat-icon>library_add</mat-icon> <mat-icon>post_add</mat-icon>
Add category Add category
</button> </button>
</div> </div>
</div> <ng-container *ngFor="let cat of workflowSpecsByCategory">
<div class="container" *ngFor="let cat of workflowSpecsByCategory"> <ng-container *ngIf="!(cat.id === null && cat.workflow_specs.length === 0)">
<h2>{{cat.display_name}}</h2> <div class="category" fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="start center">
<mat-card *ngFor="let wfs of cat.workflow_specs" class="mat-elevation-z0"> <h2>{{cat.display_name}} ({{cat.name}})</h2>
<mat-card-header> <div class="category-actions" fxLayout="row" fxLayoutGap="10px" *ngIf="cat.id !== null">
<mat-card-title> <button mat-mini-fab color="primary" (click)="editWorkflowSpecCategory(cat)">
<h3> <mat-icon>edit</mat-icon>
{{wfs.display_name}} </button>
<button mat-mini-fab color="primary" (click)="editWorkflowSpec(wfs)"><mat-icon>edit</mat-icon></button> <button mat-icon-button title="Delete this category" color="warn" (click)="confirmDeleteWorkflowSpecCategory(cat)">
</h3> <mat-icon>delete</mat-icon>
</mat-card-title> </button>
<mat-card-subtitle> </div>
<h4>{{wfs.name}}</h4> </div>
<h5>{{wfs.id}}</h5> <div *ngFor="let wfs of cat.workflow_specs" class="workflow-spec">
</mat-card-subtitle> <app-workflow-spec-card [workflowSpec]="wfs" [actionButtons]="actionButtons">
<span fxFlex></span> </app-workflow-spec-card>
<button mat-icon-button title="Delete this workflow specification" color="warn" (click)="confirmDeleteWorkflowSpec(wfs)"> <ng-template #actionButtons>
<mat-icon>delete</mat-icon> <div class="workflow-spec-actions">
</button> <button mat-mini-fab color="primary" (click)="editWorkflowSpec(wfs)">
</mat-card-header> <mat-icon>edit</mat-icon>
<mat-card-content> </button>
<p>{{wfs.description}}</p> <button mat-icon-button title="Delete this workflow specification" color="warn" (click)="confirmDeleteWorkflowSpec(wfs)">
<app-file-list [workflowSpec]="wfs"></app-file-list> <mat-icon>delete</mat-icon>
</mat-card-content> </button>
<mat-card-actions> </div>
<button mat-button [routerLink]="['/modeler/' + wfs.id]" [queryParams]="{action: 'newFile'}"> </ng-template>
<mat-icon>note_add</mat-icon> </div>
Add new BPMN or DMN file <div *ngIf="cat.workflow_specs.length === 0">No workflow specs in this category</div>
</button> </ng-container>
<button mat-button [routerLink]="['/modeler/' + wfs.id]" [queryParams]="{action: 'openFile'}"> </ng-container>
<mat-icon>cloud_upload</mat-icon>
Upload BPMN or DMN file
</button>
</mat-card-actions>
</mat-card>
</div> </div>

View File

@ -1,4 +1,25 @@
mat-card { .container {
margin-bottom: 1em; padding: 3rem;
border: 1px solid #CCCCCC; }
.category {
padding: 1rem 1rem 1rem 0;
h2 {
margin: 1rem 1rem 1rem 0;
}
.category-actions {
opacity: 0;
&:hover {
opacity: 1;
}
}
&:hover {
.category-actions {
opacity: 1;
}
}
} }

View File

@ -96,6 +96,7 @@ describe('WorkflowSpecListComponent', () => {
name: '', name: '',
display_name: '', display_name: '',
description: '', description: '',
workflow_spec_category_id: 0,
}; };
const _upsertWorkflowSpecificationSpy = spyOn((component as any), '_upsertWorkflowSpecification') const _upsertWorkflowSpecificationSpy = spyOn((component as any), '_upsertWorkflowSpecification')

View File

@ -2,10 +2,12 @@ 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, WorkflowSpec, WorkflowSpecCategory} from 'sartography-workflow-lib';
import {DeleteWorkflowSpecCategoryDialogComponent} from '../_dialogs/delete-workflow-spec-category-dialog/delete-workflow-spec-category-dialog.component';
import {DeleteWorkflowSpecDialogComponent} from '../_dialogs/delete-workflow-spec-dialog/delete-workflow-spec-dialog.component'; import {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';
import {WorkflowSpecDialogComponent} from '../_dialogs/workflow-spec-dialog/workflow-spec-dialog.component'; import {WorkflowSpecDialogComponent} from '../_dialogs/workflow-spec-dialog/workflow-spec-dialog.component';
import { import {
DeleteWorkflowSpecCategoryDialogData,
DeleteWorkflowSpecDialogData, DeleteWorkflowSpecDialogData,
WorkflowSpecCategoryDialogData, WorkflowSpecCategoryDialogData,
WorkflowSpecDialogData WorkflowSpecDialogData
@ -28,13 +30,14 @@ export class WorkflowSpecListComponent implements OnInit {
selectedSpec: WorkflowSpec; selectedSpec: WorkflowSpec;
selectedCat: WorkflowSpecCategory; selectedCat: WorkflowSpecCategory;
workflowSpecsByCategory: WorklflowSpecCategoryGroup[] = []; workflowSpecsByCategory: WorklflowSpecCategoryGroup[] = [];
categories: WorkflowSpecCategory[];
constructor( constructor(
private api: ApiService, private api: ApiService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
public dialog: MatDialog public dialog: MatDialog
) { ) {
this._loadWorkflowSpecs(); this._loadWorkflowSpecCategories();
} }
ngOnInit() { ngOnInit() {
@ -52,6 +55,7 @@ export class WorkflowSpecListComponent implements OnInit {
name: this.selectedSpec ? this.selectedSpec.name || this.selectedSpec.id : '', name: this.selectedSpec ? this.selectedSpec.name || this.selectedSpec.id : '',
display_name: this.selectedSpec ? this.selectedSpec.display_name : '', display_name: this.selectedSpec ? this.selectedSpec.display_name : '',
description: this.selectedSpec ? this.selectedSpec.description : '', description: this.selectedSpec ? this.selectedSpec.description : '',
workflow_spec_category_id: this.selectedSpec ? this.selectedSpec.workflow_spec_category_id : '',
}, },
}); });
@ -83,6 +87,21 @@ export class WorkflowSpecListComponent implements OnInit {
}); });
} }
confirmDeleteWorkflowSpecCategory(cat: WorkflowSpecCategory) {
const dialogRef = this.dialog.open(DeleteWorkflowSpecCategoryDialogComponent, {
data: {
confirm: false,
category: cat,
}
});
dialogRef.afterClosed().subscribe((data: DeleteWorkflowSpecCategoryDialogData) => {
if (data && data.confirm && data.category) {
this._deleteWorkflowSpecCategory(data.category);
}
});
}
confirmDeleteWorkflowSpec(wfs: WorkflowSpec) { confirmDeleteWorkflowSpec(wfs: WorkflowSpec) {
const dialogRef = this.dialog.open(DeleteWorkflowSpecDialogComponent, { const dialogRef = this.dialog.open(DeleteWorkflowSpecDialogComponent, {
data: { data: {
@ -98,39 +117,34 @@ export class WorkflowSpecListComponent implements OnInit {
}); });
} }
private _loadWorkflowSpecs() { private _loadWorkflowSpecCategories() {
this.api.getWorkflowSpecList().subscribe(wfs => { this.api.getWorkflowSpecCategoryList().subscribe(cats => {
this.workflowSpecs = wfs; this.categories = cats;
this.workflowSpecs.forEach(wf => { this.workflowSpecsByCategory = [{
if (wf.workflow_spec_category) { id: null,
const cat = this.workflowSpecsByCategory.find(c => c.id === wf.workflow_spec_category_id); name: 'none',
if (cat) { display_name: 'No category',
cat.workflow_specs.push(wf); workflow_specs: [],
} else { }];
this.workflowSpecsByCategory.push({
id: wf.workflow_spec_category_id, this.categories.forEach((cat, i) => {
name: wf.workflow_spec_category.name, this.workflowSpecsByCategory.push(cat);
display_name: wf.workflow_spec_category.display_name, this.workflowSpecsByCategory[i + 1].workflow_specs = [];
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],
});
}
}
}); });
this._loadWorkflowSpecs();
}); });
} }
private _loadWorkflowSpecs() {
this.api.getWorkflowSpecList().subscribe(wfs => {
this.workflowSpecs = wfs;
this.workflowSpecsByCategory.forEach(cat => {
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) {
@ -142,6 +156,7 @@ export class WorkflowSpecListComponent implements OnInit {
name: data.name, name: data.name,
display_name: data.display_name, display_name: data.display_name,
description: data.description, description: data.description,
workflow_spec_category_id: data.workflow_spec_category_id,
}; };
if (specId) { if (specId) {
@ -173,14 +188,14 @@ export class WorkflowSpecListComponent implements OnInit {
} }
private _updateWorkflowSpec(specId: string, newSpec: WorkflowSpec) { private _updateWorkflowSpec(specId: string, newSpec: WorkflowSpec) {
this.api.updateWorkflowSpecification(specId, newSpec).subscribe(spec => { this.api.updateWorkflowSpecification(specId, newSpec).subscribe(_ => {
this._loadWorkflowSpecs(); this._loadWorkflowSpecs();
this._displayMessage('Saved changes to workflow spec.'); this._displayMessage('Saved changes to workflow spec.');
}); });
} }
private _addWorkflowSpec(newSpec: WorkflowSpec) { private _addWorkflowSpec(newSpec: WorkflowSpec) {
this.api.addWorkflowSpecification(newSpec).subscribe(spec => { this.api.addWorkflowSpecification(newSpec).subscribe(_ => {
this._loadWorkflowSpecs(); this._loadWorkflowSpecs();
this._displayMessage('Saved new workflow spec.'); this._displayMessage('Saved new workflow spec.');
}); });
@ -194,22 +209,22 @@ export class WorkflowSpecListComponent implements OnInit {
} }
private _updateWorkflowSpecCategory(catId: number, newCat: WorkflowSpecCategory) { private _updateWorkflowSpecCategory(catId: number, newCat: WorkflowSpecCategory) {
this.api.updateWorkflowSpecCategory(catId, newCat).subscribe(spec => { this.api.updateWorkflowSpecCategory(catId, newCat).subscribe(_ => {
this._loadWorkflowSpecs(); this._loadWorkflowSpecCategories();
this._displayMessage('Saved changes to workflow spec category.'); this._displayMessage('Saved changes to workflow spec category.');
}); });
} }
private _addWorkflowSpecCategory(newCat: WorkflowSpecCategory) { private _addWorkflowSpecCategory(newCat: WorkflowSpecCategory) {
this.api.addWorkflowSpecCategory(newCat).subscribe(spec => { this.api.addWorkflowSpecCategory(newCat).subscribe(_ => {
this._loadWorkflowSpecs(); this._loadWorkflowSpecCategories();
this._displayMessage('Saved new workflow spec category.'); this._displayMessage('Saved new workflow spec category.');
}); });
} }
private _deleteWorkflowSpecCategory(workflowSpecCategory: WorkflowSpecCategory) { private _deleteWorkflowSpecCategory(workflowSpecCategory: WorkflowSpecCategory) {
this.api.deleteWorkflowSpecCategory(workflowSpecCategory.id).subscribe(() => { this.api.deleteWorkflowSpecCategory(workflowSpecCategory.id).subscribe(() => {
this._loadWorkflowSpecs(); this._loadWorkflowSpecCategories();
this._displayMessage(`Deleted workflow spec category ${workflowSpecCategory.name}.`); this._displayMessage(`Deleted workflow spec category ${workflowSpecCategory.name}.`);
}); });
} }