Merge branch 'dev' of github.com:sartography/cr-connect-bpmn into dev

This commit is contained in:
Dan 2021-08-12 10:16:25 -04:00
commit 2fa2921fa9
9 changed files with 132 additions and 424 deletions

View File

@ -35,7 +35,14 @@
},
"configurations": {
"production": {
"optimization": true,
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
@ -53,7 +60,14 @@
]
},
"staging": {
"optimization": true,
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
@ -71,7 +85,14 @@
]
},
"test": {
"optimization": true,
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,

14
package-lock.json generated
View File

@ -41,7 +41,7 @@
"ngx-markdown": "^12.0.1",
"protractor": "~7.0.0",
"rxjs": "^6.5.5",
"sartography-workflow-lib": "^0.0.537",
"sartography-workflow-lib": "^0.0.538",
"tslib": "^2.0.0",
"uuid": "^7.0.2",
"zone.js": "~0.11.4"
@ -18748,9 +18748,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sartography-workflow-lib": {
"version": "0.0.537",
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.537.tgz",
"integrity": "sha512-eVCgXpOyyS+rKx+KFM0xG+CHWxqGlrl3vDGJSG9WaGKgUI/gvSyPkRiSpXZURbdZgYz2Jz+djKKH+NxZ3c8K1A==",
"version": "0.0.538",
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.538.tgz",
"integrity": "sha512-KE6a/ZN9DVlwLXF0A5iq/JYYVIB6T+DaqHQNzGpAAnSSO5vRhQxNeiTqeL30aZhg8js2OzvVivLOAU4i4vKqoQ==",
"dependencies": {
"tslib": "^2.2.0"
}
@ -36093,9 +36093,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sartography-workflow-lib": {
"version": "0.0.537",
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.537.tgz",
"integrity": "sha512-eVCgXpOyyS+rKx+KFM0xG+CHWxqGlrl3vDGJSG9WaGKgUI/gvSyPkRiSpXZURbdZgYz2Jz+djKKH+NxZ3c8K1A==",
"version": "0.0.538",
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.538.tgz",
"integrity": "sha512-KE6a/ZN9DVlwLXF0A5iq/JYYVIB6T+DaqHQNzGpAAnSSO5vRhQxNeiTqeL30aZhg8js2OzvVivLOAU4i4vKqoQ==",
"requires": {
"tslib": "^2.2.0"
}

View File

@ -53,8 +53,9 @@ import {MatSidenavModule} from '@angular/material/sidenav';
import { ConfirmDialogComponent } from './_dialogs/confirm-dialog/confirm-dialog.component';
import {MatExpansionModule} from '@angular/material/expansion';
import { SettingsComponent } from './settings/settings.component';
import {MatSelect, MatSelectModule} from '@angular/material/select';
import { MatSelectModule} from '@angular/material/select';
import {LibraryListComponent} from './library-list/library-list.component';
import {MatCheckboxModule} from '@angular/material/checkbox';
@Injectable()
export class ThisEnvironment implements AppEnvironment {
@ -107,36 +108,37 @@ export function getBaseHref(platformLocation: PlatformLocation): string {
ConfirmDialogComponent,
SettingsComponent,
],
imports: [
BrowserAnimationsModule,
BrowserModule,
FlexLayoutModule,
FormlyModule,
FormsModule,
HttpClientModule,
MatBottomSheetModule,
MatButtonModule,
MatCardModule,
MatDialogModule,
MatDividerModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatSnackBarModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatSelectModule,
ReactiveFormsModule,
SartographyFormsModule,
SartographyPipesModule,
SartographyWorkflowLibModule,
AppRoutingModule,
MatSidenavModule,
MatExpansionModule,
// <-- This line MUST be last (https://angular.io/guide/router#module-import-order-matters)
],
imports: [
BrowserAnimationsModule,
BrowserModule,
FlexLayoutModule,
FormlyModule,
FormsModule,
HttpClientModule,
MatBottomSheetModule,
MatButtonModule,
MatCardModule,
MatDialogModule,
MatDividerModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatSnackBarModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatSelectModule,
ReactiveFormsModule,
SartographyFormsModule,
SartographyPipesModule,
SartographyWorkflowLibModule,
AppRoutingModule,
MatSidenavModule,
MatExpansionModule,
MatCheckboxModule,
// <-- This line MUST be last (https://angular.io/guide/router#module-import-order-matters)
],
bootstrap: [AppComponent],
entryComponents: [
DeleteFileDialogComponent,

View File

@ -1,46 +1,10 @@
<div class="file-list">
<mat-list>
<mat-list-item
*ngFor="let fm of fileMetas"
[attr.data-file-meta-id]="fm.id"
[attr.data-workflow-spec-id]="workflowSpec.id"
*ngFor="let fm of workflowLibraries"
>
<mat-icon (click)="editFile(fm)" mat-list-icon>{{fm.type | getIconCode}}</mat-icon>
<ng-container *ngIf="fm.type === fileType.BPMN">
<button (click)="makePrimary(fm)" *ngIf="!fm.primary" mat-flat-button class="make-primary">
<mat-icon>radio_button_unchecked</mat-icon>
Make primary process
</button>
<button *ngIf="fm.primary" mat-flat-button class="make-primary">
<mat-icon>radio_button_checked</mat-icon>
Primary process
</button>
</ng-container>
<h4 (click)="editFile(fm)" mat-line>{{fm.name}}</h4>
<p (click)="editFile(fm)" mat-line> Updated: {{fm.last_modified | date:'medium'}}</p>
<button (click)="downloadFile(fm)" class="mat-elevation-z0" color="primary" mat-icon-button>
<mat-icon>save_alt</mat-icon>
</button>
<button (click)="editFile(fm)" class="mat-elevation-z0" color="primary" mat-icon-button>
<mat-icon>edit</mat-icon>
</button>
<button (click)="confirmDelete(fm)" class="mat-elevation-z0" color="warn" mat-icon-button>
<mat-icon>delete</mat-icon>
</button>
<mat-checkbox (click)="updateItem(fm,isChecked(fm))" [checked]="isChecked(fm)">{{fm.display_name}}</mat-checkbox>
</mat-list-item>
</mat-list>
</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,241 +1,55 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpHeaders} from '@angular/common/http';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {MatIconModule} from '@angular/material/icon';
import {MatListModule} from '@angular/material/list';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {RouterTestingModule} from '@angular/router/testing';
import createClone from 'rfdc';
import {of} from 'rxjs';
import {
ApiService,
FileMeta,
FileType,
MockEnvironment, mockFile0,
mockFileMeta0,
mockFileMetas,
mockWorkflowSpec0
} from 'sartography-workflow-lib';
import {DeleteFileDialogComponent} from '../_dialogs/delete-file-dialog/delete-file-dialog.component';
import {DeleteFileDialogData} from '../_interfaces/dialog-data';
import {GetIconCodePipe} from '../_pipes/get-icon-code.pipe';
import {FileListComponent} from '../file-list/file-list.component';
import {MatMenuModule} from '@angular/material/menu';
import {ApiService, MockEnvironment, mockWorkflowSpec0, mockWorkflowSpec1, WorkflowSpec} from 'sartography-workflow-lib';
import {LibraryListComponent} from './library-list.component';
describe('FileListComponent', () => {
describe('LibraryListComponent', () => {
let component: LibraryListComponent;
let fixture: ComponentFixture<LibraryListComponent>;
let httpMock: HttpTestingController;
let component: FileListComponent;
let fixture: ComponentFixture<FileListComponent>;
const timeString = '2020-01-23T12:34:12.345Z';
const timeCode = new Date(timeString).getTime();
let libraries: WorkflowSpec[];
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
HttpClientTestingModule,
MatDialogModule,
MatIconModule,
MatListModule,
MatSnackBarModule,
RouterTestingModule,
],
declarations: [
DeleteFileDialogComponent,
FileListComponent,
GetIconCodePipe,
LibraryListComponent
],
imports: [
HttpClientTestingModule,
MatIconModule,
MatMenuModule,
// RouterTestingModule,
],
providers: [
ApiService,
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment},
{provide: APP_BASE_HREF, useValue: ''},
{
provide: MatDialogRef,
useValue: {
close: (dialogResult: any) => {
}
}
},
{provide: MAT_DIALOG_DATA, useValue: []},
]
}).overrideModule(BrowserDynamicTestingModule, {
set: {
entryComponents: [
DeleteFileDialogComponent,
]
}
],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FileListComponent);
component = fixture.componentInstance;
localStorage.setItem('token', 'some_token');
httpMock = TestBed.inject(HttpTestingController);
component.workflowSpec = mockWorkflowSpec0;
fixture = TestBed.createComponent(LibraryListComponent);
component = fixture.componentInstance;
libraries = [mockWorkflowSpec0, mockWorkflowSpec1];
libraries[0].library = true;
libraries[1].library = true;
fixture.detectChanges();
const fmsReq = httpMock.expectOne(`apiRoot/file?workflow_spec_id=${mockWorkflowSpec0.id}`);
expect(fmsReq.request.method).toEqual('GET');
fmsReq.flush(mockFileMetas);
expect(component.fileMetas.length).toBeGreaterThan(0);
});
afterEach(() => {
httpMock.verify();
fixture.destroy();
const uReq = httpMock.expectOne('apiRoot/workflow-specification?libraries=true');
expect(uReq.request.method).toEqual('GET');
uReq.flush(libraries);
expect(component.workflowLibraries).toEqual(libraries);
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should sort files by name', () => {
let prevFileMeta;
for (const thisFileMeta of component.fileMetas) {
if (!prevFileMeta) {
prevFileMeta = thisFileMeta;
} else {
expect(thisFileMeta.name).toBeGreaterThan(prevFileMeta.name);
prevFileMeta = thisFileMeta;
}
}
});
it('should show a confirmation dialog before deleting a file', () => {
const mockConfirmDeleteData: DeleteFileDialogData = {
confirm: false,
fileMeta: mockFileMeta0,
};
const _deleteFileSpy = spyOn((component as any), '_deleteFile').and.stub();
const openDialogSpy = spyOn(component.dialog, 'open')
.and.returnValue({afterClosed: () => of(mockConfirmDeleteData)} as any);
component.confirmDelete(mockFileMeta0);
expect(openDialogSpy).toHaveBeenCalled();
expect(_deleteFileSpy).not.toHaveBeenCalled();
mockConfirmDeleteData.confirm = true;
component.confirmDelete(mockFileMeta0);
expect(openDialogSpy).toHaveBeenCalled();
expect(_deleteFileSpy).toHaveBeenCalled();
});
it('should delete a file', () => {
const loadFileMetasSpy = spyOn((component as any), '_loadFileMetas').and.stub();
(component as any)._deleteFile(mockFileMeta0);
const fmsReq = httpMock.expectOne(`apiRoot/file/${mockFileMeta0.id}`);
expect(fmsReq.request.method).toEqual('DELETE');
fmsReq.flush(null);
expect(loadFileMetasSpy).toHaveBeenCalled();
});
it('should navigate to modeler to edit a BPMN or DMN file', () => {
const routerNavigateSpy = spyOn((component as any).router, 'navigate');
component.workflowSpec = mockWorkflowSpec0;
component.editFile(mockFileMeta0);
expect(routerNavigateSpy).toHaveBeenCalledWith([`/modeler/${mockWorkflowSpec0.id}/${mockFileMeta0.id}`]);
routerNavigateSpy.calls.reset();
const mockDmnMeta = createClone()(mockFileMeta0);
mockDmnMeta.type = FileType.DMN;
component.editFile(mockDmnMeta);
expect(routerNavigateSpy).toHaveBeenCalledWith([`/modeler/${mockWorkflowSpec0.id}/${mockDmnMeta.id}`]);
});
it('should open file metadata dialog for non-BPMN files', () => {
const routerNavigateSpy = spyOn((component as any).router, 'navigate');
const editFileMetaSpy = spyOn(component, 'editFileMeta');
component.workflowSpec = mockWorkflowSpec0;
const mockDocMeta = createClone()(mockFileMeta0);
mockDocMeta.type = FileType.DOCX;
component.editFile(mockDocMeta);
expect(routerNavigateSpy).not.toHaveBeenCalled();
expect(editFileMetaSpy).toHaveBeenCalledWith(mockDocMeta);
routerNavigateSpy.calls.reset();
editFileMetaSpy.calls.reset();
component.editFile(null);
expect(routerNavigateSpy).not.toHaveBeenCalled();
expect(editFileMetaSpy).toHaveBeenCalledWith(null);
});
it('should open file metadata dialog', () => {
const _openFileDialogSpy = spyOn((component as any), '_openFileDialog').and.stub();
component.workflowSpec = mockWorkflowSpec0;
const mockDocMeta: FileMeta = createClone()(mockFileMeta0);
mockDocMeta.type = FileType.DOCX;
component.editFileMeta(mockDocMeta);
const expectedFile = new File([], mockDocMeta.name, {
type: mockDocMeta.content_type,
lastModified: timeCode
});
const fReq = httpMock.expectOne(`apiRoot/file/${mockDocMeta.id}/data`);
const mockHeaders = new HttpHeaders()
.append('last-modified', expectedFile.lastModified.toString())
.append('content-type', mockDocMeta.content_type);
expect(fReq.request.method).toEqual('GET');
fReq.flush(new ArrayBuffer(8), {headers: mockHeaders});
expect(fReq.request.method).toEqual('GET');
expect(_openFileDialogSpy).toHaveBeenCalledWith(mockDocMeta, expectedFile);
_openFileDialogSpy.calls.reset();
component.editFileMeta(null);
expect(_openFileDialogSpy).toHaveBeenCalledWith();
});
it('should upload new file from file dialog', () => {
const openDialogSpy = spyOn(component.dialog, 'open')
.and.returnValue({afterClosed: () => of({file: mockFile0})} as any);
const _loadFileMetasSpy = spyOn((component as any), '_loadFileMetas').and.stub();
component.workflowSpec = mockWorkflowSpec0;
(component as any)._openFileDialog();
const addReq = httpMock.expectOne(`apiRoot/file?workflow_spec_id=${mockWorkflowSpec0.id}`);
expect(addReq.request.method).toEqual('POST');
addReq.flush(mockFileMeta0);
expect(openDialogSpy).toHaveBeenCalled();
expect(_loadFileMetasSpy).toHaveBeenCalled();
});
it('should update existing file from file dialog', () => {
const openDialogSpy = spyOn(component.dialog, 'open')
.and.returnValue({afterClosed: () => of({fileMetaId: mockFileMeta0.id, file: mockFile0})} as any);
const _loadFileMetasSpy = spyOn((component as any), '_loadFileMetas').and.stub();
component.workflowSpec = mockWorkflowSpec0;
(component as any)._openFileDialog(mockFileMeta0, mockFile0);
const updateReq = httpMock.expectOne(`apiRoot/file/${mockFileMeta0.id}/data`);
expect(updateReq.request.method).toEqual('PUT');
updateReq.flush(mockFileMeta0);
expect(openDialogSpy).toHaveBeenCalled();
expect(_loadFileMetasSpy).toHaveBeenCalled();
});
it('should flag a file as primary', () => {
const updateFileMetaSpy = spyOn((component as any).api, 'updateFileMeta').and.returnValue(of(mockFileMeta0));
const _loadFileMetasSpy = spyOn((component as any), '_loadFileMetas').and.stub();
expect(component.fileMetas.length).toEqual(mockFileMetas.length);
component.makePrimary(mockFileMeta0);
expect(updateFileMetaSpy).toHaveBeenCalledTimes(mockFileMetas.length);
expect(component.fileMetas.length).toEqual(mockFileMetas.length);
expect(component.fileMetas.reduce((sum, fm) => fm.primary ? sum + 1 : sum, 0)).toEqual(1);
expect(_loadFileMetasSpy).toHaveBeenCalled();
});
});

View File

@ -1,20 +1,8 @@
import {Component, Input, OnChanges, OnInit} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {MatSnackBar} from '@angular/material/snack-bar';
import {ActivatedRoute, Router} from '@angular/router';
import {
ApiService,
FileMeta,
FileParams,
FileType,
getFileType,
isNumberDefined, newFileFromResponse,
WorkflowSpec
} from 'sartography-workflow-lib';
import {DeleteFileDialogComponent} from '../_dialogs/delete-file-dialog/delete-file-dialog.component';
import {OpenFileDialogComponent} from '../_dialogs/open-file-dialog/open-file-dialog.component';
import {DeleteFileDialogData, OpenFileDialogData} from '../_interfaces/dialog-data';
import * as fileSaver from 'file-saver';
@Component({
selector: 'app-library-list',
@ -22,136 +10,47 @@ import * as fileSaver from 'file-saver';
styleUrls: ['./library-list.component.scss']
})
export class LibraryListComponent implements OnInit, OnChanges {
@Input() workflowSpec: WorkflowSpec;
fileMetas: FileMeta[];
fileType = FileType;
@Input() workflowSpecId: string;
workflowLibraries: WorkflowSpec[];
constructor(
private api: ApiService,
public dialog: MatDialog,
private route: ActivatedRoute,
private router: Router,
private snackBar: MatSnackBar,
) {
}
ngOnInit() {
this._loadFileMetas();
this._loadWorkflowLibraries();
}
ngOnChanges() {
this._loadFileMetas();
this._loadWorkflowLibraries();
}
editFile(fileMeta?: FileMeta) {
if (fileMeta && ((fileMeta.type === FileType.BPMN) || (fileMeta.type === FileType.DMN))) {
this.router.navigate([`/modeler/${this.workflowSpec.id}/${fileMeta.id}`]);
} else {
// Show edit file meta dialog
this.editFileMeta(fileMeta);
isChecked(libraryspec): boolean {
let checked = false;
for (const item of libraryspec.parents) {
checked = checked || (item.id === this.workflowSpecId);
}
return checked;
}
editFileMeta(fm: FileMeta) {
if (fm && isNumberDefined(fm.id)) {
this.api.getFileData(fm.id).subscribe(response => {
const file = newFileFromResponse(fm, response);
this._openFileDialog(fm, file);
updateItem(library: WorkflowSpec , checked: boolean) {
if (checked) {
this.api.deleteWorkflowLibrary(this.workflowSpecId, library.id).subscribe(() => {
this._loadWorkflowLibraries();
});
} else {
this._openFileDialog();
}
}
confirmDelete(fm: FileMeta) {
const dialogRef = this.dialog.open(DeleteFileDialogComponent, {
data: {
confirm: false,
fileMeta: fm,
}
});
dialogRef.afterClosed().subscribe((data: DeleteFileDialogData) => {
if (data && data.confirm && data.fileMeta) {
this._deleteFile(data.fileMeta);
}
});
}
makePrimary(fmPrimary: FileMeta) {
if (fmPrimary.type === FileType.BPMN) {
let numUpdated = 0;
// Fixme: This buisness rule does not belong here.
this.fileMetas.forEach(fm => {
fm.primary = (fmPrimary.id === fm.id);
this.api.updateFileMeta(fm).subscribe(() => {
numUpdated++;
// Reload all fileMetas when all have been updated.
if (numUpdated === this.fileMetas.length) {
this._loadFileMetas();
}
});
this.api.addWorkflowLibrary(this.workflowSpecId, library.id).subscribe(() => {
this._loadWorkflowLibraries();
});
}
}
private _openFileDialog(fm?: FileMeta, file?: File) {
const dialogData: OpenFileDialogData = {
fileMetaId: fm ? fm.id : undefined,
file: file,
mode: 'local',
fileTypes: [FileType.DOC, FileType.DOCX, FileType.XLSX, FileType.XLS],
};
const dialogRef = this.dialog.open(OpenFileDialogComponent, {data: dialogData});
private _loadWorkflowLibraries() {
dialogRef.afterClosed().subscribe((data: OpenFileDialogData) => {
if (data && data.file) {
const newFileMeta: FileMeta = {
id: data.fileMetaId,
content_type: data.file.type,
name: data.file.name,
type: getFileType(data.file),
workflow_spec_id: this.workflowSpec.id,
};
if (isNumberDefined(data.fileMetaId)) {
// Update existing file
this.api.updateFileData(newFileMeta, data.file).subscribe(() => {
this._loadFileMetas();
});
} else {
// Add new file
const fileParams: FileParams = {
workflow_spec_id: this.workflowSpec.id,
};
this.api.addFile(fileParams, newFileMeta, data.file).subscribe(dbFm => {
this._loadFileMetas();
});
}
}
});
}
private _deleteFile(fileMeta: FileMeta) {
this.api.deleteFileMeta(fileMeta.id).subscribe(() => {
this._loadFileMetas();
this.snackBar.open(`Deleted file ${fileMeta.name}.`, 'Ok', {duration: 3000});
});
}
private _loadFileMetas() {
this.api.getFileMetas({workflow_spec_id: this.workflowSpec.id}).subscribe(fms => {
this.fileMetas = fms.sort((a, b) => (a.name > b.name) ? 1 : -1);
});
}
downloadFile(fm: FileMeta) {
this.api.getFileData(fm.id).subscribe(response => {
const blob = new Blob([response.body], {type: fm.content_type});
fileSaver.saveAs(blob, fm.name);
this.api.getWorkflowSpecificationLibraries().subscribe(wfs => {
this.workflowLibraries = wfs;
});
}
}

View File

@ -15,7 +15,7 @@
</mat-card-title>
</mat-card-header>
<mat-card-content>
<dl gdAreas="field value" gdColumns="8ch auto">
<dl gdAreas="field value" gdColumns="10ch auto">
<dt>ID</dt><dd>{{workflowSpec.id}}</dd>
<dt>Name</dt><dd>{{workflowSpec.name}}</dd>
<dt>Description</dt><dd>{{workflowSpec.description}}</dd>
@ -23,9 +23,12 @@
<ng-template #thenBlock><dd>True</dd></ng-template>
<ng-template #elseBlock><dd>False</dd></ng-template>
</dl>
<h4>Workflow Spec Files</h4>
<app-file-list [workflowSpec]="workflowSpec"></app-file-list>
<div *ngIf="!workflowSpec.library">
<h4>Included Libraries</h4>
<app-library-list [workflowSpecId]="workflowSpec.id"></app-library-list>
</div>
</mat-card-content>
<mat-card-actions>
</mat-card-actions>

View File

@ -1,11 +1,11 @@
<div class="container mat-typography" fxLayout="column" fxLayoutGap="10px">
<div class="workflow-specs" fxLayout="column" fxLayoutGap="10px">
<h1>Workflow Specifications</h1>
<div fxLayout="row" fxLayoutGap="10px">
<button id="add_spec" mat-flat-button color="primary" (click)="editWorkflowSpec()">
<div class="buttons" fxLayout="row" fxLayoutGap="10px">
<button id="add_spec" mat-flat-button color="primary" (click)="editWorkflowSpec()" fxLayoutAlign="stretch">
<mat-icon>library_add</mat-icon>
Add new workflow specification
</button>
<button id="add_category" mat-flat-button color="accent" (click)="editWorkflowSpecCategory()">
<button id="add_category" mat-flat-button color="accent" (click)="editWorkflowSpecCategory()" fxLayoutAlign="stretch">
<mat-icon>post_add</mat-icon>
Add category
</button>
@ -20,7 +20,7 @@
<h4>Master Specification</h4>
<mat-list>
<mat-list-item class="workflow-spec" fxLayout="row">
<a class="spec_menu_item" (click)="selectSpec(masterStatusSpec)">{{masterStatusSpec.display_name}}</a>
<span class="spec_menu_item" (click)="selectSpec(masterStatusSpec)">{{masterStatusSpec.display_name}}</span>
</mat-list-item>
</mat-list>
</div>
@ -31,7 +31,7 @@
<h4>Libraries</h4>
<mat-list>
<mat-list-item *ngFor="let wfs of workflowLibraries" class="workflow-spec" fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="start center">
<a [ngClass]="{'spec_menu_item':true, 'spec-selected': selectedSpec && wfs.id === selectedSpec.id}" (click)="selectSpec(wfs)">{{wfs.display_name}}</a>
<span [ngClass]="{'spec_menu_item':true, 'spec-selected': selectedSpec && wfs.id === selectedSpec.id}" (click)="selectSpec(wfs)">{{wfs.display_name}}</span>
<span class="spec-actions" fxLayout="row" fxLayoutGap="10px">
<button
mat-icon-button
@ -85,7 +85,7 @@
</mat-expansion-panel-header>
<mat-list>
<mat-list-item *ngFor="let wfs of cat.workflow_specs" class="workflow-spec" fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="start center">
<a [ngClass]="{'spec_menu_item':true, 'spec-selected': selectedSpec && wfs.id === selectedSpec.id}" (click)="selectSpec(wfs)">{{wfs.display_name}}</a>
<span [ngClass]="{'spec_menu_item':true, 'spec-selected': selectedSpec && wfs.id === selectedSpec.id}" (click)="selectSpec(wfs)">{{wfs.display_name}}</span>
<span class="spec-actions" fxLayout="row" fxLayoutGap="10px" *ngIf="cat && cat.id !== null">
<button
*ngIf="cat && cat.workflow_specs.length > 0 && wfs.display_order !== 0"

View File

@ -4,10 +4,14 @@
margin: 0;
max-height: 40px;
}
.container {
.workflow-specs {
padding: 3rem;
}
.buttons {
height: 60px;
}
.content {
min-height: 600px;
}
@ -42,13 +46,14 @@
.workflow-spec {
border-left: 4px solid $brand-gray-light;
font-size: 1em;
cursor: pointer;
&:hover {
border-left: 4px solid $brand-primary;
background-color: $brand-primary-tint-4;
cursor: pointer;
}
cursor: pointer;
.spec_menu_item {
flex-flow: row;
display: inline-block;