From a4a747e922b23c23b46aa931c202ac18b7b44ce8 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 13 Apr 2021 12:28:27 +0200 Subject: [PATCH 1/4] fix issue where the submission form wouldn't update after a relationship was added --- src/app/core/shared/operators.ts | 64 +++++++-------- .../edit/submission-edit.component.ts | 82 +++++++++++++------ .../submit/submission-submit.component.ts | 40 ++++++++- 3 files changed, 124 insertions(+), 62 deletions(-) diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index dd610b6ca7..3128538ea9 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -45,32 +45,32 @@ export const sendRequest = (requestService: RequestService) => (source: Observable): Observable => source.pipe(tap((request: RestRequest) => requestService.send(request))); -export const getRemoteDataPayload = () => - (source: Observable>): Observable => +export const getRemoteDataPayload = () => + (source: Observable>): Observable => source.pipe(map((remoteData: RemoteData) => remoteData.payload)); -export const getPaginatedListPayload = () => - (source: Observable>): Observable => +export const getPaginatedListPayload = () => + (source: Observable>): Observable => source.pipe(map((list: PaginatedList) => list.page)); -export const getAllCompletedRemoteData = () => - (source: Observable>): Observable> => +export const getAllCompletedRemoteData = () => + (source: Observable>): Observable> => source.pipe(filter((rd: RemoteData) => hasValue(rd) && rd.hasCompleted)); -export const getFirstCompletedRemoteData = () => - (source: Observable>): Observable> => +export const getFirstCompletedRemoteData = () => + (source: Observable>): Observable> => source.pipe(getAllCompletedRemoteData(), take(1)); -export const takeUntilCompletedRemoteData = () => - (source: Observable>): Observable> => +export const takeUntilCompletedRemoteData = () => + (source: Observable>): Observable> => source.pipe(takeWhile((rd: RemoteData) => hasNoValue(rd) || rd.isLoading, true)); -export const getFirstSucceededRemoteData = () => - (source: Observable>): Observable> => +export const getFirstSucceededRemoteData = () => + (source: Observable>): Observable> => source.pipe(filter((rd: RemoteData) => rd.hasSucceeded), take(1)); -export const getFirstSucceededRemoteWithNotEmptyData = () => - (source: Observable>): Observable> => +export const getFirstSucceededRemoteWithNotEmptyData = () => + (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => rd.hasSucceeded && isNotEmpty(rd.payload))); /** @@ -83,8 +83,8 @@ export const getFirstSucceededRemoteWithNotEmptyData = () => * These operators were created as a first step in refactoring * out all the instances where this is used incorrectly. */ -export const getFirstSucceededRemoteDataPayload = () => - (source: Observable>): Observable => +export const getFirstSucceededRemoteDataPayload = () => + (source: Observable>): Observable => source.pipe( getFirstSucceededRemoteData(), getRemoteDataPayload() @@ -100,8 +100,8 @@ export const getFirstSucceededRemoteDataPayload = () => * These operators were created as a first step in refactoring * out all the instances where this is used incorrectly. */ -export const getFirstSucceededRemoteDataWithNotEmptyPayload = () => - (source: Observable>): Observable => +export const getFirstSucceededRemoteDataWithNotEmptyPayload = () => + (source: Observable>): Observable => source.pipe( getFirstSucceededRemoteWithNotEmptyData(), getRemoteDataPayload() @@ -117,8 +117,8 @@ export const getFirstSucceededRemoteDataWithNotEmptyPayload = () => * These operators were created as a first step in refactoring * out all the instances where this is used incorrectly. */ -export const getAllSucceededRemoteDataPayload = () => - (source: Observable>): Observable => +export const getAllSucceededRemoteDataPayload = () => + (source: Observable>): Observable => source.pipe( getAllSucceededRemoteData(), getRemoteDataPayload() @@ -138,8 +138,8 @@ export const getAllSucceededRemoteDataPayload = () => * These operators were created as a first step in refactoring * out all the instances where this is used incorrectly. */ -export const getFirstSucceededRemoteListPayload = () => - (source: Observable>>): Observable => +export const getFirstSucceededRemoteListPayload = () => + (source: Observable>>): Observable => source.pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), @@ -160,8 +160,8 @@ export const getFirstSucceededRemoteListPayload = () => * These operators were created as a first step in refactoring * out all the instances where this is used incorrectly. */ -export const getAllSucceededRemoteListPayload = () => - (source: Observable>>): Observable => +export const getAllSucceededRemoteListPayload = () => + (source: Observable>>): Observable => source.pipe( getAllSucceededRemoteData(), getRemoteDataPayload(), @@ -174,8 +174,8 @@ export const getAllSucceededRemoteListPayload = () => * @param router The router used to navigate to a new page * @param authService Service to check if the user is authenticated */ -export const redirectOn4xx = (router: Router, authService: AuthService) => - (source: Observable>): Observable> => +export const redirectOn4xx = (router: Router, authService: AuthService) => + (source: Observable>): Observable> => observableCombineLatest(source, authService.isAuthenticated()).pipe( map(([rd, isAuthenticated]: [RemoteData, boolean]) => { if (rd.hasFailed) { @@ -229,16 +229,16 @@ export const returnEndUserAgreementUrlTreeOnFalse = (router: Router, redirect: s return hasAgreed ? hasAgreed : router.createUrlTree([getEndUserAgreementPath()], { queryParams }); })); -export const getFinishedRemoteData = () => - (source: Observable>): Observable> => +export const getFinishedRemoteData = () => + (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => !rd.isLoading)); -export const getAllSucceededRemoteData = () => - (source: Observable>): Observable> => +export const getAllSucceededRemoteData = () => + (source: Observable>): Observable> => source.pipe(filter((rd: RemoteData) => rd.hasSucceeded)); -export const toDSpaceObjectListRD = () => - (source: Observable>>>): Observable>> => +export const toDSpaceObjectListRD = () => + (source: Observable>>>): Observable>> => source.pipe( filter((rd: RemoteData>>) => rd.hasSucceeded), map((rd: RemoteData>>) => { diff --git a/src/app/submission/edit/submission-edit.component.ts b/src/app/submission/edit/submission-edit.component.ts index 7908a052b7..34fdcba104 100644 --- a/src/app/submission/edit/submission-edit.component.ts +++ b/src/app/submission/edit/submission-edit.component.ts @@ -2,11 +2,11 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { filter, switchMap } from 'rxjs/operators'; +import { filter, switchMap, debounceTime } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; -import { hasValue, isEmpty, isNotNull } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotNull, isNotEmptyOperator } from '../../shared/empty.util'; import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; import { SubmissionService } from '../submission.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -14,6 +14,9 @@ import { SubmissionObject } from '../../core/submission/models/submission-object import { Collection } from '../../core/shared/collection.model'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; +import { getAllSucceededRemoteData } from '../../core/shared/operators'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; /** * This component allows to edit an existing workspaceitem/workflowitem. @@ -60,6 +63,16 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { * @type {Array} */ private subs: Subscription[] = []; + + /** + * BehaviorSubject containing the self link to the item for this submission + * @private + */ + private itemLink$: BehaviorSubject = new BehaviorSubject(undefined); + + /** + * The item for this submission. + */ public item: Item; /** @@ -69,6 +82,7 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { * @param {NotificationsService} notificationsService * @param {ActivatedRoute} route * @param {Router} router + * @param {ItemDataService} itemDataService * @param {SubmissionService} submissionService * @param {TranslateService} translate */ @@ -76,6 +90,7 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { private notificationsService: NotificationsService, private route: ActivatedRoute, private router: Router, + private itemDataService: ItemDataService, private submissionService: SubmissionService, private translate: TranslateService) { } @@ -84,32 +99,47 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { * Retrieve workspaceitem/workflowitem from server and initialize all instance variables */ ngOnInit() { - this.subs.push(this.route.paramMap.pipe( - switchMap((params: ParamMap) => this.submissionService.retrieveSubmission(params.get('id'))), - // NOTE new submission is retrieved on the browser side only, so get null on server side rendering - filter((submissionObjectRD: RemoteData) => isNotNull(submissionObjectRD)) - ).subscribe((submissionObjectRD: RemoteData) => { - if (submissionObjectRD.hasSucceeded) { - if (isEmpty(submissionObjectRD.payload)) { - this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); - this.router.navigate(['/mydspace']); + this.subs.push( + this.route.paramMap.pipe( + switchMap((params: ParamMap) => this.submissionService.retrieveSubmission(params.get('id'))), + // NOTE new submission is retrieved on the browser side only, so get null on server side rendering + filter((submissionObjectRD: RemoteData) => isNotNull(submissionObjectRD)) + ).subscribe((submissionObjectRD: RemoteData) => { + if (submissionObjectRD.hasSucceeded) { + if (isEmpty(submissionObjectRD.payload)) { + this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); + this.router.navigate(['/mydspace']); + } else { + this.submissionId = submissionObjectRD.payload.id.toString(); + this.collectionId = (submissionObjectRD.payload.collection as Collection).id; + this.selfUrl = submissionObjectRD.payload._links.self.href; + this.sections = submissionObjectRD.payload.sections; + this.itemLink$.next(submissionObjectRD.payload._links.item.href); + this.item = submissionObjectRD.payload.item; + this.submissionDefinition = (submissionObjectRD.payload.submissionDefinition as SubmissionDefinitionsModel); + } } else { - this.submissionId = submissionObjectRD.payload.id.toString(); - this.collectionId = (submissionObjectRD.payload.collection as Collection).id; - this.selfUrl = submissionObjectRD.payload._links.self.href; - this.sections = submissionObjectRD.payload.sections; - this.item = submissionObjectRD.payload.item as Item; - this.submissionDefinition = (submissionObjectRD.payload.submissionDefinition as SubmissionDefinitionsModel); - this.changeDetectorRef.detectChanges(); + if (submissionObjectRD.statusCode === 404) { + // redirect to not found page + this.router.navigate(['/404'], { skipLocationChange: true }); + } + // TODO handle generic error } - } else { - if (submissionObjectRD.statusCode === 404) { - // redirect to not found page - this.router.navigate(['/404'], { skipLocationChange: true }); - } - // TODO handle generic error - } - })); + }), + this.itemLink$.pipe( + isNotEmptyOperator(), + switchMap((itemLink: string) => + this.itemDataService.findByHref(itemLink) + ), + getAllSucceededRemoteData(), + // Multiple sources can update the item in quick succession. + // We only want to rerender the form if the item is unchanged for some time + debounceTime(300), + ).subscribe((itemRd: RemoteData) => { + this.item = itemRd.payload; + this.changeDetectorRef.detectChanges(); + }), + ); } /** diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index 003f5280a8..0c2172368a 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { hasValue, isEmpty, isNotNull } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotNull, isNotEmptyOperator } from '../../shared/empty.util'; import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; import { TranslateService } from '@ngx-translate/core'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -12,6 +12,11 @@ import { SubmissionObject } from '../../core/submission/models/submission-object import { Collection } from '../../core/shared/collection.model'; import { Item } from '../../core/shared/item.model'; import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { switchMap, debounceTime } from 'rxjs/operators'; +import { getAllSucceededRemoteData } from '../../core/shared/operators'; +import { RemoteData } from '../../core/data/remote-data'; +import { ItemDataService } from '../../core/data/item-data.service'; /** * This component allows to submit a new workspaceitem. @@ -28,6 +33,16 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { * @type {string} */ public collectionId: string; + + /** + * BehaviorSubject containing the self link to the item for this submission + * @private + */ + private itemLink$: BehaviorSubject = new BehaviorSubject(undefined); + + /** + * The item for this submission. + */ public item: Item; /** @@ -71,6 +86,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { * * @param {ChangeDetectorRef} changeDetectorRef * @param {NotificationsService} notificationsService + * @param {ItemDataService} itemDataService * @param {SubmissionService} submissionService * @param {Router} router * @param {TranslateService} translate @@ -80,13 +96,16 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { constructor(private changeDetectorRef: ChangeDetectorRef, private notificationsService: NotificationsService, private router: Router, + private itemDataService: ItemDataService, private submissionService: SubmissionService, private translate: TranslateService, private viewContainerRef: ViewContainerRef, private route: ActivatedRoute) { this.route .queryParams - .subscribe((params) => { this.collectionParam = (params.collection); }); + .subscribe((params) => { + this.collectionParam = (params.collection); + }); } /** @@ -108,11 +127,24 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { this.selfUrl = submissionObject._links.self.href; this.submissionDefinition = (submissionObject.submissionDefinition as SubmissionDefinitionsModel); this.submissionId = submissionObject.id; + this.itemLink$.next(submissionObject._links.item.href); this.item = submissionObject.item as Item; - this.changeDetectorRef.detectChanges(); } } - }) + }), + this.itemLink$.pipe( + isNotEmptyOperator(), + switchMap((itemLink: string) => + this.itemDataService.findByHref(itemLink) + ), + getAllSucceededRemoteData(), + // Multiple sources can update the item in quick succession. + // We only want to rerender the form if the item is unchanged for some time + debounceTime(300), + ).subscribe((itemRd: RemoteData) => { + this.item = itemRd.payload; + this.changeDetectorRef.detectChanges(); + }) ); } From 56a54ae1c99b787ca3e9f1c4168a4ce90db2f365 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 13 Apr 2021 13:54:16 +0200 Subject: [PATCH 2/4] don't hide the lookup button for virtual relationship fields --- .../ds-dynamic-form-control-container.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index bfa9c214e9..085ccb6248 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -35,7 +35,7 @@ -
+