diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index bebec482c2..f4b74807cf 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -24,7 +24,7 @@ import { SaveSubmissionFormSuccessAction, SaveSubmissionSectionFormAction, SaveSubmissionSectionFormErrorAction, - SaveSubmissionSectionFormSuccessAction, + SaveSubmissionSectionFormSuccessAction, SubmissionObjectAction, SubmissionObjectActionTypes, UpdateSectionDataAction } from './submission-objects.actions'; @@ -48,6 +48,9 @@ import { SubmissionJsonPatchOperationsService } from '../../core/submission/subm @Injectable() export class SubmissionObjectEffects { + /** + * Dispatch a [InitSectionAction] for every submission sections and dispatch a [CompleteInitSubmissionFormAction] + */ @Effect() loadForm$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.INIT_SUBMISSION_FORM), map((action: InitSubmissionFormAction) => { @@ -83,6 +86,9 @@ export class SubmissionObjectEffects { )); })); + /** + * Dispatch a [InitSubmissionFormAction] + */ @Effect() resetForm$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.RESET_SUBMISSION_FORM), map((action: ResetSubmissionFormAction) => @@ -95,6 +101,9 @@ export class SubmissionObjectEffects { null ))); + /** + * Dispatch a [SaveSubmissionFormSuccessAction] or a [SaveSubmissionFormErrorAction] on error + */ @Effect() saveSubmission$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM), switchMap((action: SaveSubmissionFormAction) => { @@ -106,6 +115,9 @@ export class SubmissionObjectEffects { catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); })); + /** + * Dispatch a [SaveForLaterSubmissionFormSuccessAction] or a [SaveSubmissionFormErrorAction] on error + */ @Effect() saveForLaterSubmission$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM), switchMap((action: SaveForLaterSubmissionFormAction) => { @@ -117,6 +129,9 @@ export class SubmissionObjectEffects { catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); })); + /** + * Call parseSaveResponse and dispatch actions + */ @Effect() saveSubmissionSuccess$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS), withLatestFrom(this.store$), @@ -125,6 +140,9 @@ export class SubmissionObjectEffects { }), mergeMap((actions) => observableFrom(actions))); + /** + * Dispatch a [SaveSubmissionSectionFormSuccessAction] or a [SaveSubmissionSectionFormErrorAction] on error + */ @Effect() saveSection$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM), switchMap((action: SaveSubmissionSectionFormAction) => { @@ -137,11 +155,17 @@ export class SubmissionObjectEffects { catchError(() => observableOf(new SaveSubmissionSectionFormErrorAction(action.payload.submissionId)))); })); + /** + * Show a notification on error + */ @Effect({dispatch: false}) saveError$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_ERROR), withLatestFrom(this.store$), tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.save_error_notice')))); + /** + * Call parseSaveResponse and dispatch actions or dispatch [SaveSubmissionFormErrorAction] on error + */ @Effect() saveAndDeposit$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.SAVE_AND_DEPOSIT_SUBMISSION), withLatestFrom(this.store$), @@ -161,6 +185,9 @@ export class SubmissionObjectEffects { catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); })); + /** + * Dispatch a [DepositSubmissionSuccessAction] or a [DepositSubmissionErrorAction] on error + */ @Effect() depositSubmission$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION), withLatestFrom(this.store$), @@ -170,20 +197,32 @@ export class SubmissionObjectEffects { catchError(() => observableOf(new DepositSubmissionErrorAction(action.payload.submissionId)))); })); + /** + * Show a notification on success and redirect to MyDSpace page + */ @Effect({dispatch: false}) saveForLaterSubmissionSuccess$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS), tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.save_success_notice'))), tap(() => this.submissionService.redirectToMyDSpace())); + /** + * Show a notification on success and redirect to MyDSpace page + */ @Effect({dispatch: false}) depositSubmissionSuccess$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_SUCCESS), tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.deposit_success_notice'))), tap(() => this.submissionService.redirectToMyDSpace())); + /** + * Show a notification on error + */ @Effect({dispatch: false}) depositSubmissionError$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_ERROR), tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.deposit_error_notice')))); + /** + * Dispatch a [DiscardSubmissionSuccessAction] or a [DiscardSubmissionErrorAction] on error + */ @Effect() discardSubmission$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION), switchMap((action: DepositSubmissionAction) => { @@ -192,11 +231,17 @@ export class SubmissionObjectEffects { catchError(() => observableOf(new DiscardSubmissionErrorAction(action.payload.submissionId)))); })); + /** + * Show a notification on success and redirect to MyDSpace page + */ @Effect({dispatch: false}) discardSubmissionSuccess$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_SUCCESS), tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.discard_success_notice'))), tap(() => this.submissionService.redirectToMyDSpace())); + /** + * Show a notification on error + */ @Effect({dispatch: false}) discardSubmissionError$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_ERROR), tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.discard_error_notice')))); @@ -210,6 +255,12 @@ export class SubmissionObjectEffects { private translate: TranslateService) { } + /** + * Check if the submission object retrieved from REST haven't section errors + * + * @param response + * The submission object retrieved from REST + */ protected canDeposit(response: SubmissionObject[]) { let canDeposit = true; @@ -225,7 +276,26 @@ export class SubmissionObjectEffects { return canDeposit; } - protected parseSaveResponse(currentState: SubmissionObjectEntry, response: SubmissionObject[], submissionId: string, notify: boolean = true) { + /** + * Parse the submission object retrieved from REST haven't section errors and return actions to dispatch + * + * @param currentState + * The current SubmissionObjectEntry + * @param response + * The submission object retrieved from REST + * @param submissionId + * The submission id + * @param notify + * A boolean that indicate if show notification or not + * @return SubmissionObjectAction[] + * List of SubmissionObjectAction to dispatch + */ + protected parseSaveResponse( + currentState: SubmissionObjectEntry, + response: SubmissionObject[], + submissionId: string, + notify: boolean = true): SubmissionObjectAction[] { + const mappedActions = []; if (isNotEmpty(response)) { diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index c02b2fd0f1..68ac7d56b9 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -38,42 +38,138 @@ import { WorkspaceitemSectionDataType } from '../../core/submission/models/works import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; import { SectionsType } from '../sections/sections-type'; +/** + * An interface to represent section visibility + */ export interface SectionVisibility { main: any; other: any; } +/** + * An interface to represent section object state + */ export interface SubmissionSectionObject { + /** + * The section header + */ header: string; + + /** + * The section configuration url + */ config: string; + + /** + * A boolean representing if this section is mandatory + */ mandatory: boolean; + + /** + * The section type + */ sectionType: SectionsType; + + /** + * The section visibility + */ visibility: SectionVisibility; + + /** + * A boolean representing if this section is collapsed + */ collapsed: boolean, + + /** + * A boolean representing if this section is enabled + */ enabled: boolean; + + /** + * The section data object + */ data: WorkspaceitemSectionDataType; + + /** + * The list of the section errors + */ errors: SubmissionSectionError[]; + + /** + * A boolean representing if this section is loading + */ isLoading: boolean; + + /** + * A boolean representing if this section is valid + */ isValid: boolean; } +/** + * An interface to represent section error + */ export interface SubmissionSectionError { + /** + * A string representing error path + */ path: string; + + /** + * The error message + */ message: string; } +/** + * An interface to represent SubmissionSectionObject entry + */ export interface SubmissionSectionEntry { [sectionId: string]: SubmissionSectionObject; } +/** + * An interface to represent submission object state + */ export interface SubmissionObjectEntry { + /** + * The collection this submission belonging to + */ collection?: string, + + /** + * The configuration name tha define this submission + */ definition?: string, + + /** + * The submission self url + */ selfUrl?: string; + + /** + * The submission active section + */ activeSection?: string; + + /** + * The list of submission's sections + */ sections?: SubmissionSectionEntry; + + /** + * A boolean representing if this submission is loading + */ isLoading?: boolean; + + /** + * A boolean representing if a submission save operation is pending + */ savePending?: boolean; + + /** + * A boolean representing if a submission deposit operation is pending + */ depositPending?: boolean; } diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index 00c2b5a690..aed83143a5 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -35,9 +35,20 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { SubmissionService } from '../submission.service'; import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; +/** + * A service that provides methods used in submission process. + */ @Injectable() export class SectionsService { + /** + * Initialize service variables + * @param {NotificationsService} notificationsService + * @param {ScrollToService} scrollToService + * @param {SubmissionService} submissionService + * @param {Store} store + * @param {TranslateService} translate + */ constructor(private notificationsService: NotificationsService, private scrollToService: ScrollToService, private submissionService: SubmissionService, @@ -45,17 +56,35 @@ export class SectionsService { private translate: TranslateService) { } + /** + * Compare the list of the current section errors with the previous one, + * and dispatch actions to add/remove to/from the section state + * + * @param submissionId + * The submission id + * @param sectionId + * The workspaceitem self url + * @param formId + * The [SubmissionDefinitionsModel] that define submission configuration + * @param currentErrors + * The [SubmissionSectionError] that define submission sections init data + * @param prevErrors + * The [SubmissionSectionError] that define submission sections init errors + */ public checkSectionErrors( submissionId: string, sectionId: string, formId: string, currentErrors: SubmissionSectionError[], prevErrors: SubmissionSectionError[] = []) { + // Remove previous error list if the current is empty if (isEmpty(currentErrors)) { this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId)); this.store.dispatch(new FormClearErrorsAction(formId)); - } else if (!isEqual(currentErrors, prevErrors)) { + } else if (!isEqual(currentErrors, prevErrors)) { // compare previous error list with the current one const dispatchedErrors = []; + + // Itereate over the current error list currentErrors.forEach((error: SubmissionSectionError) => { const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path); @@ -63,7 +92,7 @@ export class SectionsService { if (path.fieldId) { const fieldId = path.fieldId.replace(/\./g, '_'); - // Dispatch action to the form state; + // Dispatch action to add form error to the state; const formAddErrorAction = new FormAddError(formId, fieldId, path.fieldIndex, error.message); this.store.dispatch(formAddErrorAction); dispatchedErrors.push(fieldId); @@ -71,6 +100,7 @@ export class SectionsService { }); }); + // Itereate over the previous error list prevErrors.forEach((error: SubmissionSectionError) => { const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path); @@ -79,6 +109,7 @@ export class SectionsService { const fieldId = path.fieldId.replace(/\./g, '_'); if (!dispatchedErrors.includes(fieldId)) { + // Dispatch action to remove form error from the state; const formRemoveErrorAction = new FormRemoveErrorAction(formId, fieldId, path.fieldIndex); this.store.dispatch(formRemoveErrorAction); } @@ -88,20 +119,58 @@ export class SectionsService { } } + /** + * Dispatch a new [RemoveSectionErrorsAction] + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + */ public dispatchRemoveSectionErrors(submissionId, sectionId) { this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId)); } + /** + * Return the data object for the specified section + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @return Observable + * observable of [WorkspaceitemSectionDataType] + */ public getSectionData(submissionId: string, sectionId: string): Observable { return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe( distinctUntilChanged()); } + /** + * Return the error list object data for the specified section + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @return Observable + * observable of array of [SubmissionSectionError] + */ public getSectionErrors(submissionId: string, sectionId: string): Observable { return this.store.select(submissionSectionErrorsFromIdSelector(submissionId, sectionId)).pipe( distinctUntilChanged()); } + /** + * Return the state object for the specified section + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @return Observable + * observable of [SubmissionSectionObject] + */ public getSectionState(submissionId: string, sectionId: string): Observable { return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( filter((sectionObj: SubmissionSectionObject) => hasValue(sectionObj)), @@ -109,6 +178,16 @@ export class SectionsService { distinctUntilChanged()); } + /** + * Check if a given section is valid + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @return Observable + * Emits true whenever a given section should be valid + */ public isSectionValid(submissionId: string, sectionId: string): Observable { return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( filter((sectionObj) => hasValue(sectionObj)), @@ -116,12 +195,32 @@ export class SectionsService { distinctUntilChanged()); } + /** + * Check if a given section is active + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @return Observable + * Emits true whenever a given section should be active + */ public isSectionActive(submissionId: string, sectionId: string): Observable { return this.submissionService.getActiveSectionId(submissionId).pipe( map((activeSectionId: string) => sectionId === activeSectionId), distinctUntilChanged()); } + /** + * Check if a given section is enabled + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @return Observable + * Emits true whenever a given section should be enabled + */ public isSectionEnabled(submissionId: string, sectionId: string): Observable { return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( filter((sectionObj) => hasValue(sectionObj)), @@ -129,6 +228,18 @@ export class SectionsService { distinctUntilChanged()); } + /** + * Check if a given section is a read only section + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @param submissionScope + * The submission scope + * @return Observable + * Emits true whenever a given section should be read only + */ public isSectionReadOnly(submissionId: string, sectionId: string, submissionScope: SubmissionScopeType): Observable { return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( filter((sectionObj) => hasValue(sectionObj)), @@ -140,6 +251,16 @@ export class SectionsService { distinctUntilChanged()); } + /** + * Check if a given section is a read only available + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @return Observable + * Emits true whenever a given section should be available + */ public isSectionAvailable(submissionId: string, sectionId: string): Observable { return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe( filter((submissionState: SubmissionObjectEntry) => isNotUndefined(submissionState)), @@ -149,8 +270,15 @@ export class SectionsService { distinctUntilChanged()); } - public addSection(submissionId: string, - sectionId: string) { + /** + * Dispatch a new [EnableSectionAction] to add a new section and move page target to it + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + */ + public addSection(submissionId: string, sectionId: string) { this.store.dispatch(new EnableSectionAction(submissionId, sectionId)); const config: ScrollToConfigOptions = { target: sectionId, @@ -160,11 +288,31 @@ export class SectionsService { this.scrollToService.scrollTo(config); } + /** + * Dispatch a new [DisableSectionAction] to remove section + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + */ public removeSection(submissionId: string, sectionId: string) { this.store.dispatch(new DisableSectionAction(submissionId, sectionId)) } - public updateSectionData(submissionId: string, sectionId: string, data, errors = []) { + /** + * Dispatch a new [UpdateSectionDataAction] to update section state with new data and errors + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @param data + * The section data + * @param errors + * The list of section errors + */ + public updateSectionData(submissionId: string, sectionId: string, data: WorkspaceitemSectionDataType, errors: SubmissionSectionError[] = []) { if (isNotEmpty(data)) { const isAvailable$ = this.isSectionAvailable(submissionId, sectionId); const isEnabled$ = this.isSectionEnabled(submissionId, sectionId); @@ -182,10 +330,30 @@ export class SectionsService { } } + /** + * Dispatch a new [InertSectionErrorsAction] to update section state with new error + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @param error + * The section error + */ public setSectionError(submissionId: string, sectionId: string, error: SubmissionSectionError) { this.store.dispatch(new InertSectionErrorsAction(submissionId, sectionId, error)); } + /** + * Dispatch a new [SectionStatusChangeAction] to update section state with new status + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @param status + * The section status + */ public setSectionStatus(submissionId: string, sectionId: string, status: boolean) { this.store.dispatch(new SectionStatusChangeAction(submissionId, sectionId, status)); } diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 7dde19a306..0522f279dd 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -777,7 +777,7 @@ describe('SubmissionService test suite', () => { service.notifyNewSection(submissionId, sectionId); flush(); - expect((service as any).notificationsService.info).toHaveBeenCalledWith(null, 'test', null, true); + expect((service as any).notificationsService.info).toHaveBeenCalledWith(null, 'submission.sections.general.metadata-extracted-new-section', null, true); })); }); @@ -890,7 +890,7 @@ describe('SubmissionService test suite', () => { const duration = config.submission.autosave.timer * (1000 * 60); service.startAutoSave('826'); - const sub = (service as any).timerObs.subscribe(); + const sub = (service as any).timer$.subscribe(); tick(duration / 2); expect((service as any).store.dispatch).not.toHaveBeenCalled(); diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index dab6a9d9d5..d086f6f3d4 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -44,12 +44,32 @@ import { RemoteData } from '../core/data/remote-data'; import { ErrorResponse } from '../core/cache/response.models'; import { RemoteDataError } from '../core/data/remote-data-error'; +/** + * A service that provides methods used in submission process. + */ @Injectable() export class SubmissionService { + /** + * Subscription + */ protected autoSaveSub: Subscription; - protected timerObs: Observable; + /** + * Observable used as timer + */ + protected timer$: Observable; + + /** + * Initialize service variables + * @param {GlobalConfig} EnvConfig + * @param {NotificationsService} notificationsService + * @param {SubmissionRestService} restService + * @param {Router} restSerroutervice + * @param {RouteService} routeService + * @param {Store} store + * @param {TranslateService} translate + */ constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected notificationsService: NotificationsService, protected restService: SubmissionRestService, @@ -59,28 +79,74 @@ export class SubmissionService { protected translate: TranslateService) { } + /** + * Dispatch a new [ChangeSubmissionCollectionAction] + * + * @param submissionId + * The submission id + * @param collectionId + * The collection id + */ changeSubmissionCollection(submissionId, collectionId) { this.store.dispatch(new ChangeSubmissionCollectionAction(submissionId, collectionId)); } + /** + * Perform a REST call to create a new workspaceitem and return response + * + * @return Observable + * observable of SubmissionObject + */ createSubmission(): Observable { return this.restService.postToEndpoint('workspaceitems', {}).pipe( map((workspaceitem: SubmissionObject) => workspaceitem[0]), catchError(() => observableOf({}))) } - depositSubmission(selfUrl: string): Observable { + /** + * Perform a REST call to deposit a workspaceitem and return response + * + * @param selfUrl + * The workspaceitem self url + * @return Observable + * observable of SubmissionObject + */ + depositSubmission(selfUrl: string): Observable { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'text/uri-list'); options.headers = headers; - return this.restService.postToEndpoint('workflowitems', selfUrl, null, options); + return this.restService.postToEndpoint('workflowitems', selfUrl, null, options) as Observable; } - discardSubmission(submissionId: string): Observable { - return this.restService.deleteById(submissionId); + /** + * Perform a REST call to delete a workspaceitem and return response + * + * @param submissionId + * The submission id + * @return Observable + * observable of SubmissionObject + */ + discardSubmission(submissionId: string): Observable { + return this.restService.deleteById(submissionId) as Observable; } + /** + * Dispatch a new [InitSubmissionFormAction] + * + * @param collectionId + * The collection id + * @param submissionId + * The submission id + * @param selfUrl + * The workspaceitem self url + * @param submissionDefinition + * The [SubmissionDefinitionsModel] that define submission configuration + * @param sections + * The [WorkspaceitemSectionsObject] that define submission sections init data + * @param errors + * The [SubmissionSectionError] that define submission sections init errors + */ dispatchInit( collectionId: string, submissionId: string, @@ -91,36 +157,90 @@ export class SubmissionService { this.store.dispatch(new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, sections, errors)); } + /** + * Dispatch a new [SaveAndDepositSubmissionAction] + * + * @param submissionId + * The submission id + */ dispatchDeposit(submissionId) { this.store.dispatch(new SaveAndDepositSubmissionAction(submissionId)); } + /** + * Dispatch a new [DiscardSubmissionAction] + * + * @param submissionId + * The submission id + */ dispatchDiscard(submissionId) { this.store.dispatch(new DiscardSubmissionAction(submissionId)); } + /** + * Dispatch a new [SaveSubmissionFormAction] + * + * @param submissionId + * The submission id + */ dispatchSave(submissionId) { this.store.dispatch(new SaveSubmissionFormAction(submissionId)); } + /** + * Dispatch a new [SaveForLaterSubmissionFormAction] + * + * @param submissionId + * The submission id + */ dispatchSaveForLater(submissionId) { this.store.dispatch(new SaveForLaterSubmissionFormAction(submissionId)); } + /** + * Dispatch a new [SaveSubmissionSectionFormAction] + * + * @param submissionId + * The submission id + */ dispatchSaveSection(submissionId, sectionId) { this.store.dispatch(new SaveSubmissionSectionFormAction(submissionId, sectionId)); } + /** + * Return the id of the current focused section for the specified submission + * + * @param submissionId + * The submission id + * @return Observable + * observable of section id + */ getActiveSectionId(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( map((submission: SubmissionObjectEntry) => submission.activeSection)); } + /** + * Return the [SubmissionObjectEntry] for the specified submission + * + * @param submissionId + * The submission id + * @return Observable + * observable of SubmissionObjectEntry + */ getSubmissionObject(submissionId: string): Observable { return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe( filter((submission: SubmissionObjectEntry) => isNotUndefined(submission))); } + /** + * Return a list of the active [SectionDataObject] belonging to the specified submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with the list of active submission's sections + */ getSubmissionSections(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( find((submission: SubmissionObjectEntry) => isNotUndefined(submission.sections) && !submission.isLoading), @@ -146,6 +266,14 @@ export class SubmissionService { distinctUntilChanged()); } + /** + * Return a list of the disabled [SectionDataObject] belonging to the specified submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with the list of disabled submission's sections + */ getDisabledSectionsList(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( filter((submission: SubmissionObjectEntry) => isNotUndefined(submission.sections) && !submission.isLoading), @@ -167,6 +295,12 @@ export class SubmissionService { distinctUntilChanged()); } + /** + * Return the correct REST endpoint link path depending on the page route + * + * @return string + * link path + */ getSubmissionObjectLinkName(): string { const url = this.router.routerState.snapshot.url; if (url.startsWith('/workspaceitems') || url.startsWith('/submit')) { @@ -178,6 +312,12 @@ export class SubmissionService { } } + /** + * Return the submission scope + * + * @return SubmissionScopeType + * the SubmissionScopeType + */ getSubmissionScope(): SubmissionScopeType { let scope: SubmissionScopeType; switch (this.getSubmissionObjectLinkName()) { @@ -194,6 +334,14 @@ export class SubmissionService { return scope; } + /** + * Return the validity status of the submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with submission validity status + */ getSubmissionStatus(submissionId: string): Observable { return this.store.select(submissionSelector).pipe( map((submissions: SubmissionState) => submissions.objects[submissionId]), @@ -219,6 +367,14 @@ export class SubmissionService { startWith(false)); } + /** + * Return the save processing status of the submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with submission save processing status + */ getSubmissionSaveProcessingStatus(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( map((state: SubmissionObjectEntry) => state.savePending), @@ -226,6 +382,14 @@ export class SubmissionService { startWith(false)); } + /** + * Return the deposit processing status of the submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with submission deposit processing status + */ getSubmissionDepositProcessingStatus(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( map((state: SubmissionObjectEntry) => state.depositPending), @@ -233,27 +397,50 @@ export class SubmissionService { startWith(false)); } - isSectionHidden(sectionData: SubmissionSectionObject) { + /** + * Return the visibility status of the specified section + * + * @param submissionId + * The submission id + * @return boolean + * true if section is hidden, false otherwise + */ + isSectionHidden(sectionData: SubmissionSectionObject): boolean { return (isNotUndefined(sectionData.visibility) && sectionData.visibility.main === 'HIDDEN' && sectionData.visibility.other === 'HIDDEN'); - } + /** + * Return the loading status of the submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with submission loading status + */ isSubmissionLoading(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( map((submission: SubmissionObjectEntry) => submission.isLoading), distinctUntilChanged()); } + /** + * Show a notification when a new section is added to submission form + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + */ notifyNewSection(submissionId: string, sectionId: string, sectionType?: SectionsType) { - this.translate.get('submission.sections.general.metadata-extracted-new-section', { sectionId }).pipe( - first()) - .subscribe((m) => { - this.notificationsService.info(null, m, null, true); - }); + const m = this.translate.instant('submission.sections.general.metadata-extracted-new-section', { sectionId }); + this.notificationsService.info(null, m, null, true); } + /** + * Redirect to MyDspace page + */ redirectToMyDSpace() { this.routeService.getPreviousUrl().pipe( first() @@ -267,10 +454,27 @@ export class SubmissionService { } + /** + * Dispatch a new [CancelSubmissionFormAction] + */ resetAllSubmissionObjects() { this.store.dispatch(new CancelSubmissionFormAction()); } + /** + * Dispatch a new [ResetSubmissionFormAction] + * + * @param collectionId + * The collection id + * @param submissionId + * The submission id + * @param selfUrl + * The workspaceitem self url + * @param submissionDefinition + * The [SubmissionDefinitionsModel] that define submission configuration + * @param sections + * The [WorkspaceitemSectionsObject] that define submission sections init data + */ resetSubmissionObject( collectionId: string, submissionId: string, @@ -281,6 +485,12 @@ export class SubmissionService { this.store.dispatch(new ResetSubmissionFormAction(collectionId, submissionId, selfUrl, sections, submissionDefinition)); } + /** + * Perform a REST call to retrieve an existing workspaceitem/workflowitem and return response + * + * @return Observable> + * observable of RemoteData + */ retrieveSubmission(submissionId): Observable> { return this.restService.getDataById(this.getSubmissionObjectLinkName(), submissionId).pipe( find((submissionObjects: SubmissionObject[]) => isNotUndefined(submissionObjects)), @@ -302,21 +512,38 @@ export class SubmissionService { ); } + /** + * Dispatch a new [SetActiveSectionAction] + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + */ setActiveSection(submissionId, sectionId) { this.store.dispatch(new SetActiveSectionAction(submissionId, sectionId)); } + /** + * Allow to save automatically the submission + * + * @param submissionId + * The submission id + */ startAutoSave(submissionId) { this.stopAutoSave(); // AUTOSAVE submission // Retrieve interval from config and convert to milliseconds const duration = this.EnvConfig.submission.autosave.timer * (1000 * 60); // Dispatch save action after given duration - this.timerObs = observableTimer(duration, duration); - this.autoSaveSub = this.timerObs + this.timer$ = observableTimer(duration, duration); + this.autoSaveSub = this.timer$ .subscribe(() => this.store.dispatch(new SaveSubmissionFormAction(submissionId))); } + /** + * Unsubscribe subscription to timer + */ stopAutoSave() { if (hasValue(this.autoSaveSub)) { this.autoSaveSub.unsubscribe(); diff --git a/src/app/submission/utils/parseSectionErrorPaths.ts b/src/app/submission/utils/parseSectionErrorPaths.ts index b47b9d0b05..4c973dedcf 100644 --- a/src/app/submission/utils/parseSectionErrorPaths.ts +++ b/src/app/submission/utils/parseSectionErrorPaths.ts @@ -1,9 +1,28 @@ import { hasValue } from '../../shared/empty.util'; +/** + * An interface to represent the path of a section error + */ export interface SectionErrorPath { + + /** + * The section id + */ sectionId: string; + + /** + * The form field id + */ fieldId?: string; + + /** + * The form field index + */ fieldIndex?: number; + + /** + * The complete path + */ originalPath: string; } @@ -12,7 +31,7 @@ const regex = /([^\/]+)/g; const regexShort = /\/sections\/(.*)/; /** - * the following method accept an array of section path strings and return a path object + * The following method accept an array of section path strings and return a path object * @param {string | string[]} path * @returns {SectionErrorPath[]} */