diff --git a/package.json b/package.json index 5ceb899322..33a0cb4055 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "clean:bld": "rimraf build", "clean:node": "rimraf node_modules", "clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json && yarn run clean:bld", - "clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:env", + "clean": "yarn run clean:prod && yarn run clean:env && yarn run clean:node", "clean:env": "rimraf src/environments/environment.ts", "sync-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts" }, diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index 98552ed40b..beb7413415 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -3,37 +3,41 @@ *ngVar="(collectionRD$ | async) as collectionRD">
- -
+ +
+
- - - - - + + + + + - - - - - - - - - - -
+ + + + + + + + + +
+
+ +
+
-
- - - - - - - - - - - - - - - -
+
+
+ + + + + + + + + + + + + + +
+
+ +
+
diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index bde2b5a1b0..f3055d3e51 100644 --- a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -48,7 +48,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl ngOnInit(): void { observableCombineLatest(this.route.data, this.route.parent.data).pipe( map(([data, parentData]) => Object.assign({}, data, parentData)), - map((data) => data.item), + map((data) => data.dso), first(), map((data: RemoteData) => data.payload) ).subscribe((item: Item) => { diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.ts b/src/app/+item-page/edit-item-page/edit-item-page.component.ts index 655582064c..2bd9a30ca3 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.component.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.component.ts @@ -47,7 +47,7 @@ export class EditItemPageComponent implements OnInit { this.pages = this.route.routeConfig.children .map((child: any) => child.path) .filter((path: string) => isNotEmpty(path)); // ignore reroutes - this.itemRD$ = this.route.data.pipe(map((data) => data.item)); + this.itemRD$ = this.route.data.pipe(map((data) => data.dso)); } /** diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts index c687c829eb..dcf70a30cb 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts @@ -74,7 +74,7 @@ describe('ItemAuthorizationsComponent test suite', () => { const routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject(item) + dso: createSuccessfulRemoteDataObject(item) }) }; diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index 8153990a02..8b89de7c89 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -75,7 +75,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { */ ngOnInit(): void { this.item$ = this.route.data.pipe( - map((data) => data.item), + map((data) => data.dso), getFirstSucceededRemoteDataWithNotEmptyPayload(), map((item: Item) => this.linkService.resolveLink( item, diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index f86c57d69e..5f6e3a06c4 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -140,7 +140,7 @@ describe('ItemBitstreamsComponent', () => { }); route = Object.assign({ parent: { - data: observableOf({ item: createMockRD(item) }) + data: observableOf({ dso: createMockRD(item) }) }, data: observableOf({}), url: url diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index 15b860a782..9aeb1522a6 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -89,7 +89,7 @@ describe('ItemCollectionMapperComponent', () => { clearDiscoveryRequests: () => {} /* tslint:enable:no-empty */ }); - const activatedRouteStub = new ActivatedRouteStub({}, { item: mockItemRD }); + const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockItemRD }); const translateServiceStub = { get: () => of('test-message of item ' + mockItem.name), onLangChange: new EventEmitter(), diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 1409e06ddb..df406f826b 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -92,7 +92,7 @@ export class ItemCollectionMapperComponent implements OnInit { } ngOnInit(): void { - this.itemRD$ = this.route.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable>; + this.itemRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable>; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.loadCollectionLists(); } diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts index 18cbd6e855..e7b454e92b 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts @@ -138,7 +138,7 @@ describe('ItemDeleteComponent', () => { routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject(mockItem) + dso: createSuccessfulRemoteDataObject(mockItem) }) }; diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts index ed9ab4a891..f30b5cc3b0 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts @@ -130,7 +130,7 @@ describe('ItemMetadataComponent', () => { routeStub = { data: observableOf({}), parent: { - data: observableOf({ item: createSuccessfulRemoteDataObject(item) }) + data: observableOf({ dso: createSuccessfulRemoteDataObject(item) }) } }; paginatedMetadataFields = new PaginatedList(undefined, [mdField1, mdField2, mdField3]); diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts index 77aefe2356..c8c49b118b 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts @@ -44,7 +44,7 @@ describe('ItemMoveComponent', () => { const routeStub = { data: observableOf({ - item: new RemoteData(false, false, true, null, { + dso: new RemoteData(false, false, true, null, { id: 'item1' }) }) diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index abadd2ec4a..1a544af7dc 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -55,7 +55,7 @@ export class ItemMoveComponent implements OnInit { } ngOnInit(): void { - this.itemRD$ = this.route.data.pipe(map((data) => data.item), getSucceededRemoteData()) as Observable>; + this.itemRD$ = this.route.data.pipe(map((data) => data.dso), getSucceededRemoteData()) as Observable>; this.itemRD$.subscribe((rd) => { this.itemId = rd.payload.id; } diff --git a/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts index 95b08a9936..52ccbc2133 100644 --- a/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts @@ -51,7 +51,7 @@ describe('ItemPrivateComponent', () => { routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject({ + dso: createSuccessfulRemoteDataObject({ id: 'fake-id' }) }) diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts index 53df20bf04..1143874709 100644 --- a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts @@ -51,7 +51,7 @@ describe('ItemPublicComponent', () => { routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject({ + dso: createSuccessfulRemoteDataObject({ id: 'fake-id' }) }) diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts index 5e75b59292..005f330df9 100644 --- a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts @@ -51,7 +51,7 @@ describe('ItemReinstateComponent', () => { routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject({ + dso: createSuccessfulRemoteDataObject({ id: 'fake-id' }) }) diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html index 1a7cc2e2df..5583de5fd5 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html @@ -1,15 +1,26 @@ -
{{getRelationshipMessageKey() | async | translate}}
+
+ {{getRelationshipMessageKey() | async | translate}} + +
+ [ngClass]="{ + 'alert-success': updateValue.changeType === 1, + 'alert-warning': updateValue.changeType === 0, + 'alert-danger': updateValue.changeType === 2 + }"> +
{{"item.edit.relationships.no-relationships" | translate}}
-
no relationships
+
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss index e0c20d299f..54498499d7 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss @@ -1,8 +1,8 @@ -.relationship-row:not(.alert-danger) { +.relationship-row:not(.alert) { padding: $alert-padding-y 0; } -.relationship-row.alert-danger { +.relationship-row.alert { margin-left: -$alert-padding-x; margin-right: -$alert-padding-x; margin-top: -1px; diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index 1d4d91da0b..cd583fd22b 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -1,5 +1,5 @@ import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs/internal/observable/of'; @@ -8,6 +8,7 @@ import { FieldChangeType } from '../../../../core/data/object-updates/object-upd import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { RelationshipTypeService } from '../../../../core/data/relationship-type.service'; +import { RelationshipService } from '../../../../core/data/relationship.service'; import { RemoteData } from '../../../../core/data/remote-data'; import { ItemType } from '../../../../core/shared/item-relationships/item-type.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; @@ -15,6 +16,7 @@ import { Relationship } from '../../../../core/shared/item-relationships/relatio import { Item } from '../../../../core/shared/item.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { getMockLinkService } from '../../../../shared/mocks/link-service.mock'; +import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; import { SharedModule } from '../../../../shared/shared.module'; import { EditRelationshipListComponent } from './edit-relationship-list.component'; @@ -22,72 +24,123 @@ let comp: EditRelationshipListComponent; let fixture: ComponentFixture; let de: DebugElement; +let linkService; let objectUpdatesService; -let entityTypeService; +let relationshipService; +let selectableListService; const url = 'http://test-url.com/test-url'; let item; +let entityType; +let relatedEntityType; let author1; let author2; let fieldUpdate1; let fieldUpdate2; -let relationship1; -let relationship2; +let relationships; let relationshipType; -let entityType; -let relatedEntityType; describe('EditRelationshipListComponent', () => { - beforeEach(() => { + beforeEach(async(() => { entityType = Object.assign(new ItemType(), { - id: 'entityType', + id: 'Publication', + uuid: 'Publication', + label: 'Publication', }); relatedEntityType = Object.assign(new ItemType(), { - id: 'relatedEntityType', + id: 'Author', + uuid: 'Author', + label: 'Author', }); relationshipType = Object.assign(new RelationshipType(), { id: '1', uuid: '1', + leftType: observableOf(new RemoteData( + false, + false, + true, + undefined, + entityType, + )), + rightType: observableOf(new RemoteData( + false, + false, + true, + undefined, + relatedEntityType, + )), leftwardType: 'isAuthorOfPublication', rightwardType: 'isPublicationOfAuthor', - leftType: observableOf(new RemoteData(false, false, true, undefined, entityType)), - rightType: observableOf(new RemoteData(false, false, true, undefined, relatedEntityType)), }); - relationship1 = Object.assign(new Relationship(), { - _links: { - self: { - href: url + '/2' - } - }, - id: '2', - uuid: '2', - leftId: 'author1', - rightId: 'publication', - leftItem: observableOf(new RemoteData(false, false, true, undefined, item)), - rightItem: observableOf(new RemoteData(false, false, true, undefined, author1)), - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + author1 = Object.assign(new Item(), { + id: 'author1', + uuid: 'author1' + }); + author2 = Object.assign(new Item(), { + id: 'author2', + uuid: 'author2' }); - relationship2 = Object.assign(new Relationship(), { - _links: { - self: { - href: url + '/3' - } - }, - id: '3', - uuid: '3', - leftId: 'author2', - rightId: 'publication', - leftItem: observableOf(new RemoteData(false, false, true, undefined, item)), - rightItem: observableOf(new RemoteData(false, false, true, undefined, author2)), - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) - }); + relationships = [ + Object.assign(new Relationship(), { + self: url + '/2', + id: '2', + uuid: '2', + relationshipType: observableOf(new RemoteData( + false, + false, + true, + undefined, + relationshipType + )), + leftItem: observableOf(new RemoteData( + false, + false, + true, + undefined, + item, + )), + rightItem: observableOf(new RemoteData( + false, + false, + true, + undefined, + author1, + )), + }), + Object.assign(new Relationship(), { + self: url + '/3', + id: '3', + uuid: '3', + relationshipType: observableOf(new RemoteData( + false, + false, + true, + undefined, + relationshipType + )), + leftItem: observableOf(new RemoteData( + false, + false, + true, + undefined, + item, + )), + rightItem: observableOf(new RemoteData( + false, + false, + true, + undefined, + author2, + )), + }) + ]; item = Object.assign(new Item(), { _links: { @@ -100,84 +153,82 @@ describe('EditRelationshipListComponent', () => { false, true, undefined, - new PaginatedList(new PageInfo(), [relationship1, relationship2]) + new PaginatedList(new PageInfo(), relationships), )) }); - author1 = Object.assign(new Item(), { - id: 'author1', - uuid: 'author1' - }); - author2 = Object.assign(new Item(), { - id: 'author2', - uuid: 'author2' - }); - fieldUpdate1 = { - field: author1, + field: { + uuid: relationships[0].uuid, + relationship: relationships[0], + type: relationshipType, + }, changeType: undefined }; fieldUpdate2 = { - field: author2, + field: { + uuid: relationships[1].uuid, + relationship: relationships[1], + type: relationshipType, + }, changeType: FieldChangeType.REMOVE }; objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { getFieldUpdates: observableOf({ - [author1.uuid]: fieldUpdate1, - [author2.uuid]: fieldUpdate2 + [relationships[0].uuid]: fieldUpdate1, + [relationships[1].uuid]: fieldUpdate2 }) } ); - entityTypeService = jasmine.createSpyObj('entityTypeService', + relationshipService = jasmine.createSpyObj('relationshipService', { - getEntityTypeByLabel: observableOf(new RemoteData( - false, - false, - true, - null, - entityType, - )), - getEntityTypeRelationships: observableOf(new RemoteData( - false, - false, - true, - null, - new PaginatedList(new PageInfo(), [relationshipType]), - )), + getRelatedItemsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [author1, author2]))), + getItemRelationshipsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), relationships))), + isLeftItem: observableOf(true), } ); + selectableListService = {}; + + linkService = { + resolveLink: () => null, + resolveLinks: () => null, + }; + TestBed.configureTestingModule({ imports: [SharedModule, TranslateModule.forRoot()], declarations: [EditRelationshipListComponent], providers: [ { provide: ObjectUpdatesService, useValue: objectUpdatesService }, - { provide: RelationshipTypeService, useValue: {} }, - { provide: LinkService, useValue: getMockLinkService() }, + { provide: RelationshipService, useValue: relationshipService }, + { provide: SelectableListService, useValue: selectableListService }, + { provide: LinkService, useValue: linkService }, ], schemas: [ NO_ERRORS_SCHEMA ] }).compileComponents(); + })); + beforeEach(() => { fixture = TestBed.createComponent(EditRelationshipListComponent); comp = fixture.componentInstance; de = fixture.debugElement; - comp.item = item; comp.itemType = entityType; comp.url = url; comp.relationshipType = relationshipType; - fixture.detectChanges(); }); describe('changeType is REMOVE', () => { - it('the div should have class alert-danger', () => { - + beforeEach(() => { fieldUpdate1.changeType = FieldChangeType.REMOVE; + fixture.detectChanges(); + }); + it('the div should have class alert-danger', () => { const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement; expect(element.classList).toContain('alert-danger'); }); diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index c17762e4a0..a9434eef6f 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -1,11 +1,23 @@ import { Component, Input, OnInit } from '@angular/core'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { LinkService } from '../../../../core/cache/builders/link.service'; +import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { Observable } from 'rxjs/internal/Observable'; -import {FieldUpdate, FieldUpdates} from '../../../../core/data/object-updates/object-updates.reducer'; +import { + FieldUpdate, + FieldUpdates, + RelationshipIdentifiable +} from '../../../../core/data/object-updates/object-updates.reducer'; +import { RelationshipService } from '../../../../core/data/relationship.service'; import {Item} from '../../../../core/shared/item.model'; -import { map, switchMap, tap } from 'rxjs/operators'; -import {hasValue} from '../../../../shared/empty.util'; +import { + defaultIfEmpty, filter, flatMap, + map, + switchMap, + take, tap, +} from 'rxjs/operators'; +import { hasValue } from '../../../../shared/empty.util'; import {Relationship} from '../../../../core/shared/item-relationships/relationship.model'; import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model'; import { @@ -13,8 +25,13 @@ import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; -import { combineLatest as observableCombineLatest } from 'rxjs'; +import { combineLatest as observableCombineLatest, of } from 'rxjs'; import { ItemType } from '../../../../core/shared/item-relationships/item-type.model'; +import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component'; +import { RelationshipOptions } from '../../../../shared/form/builder/models/relationship-options.model'; +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; +import { SearchResult } from '../../../../shared/search/search-result.model'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; @Component({ @@ -46,14 +63,29 @@ export class EditRelationshipListComponent implements OnInit { */ @Input() relationshipType: RelationshipType; + private relatedEntityType$: Observable; + + /** + * The list ID to save selected entities under + */ + listId: string; + /** * The FieldUpdates for the relationships in question */ updates$: Observable; + /** + * A reference to the lookup window + */ + modalRef: NgbModalRef; + constructor( protected objectUpdatesService: ObjectUpdatesService, - protected linkService: LinkService + protected linkService: LinkService, + protected relationshipService: RelationshipService, + protected modalService: NgbModal, + protected selectableListService: SelectableListService, ) { } @@ -62,10 +94,18 @@ export class EditRelationshipListComponent implements OnInit { */ public getRelationshipMessageKey(): Observable { - return this.getLabel().pipe( - map((label) => { - if (hasValue(label) && label.indexOf('Of') > -1) { - return `relationships.${label.substring(0, label.indexOf('Of') + 2)}` + return observableCombineLatest( + this.getLabel(), + this.relatedEntityType$, + ).pipe( + map(([label, relatedEntityType]) => { + if (hasValue(label) && label.indexOf('is') > -1 && label.indexOf('Of') > -1) { + const relationshipLabel = `${label.substring(2, label.indexOf('Of'))}`; + if (relationshipLabel !== relatedEntityType.label) { + return `relationships.is${relationshipLabel}Of.${relatedEntityType.label}` + } else { + return `relationships.is${relationshipLabel}Of` + } } else { return label; } @@ -77,7 +117,6 @@ export class EditRelationshipListComponent implements OnInit { * Get the relevant label for this relationship type */ private getLabel(): Observable { - return observableCombineLatest([ this.relationshipType.leftType, this.relationshipType.rightType, @@ -99,19 +138,197 @@ export class EditRelationshipListComponent implements OnInit { return update && update.field ? update.field.uuid : undefined; } + /** + * Open the dynamic lookup modal to search for items to add as relationships + */ + openLookup() { + + this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, { + size: 'lg' + }); + const modalComp: DsDynamicLookupRelationModalComponent = this.modalRef.componentInstance; + modalComp.repeatable = true; + modalComp.listId = this.listId; + modalComp.item = this.item; + modalComp.select = (...selectableObjects: Array>) => { + selectableObjects.forEach((searchResult) => { + const relatedItem: Item = searchResult.indexableObject; + this.getFieldUpdatesForRelatedItem(relatedItem) + .subscribe((identifiables) => { + identifiables.forEach((identifiable) => + this.objectUpdatesService.removeSingleFieldUpdate(this.url, identifiable.uuid) + ); + if (identifiables.length === 0) { + this.relationshipService.getNameVariant(this.listId, relatedItem.uuid) + .subscribe((nameVariant) => { + const update = { + uuid: this.relationshipType.id + '-' + relatedItem.uuid, + nameVariant, + type: this.relationshipType, + relatedItem, + } as RelationshipIdentifiable; + this.objectUpdatesService.saveAddFieldUpdate(this.url, update); + }) + } + }); + }) + }; + modalComp.deselect = (...selectableObjects: Array>) => { + selectableObjects.forEach((searchResult) => { + const relatedItem: Item = searchResult.indexableObject; + this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.relationshipType.id + '-' + relatedItem.uuid); + this.getFieldUpdatesForRelatedItem(relatedItem) + .subscribe((identifiables) => + identifiables.forEach((identifiable) => + this.objectUpdatesService.saveRemoveFieldUpdate(this.url, identifiable) + ) + ); + }) + }; + this.relatedEntityType$ + .pipe(take(1)) + .subscribe((relatedEntityType) => { + modalComp.relationshipOptions = Object.assign( + new RelationshipOptions(), { + relationshipType: relatedEntityType.label, + // filter: this.getRelationshipMessageKey(), + searchConfiguration: relatedEntityType.label.toLowerCase(), + nameVariants: true, + } + ); + }); + + this.selectableListService.deselectAll(this.listId); + this.updates$.pipe( + switchMap((updates) => + Object.values(updates).length > 0 ? + observableCombineLatest( + Object.values(updates) + .filter((update) => update.changeType !== FieldChangeType.REMOVE) + .map((update) => { + const field = update.field as RelationshipIdentifiable; + if (field.relationship) { + return this.getRelatedItem(field.relationship); + } else { + return of(field.relatedItem); + } + }) + ) : of([]) + ), + take(1), + map((items) => items.map((item) => { + const searchResult = new ItemSearchResult(); + searchResult.indexableObject = item; + searchResult.hitHighlights = {}; + return searchResult; + })), + ).subscribe((items) => { + this.selectableListService.select(this.listId, items); + }); + } + + /** + * Get the existing field updates regarding a relationship with a given item + * @param relatedItem The item for which to get the existing field updates + */ + private getFieldUpdatesForRelatedItem(relatedItem: Item): Observable { + + return this.updates$.pipe( + take(1), + map((updates) => Object.values(updates) + .map((update) => update.field as RelationshipIdentifiable) + .filter((field) => field.relationship) + ), + flatMap((identifiables) => + observableCombineLatest( + identifiables.map((identifiable) => this.getRelatedItem(identifiable.relationship)) + ).pipe( + defaultIfEmpty([]), + map((relatedItems) => + identifiables.filter((identifiable, index) => relatedItems[index].uuid === relatedItem.uuid) + ), + ) + ), + ); + } + + /** + * Get the related item for a given relationship + * @param relationship The relationship for which to get the related item + */ + private getRelatedItem(relationship: Relationship): Observable { + return this.relationshipService.isLeftItem(relationship, this.item).pipe( + switchMap((isLeftItem) => isLeftItem ? relationship.rightItem : relationship.leftItem), + getSucceededRemoteData(), + getRemoteDataPayload(), + ) + } + ngOnInit(): void { - this.updates$ = this.item.relationships.pipe( + + this.relatedEntityType$ = + observableCombineLatest([ + this.relationshipType.leftType, + this.relationshipType.rightType, + ].map((type) => type.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + ))).pipe( + map((relatedTypes) => relatedTypes.find((relatedType) => relatedType.uuid !== this.itemType.uuid)), + ); + + this.relatedEntityType$.pipe( + take(1) + ).subscribe( + (relatedEntityType) => this.listId = `edit-relationship-${this.itemType.id}-${relatedEntityType.id}` + ); + + this.updates$ = this.getItemRelationships().pipe( + switchMap((relationships) => + observableCombineLatest( + relationships.map((relationship) => this.relationshipService.isLeftItem(relationship, this.item)) + ).pipe( + defaultIfEmpty([]), + map((isLeftItemArray) => isLeftItemArray.map((isLeftItem, index) => { + const relationship = relationships[index]; + const nameVariant = isLeftItem ? relationship.rightwardValue : relationship.leftwardValue; + return { + uuid: relationship.id, + type: this.relationshipType, + relationship, + nameVariant, + } as RelationshipIdentifiable + })), + )), + switchMap((initialFields) => this.objectUpdatesService.getFieldUpdates(this.url, initialFields).pipe( + map((fieldUpdates) => { + const fieldUpdatesFiltered: FieldUpdates = {}; + Object.keys(fieldUpdates).forEach((uuid) => { + const field = fieldUpdates[uuid].field; + if ((field as RelationshipIdentifiable).type.id === this.relationshipType.id) { + fieldUpdatesFiltered[uuid] = fieldUpdates[uuid]; + } + }); + return fieldUpdatesFiltered; + }), + )), + ); + } + + private getItemRelationships() { + this.linkService.resolveLink(this.item, followLink('relationships')); + return this.item.relationships.pipe( getAllSucceededRemoteData(), map((relationships) => relationships.payload.page.filter((relationship) => relationship)), - map((relationships: Relationship[]) => - relationships.map((relationship: Relationship) => { + filter((relationships) => relationships.every((relationship) => !!relationship)), + tap((relationships: Relationship[]) => + relationships.forEach((relationship: Relationship) => { this.linkService.resolveLinks( relationship, followLink('relationshipType'), followLink('leftItem'), followLink('rightItem'), ); - return relationship; }) ), switchMap((itemRelationships: Relationship[]) => @@ -122,15 +339,12 @@ export class EditRelationshipListComponent implements OnInit { getRemoteDataPayload(), )) ).pipe( + defaultIfEmpty([]), map((relationshipTypes) => itemRelationships.filter( (relationship, index) => relationshipTypes[index].id === this.relationshipType.id) ), - map((relationships) => relationships.map((relationship) => - Object.assign(new Relationship(), relationship, {uuid: relationship.id}) - )), ) ), - switchMap((initialFields) => this.objectUpdatesService.getFieldUpdates(this.url, initialFields)), ); } } diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html index 7e61e8958f..e65cd237a3 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html @@ -1,6 +1,11 @@
- + +
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts index 9eca3f270d..6e81319f28 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts @@ -1,5 +1,5 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs/internal/observable/of'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; @@ -25,7 +25,7 @@ let fieldUpdate2; let relationships; let relationshipType; -let fixture; +let fixture: ComponentFixture; let comp: EditRelationshipComponent; let de; let el; @@ -91,11 +91,17 @@ describe('EditRelationshipComponent', () => { }); fieldUpdate1 = { - field: relationships[0], + field: { + uuid: relationships[0].uuid, + relationship: relationships[0], + }, changeType: undefined }; fieldUpdate2 = { - field: relationships[1], + field: { + uuid: relationships[1].uuid, + relationship: relationships[1], + }, changeType: FieldChangeType.REMOVE }; diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts index 2badaf80b0..265bca7529 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts @@ -1,10 +1,13 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core'; -import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable, of } from 'rxjs'; import { filter, map, switchMap, take, tap } from 'rxjs/operators'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; -import { DeleteRelationship, FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; +import { + DeleteRelationship, + FieldUpdate, + RelationshipIdentifiable +} from '../../../../core/data/object-updates/object-updates.reducer'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../../core/shared/item.model'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { ViewMode } from '../../../../core/shared/view-mode.model'; @@ -36,8 +39,16 @@ export class EditRelationshipComponent implements OnChanges { /** * The relationship being edited */ - get relationship(): Relationship { - return this.fieldUpdate.field as Relationship; + get relationship() { + return this.update.relationship; + } + + get update() { + return this.fieldUpdate.field as RelationshipIdentifiable; + } + + get nameVariant() { + return this.update.nameVariant; } public leftItem$: Observable; @@ -68,24 +79,28 @@ export class EditRelationshipComponent implements OnChanges { * Sets the current relationship based on the fieldUpdate input field */ ngOnChanges(): void { - this.leftItem$ = this.relationship.leftItem.pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) - ); - this.rightItem$ = this.relationship.rightItem.pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) - ); - this.relatedItem$ = observableCombineLatest( - this.leftItem$, - this.rightItem$, - ).pipe( - map((items: Item[]) => - items.find((item) => item.uuid !== this.editItem.uuid) - ) - ); + if (this.relationship) { + this.leftItem$ = this.relationship.leftItem.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) + ); + this.rightItem$ = this.relationship.rightItem.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) + ); + this.relatedItem$ = observableCombineLatest( + this.leftItem$, + this.rightItem$, + ).pipe( + map((items: Item[]) => + items.find((item) => item.uuid !== this.editItem.uuid) + ) + ); + } else { + this.relatedItem$ = of(this.update.relatedItem); + } } /** @@ -136,7 +151,8 @@ export class EditRelationshipComponent implements OnChanges { * Check if a user should be allowed to remove this field */ canRemove(): boolean { - return this.fieldUpdate.changeType !== FieldChangeType.REMOVE; + return this.fieldUpdate.changeType !== FieldChangeType.REMOVE + && this.fieldUpdate.changeType !== FieldChangeType.ADD; } /** diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html index 0c9d92dfbf..7692494fd8 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html @@ -19,14 +19,19 @@  {{"item.edit.metadata.save-button" | translate}}
-
- -
+ + +
+ +
+
+ +
- + +
diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss b/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss new file mode 100644 index 0000000000..37d2ebeca7 --- /dev/null +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss @@ -0,0 +1,5 @@ +.scrollable-menu { + height: auto; + max-height: $dso-selector-list-max-height; + overflow-x: hidden; +} diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts index 50b6090aef..7671f012ad 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts @@ -6,10 +6,10 @@ import { SearchService } from '../../../core/shared/search/search.service'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model'; import { Item } from '../../../core/shared/item.model'; -import { PaginatedList } from '../../../core/data/paginated-list'; -import { MetadataValue } from '../../../core/shared/metadata.models'; import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { PaginatedSearchOptions } from '../../search/paginated-search-options.model'; +import { hasValue } from '../../empty.util'; +import { createPaginatedList } from '../../testing/utils.test'; describe('DSOSelectorComponent', () => { let component: DSOSelectorComponent; @@ -18,19 +18,46 @@ describe('DSOSelectorComponent', () => { const currentDSOId = 'test-uuid-ford-sose'; const type = DSpaceObjectType.ITEM; - const searchResult = new ItemSearchResult(); - const item = new Item(); - item.metadata = { - 'dc.title': [Object.assign(new MetadataValue(), { - value: 'Item title', - language: undefined - })] - }; - searchResult.indexableObject = item; - searchResult.hitHighlights = {}; - const searchService = jasmine.createSpyObj('searchService', { - search: createSuccessfulRemoteDataObject$(new PaginatedList(undefined, [searchResult])) - }); + const searchResult = createSearchResult('current'); + + const firstPageResults = [ + createSearchResult('1'), + createSearchResult('2'), + createSearchResult('3'), + ]; + + const nextPageResults = [ + createSearchResult('4'), + createSearchResult('5'), + createSearchResult('6'), + ]; + + const searchService = { + search: (options: PaginatedSearchOptions) => { + if (hasValue(options.query) && options.query.startsWith('search.resourceid')) { + return createSuccessfulRemoteDataObject$(createPaginatedList([searchResult])); + } else if (options.pagination.currentPage === 1) { + return createSuccessfulRemoteDataObject$(createPaginatedList(firstPageResults)); + } else { + return createSuccessfulRemoteDataObject$(createPaginatedList(nextPageResults)); + } + } + } + + function createSearchResult(name: string): ItemSearchResult { + return Object.assign(new ItemSearchResult(), { + indexableObject: Object.assign(new Item(), { + id: `test-result-${name}`, + metadata: { + 'dc.title': [ + { + value: `test result - ${name}` + } + ] + } + }) + }) + } beforeEach(async(() => { TestBed.configureTestingModule({ @@ -58,13 +85,23 @@ describe('DSOSelectorComponent', () => { expect(component).toBeTruthy(); }); - it('should initially call the search method on the SearchService with the given DSO uuid', () => { - const searchOptions = new PaginatedSearchOptions({ - query: currentDSOId, - dsoTypes: [type], - pagination: (component as any).defaultPagination + describe('populating listEntries', () => { + it('should not be empty', () => { + expect(component.listEntries.length).toBeGreaterThan(0); }); - expect(searchService.search).toHaveBeenCalledWith(searchOptions); + it('should contain a combination of the current DSO and first page results', () => { + expect(component.listEntries).toEqual([searchResult, ...firstPageResults]); + }); + + describe('when current page increases', () => { + beforeEach(() => { + component.currentPage$.next(2); + }); + + it('should contain a combination of the current DSO, as well as first and second page results', () => { + expect(component.listEntries).toEqual([searchResult, ...firstPageResults, ...nextPageResults]); + }); + }); }); }); diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts index 37c9a99f59..5bbcec4262 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts @@ -3,28 +3,33 @@ import { ElementRef, EventEmitter, Input, + OnDestroy, OnInit, Output, QueryList, ViewChildren } from '@angular/core'; import { FormControl } from '@angular/forms'; - -import { Observable } from 'rxjs'; -import { debounceTime, startWith, switchMap } from 'rxjs/operators'; +import { debounceTime, map, startWith, switchMap, tap } from 'rxjs/operators'; import { SearchService } from '../../../core/shared/search/search.service'; import { CollectionElementLinkType } from '../../object-collection/collection-element-link.type'; import { PaginatedSearchOptions } from '../../search/paginated-search-options.model'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; -import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list'; -import { SearchResult } from '../../search/search-result.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { ViewMode } from '../../../core/shared/view-mode.model'; +import { Context } from '../../../core/shared/context.model'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { hasValue, isEmpty, isNotEmpty } from '../../empty.util'; +import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { SearchResult } from '../../search/search-result.model'; @Component({ selector: 'ds-dso-selector', - // styleUrls: ['./dso-selector.component.scss'], + styleUrls: ['./dso-selector.component.scss'], templateUrl: './dso-selector.component.html' }) @@ -32,7 +37,7 @@ import { ViewMode } from '../../../core/shared/view-mode.model'; * Component to render a list of DSO's of which one can be selected * The user can search the list by using the input field */ -export class DSOSelectorComponent implements OnInit { +export class DSOSelectorComponent implements OnInit, OnDestroy { /** * The view mode of the listed objects */ @@ -63,12 +68,29 @@ export class DSOSelectorComponent implements OnInit { /** * Default pagination for this feature */ - private defaultPagination = { id: 'dso-selector', currentPage: 1, pageSize: 5 } as any; + defaultPagination = { id: 'dso-selector', currentPage: 1, pageSize: 10 } as any; /** * List with search results of DSpace objects for the current query */ - listEntries$: Observable>>>; + listEntries: Array> = []; + + /** + * The current page to load + * Dynamically goes up as the user scrolls down until it reaches the last page possible + */ + currentPage$ = new BehaviorSubject(1); + + /** + * Whether or not the list contains a next page to load + * This allows us to avoid next pages from trying to load when there are none + */ + hasNextPage = false; + + /** + * Whether or not the list should be reset next time it receives a page to load + */ + resetList = false; /** * List of element references to all elements @@ -85,31 +107,107 @@ export class DSOSelectorComponent implements OnInit { */ linkTypes = CollectionElementLinkType; - constructor(private searchService: SearchService) { + /** + * Track whether the element has the mouse over it + */ + isMouseOver = false + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + public subs: Subscription[] = []; + + constructor(protected searchService: SearchService) { } /** - * Fills the listEntries$ variable with search results based on the input field's current value + * Fills the listEntries variable with search results based on the input field's current value and the current page * The search will always start with the initial currentDSOId value */ ngOnInit(): void { - this.input.setValue(this.currentDSOId); this.typesString = this.types.map((type: string) => type.toString().toLowerCase()).join(', '); - this.listEntries$ = this.input.valueChanges - .pipe( + + // Create an observable searching for the current DSO (return empty list if there's no current DSO) + let currentDSOResult$; + if (isNotEmpty(this.currentDSOId)) { + currentDSOResult$ = this.search(this.getCurrentDSOQuery(), 1); + } else { + currentDSOResult$ = observableOf(new PaginatedList(undefined, [])); + } + + // Combine current DSO, query and page + this.subs.push(observableCombineLatest( + currentDSOResult$, + this.input.valueChanges.pipe( debounceTime(this.debounceTime), - startWith(this.currentDSOId), - switchMap((query) => { - return this.searchService.search( - new PaginatedSearchOptions({ - query: query, - dsoTypes: this.types, - pagination: this.defaultPagination - }) - ) - } - ) - ) + startWith(''), + tap(() => this.currentPage$.next(1)) + ), + this.currentPage$ + ).pipe( + switchMap(([currentDSOResult, query, page]: [PaginatedList>, string, number]) => { + if (page === 1) { + // The first page is loading, this means we should reset the list instead of adding to it + this.resetList = true; + } + return this.search(query, page).pipe( + map((list) => { + // If it's the first page and no query is entered, add the current DSO to the start of the list + // If no query is entered, filter out the current DSO from the results, as it'll be displayed at the start of the list already + list.page = [ + ...((isEmpty(query) && page === 1) ? currentDSOResult.page : []), + ...list.page.filter((result) => isNotEmpty(query) || result.indexableObject.id !== this.currentDSOId) + ]; + return list; + }) + ); + }) + ).subscribe((list) => { + if (this.resetList) { + this.listEntries = list.page; + this.resetList = false; + } else { + this.listEntries.push(...list.page); + } + // Check if there are more pages available after the current one + this.hasNextPage = list.totalElements > this.listEntries.length; + })); + } + + /** + * Get a query to send for retrieving the current DSO + */ + getCurrentDSOQuery(): string { + return `search.resourceid:${this.currentDSOId}`; + } + + /** + * Perform a search for the current query and page + * @param query Query to search objects for + * @param page Page to retrieve + */ + search(query: string, page: number): Observable>> { + return this.searchService.search( + new PaginatedSearchOptions({ + query: query, + dsoTypes: this.types, + pagination: Object.assign({}, this.defaultPagination, { + currentPage: page + }) + }) + ).pipe( + getFirstSucceededRemoteDataPayload() + ); + } + + /** + * When the user reaches the bottom of the page (or almost) and there's a next page available, increase the current page + */ + onScrollDown() { + if (this.hasNextPage) { + this.currentPage$.next(this.currentPage$.value + 1); + } } /** @@ -120,4 +218,22 @@ export class DSOSelectorComponent implements OnInit { this.listElements.first.nativeElement.click(); } } + + /** + * Get the context for element with the given id + */ + getContext(id: string) { + if (id === this.currentDSOId) { + return Context.SideBarSearchModalCurrent; + } else { + return Context.SideBarSearchModal; + } + } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } } diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts index 9e2710c9d3..64274f21e2 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts @@ -22,6 +22,7 @@ export class CreateCollectionParentSelectorComponent extends DSOSelectorModalWra objectType = DSpaceObjectType.COLLECTION; selectorTypes = [DSpaceObjectType.COMMUNITY]; action = SelectorActionType.CREATE; + header = 'dso-selector.create.collection.sub-level'; constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { super(activeModal, route); diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html index ef8865ad87..8761e4eb9e 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html @@ -5,7 +5,7 @@
diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts index 5729ac8460..03d7732fb0 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts @@ -20,6 +20,7 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo objectType = DSpaceObjectType.ITEM; selectorTypes = [DSpaceObjectType.COLLECTION]; action = SelectorActionType.CREATE; + header = 'dso-selector.create.item.sub-level'; constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { super(activeModal, route); diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html index e1c18ec1e0..85d8797e66 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html @@ -5,6 +5,7 @@
diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts index b56a901b12..59aeceea0f 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts @@ -23,6 +23,12 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { */ @Input() dsoRD: RemoteData; + /** + * Optional header to display above the selection list + * Supports i18n keys + */ + @Input() header: string; + /** * The type of the DSO that's being edited, created or exported */ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index f762a8c1c4..105aa86204 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -407,7 +407,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo modalComp.repeatable = this.model.repeatable; modalComp.listId = this.listId; modalComp.relationshipOptions = this.model.relationship; - modalComp.label = this.model.label; + modalComp.label = this.model.relationship.relationshipType; modalComp.metadataFields = this.model.metadataFields; modalComp.item = this.item; modalComp.collection = this.collection; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts index 4a47ce5903..930dfe83d9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts @@ -54,7 +54,7 @@ describe('ExistingMetadataListElementComponent', () => { relationship = Object.assign(new Relationship(), { leftItem: leftItemRD$, rightItem: rightItemRD$ }); submissionId = '1234'; - reoRel = new ReorderableRelationship(relationship, true, relationshipService, {} as any, submissionId); + reoRel = new ReorderableRelationship(relationship, true, {} as any, {} as any, submissionId); } beforeEach(async(() => { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts index d4ce3342e7..678402a1bc 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -138,7 +138,7 @@ export class ReorderableRelationship extends Reorderable { templateUrl: './existing-metadata-list-element.component.html', styleUrls: ['./existing-metadata-list-element.component.scss'] }) -export class ExistingMetadataListElementComponent implements OnInit, OnChanges, OnDestroy { +export class ExistingMetadataListElementComponent implements OnInit, OnChanges, OnDestroy { @Input() listId: string; @Input() submissionItem: Item; @Input() reoRel: ReorderableRelationship; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts index c110c851fd..38e28d742c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; -import { combineLatest, Observable, Subscription, zip as observableZip } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { hasValue } from '../../../../empty.util'; import { map, skip, switchMap, take } from 'rxjs/operators'; @@ -157,7 +157,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy select(...selectableObjects: Array>) { this.zone.runOutsideAngular( () => { - const obs: Observable = combineLatest(...selectableObjects.map((sri: SearchResult) => { + const obs: Observable = observableCombineLatest(...selectableObjects.map((sri: SearchResult) => { this.addNameVariantSubscription(sri); return this.relationshipService.getNameVariant(this.listId, sri.indexableObject.uuid) .pipe( @@ -223,7 +223,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy switchMap((options) => this.lookupRelationService.getTotalLocalResults(this.relationshipOptions, options)) ); - const externalSourcesAndOptions$ = combineLatest( + const externalSourcesAndOptions$ = observableCombineLatest( this.externalSourcesRD$.pipe( getAllSucceededRemoteData(), getRemoteDataPayload() @@ -233,7 +233,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy this.totalExternal$ = externalSourcesAndOptions$.pipe( switchMap(([sources, options]) => - observableZip(...sources.page.map((source: ExternalSource) => this.lookupRelationService.getTotalExternalResults(source, options)))) + observableCombineLatest(...sources.page.map((source: ExternalSource) => this.lookupRelationService.getTotalExternalResults(source, options)))) ); } diff --git a/src/app/shared/hover-class.directive.spec.ts b/src/app/shared/hover-class.directive.spec.ts new file mode 100644 index 0000000000..9c593f27c8 --- /dev/null +++ b/src/app/shared/hover-class.directive.spec.ts @@ -0,0 +1,35 @@ +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HoverClassDirective } from './hover-class.directive'; +import { By } from '@angular/platform-browser'; + +@Component({ + template: `
` +}) +class TestComponent { } + +describe('HoverClassDirective', () => { + let component: TestComponent; + let fixture: ComponentFixture; + let el: DebugElement; + + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + declarations: [TestComponent, HoverClassDirective] + }).createComponent(TestComponent); + + fixture.detectChanges(); + component = fixture.componentInstance; + el = fixture.debugElement.query(By.css('div')); + }); + + it('should add the class on mouseenter and remove on mouseleave', () => { + el.triggerEventHandler('mouseenter', null); + fixture.detectChanges(); + expect(el.nativeElement.classList).toContain('ds-hover'); + + el.triggerEventHandler('mouseleave', null); + fixture.detectChanges(); + expect(el.nativeElement.classList).not.toContain('ds-hover'); + }); +}); diff --git a/src/app/shared/hover-class.directive.ts b/src/app/shared/hover-class.directive.ts new file mode 100644 index 0000000000..551e81a463 --- /dev/null +++ b/src/app/shared/hover-class.directive.ts @@ -0,0 +1,31 @@ +import { Directive, ElementRef, HostListener, Input } from '@angular/core'; + +@Directive({ + selector: '[dsHoverClass]' +}) +/** + * A directive adding a class to an element when hovered over + */ +export class HoverClassDirective { + /** + * The name of the class to add on hover + */ + @Input('dsHoverClass') hoverClass: string; +​ + constructor(public elementRef: ElementRef) { } +​ + /** + * On mouse enter, add the class to the element's class list + */ + @HostListener('mouseenter') onMouseEnter() { + this.elementRef.nativeElement.classList.add(this.hoverClass); + } +​ + /** + * On mouse leave, remove the class from the element's class list + */ + @HostListener('mouseleave') onMouseLeave() { + this.elementRef.nativeElement.classList.remove(this.hoverClass); + } +​ +} diff --git a/src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.html b/src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.html index ca3b086653..921999b540 100644 --- a/src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.html +++ b/src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.html @@ -1,4 +1,4 @@ -
+