Skip to content
Snippets Groups Projects
Commit d9b2e719 authored by Florent Poittevin's avatar Florent Poittevin
Browse files

590 Upload datafile WIP

parent 7d42ad09
No related branches found
No related tags found
1 merge request!22Fpo/590 upload datafile
Showing
with 637 additions and 13 deletions
......@@ -14,6 +14,7 @@ $height: 40px;
display: flex;
justify-content: center;
align-items: center;
z-index: $index-footer;
}
.hidden-footer {
......
<mat-tree *ngIf="dataSource"
[dataSource]="dataSource"
[treeControl]="treeControl"
class="file-tree">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
<li class="mat-tree-node">
<!-- use a disabled button to provide padding for tree leaf -->
<button mat-icon-button disabled></button>
{{node.name}} - {{getStatus(node)}}
</li>
</mat-tree-node>
<!-- This is the tree node template for expandable nodes -->
<mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
<li>
<div class="mat-tree-node">
<button mat-icon-button matTreeNodeToggle
[attr.aria-label]="'toggle ' + node.name">
<fa-icon [icon]="treeControl.isExpanded(node) ? 'chevron-down' : 'chevron-right'"></fa-icon>
</button>
{{node.name}}
</div>
<ul [class.file-tree-invisible]="!treeControl.isExpanded(node)">
<ng-container matTreeNodeOutlet></ng-container>
</ul>
</li>
</mat-nested-tree-node>
</mat-tree>
@import "abstracts/variables";
$line-item-size: 30px;
.file-tree-invisible {
display: none;
}
.mat-tree-node {
min-height: $line-item-size;
.mat-icon-button {
height: $line-item-size;
width: $line-item-size;
line-height: $line-item-size;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FileTreeComponent } from './file-tree.component';
describe('FileTreeComponent', () => {
let component: FileTreeComponent;
let fixture: ComponentFixture<FileTreeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FileTreeComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FileTreeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {NestedTreeControl} from "@angular/cdk/tree";
import {ChangeDetectionStrategy, Component, Input} from "@angular/core";
import {MatTreeNestedDataSource} from "@angular/material";
import {DepositDataFileModel} from "@app/deposit/models/deposit-data-file.model";
import {StringUtil} from "@app/shared/utils/string.util";
interface FileNode {
name: string;
file?: DepositDataFileModel;
children?: FileNode[];
}
@Component({
selector: "dlcm-file-tree",
templateUrl: "./file-tree.component.html",
styleUrls: ["./file-tree.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FileTreeComponent {
private _listDepositDataFile: DepositDataFileModel[];
@Input()
readonly: boolean;
@Input()
set listDepositDataFile(val: DepositDataFileModel[]) {
this._listDepositDataFile = val;
this.adaptListDepositDataFileToTree();
this.applyTreeToDataSource();
}
get listDepositDataFile(): DepositDataFileModel[] {
return this._listDepositDataFile;
}
fileTree: FileNode[];
treeControl = new NestedTreeControl<FileNode>(node => node.children);
dataSource = new MatTreeNestedDataSource<FileNode>();
private readonly SEPARATOR = "/";
private readonly ROOT = this.SEPARATOR;
constructor() {
}
adaptListDepositDataFileToTree(): void {
this.extractDataFile();
const copy = [...this.listDepositDataFile];
copy.sort((a, b) => {
if (a.relativeLocation < b.relativeLocation) {
return -1;
}
if (a.relativeLocation > b.relativeLocation) {
return 1;
}
return 0;
});
copy.forEach(dataFile => {
this.treatmentDataFile(dataFile);
});
}
private extractDataFile(): void {
this.fileTree = [];
}
private treatmentDataFile(dataFile): void {
const isIntoRootFolder = dataFile.relativeLocation === this.ROOT;
if (isIntoRootFolder) {
this.addFileToRootDataFile(dataFile);
} else {
const relativeLocation = dataFile.relativeLocation;
const pathParts = relativeLocation.split(this.SEPARATOR).filter(part => part !== StringUtil.stringEmpty);
this.recursiveAddFolder(this.fileTree, dataFile, pathParts);
}
}
private recursiveAddFolder(levelParent: FileNode[], dataFile: DepositDataFileModel, pathParts: string[]): void {
if (pathParts.length === 0) {
return;
}
const firstFolderName = pathParts.splice(0, 1)[0];
let parentFileNode = levelParent.find(f => f.name === firstFolderName);
parentFileNode = this.createLevelIfNotExist(parentFileNode, firstFolderName, levelParent);
this.initChildrenAttributIfUndefined(parentFileNode);
if (pathParts.length === 0) {
parentFileNode.children.push(this.getFileNodeFormDataFile(dataFile));
} else {
this.recursiveAddFolder(parentFileNode.children, dataFile, pathParts);
}
}
private createLevelIfNotExist(parentFileNode: FileNode, firstFolderName: string, levelParent: FileNode[]): FileNode {
if (parentFileNode === undefined) {
parentFileNode = {
name: firstFolderName,
} as FileNode;
levelParent.push(parentFileNode);
}
return parentFileNode;
}
private initChildrenAttributIfUndefined(parentFileNode) {
if (parentFileNode.children === undefined) {
parentFileNode.children = [];
}
}
private addFileToRootDataFile(dataFile): void {
this.fileTree.push(this.getFileNodeFormDataFile(dataFile));
}
private getFileNodeFormDataFile(dataFile): FileNode {
return {
name: dataFile.fileName,
file: dataFile,
} as FileNode;
}
applyTreeToDataSource(): void {
this.dataSource.data = this.fileTree;
}
hasChild = (_: number, node: FileNode) => !!node.children && node.children.length > 0;
getStatus(node: FileNode) {
return node.file.status;
}
}
<div *ngIf="uploadStatus">
<div *ngIf="uploadStatus.status === 'error'">
{{uploadStatus.message}}
</div>
<div *ngIf="uploadStatus.status === 'progress'">
<div role="progressbar"
[style.width.%]="uploadStatus.progress"
aria-valuenow="25"
aria-valuemin="0"
aria-valuemax="100">
{{uploadStatus.progress}}%
</div>
</div>
<div>
{{uploadStatus.result}}
</div>
</div>
<form [formGroup]="form"
(ngSubmit)="onSubmit()">
<button mat-button
color="accent"
type="button"
(click)="fileInput.click()">
{{'deposit.file.search' | translate}}
<input #fileInput
class="hide"
type="file"
[name]="file"
(change)="onFileChange($event)"/>
</button>
<span class="file-name">{{form.get(file).value.name}}</span>
<button [disabled]="!form.valid"
mat-button
color="primary"
type="submit">
{{'deposit.file.uploadButton' | translate}}
</button>
</form>
.hide {
display: none;
}
.file-name {
padding: 0 20px;
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FileUploadComponent } from './file-upload.component';
describe('FileUploadComponent', () => {
let component: FileUploadComponent;
let fixture: ComponentFixture<FileUploadComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FileUploadComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FileUploadComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output} from "@angular/core";
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {UploadFileStatusModel} from "@app/deposit/models/upload-file-status.model";
import {BaseDirective} from "@app/shared/directives/base.directive";
@Component({
selector: "dlcm-file-upload",
templateUrl: "./file-upload.component.html",
styleUrls: ["./file-upload.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FileUploadComponent extends BaseDirective implements OnInit {
@Input()
uploadStatus: UploadFileStatusModel;
@Output()
fileUploadEvent: EventEmitter<File>;
form: FormGroup;
readonly file = "file";
constructor(protected fb: FormBuilder) {
super();
this.fileUploadEvent = new EventEmitter<File>();
}
ngOnInit() {
this.form = this.fb.group({
[this.file]: ["", Validators.required],
});
}
onFileChange(event) {
if (event.target.files.length > 0) {
const file: File = event.target.files[0];
this.form.get(this.file).setValue(file);
}
}
onSubmit() {
this.fileUploadEvent.emit(this.form.get(this.file).value as File);
}
}
<dlcm-file-upload *ngIf="readonly === false"
[uploadStatus]="uploadStatus$ | async"
(fileUploadEvent)="uploadFile($event)"
></dlcm-file-upload>
<dlcm-file-tree [listDepositDataFile]="listDataFile$ | async"
[readonly]="readonly"
></dlcm-file-tree>
import {ChangeDetectionStrategy, Component, Input} from "@angular/core";
import {DepositAction} from "@app/deposit/deposit.action";
import {DepositStateModel} from "@app/deposit/deposit.state";
import {DepositDataFileModel} from "@app/deposit/models/deposit-data-file.model";
import {UploadFileStatusModel} from "@app/deposit/models/upload-file-status.model";
import {BaseDirective} from "@app/shared/directives/base.directive";
import {StateEnum} from "@app/shared/enums/state.enum";
import {Select, Store} from "@ngxs/store";
import {Observable} from "rxjs";
@Component({
selector: "dlcm-file-deposit",
templateUrl: "./file.container.html",
styleUrls: ["./file.container.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FileContainer extends BaseDirective {
@Select((state) => (state[StateEnum.deposit] as DepositStateModel).listDataFile) listDataFile$: Observable<DepositDataFileModel[]>;
@Select((state) => (state[StateEnum.deposit] as DepositStateModel).uploadStatus) uploadStatus$: Observable<UploadFileStatusModel>;
@Input()
parentResId: string;
@Input()
readonly: boolean;
constructor(protected store: Store) {
super();
this.readonly = false;
}
private uploadFile(file: File) {
this.store.dispatch(new DepositAction.SendData(this.parentResId, File));
}
}
import {DepositDataFileModel} from "@app/deposit/models/deposit-data-file.model";
import {DepositsModel} from "@app/generated-api";
import {CrudAction} from "@app/shared/crud.action";
import {ApiResourceNameEnum} from "@app/shared/enums/api-resource-name.enum";
import {StateEnum} from "@app/shared/enums/state.enum";
import {CollectionTypedModel} from "@app/shared/models/collection-typed.model";
import {QueryParametersModel} from "@app/shared/models/query-parameters.model";
......@@ -33,7 +35,9 @@ export namespace DepositAction {
}
export class ChangeQueryParameters extends CrudAction.ChangeQueryParameters {
static readonly type = StringUtil.format(CrudAction.ChangeQueryParameters.abstractType, state);
static get type() {
return StringUtil.format(this.abstractType, state);
}
constructor(public queryParameters: QueryParametersModel) {
super(queryParameters);
......@@ -159,4 +163,110 @@ export namespace DepositAction {
super();
}
}
export class SendData {
static readonly type = `[${state}] Upload file`;
// StringUtil.format(CrudAction.GetAllSubResource.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor(public parentId, public file) {
// super(parentId, queryParameters);
}
}
export class GetAllSubResourceData extends CrudAction.GetAllSubResource {
static readonly type = StringUtil.format(CrudAction.GetAllSubResource.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor(public parentId, public queryParameters?: QueryParametersModel) {
super(parentId, queryParameters);
}
}
export class GetAllSubResourceDataSuccess extends CrudAction.GetAllSubResourceSuccess<DepositDataFileModel> {
static readonly type = StringUtil.format(CrudAction.GetAllSubResourceSuccess.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor(public list: CollectionTypedModel<DepositDataFileModel>) {
super(list);
}
}
export class GetAllSubResourceDataFail extends CrudAction.GetAllSubResourceFail {
static readonly type = StringUtil.format(CrudAction.GetAllSubResourceFail.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor() {
super();
}
}
export class UpdateSubResourceData extends CrudAction.UpdateSubResource {
static readonly type = StringUtil.format(CrudAction.UpdateSubResource.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor(public parentId, public oldResId: string[], newResId: string[]) {
super(parentId, oldResId, newResId);
}
}
export class UpdateSubResourceDataSuccess extends CrudAction.UpdateSubResourceSuccess {
static readonly type = StringUtil.format(CrudAction.UpdateSubResourceSuccess.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor(public parentId) {
super(parentId);
}
}
export class UpdateSubResourceDataFail extends CrudAction.UpdateSubResourceFail {
static readonly type = StringUtil.format(CrudAction.UpdateSubResourceFail.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor(public parentId) {
super(parentId);
}
}
export class CreateSubResourceData<DepositDataFile> extends CrudAction.CreateCompoSubResource<DepositDataFile> {
static readonly type = StringUtil.format(CrudAction.CreateCompoSubResource.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor(public parentId, public model: DepositDataFile) {
super(parentId, model);
}
}
export class CreateSubResourceDataSuccess extends CrudAction.CreateSubResourceSuccess {
static readonly type = StringUtil.format(CrudAction.CreateSubResourceSuccess.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor() {
super();
}
}
export class CreateSubResourceDataFail extends CrudAction.CreateSubResourceFail {
static readonly type = StringUtil.format(CrudAction.CreateSubResourceFail.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor() {
super();
}
}
export class DeleteSubResourceData extends CrudAction.DeleteSubResource {
static readonly type = StringUtil.format(CrudAction.DeleteSubResource.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor(public parentId, public listResId: string[]) {
super(parentId, listResId);
}
}
export class DeleteSubResourceDataSuccess extends CrudAction.DeleteSubResourceSuccess {
static readonly type = StringUtil.format(CrudAction.DeleteSubResourceSuccess.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor() {
super();
}
}
export class DeleteSubResourceDataFail extends CrudAction.DeleteSubResourceFail {
static readonly type = StringUtil.format(CrudAction.DeleteSubResourceFail.abstractType, state, ApiResourceNameEnum.DATAFILE);
constructor() {
super();
}
}
}
import {NgModule} from "@angular/core";
import {FormComponent} from "@app/deposit/components/form/form.component";
import {FileContainer} from "@app/deposit/containers/file/file.container";
import {DepositRoutingModule} from "@app/deposit/deposit-routing.module";
import {DepositState} from "@app/deposit/deposit.state";
import {DeleteDialog} from "@app/deposit/dialogs/delete/delete.dialog";
......@@ -11,6 +12,8 @@ import {TranslateModule} from "@ngx-translate/core";
import {NgxsModule} from "@ngxs/store";
import {CreateView} from "./views/create/create.view";
import {DetailView} from "./views/detail/detail.view";
import { FileTreeComponent } from './components/file-tree/file-tree.component';
import { FileUploadComponent } from './components/file-upload/file-upload.component';
const views = [
ListView,
......@@ -18,7 +21,9 @@ const views = [
DetailView,
EditView,
];
const containers = [];
const containers = [
FileContainer,
];
const dialogs = [
DeleteDialog,
];
......@@ -32,6 +37,8 @@ const components = [
...containers,
...dialogs,
...components,
FileTreeComponent,
FileUploadComponent,
],
imports: [
SharedModule,
......@@ -46,7 +53,6 @@ const components = [
],
exports: [
...views,
...containers,
],
providers: [
DepositService,
......
import {HttpEventType} from "@angular/common/http";
import {DepositAction} from "@app/deposit/deposit.action";
import {DepositDataFileModel} from "@app/deposit/models/deposit-data-file.model";
import {UploadFileStatusModel} from "@app/deposit/models/upload-file-status.model";
import {DepositsModel} from "@app/generated-api";
import {CrudState, CrudStateModel} from "@app/shared/crud.state";
import {ApiActionEnum} from "@app/shared/enums/api-action.enum";
import {ApiResourceNameEnum} from "@app/shared/enums/api-resource-name.enum";
import {PreIngestResourceApiEnum, ResourceApiEnum} from "@app/shared/enums/api.enum";
import {StateEnum} from "@app/shared/enums/state.enum";
......@@ -14,9 +18,13 @@ import {SubmissionPolicyAction} from "@app/shared/submission-policy.action";
import {NotificationService} from "@app/shared/services/notification.service";
import {Action, State, StateContext} from "@ngxs/store";
import {Observable} from "rxjs";
import {map} from "rxjs/operators";
export interface DepositStateModel extends CrudStateModel<DepositsModel> {
resourceIsLoading: boolean;
listDataFile: DepositDataFileModel[];
isLoadingDataFile: boolean;
uploadStatus: UploadFileStatusModel;
}
@State<DepositStateModel>({
......@@ -28,6 +36,9 @@ export interface DepositStateModel extends CrudStateModel<DepositsModel> {
current: null,
queryParameters: new QueryParametersModel(),
resourceIsLoading: false,
listDataFile: [],
isLoadingDataFile: false,
uploadStatus: null,
},
})
export class DepositState extends CrudState<DepositsModel> {
......@@ -142,4 +153,72 @@ export class DepositState extends CrudState<DepositsModel> {
deleteFail(ctx: StateContext<DepositStateModel>, action: DepositAction.DeleteFail): void {
super.deleteFail(ctx, action);
}
@Action(DepositAction.GetAllSubResourceData, {cancelUncompleted: true})
getAllDataFile(ctx: StateContext<DepositStateModel>, action: DepositAction.GetAllSubResourceData): Observable<CollectionTypedModel<DepositDataFileModel>> {
return super.getAllSubResource<DepositDataFileModel>(ctx, action, ApiResourceNameEnum.DATAFILE, action.parentId);
}
@Action(DepositAction.GetAllSubResourceDataSuccess)
getAllDataFileSuccess(ctx: StateContext<DepositStateModel>, action: DepositAction.GetAllSubResourceDataSuccess): void {
ctx.patchState({
listDataFile: action.list._data,
isLoadingDataFile: false,
});
}
@Action(DepositAction.GetAllSubResourceDataFail)
getAllDataFileFail(ctx: StateContext<DepositStateModel>, action: DepositAction.GetAllSubResourceDataFail): void {
ctx.patchState({
isLoadingDataFile: false,
});
}
// @Action(DepositAction.CreateSubResourceData, {cancelUncompleted: true})
// createDataFile(ctx: StateContext<DepositStateModel>, action: DepositAction.CreateSubResourceData): Observable<string[]> {
// return super.createSubResource(ctx, action, ApiResourceNameEnum.DATAFILE, action.parentId);
// }
@Action(DepositAction.DeleteSubResourceData, {cancelUncompleted: true})
deleteDataFile(ctx: StateContext<DepositStateModel>, action: DepositAction.DeleteSubResourceData): Observable<string[]> {
return super.deleteSubResource(ctx, action, ApiResourceNameEnum.DATAFILE, action.parentId);
}
@Action(DepositAction.SendData)
sendData(ctx: StateContext<DepositStateModel>, action: DepositAction.SendData): Observable<any> {
const formData = new FormData();
formData.append("file", action.file);
return this.apiService.upload(`${PreIngestResourceApiEnum.deposits}/${action.parentId}/${ApiActionEnum.UL}`, action.file)
.pipe(
map((event) => {
switch (event.type) {
case HttpEventType.UploadProgress:
const progress = Math.round(100 * event.loaded / event.total);
ctx.patchState({
uploadStatus: {
progress,
status: "progress",
},
});
return;
case HttpEventType.Response:
return event.body;
default:
return `Unhandled event: ${event.type}`;
}
}),
);
// .pipe(
// tap(() => {
// // ctx.dispatch(CrudActionUtil.createSuccess(this.state));
// console.error("OK");
// }),
// catchError(error => {
// // ctx.dispatch(CrudActionUtil.createFail(this.state));
// console.error("NOT OK");
//
// throw error;
// }),
// );
}
}
export interface ChecksumModel {
checksumAlgo?: string;
checksumType?: string;
checksumOrigin?: string;
checksum?: string;
creationTime?: string;
}
import {ChecksumModel} from "@app/deposit/models/checksum.model";
import {FileFormatModel} from "@app/deposit/models/file-format.model";
import {ChangeInfoModel, DepositsModel} from "@app/generated-api";
import StatusModelEnum = DepositsModel.StatusModelEnum;
export interface DepositDataFileModel {
resId?: string;
creation?: ChangeInfoModel;
lastUpdate?: ChangeInfoModel;
packageId?: string;
sourceData?: string;
relativeLocation?: string;
finalData?: string;
fileSize?: number;
status?: StatusModelEnum;
statusMessage?: string;
dataCategory?: string; // Enum get with ApiActionEnum.LIST_DATA_CATEGORY
dataType?: string; // Enum get with ApiActionEnum.LIST_DATA_TYPE
complianceLevel?: string;
fileFormat?: FileFormatModel;
virusCheck?: any;
fileName?: string;
initialPath?: string;
smartSize?: string;
dataFile?: any; // To check
checksums?: ChecksumModel[];
available?: boolean;
}
export interface FileFormatModel {
contentType?: string;
format?: string;
version?: string;
puid?: string;
md5?: string;
details?: string;
}
export interface UploadFileStatusModel {
status?: string;
progress?: number;
message?: string;
result?: any;
}
<div class="button-toolbar">
<button mat-flat-button color="primary" (click)="edit()">{{KEY_EDIT_BUTTON | translate}}</button>
<button mat-flat-button color="secondary" (click)="delete()">{{KEY_DELETE_BUTTON | translate}}</button>
<button mat-flat-button color="accent" (click)="delete()">{{KEY_DELETE_BUTTON | translate}}</button>
</div>
<div class="wrapper">
<div class="spinner-wrapper" *ngIf="isLoading$ | async">
<mat-spinner></mat-spinner>
</div>
<dlcm-form-deposit *ngIf="(current$| async) != null"
[model]="current$| async"
[languages]="languages$ | async"
[licenses]="licenses$ | async"
[submissionPolicies]="submissionPolicies$ | async"
[preservationPolicies]="preservationPolicies$ | async"
[organizationalUnits]="organizationalUnits$ | async"
[readonly]="true"
></dlcm-form-deposit>
<mat-tab-group mat-align-tabs="center"
animationDuration="0ms"
>
<mat-tab [label]="'deposit.tab.details' | translate">
<dlcm-form-deposit *ngIf="(current$| async) != null"
[model]="current$| async"
[languages]="languages$ | async"
[licenses]="licenses$ | async"
[submissionPolicies]="submissionPolicies$ | async"
[preservationPolicies]="preservationPolicies$ | async"
[organizationalUnits]="organizationalUnits$ | async"
[readonly]="true"
></dlcm-form-deposit>
</mat-tab>
<mat-tab [label]="'deposit.tab.datafiles' | translate">
<dlcm-file-deposit [parentResId]="getResId()"
[readonly]="true"
></dlcm-file-deposit>
</mat-tab>
</mat-tab-group>
</div>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment