Fixed multiple edit relationship bugs

- Fixed issue making it impossible to add new relationships until the page is refreshed after deleting an existing one (only when you refreshed the page after creating the initial relationship)
- Fixed NPE in DsDynamicLookupRelationModalComponent
- Grouped buttons on relationship page in order to assure that they always have the same behaviour
This commit is contained in:
Alexandre Vryghem
2024-05-17 14:34:24 +02:00
parent 479adf6519
commit f65b96412a
12 changed files with 73 additions and 70 deletions

View File

@@ -35,6 +35,10 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
*/ */
updates$: Observable<FieldUpdates>; updates$: Observable<FieldUpdates>;
hasChanges$: Observable<boolean>;
isReinstatable$: Observable<boolean>;
/** /**
* Route to the item's page * Route to the item's page
*/ */
@@ -82,6 +86,8 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
this.discardTimeOut = environment.item.edit.undoTimeout; this.discardTimeOut = environment.item.edit.undoTimeout;
this.url = this.router.url; this.url = this.router.url;
this.hasChanges$ = this.hasChanges();
this.isReinstatable$ = this.isReinstatable();
if (this.url.indexOf('?') > 0) { if (this.url.indexOf('?') > 0) {
this.url = this.url.substr(0, this.url.indexOf('?')); this.url = this.url.substr(0, this.url.indexOf('?'));
} }

View File

@@ -5,18 +5,18 @@
class="fas fa-upload"></i> class="fas fa-upload"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.upload-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.upload-button" | translate}}</span>
</button> </button>
<button class="btn btn-warning" *ngIf="isReinstatable() | async" <button class="btn btn-warning" *ngIf="isReinstatable$ | async"
(click)="reinstate()"><i (click)="reinstate()"><i
class="fas fa-undo-alt"></i> class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || submitting" <button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting"
(click)="submit()"><i (click)="submit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.save-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.save-button" | translate}}</span>
</button> </button>
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)" <button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[disabled]="!(hasChanges() | async) || submitting" [disabled]="(hasChanges$ | async) !== true || submitting"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>
@@ -48,18 +48,18 @@
<div class="button-row bottom"> <div class="button-row bottom">
<div class="mt-4 float-right space-children-mr ml-gap"> <div class="mt-4 float-right space-children-mr ml-gap">
<button class="btn btn-warning" *ngIf="isReinstatable() | async" <button class="btn btn-warning" *ngIf="isReinstatable$ | async"
(click)="reinstate()"><i (click)="reinstate()"><i
class="fas fa-undo-alt"></i> class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || submitting" <button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting"
(click)="submit()"><i (click)="submit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.save-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.save-button" | translate}}</span>
</button> </button>
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)" <button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[disabled]="!(hasChanges() | async) || submitting" [disabled]="(hasChanges$ | async) !== true || submitting"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>

View File

@@ -185,8 +185,6 @@ describe('EditItemRelationshipsService', () => {
expect(itemService.invalidateByHref).toHaveBeenCalledWith(currentItem.self); expect(itemService.invalidateByHref).toHaveBeenCalledWith(currentItem.self);
expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem1.self); expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem1.self);
// TODO currently this isn't done yet
// expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem2.self);
expect(notificationsService.success).toHaveBeenCalledTimes(1); expect(notificationsService.success).toHaveBeenCalledTimes(1);
}); });

View File

@@ -1,23 +1,7 @@
<div class="item-relationships"> <div class="item-relationships">
<ng-container *ngIf="entityType$ | async as entityType"> <ng-container *ngIf="entityType$ | async as entityType; else noEntityType">
<div class="button-row top d-flex space-children-mr"> <div class="button-row top d-flex justify-content-end">
<button class="btn btn-danger ml-auto" *ngIf="!(isReinstatable() | async)" <ng-container *ngTemplateOutlet="buttons"></ng-container>
[disabled]="!(hasChanges() | async)"
(click)="discard()"><i
class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
</button>
<button class="btn btn-warning ml-auto" *ngIf="isReinstatable() | async"
(click)="reinstate()"><i
class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
</button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
(click)="submit()"><span *ngIf="isSaving$ | async" class="spinner-border spinner-border-sm" role="status"
aria-hidden="true"></span>
<i *ngIf="!(isSaving$ | async)" class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
</button>
</div> </div>
<div *ngIf="relationshipTypes$ | async as relationshipTypes; else loading" class="mb-4"> <div *ngIf="relationshipTypes$ | async as relationshipTypes; else loading" class="mb-4">
<div *ngFor="let relationshipType of relationshipTypes; trackBy: trackById" class="mb-4"> <div *ngFor="let relationshipType of relationshipTypes; trackBy: trackById" class="mb-4">
@@ -26,36 +10,46 @@
[item]="item" [item]="item"
[itemType]="entityType" [itemType]="entityType"
[relationshipType]="relationshipType" [relationshipType]="relationshipType"
[hasChanges]="hasChanges()" [hasChanges]="hasChanges$"
></ds-edit-relationship-list> ></ds-edit-relationship-list>
</div> </div>
</div> </div>
<ng-template #loading>
<ds-themed-loading></ds-themed-loading>
</ng-template>
<div class="button-row bottom"> <div class="button-row bottom">
<div class="float-right space-children-mr ml-gap"> <div class="float-right ml-gap">
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)" <ng-container *ngTemplateOutlet="buttons"></ng-container>
[disabled]="!(hasChanges() | async)"
(click)="discard()"><i
class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
</button>
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
(click)="reinstate()"><i
class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
</button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
(click)="submit()"><i
class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
</button>
</div> </div>
</div> </div>
<div *ngIf="!entityType"
class="alert alert-info mt-2" role="alert">
{{ 'item.edit.relationships.no-entity-type' | translate }}
</div>
</ng-container> </ng-container>
</div> </div>
<ng-template #noEntityType>
<ds-alert [type]="AlertType.Info" class="d-block mt-2">
{{ 'item.edit.relationships.no-entity-type' | translate }}
</ds-alert>
</ng-template>
<ng-template #loading>
<ds-themed-loading></ds-themed-loading>
</ng-template>
<ng-template #buttons>
<div class="d-flex space-children-mr justify-content-end">
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[disabled]="(hasChanges$ | async) !== true"
(click)="discard()">
<i aria-hidden="true" class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{ 'item.edit.metadata.discard-button' | translate }}</span>
</button>
<button class="btn btn-warning" *ngIf="isReinstatable() | async" (click)="reinstate()">
<i aria-hidden="true" class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{ 'item.edit.metadata.reinstate-button' | translate }}</span>
</button>
<button class="btn btn-primary"
[disabled]="(hasChanges$ | async) !== true || (isSaving$ | async) === true"
(click)="submit()">
<span *ngIf="isSaving$ | async" aria-hidden="true" class="spinner-border spinner-border-sm" role="status"></span>
<i *ngIf="(isSaving$ | async) !== true" aria-hidden="true" class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{ 'item.edit.metadata.save-button' | translate }}</span>
</button>
</div>
</ng-template>

View File

@@ -2,9 +2,7 @@ import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { getTestScheduler } from 'jasmine-marbles';
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { RestResponse } from '../../../core/cache/response.models'; import { RestResponse } from '../../../core/cache/response.models';
import { EntityTypeDataService } from '../../../core/data/entity-type-data.service'; import { EntityTypeDataService } from '../../../core/data/entity-type-data.service';
@@ -30,6 +28,7 @@ import { relationshipTypes } from '../../../shared/testing/relationship-types.mo
import { ThemeService } from '../../../shared/theme-support/theme.service'; import { ThemeService } from '../../../shared/theme-support/theme.service';
import { getMockThemeService } from '../../../shared/mocks/theme-service.mock'; import { getMockThemeService } from '../../../shared/mocks/theme-service.mock';
import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub'; import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
let comp: any; let comp: any;
let fixture: ComponentFixture<ItemRelationshipsComponent>; let fixture: ComponentFixture<ItemRelationshipsComponent>;
@@ -58,7 +57,6 @@ let itemService: ItemDataServiceStub;
const url = 'http://test-url.com/test-url'; const url = 'http://test-url.com/test-url';
router.url = url; router.url = url;
let scheduler: TestScheduler;
let item; let item;
let author1; let author1;
let author2; let author2;
@@ -206,9 +204,8 @@ describe('ItemRelationshipsComponent', () => {
} }
); );
scheduler = getTestScheduler();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [SharedModule, TranslateModule.forRoot()], imports: [NoopAnimationsModule, SharedModule, TranslateModule.forRoot()],
declarations: [ItemRelationshipsComponent], declarations: [ItemRelationshipsComponent],
providers: [ providers: [
{ provide: ThemeService, useValue: getMockThemeService() }, { provide: ThemeService, useValue: getMockThemeService() },

View File

@@ -28,6 +28,7 @@ import { PaginatedList } from '../../../core/data/paginated-list.model';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { EditItemRelationshipsService } from './edit-item-relationships.service'; import { EditItemRelationshipsService } from './edit-item-relationships.service';
import { compareArraysUsingIds } from '../../simple/item-types/shared/item-relationships-utils'; import { compareArraysUsingIds } from '../../simple/item-types/shared/item-relationships-utils';
import { AlertType } from '../../../shared/alert/aletr-type';
@Component({ @Component({
selector: 'ds-item-relationships', selector: 'ds-item-relationships',
@@ -54,6 +55,8 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
return this.editItemRelationshipsService.isSaving$; return this.editItemRelationshipsService.isSaving$;
} }
readonly AlertType = AlertType;
constructor( constructor(
public itemService: ItemDataService, public itemService: ItemDataService,
public objectUpdatesService: ObjectUpdatesService, public objectUpdatesService: ObjectUpdatesService,

View File

@@ -32,7 +32,7 @@
</ng-template> </ng-template>
</li> </li>
<li ngbNavItem *ngFor="let source of (externalSourcesRD$ | async); let idx = index"> <li ngbNavItem *ngFor="let source of (externalSourcesRD$ | async); let idx = index">
<a ngbNavLink>{{'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + source.id | translate : { count: (totalExternal$ | async)[idx] } }}</a> <a ngbNavLink>{{'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + source.id | translate : { count: (totalExternal$ | async)?.[idx] } }}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<ds-themed-dynamic-lookup-relation-external-source-tab <ds-themed-dynamic-lookup-relation-external-source-tab
[label]="label" [label]="label"

View File

@@ -30,6 +30,7 @@ import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/ope
import { followLink } from '../../../../utils/follow-link-config.model'; import { followLink } from '../../../../utils/follow-link-config.model';
import { RelationshipType } from '../../../../../core/shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../../../../../core/shared/item-relationships/relationship-type.model';
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
@Component({ @Component({
selector: 'ds-dynamic-lookup-relation-modal', selector: 'ds-dynamic-lookup-relation-modal',
@@ -217,7 +218,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
* Select (a list of) objects and add them to the store * Select (a list of) objects and add them to the store
* @param selectableObjects * @param selectableObjects
*/ */
select(...selectableObjects: SearchResult<Item>[]) { select(...selectableObjects: SearchResult<DSpaceObject>[]) {
this.zone.runOutsideAngular( this.zone.runOutsideAngular(
() => { () => {
const obs: Observable<any[]> = observableCombineLatest([...selectableObjects.map((sri: SearchResult<Item>) => { const obs: Observable<any[]> = observableCombineLatest([...selectableObjects.map((sri: SearchResult<Item>) => {
@@ -260,11 +261,11 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
* Deselect (a list of) objects and remove them from the store * Deselect (a list of) objects and remove them from the store
* @param selectableObjects * @param selectableObjects
*/ */
deselect(...selectableObjects: SearchResult<Item>[]) { deselect(...selectableObjects: SearchResult<DSpaceObject>[]) {
this.zone.runOutsideAngular( this.zone.runOutsideAngular(
() => selectableObjects.forEach((object) => { () => selectableObjects.forEach((object) => {
this.subMap[object.indexableObject.uuid].unsubscribe(); this.subMap[object.indexableObject.uuid].unsubscribe();
this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject, this.relationshipOptions.relationshipType, this.submissionId)); this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject as Item, this.relationshipOptions.relationshipType, this.submissionId));
}) })
); );
} }

View File

@@ -96,12 +96,12 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
/** /**
* Send an event to deselect an object from the list * Send an event to deselect an object from the list
*/ */
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() deselectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
/** /**
* Send an event to select an object from the list * Send an event to select an object from the list
*/ */
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
/** /**
* Search results * Search results
@@ -178,7 +178,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
this.selection$ this.selection$
.pipe(take(1)) .pipe(take(1))
.subscribe((selection: SearchResult<Item>[]) => { .subscribe((selection: SearchResult<Item>[]) => {
const filteredPage = page.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) < 0); const filteredPage: SearchResult<DSpaceObject>[] = page.filter((pageItem: SearchResult<DSpaceObject>) => selection.findIndex((selected: SearchResult<Item>) => selected.equals(pageItem)) < 0);
this.selectObject.emit(...filteredPage); this.selectObject.emit(...filteredPage);
}); });
this.selectableListService.select(this.listId, page); this.selectableListService.select(this.listId, page);

View File

@@ -43,9 +43,9 @@ export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedCompone
@Input() isEditRelationship: boolean; @Input() isEditRelationship: boolean;
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter(); @Output() deselectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter(); @Output() selectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
@Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter(); @Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter();

View File

@@ -13,6 +13,8 @@ import { PageInfo } from '../../../../../../core/shared/page-info.model';
import { Context } from '../../../../../../core/shared/context.model'; import { Context } from '../../../../../../core/shared/context.model';
import { createSuccessfulRemoteDataObject } from '../../../../../remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../../../../remote-data.utils';
import { PaginationService } from '../../../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../../../core/pagination/pagination.service';
import { SearchResult } from '../../../../../search/models/search-result.model';
import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model';
@Component({ @Component({
selector: 'ds-dynamic-lookup-relation-selection-tab', selector: 'ds-dynamic-lookup-relation-selection-tab',
@@ -63,12 +65,12 @@ export class DsDynamicLookupRelationSelectionTabComponent {
/** /**
* Send an event to deselect an object from the list * Send an event to deselect an object from the list
*/ */
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() deselectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
/** /**
* Send an event to select an object from the list * Send an event to select an object from the list
*/ */
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
/** /**
* The initial pagination to use * The initial pagination to use

View File

@@ -3088,6 +3088,8 @@
"orgunit.page.titleprefix": "Organizational Unit: ", "orgunit.page.titleprefix": "Organizational Unit: ",
"orgunit.search.results.head": "Organizational Unit Search Results",
"pagination.options.description": "Pagination options", "pagination.options.description": "Pagination options",
"pagination.results-per-page": "Results Per Page", "pagination.results-per-page": "Results Per Page",