Merge branch 'master' into w2p-65240_Community-and-collection-logos

Conflicts:
	src/app/shared/shared.module.ts
This commit is contained in:
Kristof De Langhe
2020-01-07 16:11:24 +01:00
367 changed files with 7031 additions and 1766 deletions

View File

@@ -15,7 +15,11 @@ module.exports = function (config) {
};
var configuration = {
client: {
jasmine: {
random: false
}
},
// base path that will be used to resolve all patterns (e.g. files, exclude)
basePath: '',

View File

@@ -140,6 +140,7 @@
"text-mask-core": "5.0.1",
"ts-loader": "^5.2.1",
"ts-md5": "^1.2.4",
"url-parse": "^1.4.7",
"uuid": "^3.2.1",
"webfontloader": "1.6.28",
"webpack-cli": "^3.1.0",

View File

@@ -541,6 +541,9 @@
"footer.link.duraspace": "DuraSpace",
"form.add": "Add",
"form.add-help": "Click here to add the current entry and to add another one",
"form.cancel": "Cancel",
@@ -566,6 +569,10 @@
"form.loading": "Loading...",
"form.lookup": "Lookup",
"form.lookup-help": "Click here to look up an existing relation",
"form.no-results": "No results found",
"form.no-value": "No value entered",
@@ -806,7 +813,7 @@
"item.edit.tabs.relationships.head": "Item Relationships",
"item.edit.tabs.relationships.title": "Item Edit - Relationships",
"item.edit.tabs.relationships.title": "Item Edit - Relationships",
"item.edit.tabs.status.buttons.authorizations.button": "Authorizations...",
@@ -904,9 +911,9 @@
"item.page.person.search.title": "Articles by this author",
"item.page.related-items.view-more": "View more",
"item.page.related-items.view-more": "Show {{ amount }} more",
"item.page.related-items.view-less": "View less",
"item.page.related-items.view-less": "Hide last {{ amount }}",
"item.page.relationships.isAuthorOfPublication": "Publications",
@@ -1443,6 +1450,9 @@
"search.filters.applied.f.subject": "Subject",
"search.filters.applied.f.submitter": "Submitter",
"search.filters.applied.f.jobTitle": "Job Title",
"search.filters.applied.f.birthDate.max": "End birth date",
"search.filters.applied.f.birthDate.min": "Start birth date",
@@ -1611,6 +1621,69 @@
"submission.general.save-later": "Save for later",
"submission.sections.describe.relationship-lookup.close": "Close",
"submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Deselect all",
"submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Deselect page",
"submission.sections.describe.relationship-lookup.search-tab.loading": "Loading...",
"submission.sections.describe.relationship-lookup.search-tab.placeholder": "Search query",
"submission.sections.describe.relationship-lookup.search-tab.search": "Go",
"submission.sections.describe.relationship-lookup.search-tab.select-all": "Select all",
"submission.sections.describe.relationship-lookup.search-tab.select-page": "Select page",
"submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items",
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Search for Authors",
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Search for Journals",
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Search for Journal Issues",
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Search for Journal Volumes",
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Search for Funding Agencies",
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding": "Search for Funding",
"submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})",
"submission.sections.describe.relationship-lookup.title.Journal Issue": "Journal Issues",
"submission.sections.describe.relationship-lookup.title.Journal Volume": "Journal Volumes",
"submission.sections.describe.relationship-lookup.title.Journal": "Journals",
"submission.sections.describe.relationship-lookup.title.Author": "Authors",
"submission.sections.describe.relationship-lookup.title.Funding Agency": "Funding Agency",
"submission.sections.describe.relationship-lookup.title.Funding": "Funding",
"submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Toggle dropdown",
"submission.sections.describe.relationship-lookup.selection-tab.settings": "Settings",
"submission.sections.describe.relationship-lookup.selection-tab.no-selection": "Your selection is currently empty.",
"submission.sections.describe.relationship-lookup.selection-tab.title.Author": "Selected Authors",
"submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Selected Journals",
"submission.sections.describe.relationship-lookup.selection-tab.title.Journal Volume": "Selected Journal Volume",
"submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue",
"submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.",
"submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant",
"submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Use only for this submission",
"submission.sections.general.add-more": "Add more",

View File

@@ -13,7 +13,6 @@ import { combineLatest as combineLatestObservable } from 'rxjs';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';

View File

@@ -1,29 +1,23 @@
import { CollectionItemMapperComponent } from './collection-item-mapper.component';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { CommonModule } from '@angular/common';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { SearchFormComponent } from '../../shared/search-form/search-form.component';
import { SearchPageModule } from '../../+search-page/search-page.module';
import { ObjectCollectionComponent } from '../../shared/object-collection/object-collection.component';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
import { RouterStub } from '../../shared/testing/router-stub';
import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service';
import { SearchService } from '../../+search-page/search-service/search.service';
import { SearchServiceStub } from '../../shared/testing/search-service-stub';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
import { ItemDataService } from '../../core/data/item-data.service';
import { FormsModule } from '@angular/forms';
import { SharedModule } from '../../shared/shared.module';
import { Collection } from '../../core/shared/collection.model';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { EventEmitter, NgModule } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub';
import { By } from '@angular/platform-browser';
@@ -36,13 +30,14 @@ import { ItemSelectComponent } from '../../shared/object-select/item-select/item
import { ObjectSelectService } from '../../shared/object-select/object-select.service';
import { ObjectSelectServiceStub } from '../../shared/testing/object-select-service-stub';
import { VarDirective } from '../../shared/utils/var.directive';
import { Observable } from 'rxjs/internal/Observable';
import { of as observableOf, of } from 'rxjs/internal/observable/of';
import { RestResponse } from '../../core/cache/response.models';
import { SearchFixedFilterService } from '../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { RouteService } from '../../core/services/route.service';
import { ErrorComponent } from '../../shared/error/error.component';
import { LoadingComponent } from '../../shared/loading/loading.component';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { SearchService } from '../../core/shared/search/search.service';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
describe('CollectionItemMapperComponent', () => {
let comp: CollectionItemMapperComponent;
@@ -135,7 +130,6 @@ describe('CollectionItemMapperComponent', () => {
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
{ provide: RouteService, useValue: routeServiceStub },
{ provide: SearchFixedFilterService, useValue: fixedFilterServiceStub }
]
}).compileComponents();
}));

View File

@@ -5,12 +5,9 @@ import { fadeIn, fadeInOut } from '../../shared/animations/fade';
import { ActivatedRoute, Router } from '@angular/router';
import { RemoteData } from '../../core/data/remote-data';
import { Collection } from '../../core/shared/collection.model';
import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service';
import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { map, startWith, switchMap, take, tap } from 'rxjs/operators';
import { map, startWith, switchMap, take } from 'rxjs/operators';
import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators';
import { SearchService } from '../../+search-page/search-service/search.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
@@ -22,6 +19,9 @@ import { isNotEmpty } from '../../shared/empty.util';
import { RestResponse } from '../../core/cache/response.models';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { SearchService } from '../../core/shared/search/search.service';
@Component({
selector: 'ds-collection-item-mapper',

View File

@@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, of as observableOf, Observable, Subject } from 'rxjs';
import { filter, flatMap, map, startWith, switchMap, take, tap } from 'rxjs/operators';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { SearchService } from '../+search-page/search-service/search.service';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SearchService } from '../core/shared/search/search.service';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { CollectionDataService } from '../core/data/collection-data.service';
import { PaginatedList } from '../core/data/paginated-list';

View File

@@ -8,9 +8,8 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module';
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
import { CollectionFormComponent } from './collection-form/collection-form.component';
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
import { SearchService } from '../+search-page/search-service/search.service';
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { SearchService } from '../core/shared/search/search.service';
import { StatisticsModule } from '../statistics/statistics.module';
@NgModule({
@@ -32,7 +31,6 @@ import { StatisticsModule } from '../statistics/statistics.module';
],
providers: [
SearchService,
SearchFixedFilterService
]
})
export class CollectionPageModule {

View File

@@ -1,15 +1,12 @@
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { CommonModule } from '@angular/common';
import { ItemCollectionMapperComponent } from './item-collection-mapper.component';
import { ActivatedRoute, Router } from '@angular/router';
import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service';
import { SearchService } from '../../../+search-page/search-service/search.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { ItemDataService } from '../../../core/data/item-data.service';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { RouterStub } from '../../../shared/testing/router-stub';
@@ -19,7 +16,6 @@ import { SearchServiceStub } from '../../../shared/testing/search-service-stub';
import { PaginatedList } from '../../../core/data/paginated-list';
import { PageInfo } from '../../../core/shared/page-info.model';
import { FormsModule } from '@angular/forms';
import { SharedModule } from '../../../shared/shared.module';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { HostWindowService } from '../../../shared/host-window.service';
@@ -28,7 +24,6 @@ import { By } from '@angular/platform-browser';
import { Item } from '../../../core/shared/item.model';
import { ObjectSelectService } from '../../../shared/object-select/object-select.service';
import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-service-stub';
import { Observable } from 'rxjs/internal/Observable';
import { of } from 'rxjs/internal/observable/of';
import { RestResponse } from '../../../core/cache/response.models';
import { CollectionSelectComponent } from '../../../shared/object-select/collection-select/collection-select.component';
@@ -39,6 +34,9 @@ import { SearchFormComponent } from '../../../shared/search-form/search-form.com
import { Collection } from '../../../core/shared/collection.model';
import { ErrorComponent } from '../../../shared/error/error.component';
import { LoadingComponent } from '../../../shared/loading/loading.component';
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SearchService } from '../../../core/shared/search/search.service';
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
describe('ItemCollectionMapperComponent', () => {
let comp: ItemCollectionMapperComponent;

View File

@@ -2,15 +2,12 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core';
import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { Collection } from '../../../core/shared/collection.model';
import { Item } from '../../../core/shared/item.model';
import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { SearchService } from '../../../+search-page/search-service/search.service';
import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service';
import { map, startWith, switchMap, take } from 'rxjs/operators';
import { ItemDataService } from '../../../core/data/item-data.service';
import { TranslateService } from '@ngx-translate/core';
@@ -19,6 +16,9 @@ import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'
import { isNotEmpty } from '../../../shared/empty.util';
import { RestResponse } from '../../../core/cache/response.models';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SearchService } from '../../../core/shared/search/search.service';
@Component({
selector: 'ds-item-collection-mapper',

View File

@@ -9,7 +9,6 @@ import { ActivatedRoute, Router } from '@angular/router';
import { ItemMoveComponent } from './item-move.component';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { SearchService } from '../../../+search-page/search-service/search.service';
import { of as observableOf } from 'rxjs';
import { FormsModule } from '@angular/forms';
import { ItemDataService } from '../../../core/data/item-data.service';
@@ -18,6 +17,7 @@ import { PaginatedList } from '../../../core/data/paginated-list';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RestResponse } from '../../../core/cache/response.models';
import { Collection } from '../../../core/shared/collection.model';
import { SearchService } from '../../../core/shared/search/search.service';
describe('ItemMoveComponent', () => {
let comp: ItemMoveComponent;

View File

@@ -1,12 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { SearchService } from '../../../+search-page/search-service/search.service';
import { first, map } from 'rxjs/operators';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { SearchOptions } from '../../../+search-page/search-options.model';
import { RemoteData } from '../../../core/data/remote-data';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { PaginatedList } from '../../../core/data/paginated-list';
import { SearchResult } from '../../../+search-page/search-result.model';
import { Item } from '../../../core/shared/item.model';
import { ActivatedRoute, Router } from '@angular/router';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
@@ -17,9 +14,10 @@ import { getItemEditPath } from '../../item-page-routing.module';
import { Observable, of as observableOf } from 'rxjs';
import { RestResponse } from '../../../core/cache/response.models';
import { Collection } from '../../../core/shared/collection.model';
import { tap } from 'rxjs/internal/operators/tap';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model';
import { SearchService } from '../../../core/shared/search/search.service';
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
import { SearchResult } from '../../../shared/search/search-result.model';
@Component({
selector: 'ds-item-move',

View File

@@ -156,7 +156,9 @@ describe('ItemRelationshipsComponent', () => {
getRelatedItemsByLabel: observableOf([author1, author2]),
getItemRelationshipsArray: observableOf(relationships),
deleteRelationship: observableOf(new RestResponse(true, 200, 'OK')),
getItemResolvedRelatedItemsAndRelationships: observableCombineLatest(observableOf([author1, author2]), observableOf([item, item]), observableOf(relationships))
getItemResolvedRelatedItemsAndRelationships: observableCombineLatest(observableOf([author1, author2]), observableOf([item, item]), observableOf(relationships)),
getRelationshipsByRelatedItemIds: observableOf(relationships),
getRelationshipTypeLabelsByItem: observableOf([relationshipType.leftwardType])
}
);

View File

@@ -2,8 +2,8 @@ import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
import { Observable } from 'rxjs/internal/Observable';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs';
import { filter, flatMap, map, switchMap, take, tap } from 'rxjs/operators';
import { zip as observableZip } from 'rxjs';
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
@@ -21,7 +21,6 @@ import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { getSucceededRemoteData } from '../../../core/shared/operators';
import { RequestService } from '../../../core/data/request.service';
import { Subscription } from 'rxjs/internal/Subscription';
import { getRelationsByRelatedItemIds } from '../../simple/item-types/shared/item-relationships-utils';
@Component({
selector: 'ds-item-relationships',
@@ -65,7 +64,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
*/
ngOnInit(): void {
super.ngOnInit();
this.relationLabels$ = this.relationshipService.getItemRelationshipLabels(this.item);
this.relationLabels$ = this.relationshipService.getRelationshipTypeLabelsByItem(this.item);
this.initializeItemUpdate();
}
@@ -113,8 +112,9 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
);
// Get all the relationships that should be removed
const removedRelationships$ = removedItemIds$.pipe(
getRelationsByRelatedItemIds(this.item, this.relationshipService)
flatMap((uuids) => this.relationshipService.getRelationshipsByRelatedItemIds(this.item, uuids))
);
// const removedRelationships$ = removedItemIds$.pipe(flatMap((uuids: string[]) => this.relationshipService.getRelationshipsByRelatedItemIds(this.item, uuids)));
// Request a delete for every relationship found in the observable created above
removedRelationships$.pipe(
take(1),

View File

@@ -28,6 +28,7 @@ import { MetadataValuesComponent } from './field-components/metadata-values/meta
import { MetadataFieldWrapperComponent } from './field-components/metadata-field-wrapper/metadata-field-wrapper.component';
import { TabbedRelatedEntitiesSearchComponent } from './simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component';
import { StatisticsModule } from '../statistics/statistics.module';
import { AbstractIncrementalListComponent } from './simple/abstract-incremental-list/abstract-incremental-list.component';
@NgModule({
imports: [
@@ -57,7 +58,8 @@ import { StatisticsModule } from '../statistics/statistics.module';
GenericItemPageFieldComponent,
MetadataRepresentationListComponent,
RelatedEntitiesSearchComponent,
TabbedRelatedEntitiesSearchComponent
TabbedRelatedEntitiesSearchComponent,
AbstractIncrementalListComponent
],
exports: [
ItemComponent,

View File

@@ -0,0 +1,73 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs/internal/Subscription';
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
@Component({
selector: 'ds-abstract-incremental-list',
template: ``,
})
/**
* An abstract component for displaying an incremental list of objects
*/
export class AbstractIncrementalListComponent<T> implements OnInit, OnDestroy {
/**
* The amount to increment the list by
* Define this amount in the child component overriding this component
*/
incrementBy: number;
/**
* All pages of objects to display as an array
*/
objects: T[];
/**
* A list of open subscriptions
*/
subscriptions: Subscription[];
ngOnInit(): void {
this.objects = [];
this.subscriptions = [];
this.increase();
}
/**
* Get a specific page
* > Override this method to return a specific page
* @param page The page to fetch
*/
getPage(page: number): T {
return undefined;
}
/**
* Increase the amount displayed
*/
increase() {
const page = this.getPage(this.objects.length + 1);
if (hasValue(page)) {
this.objects.push(page);
}
}
/**
* Decrease the amount displayed
*/
decrease() {
if (this.objects.length > 1) {
this.objects.pop();
}
}
/**
* Unsubscribe from any open subscriptions
*/
ngOnDestroy(): void {
if (isNotEmpty(this.subscriptions)) {
this.subscriptions.forEach((sub: Subscription) => {
sub.unsubscribe();
});
}
}
}

View File

@@ -4,7 +4,6 @@ import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loa
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { Item } from '../../../../core/shared/item.model';
@@ -15,6 +14,7 @@ import { createRelationshipsObservable } from '../shared/item.component.spec';
import { PublicationComponent } from './publication.component';
import { MetadataMap } from '../../../../core/shared/metadata.models';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
import { RelationshipService } from '../../../../core/data/relationship.service';
const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
@@ -26,12 +26,6 @@ describe('PublicationComponent', () => {
let comp: PublicationComponent;
let fixture: ComponentFixture<PublicationComponent>;
const searchFixedFilterServiceStub = {
/* tslint:disable:no-empty */
getQueryByRelations: () => {}
/* tslint:enable:no-empty */
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
@@ -43,8 +37,8 @@ describe('PublicationComponent', () => {
declarations: [PublicationComponent, GenericItemPageFieldComponent, TruncatePipe],
providers: [
{provide: ItemDataService, useValue: {}},
{provide: SearchFixedFilterService, useValue: searchFixedFilterServiceStub},
{provide: TruncatableService, useValue: {}}
{provide: TruncatableService, useValue: {}},
{provide: RelationshipService, useValue: {}}
],
schemas: [NO_ERRORS_SCHEMA]

View File

@@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ItemComponent } from '../shared/item.component';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Item } from '../../../../core/shared/item.model';
import { ItemComponent } from '../shared/item.component';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
/**
* Component that represents a publication Item page

View File

@@ -1,14 +1,12 @@
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
import { getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util';
import { Observable } from 'rxjs/internal/Observable';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { distinctUntilChanged, flatMap, map, switchMap } from 'rxjs/operators';
import { zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs';
import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { RelationshipService } from '../../../../core/data/relationship.service';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data';
/**
* Operator for comparing arrays using a mapping function
@@ -37,36 +35,6 @@ export const compareArraysUsing = <T>(mapFn: (t: T) => any) =>
export const compareArraysUsingIds = <T extends { id: string }>() =>
compareArraysUsing((t: T) => hasValue(t) ? t.id : undefined);
/**
* Fetch the relationships which match the type label given
* @param {string} label Type label
* @param thisId The item's id of which the relations belong to
* @returns {(source: Observable<[Relationship[] , RelationshipType[]]>) => Observable<Relationship[]>}
*/
export const filterRelationsByTypeLabel = (label: string, thisId?: string) =>
(source: Observable<[Relationship[], RelationshipType[]]>): Observable<Relationship[]> =>
source.pipe(
switchMap(([relsCurrentPage, relTypesCurrentPage]) => {
const relatedItems$ = observableZip(...relsCurrentPage.map((rel: Relationship) =>
observableCombineLatest(
rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()))
)
);
return relatedItems$.pipe(
map((arr) => relsCurrentPage.filter((rel: Relationship, idx: number) =>
hasValue(relTypesCurrentPage[idx]) && (
(hasNoValue(thisId) && (relTypesCurrentPage[idx].leftwardType === label ||
relTypesCurrentPage[idx].rightwardType === label)) ||
(thisId === arr[idx][0].id && relTypesCurrentPage[idx].leftwardType === label) ||
(thisId === arr[idx][1].id && relTypesCurrentPage[idx].rightwardType === label)
)
))
);
}),
distinctUntilChanged(compareArraysUsingIds())
);
/**
* Operator for turning a list of relationships into a list of the relevant items
* @param {string} thisId The item's id of which the relations belong to
@@ -128,17 +96,3 @@ export const paginatedRelationsToItems = (thisId: string) =>
)
})
);
/**
* Operator for fetching an item's relationships, but filtered by related item IDs (essentially performing a reverse lookup)
* Only relationships where leftItem or rightItem's ID is present in the list provided will be returned
* @param item
* @param relationshipService
*/
export const getRelationsByRelatedItemIds = (item: Item, relationshipService: RelationshipService) =>
(source: Observable<string[]>): Observable<Relationship[]> =>
source.pipe(
flatMap((relatedItemIds: string[]) => relationshipService.getItemResolvedRelatedItemsAndRelationships(item).pipe(
map(([leftItems, rightItems, rels]) => rels.filter((rel: Relationship, index: number) => relatedItemIds.indexOf(leftItems[index].uuid) > -1 || relatedItemIds.indexOf(rightItems[index].uuid) > -1))
))
);

View File

@@ -9,7 +9,6 @@ import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loa
import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { isNotEmpty } from '../../../../shared/empty.util';
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
@@ -24,6 +23,7 @@ import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-rep
import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.models';
import { compareArraysUsing, compareArraysUsingIds } from './item-relationships-utils';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
import { RelationshipService } from '../../../../core/data/relationship.service';
/**
* Create a generic test for an item-page-fields component using a mockItem and the type of component
@@ -37,12 +37,6 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
let comp: any;
let fixture: ComponentFixture<any>;
const searchFixedFilterServiceStub = {
/* tslint:disable:no-empty */
getQueryByRelations: () => {}
/* tslint:enable:no-empty */
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
@@ -54,8 +48,8 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
declarations: [component, GenericItemPageFieldComponent, TruncatePipe],
providers: [
{provide: ItemDataService, useValue: {}},
{provide: SearchFixedFilterService, useValue: searchFixedFilterServiceStub},
{provide: TruncatableService, useValue: {}}
{provide: TruncatableService, useValue: {}},
{provide: RelationshipService, useValue: {}}
],
schemas: [NO_ERRORS_SCHEMA]

View File

@@ -1,4 +1,4 @@
import { Component, Inject, Input } from '@angular/core';
import { Component, Input } from '@angular/core';
import { Item } from '../../../../core/shared/item.model';
@Component({

View File

@@ -1,11 +1,20 @@
<ds-metadata-field-wrapper *ngIf="representations$ && (representations$ | async)?.length > 0" [label]="label">
<ds-metadata-representation-loader *ngFor="let rep of (representations$ | async)"
[mdRepresentation]="rep">
</ds-metadata-representation-loader>
<div *ngIf="(representations$ | async)?.length < total" class="mt-2">
<a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a>
</div>
<div *ngIf="limit > originalLimit" class="mt-2">
<a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a>
</div>
<ds-metadata-field-wrapper [label]="label">
<ng-container *ngFor="let objectPage of objects; let i = index">
<ng-container *ngVar="(objectPage | async) as representations">
<ds-metadata-representation-loader *ngFor="let rep of representations"
[mdRepresentation]="rep">
</ds-metadata-representation-loader>
<ds-loading *ngIf="(i + 1) === objects.length && (i > 0) && (!representations || representations?.length === 0)" message="{{'loading.default' | translate}}"></ds-loading>
<div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && representations?.length > 0">
<div *ngIf="(objects.length * incrementBy) < total" class="float-left">
<a [routerLink]="" (click)="increase()">{{'item.page.related-items.view-more' |
translate:{ amount: (total - (objects.length * incrementBy) < incrementBy) ? total - (objects.length * incrementBy) : incrementBy } }}</a>
</div>
<div *ngIf="objects.length > 1" class="float-right">
<a [routerLink]="" (click)="decrease()">{{'item.page.related-items.view-less' |
translate:{ amount: representations?.length } }}</a>
</div>
</div>
</ng-container>
</ng-container>
</ds-metadata-field-wrapper>

View File

@@ -7,6 +7,8 @@ import { Item } from '../../../core/shared/item.model';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { TranslateModule } from '@ngx-translate/core';
import { VarDirective } from '../../../shared/utils/var.directive';
import { of as observableOf } from 'rxjs';
const itemType = 'Person';
const metadataField = 'dc.contributor.author';
@@ -64,7 +66,7 @@ describe('MetadataRepresentationListComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [MetadataRepresentationListComponent],
declarations: [MetadataRepresentationListComponent, VarDirective],
providers: [
{ provide: RelationshipService, useValue: relationshipService }
],
@@ -88,33 +90,29 @@ describe('MetadataRepresentationListComponent', () => {
expect(fields.length).toBe(2);
});
it('should initialize the original limit', () => {
expect(comp.originalLimit).toEqual(comp.limit);
it('should contain one page of items', () => {
expect(comp.objects.length).toEqual(1);
});
describe('when viewMore is called', () => {
describe('when increase is called', () => {
beforeEach(() => {
comp.viewMore();
comp.increase();
});
it('should set the limit to a high number in order to retrieve all metadata representations', () => {
expect(comp.limit).toBeGreaterThanOrEqual(999);
it('should add a new page to the list', () => {
expect(comp.objects.length).toEqual(2);
});
});
describe('when viewLess is called', () => {
let originalLimit;
describe('when decrease is called', () => {
beforeEach(() => {
// Store the original value of limit
originalLimit = comp.limit;
// Set limit to a random number
comp.limit = 458;
comp.viewLess();
// Add a second page
comp.objects.push(observableOf(undefined));
comp.decrease();
});
it('should reset the limit to the original value', () => {
expect(comp.limit).toEqual(originalLimit);
it('should decrease the list of pages', () => {
expect(comp.objects.length).toEqual(1);
});
});

View File

@@ -1,16 +1,16 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs';
import { RelationshipService } from '../../../core/data/relationship.service';
import { Item } from '../../../core/shared/item.model';
import { combineLatest as observableCombineLatest, of as observableOf, zip as observableZip } from 'rxjs';
import { MetadataValue } from '../../../core/shared/metadata.models';
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { filter, map, switchMap } from 'rxjs/operators';
import { getSucceededRemoteData } from '../../../core/shared/operators';
import { filter, map, switchMap } from 'rxjs/operators';
import { RemoteData } from '../../../core/data/remote-data';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { Item } from '../../../core/shared/item.model';
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component';
@Component({
selector: 'ds-metadata-representation-list',
@@ -22,7 +22,7 @@ import { ItemMetadataRepresentation } from '../../../core/shared/metadata-repres
* It expects an itemType to resolve the metadata to a an item
* It expects a label to put on top of the list
*/
export class MetadataRepresentationListComponent implements OnInit {
export class MetadataRepresentationListComponent extends AbstractIncrementalListComponent<Observable<MetadataRepresentation[]>> {
/**
* The parent of the list of related items to display
*/
@@ -44,22 +44,11 @@ export class MetadataRepresentationListComponent implements OnInit {
@Input() label: string;
/**
* The max amount of representations to display
* The amount to increment the list by when clicking "view more"
* Defaults to 10
* The default can optionally be overridden by providing the limit as input to the component
*/
@Input() limit = 10;
/**
* A list of metadata-representations to display
*/
representations$: Observable<MetadataRepresentation[]>;
/**
* The originally provided limit
* Used for resetting the limit to the original value when collapsing the list
*/
originalLimit: number;
@Input() incrementBy = 10;
/**
* The total amount of metadata values available
@@ -67,30 +56,28 @@ export class MetadataRepresentationListComponent implements OnInit {
total: number;
constructor(public relationshipService: RelationshipService) {
}
ngOnInit(): void {
this.originalLimit = this.limit;
this.setRepresentations();
super();
}
/**
* Initialize the metadata representations
* Get a specific page
* @param page The page to fetch
*/
setRepresentations() {
getPage(page: number): Observable<MetadataRepresentation[]> {
const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataField);
this.total = metadata.length;
this.representations$ = this.resolveMetadataRepresentations(metadata);
return this.resolveMetadataRepresentations(metadata, page);
}
/**
* Resolve a list of metadata values to a list of metadata representations
* @param metadata
* @param metadata The list of all metadata values
* @param page The page to return representations for
*/
resolveMetadataRepresentations(metadata: MetadataValue[]): Observable<MetadataRepresentation[]> {
resolveMetadataRepresentations(metadata: MetadataValue[], page: number): Observable<MetadataRepresentation[]> {
return observableZip(
...metadata
.slice(0, this.limit)
.slice((this.objects.length * this.incrementBy), (this.objects.length * this.incrementBy) + this.incrementBy)
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
.map((metadatum: MetadataValue) => {
if (metadatum.isVirtual) {
@@ -115,20 +102,4 @@ export class MetadataRepresentationListComponent implements OnInit {
})
);
}
/**
* Expand the list to display all metadata representations
*/
viewMore() {
this.limit = 9999;
this.setRepresentations();
}
/**
* Collapse the list to display the originally displayed metadata representations
*/
viewLess() {
this.limit = this.originalLimit;
this.setRepresentations();
}
}

View File

@@ -4,13 +4,11 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { Item } from '../../../../core/shared/item.model';
describe('RelatedEntitiesSearchComponent', () => {
let comp: RelatedEntitiesSearchComponent;
let fixture: ComponentFixture<RelatedEntitiesSearchComponent>;
let fixedFilterService: SearchFixedFilterService;
const mockItem = Object.assign(new Item(), {
id: 'id1'
@@ -18,17 +16,11 @@ describe('RelatedEntitiesSearchComponent', () => {
const mockRelationType = 'publicationsOfAuthor';
const mockConfiguration = 'publication';
const mockFilter= `f.${mockRelationType}=${mockItem.id}`;
const fixedFilterServiceStub = {
getFilterByRelation: () => mockFilter
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
declarations: [RelatedEntitiesSearchComponent],
providers: [
{ provide: SearchFixedFilterService, useValue: fixedFilterServiceStub }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
@@ -36,7 +28,6 @@ describe('RelatedEntitiesSearchComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(RelatedEntitiesSearchComponent);
comp = fixture.componentInstance;
fixedFilterService = (comp as any).fixedFilterService;
comp.relationType = mockRelationType;
comp.item = mockItem;
comp.configuration = mockConfiguration;

View File

@@ -1,9 +1,9 @@
import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { isNotEmpty } from '../../../../shared/empty.util';
import { of } from 'rxjs/internal/observable/of';
import { getFilterByRelation } from '../../../../shared/utils/relation-query.utils';
@Component({
selector: 'ds-related-entities-search',
@@ -47,12 +47,9 @@ export class RelatedEntitiesSearchComponent implements OnInit {
fixedFilter: string;
configuration$: Observable<string>;
constructor(private fixedFilterService: SearchFixedFilterService) {
}
ngOnInit(): void {
if (isNotEmpty(this.relationType) && isNotEmpty(this.item)) {
this.fixedFilter = this.fixedFilterService.getFilterByRelation(this.relationType, this.item.id);
this.fixedFilter = getFilterByRelation(this.relationType, this.item.id);
}
if (isNotEmpty(this.configuration)) {
this.configuration$ = of(this.configuration);

View File

@@ -1,12 +1,12 @@
import { Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { RelationshipService } from '../../../core/data/relationship.service';
import { FindListOptions } from '../../../core/data/request.models';
import { Subscription } from 'rxjs/internal/Subscription';
import { ViewMode } from '../../../core/shared/view-mode.model';
import { RelationshipService } from '../../../core/data/relationship.service';
import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component';
@Component({
selector: 'ds-related-items',
@@ -17,7 +17,7 @@ import { ViewMode } from '../../../core/shared/view-mode.model';
* This component is used for displaying relations between items
* It expects a parent item and relationship type, as well as a label to display on top
*/
export class RelatedItemsComponent implements OnInit, OnDestroy {
export class RelatedItemsComponent extends AbstractIncrementalListComponent<Observable<RemoteData<PaginatedList<Item>>>> {
/**
* The parent of the list of related items to display
*/
@@ -30,79 +30,38 @@ export class RelatedItemsComponent implements OnInit, OnDestroy {
@Input() relationType: string;
/**
* Default options to start a search request with
* Optional input, should you wish a different page size (or other options)
* The amount to increment the list by when clicking "view more"
* Defaults to 5
* The default can optionally be overridden by providing the limit as input to the component
*/
@Input() options = Object.assign(new FindListOptions(), { elementsPerPage: 5 });
@Input() incrementBy = 5;
/**
* Default options to start a search request with
* Optional input
*/
@Input() options = new FindListOptions();
/**
* An i18n label to use as a title for the list (usually describes the relation)
*/
@Input() label: string;
/**
* Completely hide the component until there's at least one item visible
*/
@HostBinding('class.d-none') hidden = true;
/**
* The list of related items
*/
items$: Observable<RemoteData<PaginatedList<Item>>>;
/**
* Search options for displaying all elements in a list
*/
allOptions = Object.assign(new FindListOptions(), { elementsPerPage: 9999 });
/**
* The view-mode we're currently on
* @type {ElementViewMode}
* @type {ViewMode}
*/
viewMode = ViewMode.ListElement;
/**
* Whether or not the list is currently expanded to show all related items
*/
showingAll = false;
/**
* Subscription on items used to update the "hidden" property of this component
*/
itemSub: Subscription;
constructor(public relationshipService: RelationshipService) {
}
ngOnInit(): void {
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.options);
this.itemSub = this.items$.subscribe((itemsRD: RemoteData<PaginatedList<Item>>) => {
this.hidden = !(itemsRD.hasSucceeded && itemsRD.payload && itemsRD.payload.page.length > 0);
});
super();
}
/**
* Expand the list to display all related items
* Get a specific page
* @param page The page to fetch
*/
viewMore() {
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.allOptions);
this.showingAll = true;
}
/**
* Collapse the list to display the originally displayed items
*/
viewLess() {
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.options);
this.showingAll = false;
}
/**
* Unsubscribe from the item subscription
*/
ngOnDestroy(): void {
if (this.itemSub) {
this.itemSub.unsubscribe();
}
getPage(page: number): Observable<RemoteData<PaginatedList<Item>>> {
return this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, Object.assign(this.options, { elementsPerPage: this.incrementBy, currentPage: page }));
}
}

View File

@@ -1,11 +1,20 @@
<ds-metadata-field-wrapper *ngIf="(items$ | async)?.payload?.page?.length > 0" [label]="label">
<ds-listable-object-component-loader *ngFor="let item of (items$ | async)?.payload?.page"
[object]="item" [viewMode]="viewMode">
</ds-listable-object-component-loader>
<div *ngIf="(items$ | async)?.payload?.page?.length < (items$ | async)?.payload?.totalElements" class="mt-2" id="view-more">
<a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a>
</div>
<div *ngIf="showingAll" class="mt-2" id="view-less">
<a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a>
</div>
<ds-metadata-field-wrapper [label]="label">
<ng-container *ngFor="let objectPage of objects; let i = index">
<ng-container *ngVar="(objectPage | async) as itemsRD">
<ds-listable-object-component-loader *ngFor="let item of itemsRD?.payload?.page"
[object]="item" [viewMode]="viewMode">
</ds-listable-object-component-loader>
<ds-loading *ngIf="(i + 1) === objects.length && (itemsRD || i > 0) && !(itemsRD?.hasSucceeded && itemsRD?.payload && itemsRD?.payload?.page?.length > 0)" message="{{'loading.default' | translate}}"></ds-loading>
<div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && itemsRD?.payload?.page?.length > 0">
<div *ngIf="itemsRD?.payload?.totalPages > objects.length" class="float-left" id="view-more">
<a [routerLink]="" (click)="increase()">{{'item.page.related-items.view-more' |
translate:{ amount: (itemsRD?.payload?.totalElements - (incrementBy * objects.length) < incrementBy) ? itemsRD?.payload?.totalElements - (incrementBy * objects.length) : incrementBy } }}</a>
</div>
<div *ngIf="objects.length > 1" class="float-right" id="view-less">
<a [routerLink]="" (click)="decrease()">{{'item.page.related-items.view-less' |
translate:{ amount: itemsRD?.payload?.page?.length } }}</a>
</div>
</div>
</ng-container>
</ng-container>
</ds-metadata-field-wrapper>

View File

@@ -9,6 +9,8 @@ import { createRelationshipsObservable } from '../item-types/shared/item.compone
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { RelationshipService } from '../../../core/data/relationship.service';
import { TranslateModule } from '@ngx-translate/core';
import { VarDirective } from '../../../shared/utils/var.directive';
import { of as observableOf } from 'rxjs';
const parentItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
@@ -42,7 +44,7 @@ describe('RelatedItemsComponent', () => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [RelatedItemsComponent],
declarations: [RelatedItemsComponent, VarDirective],
providers: [
{ provide: RelationshipService, useValue: relationshipService }
],
@@ -65,31 +67,33 @@ describe('RelatedItemsComponent', () => {
expect(fields.length).toBe(mockItems.length);
});
describe('when viewMore is called', () => {
it('should contain one page of items', () => {
expect(comp.objects.length).toEqual(1);
});
describe('when increase is called', () => {
beforeEach(() => {
comp.viewMore();
comp.increase();
});
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments', () => {
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.allOptions);
it('should add a new page to the list', () => {
expect(comp.objects.length).toEqual(2);
});
it('should set showingAll to true', () => {
expect(comp.showingAll).toEqual(true);
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments (second page)', () => {
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, Object.assign(comp.options, { elementsPerPage: comp.incrementBy, currentPage: 2 }));
});
});
describe('when viewLess is called', () => {
describe('when decrease is called', () => {
beforeEach(() => {
comp.viewLess();
// Add a second page
comp.objects.push(observableOf(undefined));
comp.decrease();
});
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments', () => {
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.options);
});
it('should set showingAll to false', () => {
expect(comp.showingAll).toEqual(false);
it('should decrease the list of pages', () => {
expect(comp.objects.length).toEqual(1);
});
});

View File

@@ -1,10 +1,10 @@
import { of as observableOf } from 'rxjs';
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { SearchFilter } from '../+search-page/search-filter.model';
import { SearchFilter } from '../shared/search/search-filter.model';
import { ActivatedRouteStub } from '../shared/testing/active-router-stub';
import { MockRoleService } from '../shared/mocks/mock-role-service';
import { cold, hot } from 'jasmine-marbles';
@@ -38,12 +38,8 @@ describe('MyDSpaceConfigurationService', () => {
const roleService: any = new MockRoleService();
const fixedFilterService = jasmine.createSpyObj('SearchFixedFilterService', {
getQueryByFilterName: observableOf(''),
});
beforeEach(() => {
service = new MyDSpaceConfigurationService(roleService, fixedFilterService, spy, activatedRoute);
service = new MyDSpaceConfigurationService(roleService, spy, activatedRoute);
});
describe('when the scope is called', () => {

View File

@@ -6,12 +6,11 @@ import { first, map } from 'rxjs/operators';
import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type';
import { RoleService } from '../core/roles/role.service';
import { SearchConfigurationOption } from '../+search-page/search-switch-configuration/search-configuration-option.model';
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
import { RouteService } from '../core/services/route.service';
import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { RouteService } from '../core/services/route.service';
/**
* Service that performs all actions that have to do with the current mydspace configuration
@@ -55,16 +54,14 @@ export class MyDSpaceConfigurationService extends SearchConfigurationService {
* Initialize class
*
* @param {roleService} roleService
* @param {SearchFixedFilterService} fixedFilterService
* @param {RouteService} routeService
* @param {ActivatedRoute} route
*/
constructor(protected roleService: RoleService,
protected fixedFilterService: SearchFixedFilterService,
protected routeService: RouteService,
protected route: ActivatedRoute) {
super(routeService, fixedFilterService, route);
super(routeService, route);
// override parent class initialization
this._defaults = null;

View File

@@ -14,7 +14,7 @@ import { UploaderOptions } from '../../shared/uploader/uploader-options.model';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { NotificationType } from '../../shared/notifications/models/notification-type';
import { hasValue } from '../../shared/empty.util';
import { SearchResult } from '../../+search-page/search-result.model';
import { SearchResult } from '../../shared/search/search-result.model';
/**
* This component represents the whole mydspace page header

View File

@@ -19,15 +19,14 @@ import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from './my-dspace-page.c
import { RouteService } from '../core/services/route.service';
import { routeServiceStub } from '../shared/testing/route-service-stub';
import { SearchConfigurationServiceStub } from '../shared/testing/search-configuration-service-stub';
import { SearchService } from '../+search-page/search-service/search.service';
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { SearchService } from '../core/shared/search/search.service';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchFilterService } from '../+search-page/search-filters/search-filter/search-filter.service';
import { SearchFilterService } from '../core/shared/search/search-filter.service';
import { RoleDirective } from '../shared/roles/role.directive';
import { RoleService } from '../core/roles/role.service';
import { MockRoleService } from '../shared/mocks/mock-role-service';
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { createSuccessfulRemoteDataObject$ } from '../shared/testing/utils';
describe('MyDSpacePageComponent', () => {
@@ -82,8 +81,6 @@ describe('MyDSpacePageComponent', () => {
collapse: () => this.isCollapsed = observableOf(true),
expand: () => this.isCollapsed = observableOf(false)
};
const mockFixedFilterService: SearchFixedFilterService = {
} as SearchFixedFilterService;
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -123,10 +120,6 @@ describe('MyDSpacePageComponent', () => {
provide: RoleService,
useValue: new MockRoleService()
},
{
provide: SearchFixedFilterService,
useValue: mockFixedFilterService
}
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(MyDSpacePageComponent, {

View File

@@ -15,19 +15,19 @@ import { RemoteData } from '../core/data/remote-data';
import { DSpaceObject } from '../core/shared/dspace-object.model';
import { pushInOut } from '../shared/animations/push';
import { HostWindowService } from '../shared/host-window.service';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { SearchService } from '../+search-page/search-service/search.service';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SearchService } from '../core/shared/search/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { hasValue } from '../shared/empty.util';
import { getSucceededRemoteData } from '../core/shared/operators';
import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service';
import { SearchConfigurationOption } from '../+search-page/search-switch-configuration/search-configuration-option.model';
import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model';
import { RoleType } from '../core/roles/role-types';
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
import { ViewMode } from '../core/shared/view-mode.model';
import { MyDSpaceRequest } from '../core/data/request.models';
import { SearchResult } from '../+search-page/search-result.model';
import { SearchResult } from '../shared/search/search-result.model';
import { Context } from '../core/shared/context.model';
export const MYDSPACE_ROUTE = '/mydspace';

View File

@@ -5,7 +5,6 @@ import { SharedModule } from '../shared/shared.module';
import { MyDspacePageRoutingModule } from './my-dspace-page-routing.module';
import { MyDSpacePageComponent } from './my-dspace-page.component';
import { SearchPageModule } from '../+search-page/search-page.module';
import { MyDSpaceResultsComponent } from './my-dspace-results/my-dspace-results.component';
import { WorkspaceItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component';
import { ClaimedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component';
@@ -27,7 +26,6 @@ import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/
CommonModule,
SharedModule,
MyDspacePageRoutingModule,
SearchPageModule
],
declarations: [
MyDSpacePageComponent,

View File

@@ -2,12 +2,12 @@ import { Component, Input } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
import { SearchOptions } from '../../+search-page/search-options.model';
import { SearchOptions } from '../../shared/search/search-options.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { ViewMode } from '../../core/shared/view-mode.model';
import { isEmpty } from '../../shared/empty.util';
import { SearchResult } from '../../+search-page/search-result.model';
import { Context } from '../../core/shared/context.model';
import { SearchResult } from '../../shared/search/search-result.model';
/**
* Component that represents all results for mydspace page

View File

@@ -1,7 +1,7 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { configureSearchComponentTestingModule } from './search.component.spec';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
describe('ConfigurationSearchPageComponent', () => {
let comp: ConfigurationSearchPageComponent;

View File

@@ -1,15 +1,14 @@
import { HostWindowService } from '../shared/host-window.service';
import { SearchService } from './search-service/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchComponent } from './search.component';
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
import { pushInOut } from '../shared/animations/push';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { Observable } from 'rxjs';
import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
import { map } from 'rxjs/operators';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { Router } from '@angular/router';
import { hasValue } from '../shared/empty.util';
import { RouteService } from '../core/services/route.service';
import { SearchService } from '../core/shared/search/search.service';
/**
* This component renders a search page using a configuration as input.
@@ -45,8 +44,9 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements
protected sidebarService: SidebarService,
protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService) {
super(service, sidebarService, windowService, searchConfigService, routeService);
protected routeService: RouteService,
protected router: Router) {
super(service, sidebarService, windowService, searchConfigService, routeService, router);
}
/**
@@ -58,24 +58,8 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements
*/
ngOnInit(): void {
super.ngOnInit();
}
/**
* Get the current paginated search options after updating the configuration using the configuration input
* This is to make sure the configuration is included in the paginated search options, as it is not part of any
* query or route parameters
* @returns {Observable<PaginatedSearchOptions>}
*/
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
return this.searchConfigService.paginatedSearchOptions.pipe(
map((options: PaginatedSearchOptions) => {
const config = this.configuration || options.configuration;
const filter = this.fixedFilterQuery || options.fixedFilter;
return Object.assign(options, {
configuration: config,
fixedFilter: filter
});
})
);
if (hasValue(this.configuration)) {
this.routeService.setParameter('configuration', this.configuration);
}
}
}

View File

@@ -1,38 +0,0 @@
import { SearchFixedFilterService } from './search-fixed-filter.service';
import { RequestService } from '../../../core/data/request.service';
import { of as observableOf } from 'rxjs';
import { RequestEntry } from '../../../core/data/request.reducer';
import { FilteredDiscoveryQueryResponse } from '../../../core/cache/response.models';
describe('SearchFixedFilterService', () => {
let service: SearchFixedFilterService;
const filterQuery = 'filter:query';
const requestServiceStub = Object.assign({
/* tslint:disable:no-empty */
configure: () => {
},
/* tslint:enable:no-empty */
generateRequestId: () => 'fake-id',
getByHref: () => observableOf(Object.assign(new RequestEntry(), {
response: new FilteredDiscoveryQueryResponse(filterQuery, 200, 'OK')
}))
}) as RequestService;
beforeEach(() => {
service = new SearchFixedFilterService();
});
describe('when getQueryByRelations is called', () => {
const relationType = 'isRelationOf';
const itemUUID = 'c5b277e6-2477-48bb-8993-356710c285f3';
it('should contain the relationType and itemUUID', () => {
const query = service.getQueryByRelations(relationType, itemUUID);
expect(query.length).toBeGreaterThan(relationType.length + itemUUID.length);
expect(query).toContain(relationType);
expect(query).toContain(itemUUID);
});
});
});

View File

@@ -1,27 +0,0 @@
import { Injectable } from '@angular/core';
/**
* Service for performing actions on the filtered-discovery-pages REST endpoint
*/
@Injectable()
export class SearchFixedFilterService {
/**
* Get the query for looking up items by relation type
* @param {string} relationType Relation type
* @param {string} itemUUID Item UUID
* @returns {string} Query
*/
getQueryByRelations(relationType: string, itemUUID: string): string {
return `query=relation.${relationType}:${itemUUID}`;
}
/**
* Get the filter for a relation with the item's UUID
* @param relationType The type of relation e.g. 'isAuthorOfPublication'
* @param itemUUID The item's UUID
*/
getFilterByRelation(relationType: string, itemUUID: string): string {
return `f.${relationType}=${itemUUID}`;
}
}

View File

@@ -1,7 +0,0 @@
<div class="row mb-3 mb-md-1">
<div class="labels col-sm-9 offset-sm-3">
<ng-container *ngFor="let key of ((appliedFilters | async) | dsObjectKeys)">
<ds-search-label *ngFor="let value of (appliedFilters | async)[key]" [inPlaceSearch]="inPlaceSearch" [key]="key" [value]="value" [appliedFilters]="appliedFilters"></ds-search-label>
</ng-container>
</div>
</div>

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SearchComponent } from './search.component';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { SearchPageComponent } from './search-page.component';

View File

@@ -3,68 +3,18 @@ import { CommonModule } from '@angular/common';
import { CoreModule } from '../core/core.module';
import { SharedModule } from '../shared/shared.module';
import { SearchPageRoutingModule } from './search-page-routing.module';
import { SearchComponent } from './search.component';
import { SearchResultsComponent } from './search-results/search-results.component';
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SidebarEffects } from '../shared/sidebar/sidebar-effects.service';
import { SearchSettingsComponent } from './search-settings/search-settings.component';
import { EffectsModule } from '@ngrx/effects';
import { SearchFiltersComponent } from './search-filters/search-filters.component';
import { SearchFilterComponent } from './search-filters/search-filter/search-filter.component';
import { SearchFacetFilterComponent } from './search-filters/search-filter/search-facet-filter/search-facet-filter.component';
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
import { SearchFixedFilterService } from './search-filters/search-filter/search-fixed-filter.service';
import { SearchLabelsComponent } from './search-labels/search-labels.component';
import { SearchRangeFilterComponent } from './search-filters/search-filter/search-range-filter/search-range-filter.component';
import { SearchTextFilterComponent } from './search-filters/search-filter/search-text-filter/search-text-filter.component';
import { SearchFacetFilterWrapperComponent } from './search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component';
import { SearchBooleanFilterComponent } from './search-filters/search-filter/search-boolean-filter/search-boolean-filter.component';
import { SearchHierarchyFilterComponent } from './search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { SearchFacetOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component';
import { SearchFacetSelectedOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component';
import { SearchFacetRangeOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component';
import { SearchSwitchConfigurationComponent } from './search-switch-configuration/search-switch-configuration.component';
import { SearchAuthorityFilterComponent } from './search-filters/search-filter/search-authority-filter/search-authority-filter.component';
import { SearchLabelComponent } from './search-labels/search-label/search-label.component';
import { SearchPageComponent } from './search-page.component';
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
import { SearchPageComponent } from './search-page.component';
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
import { StatisticsModule } from '../statistics/statistics.module';
import { SearchTrackerComponent } from './search-tracker.component';
const effects = [
SidebarEffects
];
import { StatisticsModule } from '../statistics/statistics.module';
import { SearchComponent } from './search.component';
const components = [
SearchPageComponent,
SearchComponent,
SearchResultsComponent,
SearchSidebarComponent,
SearchSettingsComponent,
SearchFiltersComponent,
SearchFilterComponent,
SearchFacetFilterComponent,
SearchLabelsComponent,
SearchLabelComponent,
SearchFacetFilterComponent,
SearchFacetFilterWrapperComponent,
SearchRangeFilterComponent,
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
SearchFacetOptionComponent,
SearchFacetSelectedOptionComponent,
SearchFacetRangeOptionComponent,
SearchSwitchConfigurationComponent,
SearchAuthorityFilterComponent,
ConfigurationSearchPageComponent,
SearchTrackerComponent,
SearchTrackerComponent
];
@NgModule({
@@ -72,30 +22,11 @@ const components = [
SearchPageRoutingModule,
CommonModule,
SharedModule,
EffectsModule.forFeature(effects),
CoreModule.forRoot(),
StatisticsModule.forRoot(),
],
providers: [ConfigurationSearchPageGuard],
declarations: components,
providers: [
SidebarService,
SidebarFilterService,
SearchFilterService,
SearchFixedFilterService,
ConfigurationSearchPageGuard,
SearchConfigurationService
],
entryComponents: [
SearchFacetFilterComponent,
SearchRangeFilterComponent,
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
SearchFacetOptionComponent,
SearchFacetSelectedOptionComponent,
SearchFacetRangeOptionComponent,
SearchAuthorityFilterComponent
],
exports: components
})

View File

@@ -1,26 +0,0 @@
import { DSpaceObject } from '../core/shared/dspace-object.model';
import { MetadataMap } from '../core/shared/metadata.models';
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
import { GenericConstructor } from '../core/shared/generic-constructor';
/**
* Represents a search result object of a certain (<T>) DSpaceObject
*/
export class SearchResult<T extends DSpaceObject> implements ListableObject {
/**
* The DSpaceObject that was found
*/
indexableObject: T;
/**
* The metadata that was used to find this item, hithighlighted
*/
hitHighlights: MetadataMap;
/**
* Method that returns as which type of object this object should be rendered
*/
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
return [this.constructor as GenericConstructor<ListableObject>];
}
}

View File

@@ -1,19 +0,0 @@
<h2 *ngIf="!disableHeader">{{ (configuration ? configuration + '.search.results.head' : 'search.results.head') | translate }}</h2>
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading && searchResults?.payload?.page.length > 0" @fadeIn>
<ds-viewable-collection
[config]="searchConfig.pagination"
[sortConfig]="searchConfig.sort"
[objects]="searchResults"
[linkType]="linkType"
[hideGear]="true">
</ds-viewable-collection></div>
<ds-loading *ngIf="hasNoValue(searchResults) || hasNoValue(searchResults.payload) || searchResults.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
<ds-error *ngIf="searchResults?.hasFailed && (!searchResults?.error || searchResults?.error?.statusCode != 400)" message="{{'error.search-results' | translate}}"></ds-error>
<div *ngIf="searchResults?.payload?.page.length == 0 || searchResults?.error?.statusCode == 400">
{{ 'search.results.no-results' | translate }}
<a [routerLink]="['/search']"
[queryParams]="{ query: surroundStringWithQuotes(searchConfig?.query) }"
queryParamsHandling="merge">
{{"search.results.no-results-link" | translate}}
</a>
</div>

View File

@@ -1,32 +0,0 @@
<ng-container *ngVar="(searchOptions$ | async) as config">
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
<div class="result-order-settings">
<ds-sidebar-dropdown
*ngIf="config?.sort"
[id]="'search-sidebar-sort'"
[label]="'search.sidebar.settings.sort-by'"
(change)="reloadOrder($event)"
>
<option *ngFor="let sortOption of searchOptionPossibilities"
[value]="sortOption.field + ',' + sortOption.direction.toString()"
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
</option>
</ds-sidebar-dropdown>
</div>
<div class="page-size-settings">
<ds-sidebar-dropdown
[id]="'search-sidebar-rpp'"
[label]="'search.sidebar.settings.rpp'"
(change)="reloadRPP($event)"
>
<option *ngFor="let pageSizeOption of config?.pagination.pageSizeOptions"
[value]="pageSizeOption"
[selected]="pageSizeOption === +config?.pagination.pageSize ? 'selected': null">
{{pageSizeOption}}
</option>
</ds-sidebar-dropdown>
</div>
</ng-container>

View File

@@ -2,16 +2,17 @@ import { Component, Inject, OnInit } from '@angular/core';
import { Angulartics2 } from 'angulartics2';
import { filter, map, switchMap } from 'rxjs/operators';
import { SearchComponent } from './search.component';
import { SearchService } from './search-service/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { HostWindowService } from '../shared/host-window.service';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
import { RouteService } from '../core/services/route.service';
import { hasValue } from '../shared/empty.util';
import { SearchQueryResponse } from './search-service/search-query-response.model';
import { SearchSuccessResponse } from '../core/cache/response.models';
import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { Router } from '@angular/router';
import { SearchService } from '../core/shared/search/search.service';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SearchQueryResponse } from '../shared/search/search-query-response.model';
/**
* This component triggers a page view statistic
@@ -30,14 +31,15 @@ import { PaginatedSearchOptions } from './paginated-search-options.model';
export class SearchTrackerComponent extends SearchComponent implements OnInit {
constructor(
protected service:SearchService,
protected sidebarService:SidebarService,
protected windowService:HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService:SearchConfigurationService,
protected routeService:RouteService,
public angulartics2:Angulartics2
protected service: SearchService,
protected sidebarService: SidebarService,
protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService,
public angulartics2: Angulartics2,
protected router: Router
) {
super(service, sidebarService, windowService, searchConfigService, routeService);
super(service, sidebarService, windowService, searchConfigService, routeService, router);
}
ngOnInit():void {
@@ -58,9 +60,9 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit {
)
)
.subscribe((entry) => {
const config:PaginatedSearchOptions = entry.searchOptions;
const searchQueryResponse:SearchQueryResponse = entry.response;
const filters:Array<{ filter:string, operator:string, value:string, label:string; }> = [];
const config: PaginatedSearchOptions = entry.searchOptions;
const searchQueryResponse: SearchQueryResponse = entry.response;
const filters:Array<{ filter: string, operator: string, value: string, label: string; }> = [];
const appliedFilters = searchQueryResponse.appliedFilters || [];
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
const appliedFilter = appliedFilters[i];

View File

@@ -46,5 +46,9 @@
[scopes]="(scopeListRD$ | async)"
[inPlaceSearch]="inPlaceSearch">
</ds-search-form>
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
<div class="row mb-3 mb-md-1">
<div class="labels col-sm-9 offset-sm-3">
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
</div>
</div>
</ng-template>

View File

@@ -11,21 +11,19 @@ import { CommunityDataService } from '../core/data/community-data.service';
import { HostWindowService } from '../shared/host-window.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SearchComponent } from './search.component';
import { SearchService } from './search-service/search.service';
import { SearchService } from '../core/shared/search/search.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute } from '@angular/router';
import { By } from '@angular/platform-browser';
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { RemoteData } from '../core/data/remote-data';
import { SearchFilterService } from '../core/shared/search/search-filter.service';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
import { RouteService } from '../core/services/route.service';
import { SearchConfigurationServiceStub } from '../shared/testing/search-configuration-service-stub';
import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SearchFixedFilterService } from './search-filters/search-filter/search-fixed-filter.service';
import { createSuccessfulRemoteDataObject$ } from '../shared/testing/utils';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
let comp: SearchComponent;
let fixture: ComponentFixture<SearchComponent>;
@@ -89,7 +87,6 @@ const routeServiceStub = {
return observableOf('')
}
};
const mockFixedFilterService: SearchFixedFilterService = {} as SearchFixedFilterService;
export function configureSearchComponentTestingModule(compType) {
TestBed.configureTestingModule({
@@ -122,10 +119,6 @@ export function configureSearchComponentTestingModule(compType) {
provide: SearchFilterService,
useValue: {}
},
{
provide: SearchFixedFilterService,
useValue: mockFixedFilterService
},
{
provide: SearchConfigurationService,
useValue: {
@@ -158,6 +151,7 @@ describe('SearchComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(SearchComponent);
comp = fixture.componentInstance; // SearchComponent test instance
comp.inPlaceSearch = false;
fixture.detectChanges();
searchServiceObject = (comp as any).service;
searchConfigurationServiceObject = (comp as any).searchConfigService;

View File

@@ -1,20 +1,22 @@
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { startWith, switchMap, } from 'rxjs/operators';
import { PaginatedList } from '../core/data/paginated-list';
import { RemoteData } from '../core/data/remote-data';
import { DSpaceObject } from '../core/shared/dspace-object.model';
import { pushInOut } from '../shared/animations/push';
import { HostWindowService } from '../shared/host-window.service';
import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SearchResult } from './search-result.model';
import { SearchService } from './search-service/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { getSucceededRemoteData } from '../core/shared/operators';
import { RouteService } from '../core/services/route.service';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { SearchResult } from '../shared/search/search-result.model';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SearchService } from '../core/shared/search/search.service';
import { currentPath } from '../shared/utils/route.utils';
import { Router } from '@angular/router';
@Component({
selector: 'ds-search',
@@ -96,7 +98,8 @@ export class SearchComponent implements OnInit {
protected sidebarService: SidebarService,
protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService) {
protected routeService: RouteService,
protected router: Router) {
this.isXsOrSm$ = this.windowService.isXsOrSm();
}
@@ -159,7 +162,7 @@ export class SearchComponent implements OnInit {
*/
private getSearchLink(): string {
if (this.inPlaceSearch) {
return './';
return currentPath(this.router);
}
return this.service.getSearchLink();
}

View File

@@ -20,7 +20,7 @@ import { Store, StoreModule } from '@ngrx/store';
// Load the implementations that should be tested
import { AppComponent } from './app.component';
import { HostWindowState } from './shared/host-window.reducer';
import { HostWindowState } from './shared/search/host-window.reducer';
import { HostWindowResizeAction } from './shared/host-window.actions';
import { MetadataService } from './core/metadata/metadata.service';

View File

@@ -1,13 +1,5 @@
import { filter, map, take } from 'rxjs/operators';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
HostListener,
Inject,
OnInit,
ViewEncapsulation
} from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, ViewEncapsulation } from '@angular/core';
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
import { select, Store } from '@ngrx/store';
@@ -18,12 +10,11 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../config';
import { MetadataService } from './core/metadata/metadata.service';
import { HostWindowResizeAction } from './shared/host-window.actions';
import { HostWindowState } from './shared/host-window.reducer';
import { HostWindowState } from './shared/search/host-window.reducer';
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
import { isAuthenticated } from './core/auth/selectors';
import { AuthService } from './core/auth/auth.service';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import { RouteService } from './core/services/route.service';
import variables from '../styles/_exposed_variables.scss';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { MenuService } from './shared/menu/menu.service';

View File

@@ -1,9 +1,13 @@
import { StoreEffects } from './store.effects';
import { NotificationsEffects } from './shared/notifications/notifications.effects';
import { NavbarEffects } from './navbar/navbar.effects';
import { SidebarEffects } from './shared/sidebar/sidebar-effects.service';
import { RelationshipEffects } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects';
export const appEffects = [
StoreEffects,
NavbarEffects,
NotificationsEffects,
SidebarEffects,
RelationshipEffects
];

View File

@@ -37,9 +37,9 @@ import { AdminSidebarComponent } from './+admin/admin-sidebar/admin-sidebar.comp
import { AdminSidebarSectionComponent } from './+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
import { NavbarModule } from './navbar/navbar.module';
import { ClientCookieService } from './core/services/client-cookie.service';
import { JournalEntitiesModule } from './entity-groups/journal-entities/journal-entities.module';
import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module';
import { ClientCookieService } from './core/services/client-cookie.service';
export function getConfig() {
return ENV_CONFIG;
@@ -76,7 +76,7 @@ const ENTITY_IMPORTS = [
IMPORTS.push(
StoreDevtoolsModule.instrument({
maxAge: 100,
maxAge: 1000,
logOnly: ENV_CONFIG.production,
})
);

View File

@@ -1,38 +1,22 @@
import { ActionReducerMap, createSelector, MemoizedSelector, State } from '@ngrx/store';
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store';
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
import { formReducer, FormState } from './shared/form/form.reducer';
import {
SidebarState,
sidebarReducer
} from './shared/sidebar/sidebar.reducer';
import {
SidebarFilterState,
sidebarFilterReducer, SidebarFiltersState
} from './shared/sidebar/filter/sidebar-filter.reducer';
import {
filterReducer,
SearchFiltersState
} from './+search-page/search-filters/search-filter/search-filter.reducer';
import {
notificationsReducer,
NotificationsState
} from './shared/notifications/notifications.reducers';
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer';
import { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer';
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
import {
metadataRegistryReducer,
MetadataRegistryState
} from './+admin/admin-registries/metadata-registry/metadata-registry.reducers';
import { metadataRegistryReducer, MetadataRegistryState } from './+admin/admin-registries/metadata-registry/metadata-registry.reducers';
import { hasValue } from './shared/empty.util';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
import { historyReducer, HistoryState } from './shared/history/history.reducer';
import {
bitstreamFormatReducer,
BitstreamFormatRegistryState
} from './+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
import { selectableListReducer, SelectableListsState } from './shared/object-list/selectable-list/selectable-list.reducer';
import { bitstreamFormatReducer, BitstreamFormatRegistryState } from './+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer';
import { NameVariantListsState, nameVariantReducer } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
export interface AppState {
router: fromRouter.RouterReducerState;
@@ -49,6 +33,8 @@ export interface AppState {
cssVariables: CSSVariablesState;
menus: MenusState;
objectSelection: ObjectSelectionListState;
selectableLists: SelectableListsState;
relationshipLists: NameVariantListsState;
communityList: CommunityListState;
}
@@ -67,6 +53,8 @@ export const appReducers: ActionReducerMap<AppState> = {
cssVariables: cssVariablesReducer,
menus: menusReducer,
objectSelection: objectSelectionReducer,
selectableLists: selectableListReducer,
relationshipLists: nameVariantReducer,
communityList: CommunityListReducer,
};

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { NormalizedObject } from '../models/normalized-object.model';
import { getMapsToType, getRelationships } from './build-decorators';
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { TypedObject } from '../object-cache.reducer';
import { CacheableObject, TypedObject } from '../object-cache.reducer';
/**
* Return true if halObj has a value for `_links.self`
@@ -34,14 +34,13 @@ export class NormalizedObjectBuildService {
*
* @param {TDomain} domainModel a domain model
*/
normalize<T extends TypedObject>(domainModel: T): NormalizedObject<T> {
normalize<T extends CacheableObject>(domainModel: T): NormalizedObject<T> {
const normalizedConstructor = getMapsToType((domainModel as any).type);
const relationships = getRelationships(normalizedConstructor) || [];
const normalizedModel = Object.assign({}, domainModel) as any;
relationships.forEach((key: string) => {
if (hasValue(domainModel[key])) {
domainModel[key] = undefined;
if (hasValue(normalizedModel[key])) {
normalizedModel[key] = normalizedModel._links[key];
}
});
return normalizedModel;

View File

@@ -116,7 +116,7 @@ export class RemoteDataBuildService {
const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
const tDomainList$ = requestEntry$.pipe(
getResourceLinksFromResponse(),
flatMap((resourceUUIDs: string[]) => {
switchMap((resourceUUIDs: string[]) => {
return this.objectCache.getList(resourceUUIDs).pipe(
map((normList: Array<NormalizedObject<T>>) => {
return normList.map((normalized: NormalizedObject<T>) => {
@@ -273,12 +273,14 @@ export class RemoteDataBuildService {
private toPaginatedList<T>(input: Observable<RemoteData<T[] | PaginatedList<T>>>, pageInfo: PageInfo): Observable<RemoteData<PaginatedList<T>>> {
return input.pipe(
map((rd: RemoteData<T[] | PaginatedList<T>>) => {
const rdAny = rd as any;
const newRD = new RemoteData(rdAny.requestPending, rdAny.responsePending, rdAny.isSuccessful, rd.error, undefined);
if (Array.isArray(rd.payload)) {
return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload) })
return Object.assign(newRD, { payload: new PaginatedList(pageInfo, rd.payload) })
} else if (isNotUndefined(rd.payload)) {
return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload.page) });
return Object.assign(newRD, { payload: new PaginatedList(pageInfo, rd.payload.page) });
} else {
return Object.assign(rd, { payload: new PaginatedList(pageInfo, []) });
return Object.assign(newRD, { payload: new PaginatedList(pageInfo, []) });
}
})
);

View File

@@ -1,4 +1,4 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize';
import { Relationship } from '../../../shared/item-relationships/relationship.model';
import { mapsTo, relationship } from '../../builders/build-decorators';
import { NormalizedObject } from '../normalized-object.model';
@@ -16,20 +16,20 @@ export class NormalizedRelationship extends NormalizedObject<Relationship> {
/**
* The identifier of this Relationship
*/
@autoserialize
@deserialize
id: string;
/**
* The item to the left of this relationship
*/
@autoserialize
@deserialize
@relationship(Item, false)
leftItem: string;
/**
* The item to the right of this relationship
*/
@autoserialize
@deserialize
@relationship(Item, false)
rightItem: string;
@@ -46,15 +46,27 @@ export class NormalizedRelationship extends NormalizedObject<Relationship> {
rightPlace: number;
/**
* The type of Relationship
* The name variant of the Item to the left side of this Relationship
*/
@autoserialize
leftwardValue: string;
/**
* The name variant of the Item to the right side of this Relationship
*/
@autoserialize
rightwardValue: string;
/**
* The type of Relationship
*/
@deserialize
@relationship(RelationshipType, false)
relationshipType: string;
/**
* The universally unique identifier of this Relationship
*/
@autoserializeAs(new IDToUUIDSerializer(Relationship.type.value), 'id')
@deserializeAs(new IDToUUIDSerializer(Relationship.type.value), 'id')
uuid: string;
}

View File

@@ -1,5 +1,5 @@
import { CacheableObject, TypedObject } from '../object-cache.reducer';
import { autoserialize } from 'cerialize';
import { autoserialize, deserialize } from 'cerialize';
import { ResourceType } from '../../shared/resource-type';
/**
* An abstract model class for a NormalizedObject.
@@ -8,10 +8,10 @@ export abstract class NormalizedObject<T extends TypedObject> implements Cacheab
/**
* The link to the rest endpoint where this object can be found
*/
@autoserialize
@deserialize
self: string;
@autoserialize
@deserialize
_links: {
[name: string]: string
};
@@ -19,6 +19,6 @@ export abstract class NormalizedObject<T extends TypedObject> implements Cacheab
/**
* A string representing the kind of object
*/
@autoserialize
@deserialize
type: string;
}

View File

@@ -196,8 +196,9 @@ export class ObjectCacheService {
* false otherwise
*/
hasByUUID(uuid: string): boolean {
let result: boolean;
let result = false;
/* NB: that this is only a solution because the select method is synchronous, see: https://github.com/ngrx/store/issues/296#issuecomment-269032571*/
this.store.pipe(
select(selfLinkFromUuidSelector(uuid)),
take(1)

View File

@@ -1,9 +1,9 @@
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
import { SearchQueryResponse } from '../../shared/search/search-query-response.model';
import { RequestError } from '../data/request.models';
import { PageInfo } from '../shared/page-info.model';
import { ConfigObject } from '../config/models/config.model';
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
import { FacetValue } from '../../shared/search/facet-value.model';
import { SearchFilterConfig } from '../../shared/search/search-filter-config.model';
import { IntegrationModel } from '../integration/models/integration.model';
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';

View File

@@ -6,6 +6,7 @@ import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.e
import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
import { RouteEffects } from './services/route.effects';
import { RouterEffects } from './router/router.effects';
export const coreEffects = [
RequestEffects,
@@ -15,5 +16,6 @@ export const coreEffects = [
JsonPatchOperationsEffects,
ServerSyncBufferEffects,
ObjectUpdatesEffects,
RouteEffects
RouteEffects,
RouterEffects
];

View File

@@ -53,7 +53,7 @@ import { UUIDService } from './shared/uuid.service';
import { AuthenticatedGuard } from './auth/authenticated.guard';
import { AuthRequestService } from './auth/auth-request.service';
import { AuthResponseParsingService } from './auth/auth-response-parsing.service';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { AuthInterceptor } from './auth/auth.interceptor';
import { HALEndpointService } from './shared/hal-endpoint.service';
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
@@ -80,7 +80,8 @@ import { NormalizedObjectBuildService } from './cache/builders/normalized-object
import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
import { DefaultChangeAnalyzer } from './data/default-change-analyzer.service';
import { SearchService } from '../+search-page/search-service/search.service';
import { SearchService } from './shared/search/search.service';
import { RelationshipService } from './data/relationship.service';
import { NormalizedCollection } from './cache/models/normalized-collection.model';
import { NormalizedCommunity } from './cache/models/normalized-community.model';
import { NormalizedDSpaceObject } from './cache/models/normalized-dspace-object.model';
@@ -101,7 +102,6 @@ import { NormalizedSubmissionFormsModel } from './config/models/normalized-confi
import { NormalizedSubmissionSectionModel } from './config/models/normalized-config-submission-section.model';
import { NormalizedAuthStatus } from './auth/models/normalized-auth-status.model';
import { NormalizedAuthorityValue } from './integration/models/normalized-authority-value.model';
import { RelationshipService } from './data/relationship.service';
import { RoleService } from './roles/role.service';
import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard';
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
@@ -124,6 +124,31 @@ import { ObjectSelectService } from '../shared/object-select/object-select.servi
import { SiteDataService } from './data/site-data.service';
import { NormalizedSite } from './cache/models/normalized-site.model';
import {
MOCK_RESPONSE_MAP,
MockResponseMap,
mockResponseMap
} from '../shared/mocks/dspace-rest-v2/mocks/mock-response-map';
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service';
import { ENV_CONFIG, GLOBAL_CONFIG, GlobalConfig } from '../../config';
import { SearchFilterService } from './shared/search/search-filter.service';
import { SearchConfigurationService } from './shared/search/search-configuration.service';
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
import { RelationshipTypeService } from './data/relationship-type.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
/**
* When not in production, endpoint responses can be mocked for testing purposes
* If there is no mock version available for the endpoint, the actual REST response will be used just like in production mode
*/
export const restServiceFactory = (cfg: GlobalConfig, mocks: MockResponseMap, http: HttpClient) => {
if (ENV_CONFIG.production) {
return new DSpaceRESTv2Service(http);
} else {
return new EndpointMockingRestService(cfg, mocks, http);
}
};
const IMPORTS = [
CommonModule,
StoreModule.forFeature('core', coreReducers, {}),
@@ -143,7 +168,8 @@ const PROVIDERS = [
CollectionDataService,
SiteDataService,
DSOResponseParsingService,
DSpaceRESTv2Service,
{ provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap },
{ provide: DSpaceRESTv2Service, useFactory: restServiceFactory, deps: [GLOBAL_CONFIG, MOCK_RESPONSE_MAP, HttpClient]},
DynamicFormLayoutService,
DynamicFormService,
DynamicFormValidationService,
@@ -214,6 +240,13 @@ const PROVIDERS = [
TaskResponseParsingService,
ClaimedTaskDataService,
PoolTaskDataService,
SearchService,
SidebarService,
SearchFilterService,
SearchFilterService,
SearchConfigurationService,
SelectableListService,
RelationshipTypeService,
// register AuthInterceptor as HttpInterceptor
{
provide: HTTP_INTERCEPTORS,

View File

@@ -25,8 +25,8 @@ import { ResponseParsingService } from './parsing.service';
import { GenericConstructor } from '../shared/generic-constructor';
import { hasValue, isNotEmptyOperator } from '../../shared/empty.util';
import { DSpaceObject } from '../shared/dspace-object.model';
import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model';
import { SearchParam } from '../cache/models/search-param.model';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
@Injectable()
export class CollectionDataService extends ComColDataService<Collection> {
@@ -71,7 +71,9 @@ export class CollectionDataService extends ComColDataService<Collection> {
*/
getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
const searchHref = 'findAuthorizedByCommunity';
options.searchParams = [new SearchParam('uuid', communityId)];
options = Object.assign({}, options, {
searchParams: [new SearchParam('uuid', communityId)]
});
return this.searchBy(searchHref, options).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));

View File

@@ -16,6 +16,7 @@ import { HttpClient } from '@angular/common/http';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { Item } from '../shared/item.model';
import * as uuidv4 from 'uuid/v4';
const endpoint = 'https://rest.api/core';
@@ -51,10 +52,11 @@ class DummyChangeAnalyzer implements ChangeAnalyzer<NormalizedTestObject> {
}
}
describe('DataService', () => {
let service: TestService;
let options: FindListOptions;
const requestService = {} as RequestService;
const requestService = {generateRequestId: () => uuidv4()} as RequestService;
const halService = {} as HALEndpointService;
const rdbService = {} as RemoteDataBuildService;
const notificationsService = {} as NotificationsService;
@@ -87,6 +89,7 @@ describe('DataService', () => {
comparator,
);
}
service = initTestService();
describe('getFindAllHref', () => {
@@ -188,7 +191,7 @@ describe('DataService', () => {
dso2.self = selfLink;
dso2.metadata = [{ key: 'dc.title', value: name2 }];
spyOn(service, 'findById').and.returnValues(observableOf(dso));
spyOn(service, 'findByHref').and.returnValues(observableOf(dso));
spyOn(objectCache, 'getObjectBySelfLink').and.returnValues(observableOf(dso));
spyOn(objectCache, 'addPatch');
});

View File

@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { distinctUntilChanged, filter, find, first, map, mergeMap, switchMap, take } from 'rxjs/operators';
import { distinctUntilChanged, filter, find, first, map, mergeMap, skipWhile, switchMap, take, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
@@ -157,7 +157,7 @@ export abstract class DataService<T extends CacheableObject> {
findById(id: string): Observable<RemoteData<T>> {
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
map((endpoint: string) => this.getIDHref(endpoint, encodeURIComponent(id))));
map((endpoint: string) => this.getIDHref(endpoint, encodeURIComponent(id))));
hrefObs.pipe(
find((href: string) => hasValue(href)))
@@ -204,15 +204,22 @@ export abstract class DataService<T extends CacheableObject> {
const hrefObs = this.getSearchByHref(searchMethod, options);
hrefObs.pipe(
first((href: string) => hasValue(href)))
.subscribe((href: string) => {
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
request.responseMsToLive = 10 * 1000;
this.requestService.configure(request);
});
return hrefObs.pipe(
find((href: string) => hasValue(href)),
tap((href: string) => {
this.requestService.removeByHrefSubstring(href);
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
request.responseMsToLive = 10 * 1000;
return this.rdbService.buildList<T>(hrefObs) as Observable<RemoteData<PaginatedList<T>>>;
this.requestService.configure(request);
}
),
switchMap((href) => this.requestService.getByHref(href)),
skipWhile((requestEntry) => hasValue(requestEntry) && requestEntry.completed),
switchMap((href) =>
this.rdbService.buildList<T>(hrefObs) as Observable<RemoteData<PaginatedList<T>>>
)
);
}
/**
@@ -236,7 +243,7 @@ export abstract class DataService<T extends CacheableObject> {
if (isNotEmpty(operations)) {
this.objectCache.addPatch(object.self, operations);
}
return this.findById(object.uuid);
return this.findByHref(object.self);
}
));

View File

@@ -4,6 +4,7 @@ import { ChangeAnalyzer } from './change-analyzer';
import { Injectable } from '@angular/core';
import { CacheableObject } from '../cache/object-cache.reducer';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
/**
* A class to determine what differs between two
@@ -11,6 +12,8 @@ import { NormalizedObject } from '../cache/models/normalized-object.model';
*/
@Injectable()
export class DefaultChangeAnalyzer<T extends CacheableObject> implements ChangeAnalyzer<T> {
constructor(private normalizeService: NormalizedObjectBuildService) {
}
/**
* Compare the metadata of two CacheableObject and return the differences as
@@ -22,6 +25,6 @@ export class DefaultChangeAnalyzer<T extends CacheableObject> implements ChangeA
* The second object to compare
*/
diff(object1: T | NormalizedObject<T>, object2: T | NormalizedObject<T>): Operation[] {
return compare(object1, object2);
return compare(this.normalizeService.normalize(object1), this.normalizeService.normalize(object2));
}
}

View File

@@ -7,7 +7,7 @@ import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
import { SearchFilterConfig } from '../../shared/search/search-filter-config.model';
import { BaseResponseParsingService } from './base-response-parsing.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';

View File

@@ -9,7 +9,7 @@ import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
import { FacetValue } from '../../shared/search/facet-value.model';
import { BaseResponseParsingService } from './base-response-parsing.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';

View File

@@ -4,7 +4,7 @@ import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
import { FacetValue } from '../../shared/search/facet-value.model';
import { BaseResponseParsingService } from './base-response-parsing.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GLOBAL_CONFIG } from '../../../config';

View File

@@ -6,7 +6,7 @@ import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { hasValue } from '../../shared/empty.util';
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
import { SearchQueryResponse } from '../../shared/search/search-query-response.model';
import { MetadataMap, MetadataValue } from '../shared/metadata.models';
@Injectable()

View File

@@ -3,7 +3,7 @@ import { hasValue } from '../../shared/empty.util';
export class PaginatedList<T> {
constructor(private pageInfo: PageInfo,
constructor(public pageInfo: PageInfo,
public page: T[]) {
}

View File

@@ -0,0 +1,99 @@
import { RequestService } from './request.service';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { PaginatedList } from './paginated-list';
import { PageInfo } from '../shared/page-info.model';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
import { RelationshipTypeService } from './relationship-type.service';
import { of as observableOf } from 'rxjs';
import { ItemType } from '../shared/item-relationships/item-type.model';
describe('RelationshipTypeService', () => {
let service: RelationshipTypeService;
let requestService: RequestService;
let restEndpointURL;
let halService: any;
let publicationTypeString;
let personTypeString;
let orgUnitTypeString;
let publicationType;
let personType;
let orgUnitType;
let relationshipType1;
let relationshipType2;
let buildList;
let rdbService;
function init() {
restEndpointURL = 'https://rest.api/relationshiptypes';
halService = new HALEndpointServiceStub(restEndpointURL);
publicationTypeString = 'Publication';
personTypeString = 'Person';
orgUnitTypeString = 'OrgUnit';
publicationType = Object.assign(new ItemType(), {label: publicationTypeString});
personType = Object.assign(new ItemType(), {label: personTypeString});
orgUnitType = Object.assign(new ItemType(), {label: orgUnitTypeString});
relationshipType1 = Object.assign(new RelationshipType(), {
id: '1',
uuid: '1',
leftwardType: 'isAuthorOfPublication',
rightwardType: 'isPublicationOfAuthor',
leftType: createSuccessfulRemoteDataObject$(publicationType),
rightType: createSuccessfulRemoteDataObject$(personType)
});
relationshipType2 = Object.assign(new RelationshipType(), {
id: '2',
uuid: '2',
leftwardType: 'isOrgUnitOfPublication',
rightwardType: 'isPublicationOfOrgUnit',
leftType: createSuccessfulRemoteDataObject$(publicationType),
rightType: createSuccessfulRemoteDataObject$(orgUnitType)
});
buildList = createSuccessfulRemoteDataObject(new PaginatedList(new PageInfo(), [relationshipType1, relationshipType2]));
rdbService = getMockRemoteDataBuildService(undefined, observableOf(buildList));
}
function initTestService() {
return new RelationshipTypeService(
requestService,
halService,
rdbService
);
}
beforeEach(() => {
init();
requestService = getMockRequestService();
service = initTestService();
});
describe('getAllRelationshipTypes', () => {
it('should return all relationshipTypes', (done) => {
const expected = service.getAllRelationshipTypes({});
expected.subscribe((e) => {
expect(e).toBe(buildList);
done();
})
});
});
describe('getRelationshipTypeByLabelAndTypes', () => {
it('should return the type filtered by label and type strings', (done) => {
const expected = service.getRelationshipTypeByLabelAndTypes(relationshipType1.leftwardType, publicationTypeString, personTypeString);
expected.subscribe((e) => {
expect(e).toBe(relationshipType1);
done();
})
});
});
});

View File

@@ -0,0 +1,81 @@
import { Injectable } from '@angular/core';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { filter, find, map, switchMap, tap } from 'rxjs/operators';
import { configureRequest, getSucceededRemoteData } from '../shared/operators';
import { Observable } from 'rxjs/internal/Observable';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
import { RemoteData } from './remote-data';
import { PaginatedList } from './paginated-list';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { ItemType } from '../shared/item-relationships/item-type.model';
import { isNotUndefined } from '../../shared/empty.util';
import { FindListOptions, FindListRequest } from './request.models';
/**
* The service handling all relationship requests
*/
@Injectable()
export class RelationshipTypeService {
protected linkPath = 'relationshiptypes';
constructor(protected requestService: RequestService,
protected halService: HALEndpointService,
protected rdbService: RemoteDataBuildService) {
}
/**
* Get the endpoint for a relationship type by ID
* @param id
*/
getRelationshipTypeEndpoint(id: number) {
return this.halService.getEndpoint(this.linkPath).pipe(
map((href: string) => `${href}/${id}`)
);
}
getAllRelationshipTypes(options: FindListOptions): Observable<RemoteData<PaginatedList<RelationshipType>>> {
const link$ = this.halService.getEndpoint(this.linkPath);
return link$
.pipe(
map((endpointURL: string) => new FindListRequest(this.requestService.generateRequestId(), endpointURL, options)),
configureRequest(this.requestService),
switchMap(() => this.rdbService.buildList(link$))
);
}
/**
* Get the RelationshipType for a relationship type by label
* @param label
*/
getRelationshipTypeByLabelAndTypes(label: string, firstType: string, secondType: string): Observable<RelationshipType> {
return this.getAllRelationshipTypes({ currentPage: 1, elementsPerPage: Number.MAX_VALUE })
.pipe(
getSucceededRemoteData(),
/* Flatten the page so we can treat it like an observable */
switchMap((typeListRD: RemoteData<PaginatedList<RelationshipType>>) => typeListRD.payload.page),
switchMap((type: RelationshipType) => {
if (type.leftwardType === label) {
return this.checkType(type, firstType, secondType);
} else if (type.rightwardType === label) {
return this.checkType(type, secondType, firstType);
} else {
return [];
}
}),
);
}
// Check if relationship type matches the given types
// returns a void observable if there's not match
// returns an observable that emits the relationship type when there is a match
private checkType(type: RelationshipType, firstType: string, secondType: string): Observable<RelationshipType> {
const entityTypes = observableCombineLatest(type.leftType.pipe(getSucceededRemoteData()), type.rightType.pipe(getSucceededRemoteData()));
return entityTypes.pipe(
find(([leftTypeRD, rightTypeRD]: [RemoteData<ItemType>, RemoteData<ItemType>]) => leftTypeRD.payload.label === firstType && rightTypeRD.payload.label === secondType),
filter((types) => isNotUndefined(types)),
map(() => type)
);
}
}

View File

@@ -71,12 +71,14 @@ describe('RelationshipService', () => {
const rdbService = getMockRemoteDataBuildService(undefined, buildList$);
const objectCache = Object.assign({
/* tslint:disable:no-empty */
remove: () => {}
remove: () => {},
hasBySelfLinkObservable: () => observableOf(false)
/* tslint:enable:no-empty */
}) as ObjectCacheService;
const itemService = jasmine.createSpyObj('itemService', {
findById: (uuid) => new RemoteData(false, false, true, undefined, relatedItems.filter((relatedItem) => relatedItem.id === uuid)[0])
findById: (uuid) => new RemoteData(false, false, true, undefined, relatedItems.find((relatedItem) => relatedItem.id === uuid)),
findByHref: createSuccessfulRemoteDataObject$(relatedItems[0])
});
function initTestService() {
@@ -90,6 +92,7 @@ describe('RelationshipService', () => {
objectCache,
null,
null,
null,
null
);
}
@@ -133,14 +136,6 @@ describe('RelationshipService', () => {
});
});
describe('getItemRelationshipLabels', () => {
it('should return the correct labels', () => {
service.getItemRelationshipLabels(item).subscribe((result) => {
expect(result).toEqual([relationshipType.rightwardType]);
});
});
});
describe('getRelatedItems', () => {
it('should return the related items', () => {
service.getRelatedItems(item).subscribe((result) => {

View File

@@ -2,38 +2,43 @@ import { Injectable } from '@angular/core';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util';
import { distinctUntilChanged, filter, flatMap, map, switchMap, take, tap } from 'rxjs/operators';
import {
configureRequest,
filterSuccessfulResponses,
getRemoteDataPayload, getResponseFromEntry,
getSucceededRemoteData
} from '../shared/operators';
import { DeleteRequest, FindListOptions, RestRequest } from './request.models';
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { distinctUntilChanged, filter, map, mergeMap, skipWhile, startWith, switchMap, take, tap } from 'rxjs/operators';
import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models';
import { Observable } from 'rxjs/internal/Observable';
import { RestResponse } from '../cache/response.models';
import { Item } from '../shared/item.model';
import { Relationship } from '../shared/item-relationships/relationship.model';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
import { RemoteData } from './remote-data';
import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest';
import { zip as observableZip } from 'rxjs';
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs';
import { PaginatedList } from './paginated-list';
import { ItemDataService } from './item-data.service';
import {
compareArraysUsingIds, filterRelationsByTypeLabel, paginatedRelationsToItems,
relationsToItems
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DataService } from './data.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { Store } from '@ngrx/store';
import { MemoizedSelector, select, Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { SearchParam } from '../cache/models/search-param.model';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { AppState, keySelector } from '../../app.reducer';
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
import { RemoveNameVariantAction, SetNameVariantAction } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
const relationshipListStateSelector = (listID: string): MemoizedSelector<AppState, NameVariantListState> => {
return keySelector<NameVariantListState>(listID, relationshipListsStateSelector);
};
const relationshipStateSelector = (listID: string, itemID: string): MemoizedSelector<AppState, string> => {
return keySelector<string>(itemID, relationshipListStateSelector(listID));
};
/**
* The service handling all relationship requests
@@ -52,7 +57,8 @@ export class RelationshipService extends DataService<Relationship> {
protected objectCache: ObjectCacheService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<Relationship>) {
protected comparator: DefaultChangeAnalyzer<Relationship>,
protected appStore: Store<AppState>) {
super();
}
@@ -65,76 +71,94 @@ export class RelationshipService extends DataService<Relationship> {
* @param uuid
*/
getRelationshipEndpoint(uuid: string) {
return this.halService.getEndpoint(this.linkPath).pipe(
return this.getBrowseEndpoint().pipe(
map((href: string) => `${href}/${uuid}`)
);
}
/**
* Find a relationship by its UUID
* @param uuid
*/
findById(uuid: string): Observable<RemoteData<Relationship>> {
const href$ = this.getRelationshipEndpoint(uuid);
return this.rdbService.buildSingle<Relationship>(href$);
}
/**
* Send a delete request for a relationship by ID
* @param uuid
* @param id
*/
deleteRelationship(uuid: string): Observable<RestResponse> {
return this.getRelationshipEndpoint(uuid).pipe(
deleteRelationship(id: string): Observable<RestResponse> {
return this.getRelationshipEndpoint(id).pipe(
isNotEmptyOperator(),
distinctUntilChanged(),
take(1),
map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)),
configureRequest(this.requestService),
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
getResponseFromEntry(),
tap(() => this.clearRelatedCache(uuid))
tap(() => this.removeRelationshipItemsFromCacheByRelationship(id))
);
}
/**
* Get a combined observable containing an array of all relationships in an item, as well as an array of the relationships their types
* This is used for easier access of a relationship's type because they exist as observables
* @param item
* Method to create a new relationship
* @param typeId The identifier of the relationship type
* @param item1 The first item of the relationship
* @param item2 The second item of the relationship
* @param leftwardValue The leftward value of the relationship
* @param rightwardValue The rightward value of the relationship
*/
getItemResolvedRelsAndTypes(item: Item): Observable<[Relationship[], RelationshipType[]]> {
return observableCombineLatest(
this.getItemRelationshipsArray(item),
this.getItemRelationshipTypesArray(item)
addRelationship(typeId: string, item1: Item, item2: Item, leftwardValue?: string, rightwardValue?: string): Observable<RestResponse> {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers;
return this.halService.getEndpoint(this.linkPath).pipe(
isNotEmptyOperator(),
take(1),
map((endpointUrl: string) => `${endpointUrl}?relationshipType=${typeId}`),
map((endpointUrl: string) => isNotEmpty(leftwardValue) ? `${endpointUrl}&leftwardValue=${leftwardValue}` : endpointUrl),
map((endpointUrl: string) => isNotEmpty(rightwardValue) ? `${endpointUrl}&rightwardValue=${rightwardValue}` : endpointUrl),
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, `${item1.self} \n ${item2.self}`, options)),
configureRequest(this.requestService),
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
getResponseFromEntry(),
tap(() => this.removeRelationshipItemsFromCache(item1)),
tap(() => this.removeRelationshipItemsFromCache(item2))
);
}
/**
* Get a combined observable containing an array of all the item's relationship's left- and right-side items, as well as an array of the relationships their types
* This is used for easier access of a relationship's type and left and right items because they exist as observables
* @param item
* Method to remove two items of a relationship from the cache using the identifier of the relationship
* @param relationshipId The identifier of the relationship
*/
getItemResolvedRelatedItemsAndTypes(item: Item): Observable<[Item[], Item[], RelationshipType[]]> {
return observableCombineLatest(
this.getItemLeftRelatedItemArray(item),
this.getItemRightRelatedItemArray(item),
this.getItemRelationshipTypesArray(item)
);
private removeRelationshipItemsFromCacheByRelationship(relationshipId: string) {
this.findById(relationshipId).pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
switchMap((relationship: Relationship) => combineLatest(
relationship.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
relationship.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
)
),
take(1)
).subscribe(([item1, item2]) => {
this.removeRelationshipItemsFromCache(item1);
this.removeRelationshipItemsFromCache(item2);
})
}
/**
* Get a combined observable containing an array of all the item's relationship's left- and right-side items, as well as an array of the relationships themselves
* This is used for easier access of the relationship and their left and right items because they exist as observables
* @param item
* Method to remove an item that's part of a relationship from the cache
* @param item The item to remove from the cache
*/
getItemResolvedRelatedItemsAndRelationships(item: Item): Observable<[Item[], Item[], Relationship[]]> {
return observableCombineLatest(
this.getItemLeftRelatedItemArray(item),
this.getItemRightRelatedItemArray(item),
this.getItemRelationshipsArray(item)
);
private removeRelationshipItemsFromCache(item) {
this.objectCache.remove(item.self);
this.requestService.removeByHrefSubstring(item.self);
combineLatest(
this.objectCache.hasBySelfLinkObservable(item.self),
this.requestService.hasByHrefObservable(item.self)
).pipe(
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
take(1),
switchMap(() => this.itemService.findByHref(item.self).pipe(take(1)))
).subscribe();
}
/**
* Get an item their relationships in the form of an array
* Get an item its relationships in the form of an array
* @param item
*/
getItemRelationshipsArray(item: Item): Observable<Relationship[]> {
@@ -148,67 +172,33 @@ export class RelationshipService extends DataService<Relationship> {
}
/**
* Get an item their relationship types in the form of an array
* @param item
*/
getItemRelationshipTypesArray(item: Item): Observable<RelationshipType[]> {
return this.getItemRelationshipsArray(item).pipe(
flatMap((rels: Relationship[]) =>
observableZip(...rels.map((rel: Relationship) => rel.relationshipType)).pipe(
map(([...arr]: Array<RemoteData<RelationshipType>>) => arr.map((d: RemoteData<RelationshipType>) => d.payload).filter((type) => hasValue(type))),
filter((arr) => arr.length === rels.length)
)
),
distinctUntilChanged(compareArraysUsingIds())
);
}
/**
* Get an item his relationship's left-side related items in the form of an array
* @param item
*/
getItemLeftRelatedItemArray(item: Item): Observable<Item[]> {
return this.getItemRelationshipsArray(item).pipe(
flatMap((rels: Relationship[]) => observableZip(...rels.map((rel: Relationship) => rel.leftItem)).pipe(
map(([...arr]: Array<RemoteData<Item>>) => arr.map((rd: RemoteData<Item>) => rd.payload).filter((i) => hasValue(i))),
filter((arr) => arr.length === rels.length)
)),
distinctUntilChanged(compareArraysUsingIds())
);
}
/**
* Get an item his relationship's right-side related items in the form of an array
* @param item
*/
getItemRightRelatedItemArray(item: Item): Observable<Item[]> {
return this.getItemRelationshipsArray(item).pipe(
flatMap((rels: Relationship[]) => observableZip(...rels.map((rel: Relationship) => rel.rightItem)).pipe(
map(([...arr]: Array<RemoteData<Item>>) => arr.map((rd: RemoteData<Item>) => rd.payload).filter((i) => hasValue(i))),
filter((arr) => arr.length === rels.length)
)),
distinctUntilChanged(compareArraysUsingIds())
);
}
/**
* Get an array of an item their unique relationship type's labels
* Get an array of the labels of an items unique relationship types
* The array doesn't contain any duplicate labels
* @param item
*/
getItemRelationshipLabels(item: Item): Observable<string[]> {
return this.getItemResolvedRelatedItemsAndTypes(item).pipe(
map(([leftItems, rightItems, relTypesCurrentPage]) => {
return relTypesCurrentPage.map((type, index) => {
if (leftItems[index].uuid === item.uuid) {
return type.leftwardType;
} else {
return type.rightwardType;
}
});
}),
getRelationshipTypeLabelsByItem(item: Item): Observable<string[]> {
return this.getItemRelationshipsArray(item).pipe(
switchMap((relationships: Relationship[]) => observableCombineLatest(relationships.map((relationship: Relationship) => this.getRelationshipTypeLabelByRelationshipAndItem(relationship, item)))),
map((labels: string[]) => Array.from(new Set(labels)))
)
);
}
private getRelationshipTypeLabelByRelationshipAndItem(relationship: Relationship, item: Item): Observable<string> {
return relationship.leftItem.pipe(
getSucceededRemoteData(),
map((itemRD: RemoteData<Item>) => itemRD.payload),
switchMap((otherItem: Item) => relationship.relationshipType.pipe(
getSucceededRemoteData(),
map((relationshipTypeRD) => relationshipTypeRD.payload),
map((relationshipType: RelationshipType) => {
if (otherItem.uuid === item.uuid) {
return relationshipType.leftwardType;
} else {
return relationshipType.rightwardType;
}
})
)
))
}
/**
@@ -244,7 +234,7 @@ export class RelationshipService extends DataService<Relationship> {
if (options) {
findListOptions = Object.assign(new FindListOptions(), options);
}
const searchParams = [ new SearchParam('label', label), new SearchParam('dso', item.id) ];
const searchParams = [new SearchParam('label', label), new SearchParam('dso', item.id)];
if (findListOptions.searchParams) {
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
} else {
@@ -254,20 +244,146 @@ export class RelationshipService extends DataService<Relationship> {
}
/**
* Clear object and request caches of the items related to a relationship (left and right items)
* @param uuid
* Method for fetching an item's relationships, but filtered by related item IDs (essentially performing a reverse lookup)
* Only relationships where leftItem or rightItem's ID is present in the list provided will be returned
* @param item
* @param uuids
*/
clearRelatedCache(uuid: string) {
this.findById(uuid).pipe(
getRelationshipsByRelatedItemIds(item: Item, uuids: string[]): Observable<Relationship[]> {
return this.getItemRelationshipsArray(item).pipe(
switchMap((relationships: Relationship[]) => {
return observableCombineLatest(...relationships.map((relationship: Relationship) => {
const isLeftItem$ = this.isItemInUUIDArray(relationship.leftItem, uuids);
const isRightItem$ = this.isItemInUUIDArray(relationship.rightItem, uuids);
return observableCombineLatest(isLeftItem$, isRightItem$).pipe(
filter(([isLeftItem, isRightItem]) => isLeftItem || isRightItem),
map(() => relationship),
startWith(undefined)
);
}))
}),
map((relationships: Relationship[]) => relationships.filter(((relationship) => hasValue(relationship)))),
)
}
private isItemInUUIDArray(itemRD$: Observable<RemoteData<Item>>, uuids: string[]) {
return itemRD$.pipe(
getSucceededRemoteData(),
flatMap((rd: RemoteData<Relationship>) => observableCombineLatest(rd.payload.leftItem.pipe(getSucceededRemoteData()), rd.payload.rightItem.pipe(getSucceededRemoteData()))),
take(1)
).subscribe(([leftItem, rightItem]) => {
this.objectCache.remove(leftItem.payload.self);
this.objectCache.remove(rightItem.payload.self);
this.requestService.removeByHrefSubstring(leftItem.payload.self);
this.requestService.removeByHrefSubstring(rightItem.payload.self);
});
map((itemRD: RemoteData<Item>) => itemRD.payload),
map((item: Item) => uuids.includes(item.uuid))
);
}
/**
* Method to retrieve a relationship based on two items and a relationship type label
* @param item1 The first item in the relationship
* @param item2 The second item in the relationship
* @param label The rightward or leftward type of the relationship
*/
getRelationshipByItemsAndLabel(item1: Item, item2: Item, label: string): Observable<Relationship> {
return this.getItemRelationshipsByLabel(item1, label)
.pipe(
getSucceededRemoteData(),
isNotEmptyOperator(),
map((relationshipListRD: RemoteData<PaginatedList<Relationship>>) => relationshipListRD.payload.page),
mergeMap((relationships: Relationship[]) => {
return observableCombineLatest(...relationships.map((relationship: Relationship) => {
return observableCombineLatest(
this.isItemMatchWithItemRD(relationship.leftItem, item2),
this.isItemMatchWithItemRD(relationship.rightItem, item2)
).pipe(
map(([isLeftItem, isRightItem]) => isLeftItem || isRightItem),
map((isMatch) => isMatch ? relationship : undefined)
);
}))
}),
map((relationships: Relationship[]) => relationships.find(((relationship) => hasValue(relationship))))
)
}
private isItemMatchWithItemRD(itemRD$: Observable<RemoteData<Item>>, itemCheck: Item): Observable<boolean> {
return itemRD$.pipe(
getSucceededRemoteData(),
map((itemRD: RemoteData<Item>) => itemRD.payload),
map((item: Item) => item.uuid === itemCheck.uuid)
);
}
/**
* Method to set the name variant for specific list and item
* @param listID The list for which to save the name variant
* @param itemID The item ID for which to save the name variant
* @param nameVariant The name variant to save
*/
public setNameVariant(listID: string, itemID: string, nameVariant: string) {
this.appStore.dispatch(new SetNameVariantAction(listID, itemID, nameVariant));
}
/**
* Method to retrieve the name variant for a specific list and item
* @param listID The list for which to retrieve the name variant
* @param itemID The item ID for which to retrieve the name variant
*/
public getNameVariant(listID: string, itemID: string): Observable<string> {
return this.appStore.pipe(
select(relationshipStateSelector(listID, itemID))
);
}
/**
* Method to remove the name variant for specific list and item
* @param listID The list for which to remove the name variant
* @param itemID The item ID for which to remove the name variant
*/
public removeNameVariant(listID: string, itemID: string) {
this.appStore.dispatch(new RemoveNameVariantAction(listID, itemID));
}
/**
* Method to retrieve all name variants for a single list
* @param listID The id of the list
*/
public getNameVariantsByListID(listID: string) {
return this.appStore.pipe(select(relationshipListStateSelector(listID)));
}
/**
* Method to update the name variant on the server
* @param item1 The first item of the relationship
* @param item2 The second item of the relationship
* @param relationshipLabel The leftward or rightward type of the relationship
* @param nameVariant The name variant to set for the matching relationship
*/
public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> {
return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
.pipe(
switchMap((relation: Relationship) =>
relation.relationshipType.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
map((type) => {
return { relation, type }
})
)
),
switchMap((relationshipAndType: { relation: Relationship, type: RelationshipType }) => {
const { relation, type } = relationshipAndType;
let updatedRelationship;
if (relationshipLabel === type.leftwardType) {
updatedRelationship = Object.assign(new Relationship(), relation, { rightwardValue: nameVariant });
} else {
updatedRelationship = Object.assign(new Relationship(), relation, { leftwardValue: nameVariant });
}
return this.update(updatedRelationship);
}),
// skipWhile((relationshipRD: RemoteData<Relationship>) => !relationshipRD.isSuccessful)
tap((relationshipRD: RemoteData<Relationship>) => {
if (relationshipRD.hasSucceeded) {
this.removeRelationshipItemsFromCache(item1);
this.removeRelationshipItemsFromCache(item2);
}
}),
)
}
}

View File

@@ -13,11 +13,11 @@ export enum RemoteDataState {
*/
export class RemoteData<T> {
constructor(
private requestPending: boolean,
private responsePending: boolean,
private isSuccessful: boolean,
public error: RemoteDataError,
public payload: T
private requestPending?: boolean,
private responsePending?: boolean,
private isSuccessful?: boolean,
public error?: RemoteDataError,
public payload?: T
) {
}

View File

@@ -3,7 +3,7 @@ import { HttpHeaders } from '@angular/common/http';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { Observable, race as observableRace } from 'rxjs';
import { filter, map, mergeMap, take } from 'rxjs/operators';
import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
import { cloneDeep, remove } from 'lodash';
import { AppState } from '../../app.reducer';
@@ -304,6 +304,7 @@ export class RequestService {
*/
hasByHref(href: string): boolean {
let result = false;
/* NB: that this is only a solution because the select method is synchronous, see: https://github.com/ngrx/store/issues/296#issuecomment-269032571*/
this.getByHref(href).pipe(
take(1)
).subscribe((requestEntry: RequestEntry) => result = this.isValid(requestEntry));

View File

@@ -6,7 +6,7 @@ import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { hasValue } from '../../shared/empty.util';
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
import { SearchQueryResponse } from '../../shared/search/search-query-response.model';
import { MetadataMap, MetadataValue } from '../shared/metadata.models';
@Injectable()

View File

@@ -0,0 +1,93 @@
import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service';
describe('StatusCodeOnlyResponseParsingService', () => {
let service;
let statusCode;
let statusText;
beforeEach(() => {
service = new StatusCodeOnlyResponseParsingService();
});
describe('parse', () => {
it('should return a RestResponse that doesn\'t contain the response body', () => {
const payload = 'd9128e44-183b-479d-aa2e-d39435838bf6';
const result = service.parse(undefined, {
payload,
statusCode: 201,
statusText: '201'
});
expect(JSON.stringify(result).indexOf(payload)).toBe(-1);
});
describe('when the response is successful', () => {
beforeEach(() => {
statusCode = 201;
statusText = `${statusCode}`;
});
it('should return a success RestResponse', () => {
const result = service.parse(undefined, {
statusCode,
statusText
});
expect(result.isSuccessful).toBe(true);
});
it('should return a RestResponse with the correct status code', () => {
const result = service.parse(undefined, {
statusCode,
statusText
});
expect(result.statusCode).toBe(statusCode);
});
it('should return a RestResponse with the correct status text', () => {
const result = service.parse(undefined, {
statusCode,
statusText
});
expect(result.statusText).toBe(statusText);
});
});
describe('when the response is unsuccessful', () => {
beforeEach(() => {
statusCode = 400;
statusText = `${statusCode}`;
});
it('should return an error RestResponse', () => {
const result = service.parse(undefined, {
statusCode,
statusText
});
expect(result.isSuccessful).toBe(false);
});
it('should return a RestResponse with the correct status code', () => {
const result = service.parse(undefined, {
statusCode,
statusText
});
expect(result.statusCode).toBe(statusCode);
});
it('should return a RestResponse with the correct status text', () => {
const result = service.parse(undefined, {
statusCode,
statusText
});
expect(result.statusText).toBe(statusText);
});
});
});
});

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { RestResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
/**
* A responseparser that will only look at the status code and status
* text of the response, and ignore anything else that might be there
*/
@Injectable({
providedIn: 'root'
})
export class StatusCodeOnlyResponseParsingService implements ResponseParsingService {
/**
* Parse the response and only extract the status code and status text
*
* @param request The request that was sent to the server
* @param data The response to parse
*/
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const isSuccessful = data.statusCode >= 200 && data.statusCode < 300;
return new RestResponse(isSuccessful, data.statusCode, data.statusText);
}
}

View File

@@ -26,7 +26,7 @@ export interface HttpOptions {
@Injectable()
export class DSpaceRESTv2Service {
constructor(private http: HttpClient) {
constructor(protected http: HttpClient) {
}

View File

@@ -9,6 +9,7 @@ import { Group } from './group.model';
@mapsTo(EPerson)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedEPerson extends NormalizedDSpaceObject<EPerson> implements CacheableObject {
/**
* A string representing the unique handle of this EPerson
*/

View File

@@ -7,7 +7,7 @@ import { GenericConstructor } from '../shared/generic-constructor';
/**
* Class the represents a metadata field
*/
export class MetadataField implements ListableObject {
export class MetadataField extends ListableObject {
static type = new ResourceType('metadatafield');
/**

View File

@@ -5,7 +5,7 @@ import { GenericConstructor } from '../shared/generic-constructor';
/**
* Class that represents a metadata schema
*/
export class MetadataSchema implements ListableObject {
export class MetadataSchema extends ListableObject {
static type = new ResourceType('metadataschema');
/**

View File

@@ -2,7 +2,6 @@ import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { mapsTo, relationship } from '../cache/builders/build-decorators';
import { MetadataField } from './metadata-field.model';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
import { MetadataSchema } from './metadata-schema.model';
/**

View File

@@ -1,7 +1,6 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { mapsTo } from '../cache/builders/build-decorators';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
import { MetadataSchema } from './metadata-schema.model';
/**
@@ -33,4 +32,5 @@ export class NormalizedMetadataSchema extends NormalizedObject<MetadataSchema> {
*/
@autoserialize
namespace: string;
}

View File

@@ -0,0 +1,22 @@
import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
/**
* The list of HrefIndexAction type definitions
*/
export const RouterActionTypes = {
ROUTE_UPDATE: type('dspace/core/router/ROUTE_UPDATE'),
};
/* tslint:disable:max-classes-per-file */
/**
* An ngrx action to be fired when the route is updated
* Note that, contrary to the router-store.ROUTER_NAVIGATION action,
* this action will only be fired when the path changes,
* not when just the query parameters change
*/
export class RouteUpdateAction implements Action {
type = RouterActionTypes.ROUTE_UPDATE;
}
/* tslint:enable:max-classes-per-file */

View File

@@ -0,0 +1,31 @@
import { filter, map, pairwise } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects'
import * as fromRouter from '@ngrx/router-store';
import { RouterNavigationAction } from '@ngrx/router-store';
import { Router } from '@angular/router';
import { RouteUpdateAction } from './router.actions';
@Injectable()
export class RouterEffects {
/**
* Effect that fires a new RouteUpdateAction when then path of route is changed
* @type {Observable<RouteUpdateAction>}
*/
@Effect() routeChange$ = this.actions$
.pipe(
ofType(fromRouter.ROUTER_NAVIGATION),
pairwise(),
map((actions: RouterNavigationAction[]) =>
actions.map((navigateAction) => {
const urlTree = this.router.parseUrl(navigateAction.payload.routerState.url);
return urlTree.root.children.primary.segments.map((it) => it.path).join('/');
})),
filter((actions: string[]) => actions[0] !== actions[1]),
map(() => new RouteUpdateAction())
);
constructor(private actions$: Actions, private router: Router) {
}
}

View File

@@ -10,6 +10,8 @@ export const RouteActionTypes = {
SET_PARAMETERS: type('dspace/core/route/SET_PARAMETERS'),
ADD_QUERY_PARAMETER: type('dspace/core/route/ADD_QUERY_PARAMETER'),
ADD_PARAMETER: type('dspace/core/route/ADD_PARAMETER'),
SET_QUERY_PARAMETER: type('dspace/core/route/SET_QUERY_PARAMETER'),
SET_PARAMETER: type('dspace/core/route/SET_PARAMETER'),
RESET: type('dspace/core/route/RESET'),
};
@@ -96,6 +98,52 @@ export class AddParameterAction implements Action {
}
}
/**
* An ngrx action to set a query parameter
*/
export class SetQueryParameterAction implements Action {
type = RouteActionTypes.SET_QUERY_PARAMETER;
payload: {
key: string;
value: string;
};
/**
* Create a new SetQueryParameterAction
*
* @param key
* the key to set
* @param value
* the value of this key
*/
constructor(key: string, value: string) {
this.payload = { key, value };
}
}
/**
* An ngrx action to set a parameter
*/
export class SetParameterAction implements Action {
type = RouteActionTypes.SET_PARAMETER;
payload: {
key: string;
value: string;
};
/**
* Create a new SetParameterAction
*
* @param key
* the key to set
* @param value
* the value of this key
*/
constructor(key: string, value: string) {
this.payload = { key, value };
}
}
/**
* An ngrx action to reset the route state
*/
@@ -113,4 +161,5 @@ export type RouteActions =
| SetParametersAction
| AddQueryParameterAction
| AddParameterAction
| ResetRouteStateAction;
| ResetRouteStateAction
| SetParameterAction;

View File

@@ -1,8 +1,9 @@
import { map } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects'
import * as fromRouter from '@ngrx/router-store';
import { ResetRouteStateAction } from './route.actions';
import { ResetRouteStateAction, RouteActionTypes } from './route.actions';
import { RouterActionTypes } from '../../core/router/router.actions';
import { RouteService } from './route.service';
@Injectable()
export class RouteEffects {
@@ -12,12 +13,16 @@ export class RouteEffects {
*/
@Effect() routeChange$ = this.actions$
.pipe(
ofType(fromRouter.ROUTER_NAVIGATION),
map(() => new ResetRouteStateAction())
ofType(RouterActionTypes.ROUTE_UPDATE),
map(() => new ResetRouteStateAction()),
);
constructor(private actions$: Actions) {
@Effect({dispatch: false }) afterResetChange$ = this.actions$
.pipe(
ofType(RouteActionTypes.RESET),
tap(() => this.service.setCurrentRouteInfo()),
);
constructor(private actions$: Actions, private service: RouteService) {
}
}

View File

@@ -3,7 +3,11 @@ import {
AddParameterAction,
AddQueryParameterAction,
RouteActions,
RouteActionTypes, SetParametersAction, SetQueryParametersAction
RouteActionTypes,
SetParameterAction,
SetParametersAction,
SetQueryParameterAction,
SetQueryParametersAction
} from './route.actions';
/**
@@ -44,6 +48,12 @@ export function routeReducer(state = initialState, action: RouteActions): RouteS
case RouteActionTypes.ADD_QUERY_PARAMETER: {
return addParameter(state, action as AddQueryParameterAction, 'queryParams');
}
case RouteActionTypes.SET_PARAMETER: {
return setParameter(state, action as SetParameterAction, 'params');
}
case RouteActionTypes.SET_QUERY_PARAMETER: {
return setParameter(state, action as SetQueryParameterAction, 'queryParams');
}
default: {
return state;
}
@@ -60,9 +70,10 @@ function addParameter(state: RouteState, action: AddParameterAction | AddQueryPa
const subState = state[paramType];
const existingValues = subState[action.payload.key] || [];
const newValues = [...existingValues, action.payload.value];
const newSubstate = Object.assign(subState, { [action.payload.key]: newValues });
const newSubstate = Object.assign({}, subState, { [action.payload.key]: newValues });
return Object.assign({}, state, { [paramType]: newSubstate });
}
/**
* Set a route or query parameter in the store
* @param state The current state
@@ -70,5 +81,17 @@ function addParameter(state: RouteState, action: AddParameterAction | AddQueryPa
* @param paramType The type of parameter to set: route or query parameter
*/
function setParameters(state: RouteState, action: SetParametersAction | SetQueryParametersAction, paramType: string): RouteState {
return Object.assign({}, state, { [paramType]: action.payload });
return Object.assign({}, state, { [paramType]: { [action.payload.key]: action.payload.value } });
}
/**
* Set a route or query parameter in the store
* @param state The current state
* @param action The set action to perform on the current state
* @param paramType The type of parameter to set: route or query parameter
*/
function setParameter(state: RouteState, action: SetParameterAction | SetQueryParameterAction, paramType: string): RouteState {
const subState = state[paramType];
const newSubstate = Object.assign({}, subState, { [action.payload.key]: action.payload.value });
return Object.assign({}, state, { [paramType]: newSubstate });
}

View File

@@ -1,4 +1,4 @@
import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators';
import { distinctUntilChanged, filter, map, take, tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import {
ActivatedRoute,
@@ -12,12 +12,17 @@ import { combineLatest, Observable } from 'rxjs';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { isEqual } from 'lodash';
import { AddUrlToHistoryAction } from '../../shared/history/history.actions';
import { historySelector } from '../../shared/history/selectors';
import { SetParametersAction, SetQueryParametersAction } from './route.actions';
import { CoreState } from '../core.reducers';
import {
AddParameterAction,
SetParameterAction,
SetParametersAction,
SetQueryParametersAction
} from './route.actions';
import { CoreState } from '../../core/core.reducers';
import { coreSelector } from '../../core/core.selectors';
import { hasValue } from '../../shared/empty.util';
import { coreSelector } from '../core.selectors';
import { historySelector } from '../../shared/history/selectors';
import { AddUrlToHistoryAction } from '../../shared/history/history.actions';
/**
* Selector to select all route parameters from the store
@@ -121,7 +126,7 @@ export class RouteService {
}
getRouteDataValue(datafield: string): Observable<any> {
return this.route.data.pipe(map((data) => data[datafield]), distinctUntilChanged(),);
return this.route.data.pipe(map((data) => data[datafield]), distinctUntilChanged());
}
/**
@@ -157,11 +162,9 @@ export class RouteService {
}
public saveRouting(): void {
combineLatest(this.router.events, this.getRouteParams(), this.route.queryParams)
.pipe(filter(([event, params, queryParams]) => event instanceof NavigationEnd))
.subscribe(([event, params, queryParams]: [NavigationEnd, Params, Params]) => {
this.store.dispatch(new SetParametersAction(params));
this.store.dispatch(new SetQueryParametersAction(queryParams));
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
this.store.dispatch(new AddUrlToHistoryAction(event.urlAfterRedirects));
});
}
@@ -183,4 +186,26 @@ export class RouteService {
map((history: string[]) => history[history.length - 2] || '')
);
}
public addParameter(key, value) {
this.store.dispatch(new AddParameterAction(key, value));
}
public setParameter(key, value) {
this.store.dispatch(new SetParameterAction(key, value));
}
/**
* Sets the current route parameters and query parameters in the store
*/
public setCurrentRouteInfo() {
combineLatest(this.getRouteParams(), this.route.queryParams)
.pipe(take(1))
.subscribe(
([params, queryParams]: [Params, Params]) => {
this.store.dispatch(new SetParametersAction(params));
this.store.dispatch(new SetQueryParametersAction(queryParams));
}
)
}
}

View File

@@ -2,12 +2,13 @@ import { ListableObject } from '../../shared/object-collection/shared/listable-o
import { TypedObject } from '../cache/object-cache.reducer';
import { ResourceType } from './resource-type';
import { GenericConstructor } from './generic-constructor';
import { excludeFromEquals } from '../utilities/equals.decorators';
/**
* Class object representing a browse entry
* This class is not normalized because browse entries do not have self links
*/
export class BrowseEntry implements ListableObject {
export class BrowseEntry extends ListableObject implements TypedObject {
static type = new ResourceType('browseEntry');
/**
@@ -28,6 +29,7 @@ export class BrowseEntry implements ListableObject {
/**
* The count of this browse entry
*/
@excludeFromEquals
count: number;
/**

View File

@@ -11,25 +11,29 @@ import { hasNoValue, isUndefined } from '../../shared/empty.util';
import { CacheableObject } from '../cache/object-cache.reducer';
import { RemoteData } from '../data/remote-data';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
import { excludeFromEquals } from '../utilities/equals.decorators';
import { ResourceType } from './resource-type';
import { GenericConstructor } from './generic-constructor';
/**
* An abstract model class for a DSpaceObject.
*/
export class DSpaceObject implements CacheableObject, ListableObject {
export class DSpaceObject extends ListableObject implements CacheableObject {
/**
* A string representing the kind of DSpaceObject, e.g. community, item, …
*/
static type = new ResourceType('dspaceobject');
@excludeFromEquals
private _name: string;
@excludeFromEquals
self: string;
/**
* The human-readable identifier of this DSpaceObject
*/
@excludeFromEquals
id: string;
/**
@@ -37,6 +41,12 @@ export class DSpaceObject implements CacheableObject, ListableObject {
*/
uuid: string;
/**
* A string representing the kind of DSpaceObject, e.g. community, item, …
*/
@excludeFromEquals
type: ResourceType;
/**
* The name for this DSpaceObject
*/
@@ -54,6 +64,7 @@ export class DSpaceObject implements CacheableObject, ListableObject {
/**
* All metadata of this DSpaceObject
*/
@excludeFromEquals
metadata: MetadataMap;
/**
@@ -66,11 +77,13 @@ export class DSpaceObject implements CacheableObject, ListableObject {
/**
* An array of DSpaceObjects that are direct parents of this DSpaceObject
*/
@excludeFromEquals
parents: Observable<RemoteData<DSpaceObject[]>>;
/**
* The DSpaceObject that owns this DSpaceObject
*/
@excludeFromEquals
owner: Observable<RemoteData<DSpaceObject>>;
/**

View File

@@ -12,6 +12,8 @@ export class ItemType implements CacheableObject {
*/
id: string;
label: string;
/**
* The link to the rest endpoint where this object can be found
*/

View File

@@ -46,6 +46,16 @@ export class Relationship implements CacheableObject {
*/
rightPlace: number;
/**
* The name variant of the Item to the left side of this Relationship
*/
leftwardValue: string;
/**
* The name variant of the Item to the right side of this Relationship
*/
rightwardValue: string;
/**
* The type of Relationship
*/

View File

@@ -81,6 +81,9 @@ export interface MetadataValueFilter {
/** The value constraint. */
value?: string;
/** The authority constraint. */
authority?: string;
/** Whether the value constraint should match without regard to case. */
ignoreCase?: boolean;

View File

@@ -8,8 +8,8 @@ import {
} from './metadata.models';
import { Metadata } from './metadata.utils';
const mdValue = (value: string, language?: string): MetadataValue => {
return Object.assign(new MetadataValue(), { uuid: uuidv4(), value: value, language: isUndefined(language) ? null : language, place: 0, authority: undefined, confidence: undefined });
const mdValue = (value: string, language?: string, authority?: string): MetadataValue => {
return Object.assign(new MetadataValue(), { uuid: uuidv4(), value: value, language: isUndefined(language) ? null : language, place: 0, authority: isUndefined(authority) ? null : authority, confidence: undefined });
};
const dcDescription = mdValue('Some description');
@@ -184,6 +184,8 @@ describe('Metadata', () => {
testValueMatches(mdValue('a'), true, { language: null });
testValueMatches(mdValue('a'), false, { language: 'en_US' });
testValueMatches(mdValue('a', 'en_US'), true, { language: 'en_US' });
testValueMatches(mdValue('a', undefined, '4321'), true, { authority: '4321' });
testValueMatches(mdValue('a', undefined, '4321'), false, { authority: '1234' });
});
describe('toViewModelList method', () => {

View File

@@ -127,6 +127,8 @@ export class Metadata {
return true;
} else if (filter.language && filter.language !== mdValue.language) {
return false;
} else if (filter.authority && filter.authority !== mdValue.authority) {
return false;
} else if (filter.value) {
let fValue = filter.value;
let mValue = mdValue.value;

Some files were not shown because too many files have changed in this diff Show More