Commit 404da41a authored by Florent POITTEVIN's avatar Florent POITTEVIN
Browse files

feat: 1513 allow to upload and crop image for deposit

parent ea8531a3
......@@ -56,6 +56,18 @@ export namespace Enums {
}
export namespace DataFile {
export type DataCategoryEnum = "Primary"
| "Secondary"
| "Package"
| "Software"
| "Internal";
export const DataCategoryEnum = {
Primary: "Primary" as DataCategoryEnum,
Secondary: "Secondary" as DataCategoryEnum,
Package: "Package" as DataCategoryEnum,
Software: "Software" as DataCategoryEnum,
Internal: "Internal" as DataCategoryEnum,
};
export type DataTypeEnum = "Observational"
| "Experimental"
| "Simulation"
......@@ -70,7 +82,9 @@ export namespace Enums {
| "CustomMetadata"
| "Code"
| "Binaries"
| "VirtualMachine";
| "VirtualMachine"
| "DatasetThumbnail"
| "DatafileThumbnail";
export const DataTypeEnum = {
Observational: "Observational" as DataTypeEnum,
Experimental: "Experimental" as DataTypeEnum,
......@@ -87,6 +101,8 @@ export namespace Enums {
Code: "Code" as DataTypeEnum,
Binaries: "Binaries" as DataTypeEnum,
VirtualMachine: "VirtualMachine" as DataTypeEnum,
DatasetThumbnail: "DatasetThumbnail" as DataTypeEnum,
DatafileThumbnail: "DatafileThumbnail" as DataTypeEnum,
};
export type StatusEnum =
......
......@@ -35,6 +35,7 @@ import {
DepositState,
DepositStateModel,
} from "@deposit/stores/deposit.state";
import {Enums} from "@enums";
import {Deposit} from "@models";
import {
Actions,
......@@ -43,7 +44,6 @@ import {
Store,
} from "@ngxs/store";
import {SharedAbstractDetailEditRoutable} from "@shared/components/routables/shared-abstract-detail-edit/shared-abstract-detail-edit.routable";
import {DataCategoryEnum} from "@shared/enums/data-category.enum";
import {LocalStateEnum} from "@shared/enums/local-state.enum";
import {AppRoutesEnum} from "@shared/enums/routes.enum";
import {SecurityService} from "@shared/services/security.service";
......@@ -76,8 +76,8 @@ export class DepositUploadContainer extends SharedAbstractDetailEditRoutable<Dep
readonly KEY_PARAM_NAME: keyof Deposit & string = undefined;
get dataCategoryEnum(): typeof DataCategoryEnum {
return DataCategoryEnum;
get dataCategoryEnum(): typeof Enums.DataFile.DataCategoryEnum {
return Enums.DataFile.DataCategoryEnum;
}
get modeDepositTabEnum(): typeof ModeDepositTabEnum {
......@@ -136,7 +136,7 @@ export class DepositUploadContainer extends SharedAbstractDetailEditRoutable<Dep
this._store.dispatch(new DepositDataFileAction.Download(this._resId, $event));
}
openModalUpload(dataCategoryEnum?: DataCategoryEnum): void {
openModalUpload(dataCategoryEnum?: Enums.DataFile.DataCategoryEnum): void {
const dialogRef = this._dialog.open(DepositFileUploadDialog, {
width: "500px",
data: {
......
......@@ -10,7 +10,6 @@ import {
} from "@angular/forms";
import {MatDialogRef} from "@angular/material/dialog";
import {SharedAbstractContainer} from "@app/shared/components/containers/shared-abstract/shared-abstract.container";
import {DataCategoryEnum} from "@app/shared/enums/data-category.enum";
import {DataCategoryHelper} from "@app/shared/helpers/data-category.helper";
import {BaseFormDefinition} from "@app/shared/models/base-form-definition.model";
import {DepositFileUploadArchiveDialog} from "@deposit/components/dialogs/deposit-file-upload-archive/deposit-file-upload-archive.dialog";
......@@ -59,7 +58,7 @@ export abstract class AbstractDepositFileUploadDialog extends SharedAbstractCont
ngOnInit(): void {
super.ngOnInit();
this.form = this._fb.group({
[this.formDefinition.dataCategory]: [DataCategoryEnum.Primary, [Validators.required]],
[this.formDefinition.dataCategory]: [Enums.DataFile.DataCategoryEnum.Primary, [Validators.required]],
[this.formDefinition.dataType]: [Enums.DataFile.DataTypeEnum.Reference, [Validators.required]],
[this.formDefinition.metadataType]: [""],
});
......@@ -93,7 +92,7 @@ export abstract class AbstractDepositFileUploadDialog extends SharedAbstractCont
abstract onSubmit(): void;
getDataCategory(): DataCategoryEnum {
getDataCategory(): Enums.DataFile.DataCategoryEnum {
return this.form.get(this.formDefinition.dataCategory).value;
}
......
......@@ -27,7 +27,6 @@ import {
Store,
} from "@ngxs/store";
import {SharedAbstractContainer} from "@shared/components/containers/shared-abstract/shared-abstract.container";
import {DataCategoryEnum} from "@shared/enums/data-category.enum";
import {DataCategoryHelper} from "@shared/helpers/data-category.helper";
import {BaseFormDefinition} from "@shared/models/base-form-definition.model";
import {Observable} from "rxjs";
......@@ -140,7 +139,7 @@ export class DepositFileChangeDataCategoryDialog extends SharedAbstractContainer
return DataCategoryHelper;
}
getDataCategory(): DataCategoryEnum {
getDataCategory(): Enums.DataFile.DataCategoryEnum {
return this.form.get(this.formDefinition.dataCategory).value;
}
......
......@@ -15,7 +15,7 @@ import {
AbstractDepositFileUploadDialog,
AbstractDepositFileUploadFormComponentFormDefinition,
} from "@deposit/components/dialogs/abstract-deposit-file-upload/abstract-deposit-file-upload.dialog";
import {DataCategoryEnum} from "@shared/enums/data-category.enum";
import {Enums} from "@enums";
import {
isNotNullNorUndefined,
isNullOrUndefined,
......@@ -50,7 +50,7 @@ export class DepositFileUploadDialog extends AbstractDepositFileUploadDialog imp
if (isNotNullNorUndefined(this.data.dataCategoryEnum)) {
const dataCategoryFormControl = this.form.get(this.formDefinition.dataCategory);
dataCategoryFormControl.setValue(this.data.dataCategoryEnum);
if (this.data.dataCategoryEnum === DataCategoryEnum.Secondary) {
if (this.data.dataCategoryEnum === Enums.DataFile.DataCategoryEnum.Secondary) {
this.isFormValid = false;
}
}
......@@ -134,5 +134,5 @@ class FileWrapper {
}
export interface DepositFileUploadDialogData {
dataCategoryEnum?: DataCategoryEnum;
dataCategoryEnum?: Enums.DataFile.DataCategoryEnum;
}
......@@ -21,6 +21,11 @@
</div>
</div>
<dlcm-shared-image-upload-wrapper-container [resourceLogoNameSpace]="depositActionNameSpace"
[resourceLogoState]="depositState"
[isEditable]="editAvailable"
></dlcm-shared-image-upload-wrapper-container>
<dlcm-shared-panel-expandable class="section"
[titleToTranslate]="labelTranslateEnum.mandatory | translate"
[isOpen]="true"
......
......@@ -22,6 +22,8 @@ import {
DepositOrderAuthorDialogWrapper,
} from "@deposit/components/dialogs/deposit-order-author/deposit-order-author.dialog";
import {DepositPersonDialog} from "@deposit/components/dialogs/deposit-person/deposit-person.dialog";
import {depositActionNameSpace} from "@deposit/stores/deposit.action";
import {DepositState} from "@deposit/stores/deposit.state";
import {Enums} from "@enums";
import {environment} from "@environments/environment";
import {
......@@ -45,6 +47,7 @@ import {
SharedPersonState,
SharedPersonStateModel,
} from "@shared/stores/person/shared-person.state";
import {ResourceLogoNameSpace} from "@shared/stores/resource-logo/resource-logo-namespace.model";
import {
distinctUntilChanged,
tap,
......@@ -84,6 +87,9 @@ export class DepositFormPresentational extends SharedAbstractFormPresentational<
addFieldsForm: AdditionalFieldsForm;
depositActionNameSpace: ResourceLogoNameSpace = depositActionNameSpace;
depositState: typeof DepositState = DepositState;
get accessEnum(): typeof Enums.Deposit.AccessEnum {
return Enums.Deposit.AccessEnum;
}
......
......@@ -423,6 +423,7 @@ export class DepositDetailEditRoutable extends SharedAbstractDetailEditRoutable<
this._store.dispatch(new DepositPersonAction.GetAll(id));
this._store.dispatch(new DepositDataFileAction.Refresh(id));
this._store.dispatch(new DepositCollectionAction.Refresh(id));
this._store.dispatch(new DepositAction.CheckPhoto(id));
}
submit(): void {
......
import {DataCategoryEnum} from "@app/shared/enums/data-category.enum";
import {Enums} from "@enums";
export interface FileUploadWrapper {
subDirectory: string;
dataCategory: DataCategoryEnum;
dataCategory: Enums.DataFile.DataCategoryEnum;
dataType: Enums.DataFile.DataTypeEnum;
metadataType: string;
file: File;
......
......@@ -6,12 +6,13 @@ import {Enums} from "@enums";
import {Deposit} from "@models";
import {FileListModel} from "@shared/models/business/file-list.model";
import {Result} from "@shared/models/business/result.model";
import {ResourceLogoNameSpace} from "@shared/stores/resource-logo/resource-logo-namespace.model";
import {ResourceLogoAction} from "@shared/stores/resource-logo/resource-logo.action";
import {
BaseAction,
BaseSubAction,
ErrorDto,
ResourceAction,
ResourceNameSpace,
TypeDefaultAction,
} from "solidify-frontend";
......@@ -146,6 +147,49 @@ export namespace DepositAction {
export class Clean extends ResourceAction.Clean {
}
@TypeDefaultAction(state)
export class GetPhoto extends ResourceLogoAction.GetPhoto {
}
@TypeDefaultAction(state)
export class GetPhotoSuccess extends ResourceLogoAction.GetPhotoSuccess {
}
@TypeDefaultAction(state)
export class GetPhotoFail extends ResourceLogoAction.GetPhotoFail {
}
@TypeDefaultAction(state)
export class UploadPhoto extends ResourceLogoAction.UploadPhoto {
}
@TypeDefaultAction(state)
export class UploadPhotoSuccess extends ResourceLogoAction.UploadPhotoSuccess {
}
@TypeDefaultAction(state)
export class UploadPhotoFail extends ResourceLogoAction.UploadPhotoFail {
}
@TypeDefaultAction(state)
export class DeletePhoto extends ResourceLogoAction.DeletePhoto {
}
@TypeDefaultAction(state)
export class DeletePhotoSuccess extends ResourceLogoAction.DeletePhotoSuccess {
}
@TypeDefaultAction(state)
export class DeletePhotoFail extends ResourceLogoAction.DeletePhotoFail {
}
export class CheckPhoto {
static readonly type: string = `[${state}] Check Photo`;
constructor(public depositId: string) {
}
}
export class Download {
static readonly type: string = `[${state}] Download`;
......@@ -244,24 +288,27 @@ export namespace DepositAction {
}
}
export class UploadDataFile {
export class UploadDataFile extends BaseAction {
static readonly type: string = `[${state}] Upload Data File`;
constructor(public parentId: string, public fileUploadWrapper: FileUploadWrapper, public isArchive: boolean = false) {
super();
}
}
export class UploadDataFileSuccess {
export class UploadDataFileSuccess extends BaseSubAction<UploadDataFile> {
static readonly type: string = `[${state}] Upload Data File Success`;
constructor(public parentId: string, public uploadFileStatus: UploadFileStatus, public depositDataFile: DepositDataFile) {
constructor(public parentAction: UploadDataFile, public parentId: string, public uploadFileStatus: UploadFileStatus, public depositDataFile: DepositDataFile) {
super(parentAction);
}
}
export class UploadDataFileFail {
export class UploadDataFileFail extends BaseSubAction<UploadDataFile> {
static readonly type: string = `[${state}] Upload Data File Fail`;
constructor(public uploadFileStatus: UploadFileStatus, public errorDto: ErrorDto | undefined) {
constructor(public parentAction: UploadDataFile, public uploadFileStatus: UploadFileStatus, public errorDto: ErrorDto | undefined) {
super(parentAction);
}
}
......@@ -395,4 +442,4 @@ export namespace DepositAction {
}
}
export const depositActionNameSpace: ResourceNameSpace = DepositAction;
export const depositActionNameSpace: ResourceLogoNameSpace = DepositAction;
import {
HttpClient,
HttpErrorResponse,
HttpEventType,
HttpHeaders,
} from "@angular/common/http";
import {Injectable} from "@angular/core";
import {FileUploadStatusEnum} from "@app/features/deposit/enums/file-upload-status.enum";
......@@ -73,17 +75,30 @@ import {
StateContext,
Store,
} from "@ngxs/store";
import {ApiResourceNameEnum} from "@shared/enums/api-resource-name.enum";
import {LabelTranslateEnum} from "@shared/enums/label-translate.enum";
import {PollingHelper} from "@shared/helpers/polling.helper";
import {DataFile} from "@shared/models/business/data-file.model";
import {FileListModel} from "@shared/models/business/file-list.model";
import {Result} from "@shared/models/business/result.model";
import {DownloadService} from "@shared/services/download.service";
import {SecurityService} from "@shared/services/security.service";
import {SharedLanguageAction} from "@shared/stores/language/shared-language.action";
import {ResourceLogoStateModel} from "@shared/stores/resource-logo/resource-logo-state.model";
import {
defaultResourceLogoStateInitValue,
ResourceLogoState,
ResourceLogoStateModeEnum,
} from "@shared/stores/resource-logo/resource-logo.state";
import {defaultStatusHistoryInitValue} from "@shared/stores/status-history/status-history.state";
import {Observable} from "rxjs";
import {
Observable,
of,
} from "rxjs";
import {
catchError,
map,
switchMap,
take,
tap,
} from "rxjs/operators";
......@@ -108,15 +123,13 @@ import {
OverrideDefaultAction,
QueryParameters,
QueryParametersUtil,
ResourceState,
ResourceStateModel,
SolidifyStateError,
StoreUtil,
StringUtil,
urlSeparator,
} from "solidify-frontend";
export interface DepositStateModel extends ResourceStateModel<Deposit> {
export interface DepositStateModel extends ResourceLogoStateModel<Deposit> {
deposit_authorizedOrganizationalUnit: DepositAuthorizedOrganizationalUnitStateModel;
deposit_dataFile: DepositDataFileStateModel;
deposit_person: DepositPersonStateModel;
......@@ -139,13 +152,14 @@ export interface DepositStateModel extends ResourceStateModel<Deposit> {
canCreate: boolean;
formPresentational: DepositFormPresentational | undefined;
depositModeTabEnum: ModeDepositTabEnum;
dataFileLogo: DataFile | undefined;
}
@Injectable()
@State<DepositStateModel>({
name: LocalStateEnum.deposit,
defaults: {
...defaultResourceStateInitValue(),
...defaultResourceLogoStateInitValue(),
deposit_authorizedOrganizationalUnit: defaultDepositAuthorizedOrgUnitValue(),
queryParameters: new QueryParameters(environment.defaultPageSize),
deposit_dataFile: defaultDepositDataFileValue(),
......@@ -169,6 +183,8 @@ export interface DepositStateModel extends ResourceStateModel<Deposit> {
canCreate: false,
formPresentational: undefined,
depositModeTabEnum: ModeDepositTabEnum.UNDEFINED,
dataFileLogo: undefined,
isLoadingPhoto: false,
},
children: [
DepositDataFileState,
......@@ -180,8 +196,7 @@ export interface DepositStateModel extends ResourceStateModel<Deposit> {
DepositAuthorizedOrganizationalUnitState,
],
})
export class DepositState extends ResourceState<DepositStateModel, Deposit> {
private readonly _FILE_KEY: string = "file";
export class DepositState extends ResourceLogoState<DepositStateModel, Deposit> {
private readonly _CATEGORY_KEY: string = "category";
private readonly _TYPE_KEY: string = "type";
private readonly _METADATA_TYPE_KEY: string = "metadataType";
......@@ -195,7 +210,8 @@ export class DepositState extends ResourceState<DepositStateModel, Deposit> {
protected notificationService: NotificationService,
protected actions$: Actions,
protected readonly _securityService: SecurityService,
private downloadService: DownloadService) {
private downloadService: DownloadService,
protected _httpClient: HttpClient) {
super(apiService, store, notificationService, actions$, {
nameSpace: depositActionNameSpace,
routeRedirectUrlAfterSuccessDeleteAction: RoutesEnum.deposit,
......@@ -203,7 +219,7 @@ export class DepositState extends ResourceState<DepositStateModel, Deposit> {
notificationResourceDeleteSuccessTextToTranslate: MARK_AS_TRANSLATABLE("deposit.notification.resource.delete"),
notificationResourceUpdateSuccessTextToTranslate: MARK_AS_TRANSLATABLE("deposit.notification.resource.update"),
keepCurrentStateAfterUpdate: true,
});
}, _httpClient, ResourceLogoStateModeEnum.dataset);
}
protected get _urlResource(): string {
......@@ -548,9 +564,9 @@ export class DepositState extends ResourceState<DepositStateModel, Deposit> {
return;
case HttpEventType.Response:
if (event.status === HttpStatus.OK) {
ctx.dispatch(new DepositAction.UploadDataFileSuccess(action.parentId, uploadFileStatus, event.body));
ctx.dispatch(new DepositAction.UploadDataFileSuccess(action, action.parentId, uploadFileStatus, event.body));
} else {
ctx.dispatch(new DepositAction.UploadDataFileFail(uploadFileStatus, undefined));
ctx.dispatch(new DepositAction.UploadDataFileFail(action, uploadFileStatus, undefined));
}
return;
default:
......@@ -565,7 +581,7 @@ export class DepositState extends ResourceState<DepositStateModel, Deposit> {
if (error instanceof HttpErrorResponse) {
errorDto = error.error;
}
ctx.dispatch(new DepositAction.UploadDataFileFail(uploadFileStatus, errorDto));
ctx.dispatch(new DepositAction.UploadDataFileFail(action, uploadFileStatus, errorDto));
throw new SolidifyStateError(this, error);
}),
);
......@@ -876,4 +892,178 @@ export class DepositState extends ResourceState<DepositStateModel, Deposit> {
depositModeTabEnum: mode,
});
}
@OverrideDefaultAction()
@Action(DepositAction.UploadPhoto)
uploadPhoto(ctx: StateContext<DepositStateModel>, action: DepositAction.UploadPhoto): Observable<any> {
if (isNotNullNorUndefined(ctx.getState().dataFileLogo)) {
ctx.dispatch(new DepositAction.DeletePhoto());
return this.actions$.pipe(
ofActionCompleted(DepositAction.DeletePhotoSuccess),
switchMap(result => {
if (isTrue(result.result.successful)) {
return this._uploadPhotoReady(ctx, action);
}
return of(false);
}));
} else {
return this._uploadPhotoReady(ctx, action);
}
}
private _uploadPhotoReady(ctx: StateContext<DepositStateModel>, action: DepositAction.UploadPhoto): Observable<any> {
const parentId = ctx.getState().current?.resId;
const actionUploadDataFile = new DepositAction.UploadDataFile(parentId, {
dataCategory: Enums.DataFile.DataCategoryEnum.Internal,
dataType: Enums.DataFile.DataTypeEnum.DatasetThumbnail,
metadataType: undefined,
subDirectory: undefined,
file: action.file,
});
this.actions$.pipe(
ofActionCompleted(DepositAction.UploadDataFileSuccess),
take(1),
tap((result) => {
const actionUpdateSuccess = result.action as DepositAction.UploadDataFileSuccess;
if (isTrue(result.result.successful) && actionUpdateSuccess.parentAction === actionUploadDataFile) {
const actionUploadDatafileSuccess = actionUpdateSuccess.depositDataFile[0];
ctx.patchState({
dataFileLogo: actionUploadDatafileSuccess,
});
ctx.dispatch(new DepositAction.UploadPhotoSuccess(action));
}
}),
).subscribe();
this.actions$.pipe(
ofActionCompleted(DepositAction.UploadDataFileFail),
take(1),
tap((result) => {
const actionUpdateSuccess = result.action as DepositAction.UploadDataFileFail;
if (isTrue(result.result.successful) && actionUpdateSuccess.parentAction === actionUploadDataFile) {
ctx.dispatch(new DepositAction.UploadPhotoFail(action));
}
}),
).subscribe();
return ctx.dispatch(actionUploadDataFile);
}
@Action(DepositAction.CheckPhoto)
checkPhoto(ctx: StateContext<DepositStateModel>, action: DepositAction.CheckPhoto): Observable<any> {
const depositId = action.depositId;
const queryParameters = new QueryParameters(1);
const searchItems = QueryParametersUtil.getSearchItems(queryParameters);
MappingObjectUtil.set(searchItems, "dataCategory", Enums.DataFile.DataCategoryEnum.Internal);
MappingObjectUtil.set(searchItems, "dataType", Enums.DataFile.DataTypeEnum.DatasetThumbnail);
return this.apiService.get<DataFile>(this._urlResource + urlSeparator + depositId + urlSeparator + ApiResourceNameEnum.DATAFILE, queryParameters)
.pipe(
tap((collection: CollectionTyped<DataFile>) => {
if (collection._data.length === 1) {
ctx.patchState({
dataFileLogo: collection._data[0],
});
ctx.dispatch(new DepositAction.GetPhoto());
}
}),
catchError(error => {
throw new SolidifyStateError(this, error);
}),
);
}
@OverrideDefaultAction()
@Action(DepositAction.GetPhoto)
getPhoto(ctx: StateContext<DepositStateModel>, action: DepositAction.GetPhoto): Observable<any> {
ctx.patchState({
isLoadingPhoto: true,
});
const depositId = ctx.getState().current.resId;
const datafileId = ctx.getState().dataFileLogo.resId;
let isReady = false;
return PollingHelper.startPollingObs({
intervalRefreshInSecond: 1,
continueUntil: () => !isReady,
actionToDo: () => {
const url = `${this._urlResource}/${depositId}/${ApiResourceNameEnum.DATAFILE}/${datafileId}`;
this.apiService.getByIdInPath(url)
.pipe(
tap((result: DataFile) => {
if (result.status === Enums.DataFile.StatusEnum.READY) {
isReady = true;
this._getPhotoReady(depositId, datafileId, ctx, action);
}
if (result.status === Enums.DataFile.StatusEnum.IN_ERROR) {
isReady = true;
ctx.dispatch(new DepositAction.GetPhotoFail(action));
}
if (result.status === Enums.DataFile.StatusEnum.CLEANED) {
isReady = true;
ctx.dispatch(new DepositAction.GetPhotoFail(action));
}
}),
catchError(e => {
isReady = true;
ctx.dispatch(new DepositAction.GetPhotoFail(action));
throw new SolidifyStateError(this, e);
}),
).subscribe();
},
});
}
private _getPhotoReady(depositId: string, datafileId: string, ctx: StateContext<DepositStateModel>, action: DepositAction.GetPhoto): void {
const url = `${this._urlResource}/${depositId}/${ApiResourceNameEnum.DATAFILE}/${datafileId}/${ApiActionEnum.DL}`;
let headers = new HttpHeaders();
headers = headers.set("Content-Disposition", "attachment; filename=logo");
this._httpClient.get(url, {
headers,
responseType: "blob",
}).pipe(
tap((blobContent: Blob) => {
ctx.dispatch(new DepositAction.GetPhotoSuccess(action, blobContent));
}),
catchError(e => {
ctx.dispatch(new DepositAction.GetPhotoFail(action));
throw new SolidifyStateError(this, e);
}),