diff --git a/package-lock.json b/package-lock.json index 98aabaa..265604a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12171,9 +12171,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sartography-workflow-lib": { - "version": "0.0.74", - "resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.74.tgz", - "integrity": "sha512-v8Kz89ta85O1nO7ICtJnok0LbxwEofwcqG2eivmo78xR4qYGisUl68GUTYOHdctAQwNdNbyUW7DxkqpkaPb8ew==" + "version": "0.0.78", + "resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.78.tgz", + "integrity": "sha512-8z27+0hL4ZpnTpA6nHQUzQsspr+1d/y2T5oXtX3BwLj+YDvbWMcO3hzw7BfJnAqanxszNPjcdoFZatWaNPYb5w==" }, "sass": { "version": "1.23.3", diff --git a/package.json b/package.json index de55c6e..df45ab5 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "ngx-file-drop": "^8.0.8", "ngx-markdown": "^9.0.0", "rxjs": "~6.5.4", - "sartography-workflow-lib": "^0.0.74", + "sartography-workflow-lib": "^0.0.78", "tslib": "^1.11.1", "uuid": "^7.0.2", "zone.js": "^0.10.3" diff --git a/src/app/_dialogs/file-meta-dialog/file-meta-dialog.component.html b/src/app/_dialogs/file-meta-dialog/file-meta-dialog.component.html index cbb6443..37dd73c 100644 --- a/src/app/_dialogs/file-meta-dialog/file-meta-dialog.component.html +++ b/src/app/_dialogs/file-meta-dialog/file-meta-dialog.component.html @@ -5,7 +5,6 @@ [fields]="fields" [options]="options" [form]="form" - (modelChange)="onModelChange($event)" > diff --git a/src/app/_dialogs/file-meta-dialog/file-meta-dialog.component.ts b/src/app/_dialogs/file-meta-dialog/file-meta-dialog.component.ts index 9f4b98d..bdec03b 100644 --- a/src/app/_dialogs/file-meta-dialog/file-meta-dialog.component.ts +++ b/src/app/_dialogs/file-meta-dialog/file-meta-dialog.component.ts @@ -1,8 +1,8 @@ -import {Component, Inject} from '@angular/core'; +import {AfterViewInit, Component, Inject, ViewChild} from '@angular/core'; import {FormControl, FormGroup} from '@angular/forms'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {FormlyFieldConfig, FormlyFormOptions} from '@ngx-formly/core'; -import {cleanUpFilename, FileType} from 'sartography-workflow-lib'; +import {cleanUpFilename, FileType, FileFieldComponent, ApiService} from 'sartography-workflow-lib'; import {FileMetaDialogData} from '../../_interfaces/dialog-data'; @Component({ @@ -18,7 +18,7 @@ export class FileMetaDialogComponent { constructor( public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: FileMetaDialogData + @Inject(MAT_DIALOG_DATA) public data: FileMetaDialogData, ) { const fileTypeOptions = Object.entries(FileType).map(ft => { return { @@ -51,18 +51,6 @@ export class FileMetaDialogComponent { options: fileTypeOptions, }, }, - { - key: 'file', - type: 'file', - defaultValue: this.data.file, - templateOptions: { - label: 'File', - required: true, - }, - modelOptions: { - updateOn: 'change' - }, - } ]; } @@ -74,17 +62,4 @@ export class FileMetaDialogComponent { this.model.fileName = cleanUpFilename(this.model.fileName, this.model.fileType); this.dialogRef.close(this.model); } - - onModelChange(model: any) { - console.log('model', model); - if (model.file && typeof model.file === 'object' && model.file instanceof Blob) { - // Upload file - const fileReader = new (window as any).FileReader(); - fileReader.onload = (event: ProgressEvent) => { - const stringContent = (event.target as FileReader).result.toString(); - console.log(stringContent); - }; - fileReader.readAsText(model.file); - } - } } diff --git a/src/app/_dialogs/open-file-dialog/open-file-dialog.component.html b/src/app/_dialogs/open-file-dialog/open-file-dialog.component.html index 21b3e35..27a3bb4 100644 --- a/src/app/_dialogs/open-file-dialog/open-file-dialog.component.html +++ b/src/app/_dialogs/open-file-dialog/open-file-dialog.component.html @@ -1,7 +1,7 @@
Where is your file? - Upload a local BPMN/DMN file - Open a BPMN/DMN file from URL + Upload a local {{fileTypesString()}} file + Open a {{fileTypesString()}} file from URL
@@ -9,11 +9,11 @@
@@ -22,18 +22,18 @@ folder_open   - - - + +
link   - + - +
diff --git a/src/app/_dialogs/open-file-dialog/open-file-dialog.component.spec.ts b/src/app/_dialogs/open-file-dialog/open-file-dialog.component.spec.ts index ff26d2f..f90f557 100644 --- a/src/app/_dialogs/open-file-dialog/open-file-dialog.component.spec.ts +++ b/src/app/_dialogs/open-file-dialog/open-file-dialog.component.spec.ts @@ -71,18 +71,16 @@ describe('OpenFileDialogComponent', () => { it('should save data on submit', () => { const closeSpy = spyOn(component.dialogRef, 'close').and.stub(); - const expectedData: OpenFileDialogData = { file: mockFileMeta0.file }; - - component.diagramFile = expectedData.file; + component.data.file = mockFileMeta0.file; component.onSubmit(); - expect(closeSpy).toHaveBeenCalledWith(expectedData); + expect(closeSpy).toHaveBeenCalledWith(component.data); }); it('should not change data on cancel', () => { const closeSpy = spyOn(component.dialogRef, 'close').and.stub(); const expectedData: OpenFileDialogData = { file: mockFileMeta0.file }; - component.diagramFile = expectedData.file; + component.data.file = expectedData.file; component.onNoClick(); expect(closeSpy).toHaveBeenCalledWith(); }); @@ -97,23 +95,23 @@ describe('OpenFileDialogComponent', () => { const req = httpMock.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush(''); - expect(component.diagramFile).toBeTruthy(); - expect(component.diagramFile.name).toEqual(expectedName); + expect(component.data.file).toBeTruthy(); + expect(component.data.file.name).toEqual(expectedName); expect(onSubmitSpy).toHaveBeenCalled(); }); it('should get the diagram file name', () => { - component.diagramFile = undefined; + component.data.file = undefined; expect(component.getFileName()).toEqual('Click to select a file'); - component.diagramFile = mockFileMeta0.file; + component.data.file = mockFileMeta0.file; expect(component.getFileName()).toEqual(mockFileMeta0.file.name); }); it('should get a file from the file input field event', () => { const event = {target: {files: [mockFileMeta0.file]}}; (component as any).onFileSelected(event); - expect(component.diagramFile).toEqual(mockFileMeta0.file); + expect(component.data.file).toEqual(mockFileMeta0.file); }); it('should determine if a string is a valid URL', () => { diff --git a/src/app/_dialogs/open-file-dialog/open-file-dialog.component.ts b/src/app/_dialogs/open-file-dialog/open-file-dialog.component.ts index 363d4f6..cdfae9a 100644 --- a/src/app/_dialogs/open-file-dialog/open-file-dialog.component.ts +++ b/src/app/_dialogs/open-file-dialog/open-file-dialog.component.ts @@ -1,6 +1,7 @@ -import {Component} from '@angular/core'; -import {MatDialogRef} from '@angular/material/dialog'; -import {ApiService, cleanUpFilename, getDiagramTypeFromXml} from 'sartography-workflow-lib'; +import {Component, Inject} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {ApiService, cleanUpFilename, FileType, getDiagramTypeFromXml} from 'sartography-workflow-lib'; +import {OpenFileDialogData} from '../../_interfaces/dialog-data'; @Component({ selector: 'app-open-file-dialog', @@ -9,13 +10,20 @@ import {ApiService, cleanUpFilename, getDiagramTypeFromXml} from 'sartography-wo }) export class OpenFileDialogComponent { mode: string; - diagramFile: File; url: string; + fileTypes: FileType[]; + fileMetaId: number; constructor( public dialogRef: MatDialogRef, - private api: ApiService + private api: ApiService, + @Inject(MAT_DIALOG_DATA) public data: OpenFileDialogData, ) { + if (this.data) { + this.mode = this.data.mode || undefined; + this.fileTypes = this.data.fileTypes; + this.fileMetaId = this.data.fileMetaId; + } } onNoClick() { @@ -23,15 +31,15 @@ export class OpenFileDialogComponent { } onSubmit() { - this.dialogRef.close({file: this.diagramFile}); + this.dialogRef.close(this.data); } onFileSelected($event: Event) { - this.diagramFile = ($event.target as HTMLFormElement).files[0]; + this.data.file = ($event.target as HTMLFormElement).files[0]; } getFileName() { - return this.diagramFile ? this.diagramFile.name : 'Click to select a file'; + return this.data.file ? this.data.file.name : 'Click to select a file'; } onSubmitUrl() { @@ -41,7 +49,7 @@ export class OpenFileDialogComponent { const fileName = fileArray[fileArray.length - 1]; const fileType = getDiagramTypeFromXml(s); const name = cleanUpFilename(fileName, fileType); - this.diagramFile = new File([s], name, {type: 'text/xml'}); + this.data.file = new File([s], name, {type: 'text/xml'}); this.onSubmit(); }); } @@ -52,4 +60,24 @@ export class OpenFileDialogComponent { const re = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.​\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[​6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1​,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00​a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u​00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/i; return re.test(this.url); } + + fileTypesString(): string { + if (this.fileTypes && (this.fileTypes.length > 0)) { + return this.fileTypes.map(t => t.toString().toUpperCase()).join('/'); + } + } + + fileExtensions(): string { + if (this.fileTypes && (this.fileTypes.length > 0)) { + return this.fileTypes.map(t => '.' + t.toString()).join(','); + } + } + + cancel() { + if (this.data.mode) { + this.onNoClick(); + } else { + this.mode = undefined; + } + } } diff --git a/src/app/_interfaces/dialog-data.ts b/src/app/_interfaces/dialog-data.ts index bd65806..138135d 100644 --- a/src/app/_interfaces/dialog-data.ts +++ b/src/app/_interfaces/dialog-data.ts @@ -12,7 +12,10 @@ export interface NewFileDialogData { } export interface OpenFileDialogData { + fileMetaId?: number; file: File; + mode?: string; + fileTypes?: FileType[]; } export interface WorkflowSpecDialogData { diff --git a/src/app/file-list/file-list.component.html b/src/app/file-list/file-list.component.html index bf4a515..463d567 100644 --- a/src/app/file-list/file-list.component.html +++ b/src/app/file-list/file-list.component.html @@ -18,6 +18,9 @@

{{fm.name}}

{{fm.last_updated | date}}

+ diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index 42d7ad9..2e002d7 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -179,30 +179,56 @@ describe('FileListComponent', () => { expect(editFileMetaSpy).toHaveBeenCalledWith(null); }); - it('should change route and then open file metadata dialog', () => { - const routerNavigateSpy = spyOn((component as any).router, 'navigate') - .and.returnValue({finally: finallyCallback => finallyCallback()}); + it('should open file metadata dialog', () => { const _openFileDialogSpy = spyOn((component as any), '_openFileDialog').and.stub(); component.workflowSpec = mockWorkflowSpec0; - const mockDocMeta = createClone()(mockFileMeta0); + const mockDocMeta: FileMeta = createClone()(mockFileMeta0); mockDocMeta.type = FileType.DOCX; component.editFileMeta(mockDocMeta); - expect(routerNavigateSpy).toHaveBeenCalled(); const fakeBlob = new Blob(['I am a fake blob. A real blob says "blorp blorp blorp."']); + const expectedFile = new File([fakeBlob], mockDocMeta.name, {type: mockDocMeta.content_type}); const fReq = httpMock.expectOne(`apiRoot/file/${mockDocMeta.id}/data`); expect(fReq.request.method).toEqual('GET'); fReq.flush(fakeBlob); - expect(_openFileDialogSpy).toHaveBeenCalledWith(mockDocMeta, fakeBlob); + expect(_openFileDialogSpy).toHaveBeenCalledWith(mockDocMeta, expectedFile); - routerNavigateSpy.calls.reset(); _openFileDialogSpy.calls.reset(); component.editFileMeta(null); - expect(routerNavigateSpy).toHaveBeenCalled(); expect(_openFileDialogSpy).toHaveBeenCalledWith(); }); + it('should upload new file from file dialog', () => { + const openDialogSpy = spyOn(component.dialog, 'open') + .and.returnValue({afterClosed: () => of({file: mockFileMeta0.file})} 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: mockFileMeta0.file})} as any); + const _loadFileMetasSpy = spyOn((component as any), '_loadFileMetas').and.stub(); + component.workflowSpec = mockWorkflowSpec0; + + (component as any)._openFileDialog(mockFileMeta0, mockFileMeta0.file); + 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(); diff --git a/src/app/file-list/file-list.component.ts b/src/app/file-list/file-list.component.ts index 85deb0e..fbc70e1 100644 --- a/src/app/file-list/file-list.component.ts +++ b/src/app/file-list/file-list.component.ts @@ -1,11 +1,20 @@ import {Component, Input, OnInit} from '@angular/core'; import {MatDialog} from '@angular/material/dialog'; import {MatSnackBar} from '@angular/material/snack-bar'; -import {ActivatedRoute, Params, Router} from '@angular/router'; -import {ApiService, FileMeta, FileParams, FileType, isNumberDefined, WorkflowSpec} from 'sartography-workflow-lib'; +import {ActivatedRoute, Router} from '@angular/router'; +import { + ApiService, + FileMeta, + FileParams, + FileType, + getFileType, + isNumberDefined, + WorkflowSpec +} from 'sartography-workflow-lib'; import {DeleteFileDialogComponent} from '../_dialogs/delete-file-dialog/delete-file-dialog.component'; -import {FileMetaDialogComponent} from '../_dialogs/file-meta-dialog/file-meta-dialog.component'; -import {DeleteFileDialogData, FileMetaDialogData} from '../_interfaces/dialog-data'; +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-file-list', @@ -40,63 +49,14 @@ export class FileListComponent implements OnInit { } 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, { - data: { - fileName: fm ? fm.name : undefined, - fileType: fm ? fm.type : undefined, - file: fileData ? fileData : undefined, - } - }); - - dialogRef.afterClosed().subscribe((data: FileMetaDialogData) => { - if (data && data.fileName && data.fileType) { - const newFileMeta: FileMeta = { - id: data.id, - content_type: data.fileType, - name: data.fileName, - type: data.fileType, - file: data.file, - }; - - if (isNumberDefined(data.id)) { - // Update existing file - this.api.updateFileMeta(newFileMeta).subscribe(() => { - this.api.updateFileData(newFileMeta).subscribe(() => { - this._loadFileMetas(); - }); - }); - } else { - // Add new file - const fileParams: FileParams = { - workflow_spec_id: this.workflowSpec.id, - }; - - this.api.addFileMeta(fileParams, newFileMeta).subscribe(dbFm => { - this._loadFileMetas(); - }); - } - } - }); - + if (fm && isNumberDefined(fm.id)) { + this.api.getFileData(fm.id).subscribe(fileData => { + const file = new File([fileData], fm.name, {type: fm.content_type}); + this._openFileDialog(fm, file); + }); + } else { + this._openFileDialog(); + } } confirmDelete(fm: FileMeta) { @@ -131,6 +91,49 @@ export class FileListComponent implements OnInit { } } + private _openFileDialog(fm?: FileMeta, file?: File) { + console.log('fm.id', fm && fm.id); + const dialogData: OpenFileDialogData = { + fileMetaId: fm ? fm.id : undefined, + file: file, + mode: 'local', + fileTypes: [FileType.DOCX], + }; + const dialogRef = this.dialog.open(OpenFileDialogComponent, {data: dialogData}); + + 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), + file: data.file, + workflow_spec_id: this.workflowSpec.id, + }; + + console.log('data.fileMetaId', data.fileMetaId); + console.log('isNumberDefined(data.fileMetaId)', isNumberDefined(data.fileMetaId)); + if (isNumberDefined(data.fileMetaId)) { + // Update existing file + this.api.updateFileData(newFileMeta).subscribe(() => { + this._loadFileMetas(); + }); + } else { + // Add new file + const fileParams: FileParams = { + workflow_spec_id: this.workflowSpec.id, + }; + + this.api.addFileMeta(fileParams, newFileMeta).subscribe(dbFm => { + this._loadFileMetas(); + }); + } + } + }); + + } + private _deleteFile(fileMeta: FileMeta) { this.api.deleteFileMeta(fileMeta.id).subscribe(() => { this._loadFileMetas(); @@ -151,4 +154,10 @@ export class FileListComponent implements OnInit { }); } + downloadFile(fm: FileMeta) { + this.api.getFileData(fm.id).subscribe(fileBlob => { + const blob = new Blob([fileBlob], {type: fm.content_type}); + fileSaver.saveAs(blob, fm.name); + }); + } } diff --git a/src/app/modeler/modeler.component.ts b/src/app/modeler/modeler.component.ts index c5fd99c..3b83d62 100644 --- a/src/app/modeler/modeler.component.ts +++ b/src/app/modeler/modeler.component.ts @@ -162,7 +162,11 @@ export class ModelerComponent implements AfterViewInit { } openFileDialog() { - const dialogRef = this.dialog.open(OpenFileDialogComponent, {}); + const dialogData: OpenFileDialogData = { + file: undefined, + fileTypes: [FileType.DMN, FileType.BPMN], + }; + const dialogRef = this.dialog.open(OpenFileDialogComponent, {data: dialogData}); dialogRef.afterClosed().subscribe((data: OpenFileDialogData) => { if (data && data.file) {