mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge remote-tracking branch 'atmire/clean-relationships-in-submission' into w2p-67478_Search-external-sources-in-submission
Conflicts: src/app/+item-page/simple/related-items/related-items-component.ts src/app/+search-page/configuration-search-page.component.ts src/app/+search-page/search-page.module.ts src/app/+search-page/search-tracker.component.ts src/app/+search-page/search.component.ts src/app/app.reducer.ts src/app/core/data/data.service.ts src/app/core/data/relationship.service.ts src/app/core/services/route.service.ts src/app/core/utilities/equatable.ts src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts src/app/shared/form/builder/models/relationship-options.model.ts
This commit is contained in:
@@ -9,11 +9,10 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
// The REST API server settings.
|
// The REST API server settings.
|
||||||
rest: {
|
rest: {
|
||||||
ssl: true,
|
ssl: true,
|
||||||
host: 'dspace7.4science.cloud',
|
host: 'dspace7-entities.atmire.com',
|
||||||
port: 443,
|
port: 443,
|
||||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
nameSpace: '/server/api'
|
||||||
nameSpace: '/server/api'
|
|
||||||
},
|
},
|
||||||
// Caching settings
|
// Caching settings
|
||||||
cache: {
|
cache: {
|
||||||
|
@@ -829,9 +829,9 @@
|
|||||||
|
|
||||||
"item.page.person.search.title": "Articles by this author",
|
"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",
|
"item.page.relationships.isAuthorOfPublication": "Publications",
|
||||||
|
|
||||||
@@ -1575,6 +1575,10 @@
|
|||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
"submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||||
|
|
||||||
|
"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.selection-tab.tab-title": "Current Selection ({{ count }})",
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.title.Journal Issue": "Journal Issues",
|
"submission.sections.describe.relationship-lookup.title.Journal Issue": "Journal Issues",
|
||||||
@@ -1585,6 +1589,10 @@
|
|||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.title.Author": "Authors",
|
"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.search-tab.toggle-dropdown": "Toggle dropdown",
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.selection-tab.settings": "Settings",
|
"submission.sections.describe.relationship-lookup.selection-tab.settings": "Settings",
|
||||||
|
@@ -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 { 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 { TabbedRelatedEntitiesSearchComponent } from './simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component';
|
||||||
import { StatisticsModule } from '../statistics/statistics.module';
|
import { StatisticsModule } from '../statistics/statistics.module';
|
||||||
|
import { AbstractIncrementalListComponent } from './simple/abstract-incremental-list/abstract-incremental-list.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -57,7 +58,8 @@ import { StatisticsModule } from '../statistics/statistics.module';
|
|||||||
GenericItemPageFieldComponent,
|
GenericItemPageFieldComponent,
|
||||||
MetadataRepresentationListComponent,
|
MetadataRepresentationListComponent,
|
||||||
RelatedEntitiesSearchComponent,
|
RelatedEntitiesSearchComponent,
|
||||||
TabbedRelatedEntitiesSearchComponent
|
TabbedRelatedEntitiesSearchComponent,
|
||||||
|
AbstractIncrementalListComponent
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ItemComponent,
|
ItemComponent,
|
||||||
|
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,11 +1,20 @@
|
|||||||
<ds-metadata-field-wrapper *ngIf="representations$ && (representations$ | async)?.length > 0" [label]="label">
|
<ds-metadata-field-wrapper [label]="label">
|
||||||
<ds-metadata-representation-loader *ngFor="let rep of (representations$ | async)"
|
<ng-container *ngFor="let objectPage of objects; let i = index">
|
||||||
[mdRepresentation]="rep">
|
<ng-container *ngVar="(objectPage | async) as representations">
|
||||||
</ds-metadata-representation-loader>
|
<ds-metadata-representation-loader *ngFor="let rep of representations"
|
||||||
<div *ngIf="(representations$ | async)?.length < total" class="mt-2">
|
[mdRepresentation]="rep">
|
||||||
<a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a>
|
</ds-metadata-representation-loader>
|
||||||
</div>
|
<ds-loading *ngIf="(i + 1) === objects.length && (i > 0) && (!representations || representations?.length === 0)" message="{{'loading.default' | translate}}"></ds-loading>
|
||||||
<div *ngIf="limit > originalLimit" class="mt-2">
|
<div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && representations?.length > 0">
|
||||||
<a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a>
|
<div *ngIf="(objects.length * incrementBy) < total" class="float-left">
|
||||||
</div>
|
<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>
|
</ds-metadata-field-wrapper>
|
||||||
|
@@ -7,6 +7,8 @@ import { Item } from '../../../core/shared/item.model';
|
|||||||
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
const itemType = 'Person';
|
const itemType = 'Person';
|
||||||
const metadataField = 'dc.contributor.author';
|
const metadataField = 'dc.contributor.author';
|
||||||
@@ -64,7 +66,7 @@ describe('MetadataRepresentationListComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [TranslateModule.forRoot()],
|
||||||
declarations: [MetadataRepresentationListComponent],
|
declarations: [MetadataRepresentationListComponent, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RelationshipService, useValue: relationshipService }
|
{ provide: RelationshipService, useValue: relationshipService }
|
||||||
],
|
],
|
||||||
@@ -88,33 +90,29 @@ describe('MetadataRepresentationListComponent', () => {
|
|||||||
expect(fields.length).toBe(2);
|
expect(fields.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize the original limit', () => {
|
it('should contain one page of items', () => {
|
||||||
expect(comp.originalLimit).toEqual(comp.limit);
|
expect(comp.objects.length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when viewMore is called', () => {
|
describe('when increase is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.viewMore();
|
comp.increase();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the limit to a high number in order to retrieve all metadata representations', () => {
|
it('should add a new page to the list', () => {
|
||||||
expect(comp.limit).toBeGreaterThanOrEqual(999);
|
expect(comp.objects.length).toEqual(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when viewLess is called', () => {
|
describe('when decrease is called', () => {
|
||||||
let originalLimit;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Store the original value of limit
|
// Add a second page
|
||||||
originalLimit = comp.limit;
|
comp.objects.push(observableOf(undefined));
|
||||||
// Set limit to a random number
|
comp.decrease();
|
||||||
comp.limit = 458;
|
|
||||||
comp.viewLess();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reset the limit to the original value', () => {
|
it('should decrease the list of pages', () => {
|
||||||
expect(comp.limit).toEqual(originalLimit);
|
expect(comp.objects.length).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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 { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs';
|
||||||
import { RelationshipService } from '../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../core/data/relationship.service';
|
||||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||||
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { filter, map, switchMap } from 'rxjs/operators';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.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 { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||||
import { map, filter } from 'rxjs/operators';
|
import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-metadata-representation-list',
|
selector: 'ds-metadata-representation-list',
|
||||||
@@ -22,7 +22,7 @@ import { map, filter } from 'rxjs/operators';
|
|||||||
* It expects an itemType to resolve the metadata to a an item
|
* It expects an itemType to resolve the metadata to a an item
|
||||||
* It expects a label to put on top of the list
|
* 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
|
* The parent of the list of related items to display
|
||||||
*/
|
*/
|
||||||
@@ -44,22 +44,11 @@ export class MetadataRepresentationListComponent implements OnInit {
|
|||||||
@Input() label: string;
|
@Input() label: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The max amount of representations to display
|
* The amount to increment the list by when clicking "view more"
|
||||||
* Defaults to 10
|
* Defaults to 10
|
||||||
* The default can optionally be overridden by providing the limit as input to the component
|
* The default can optionally be overridden by providing the limit as input to the component
|
||||||
*/
|
*/
|
||||||
@Input() limit = 10;
|
@Input() incrementBy = 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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The total amount of metadata values available
|
* The total amount of metadata values available
|
||||||
@@ -67,30 +56,28 @@ export class MetadataRepresentationListComponent implements OnInit {
|
|||||||
total: number;
|
total: number;
|
||||||
|
|
||||||
constructor(public relationshipService: RelationshipService) {
|
constructor(public relationshipService: RelationshipService) {
|
||||||
}
|
super();
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.originalLimit = this.limit;
|
|
||||||
this.setRepresentations();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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);
|
const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataField);
|
||||||
this.total = metadata.length;
|
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
|
* 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(
|
return observableZip(
|
||||||
...metadata
|
...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: any) => Object.assign(new MetadataValue(), metadatum))
|
||||||
.map((metadatum: MetadataValue) => {
|
.map((metadatum: MetadataValue) => {
|
||||||
if (metadatum.isVirtual) {
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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 { Item } from '../../../core/shared/item.model';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
import { RelationshipService } from '../../../core/data/relationship.service';
|
|
||||||
import { FindListOptions } from '../../../core/data/request.models';
|
import { FindListOptions } from '../../../core/data/request.models';
|
||||||
import { Subscription } from 'rxjs/internal/Subscription';
|
|
||||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
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({
|
@Component({
|
||||||
selector: 'ds-related-items',
|
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
|
* 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
|
* 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
|
* The parent of the list of related items to display
|
||||||
*/
|
*/
|
||||||
@@ -30,79 +30,38 @@ export class RelatedItemsComponent implements OnInit, OnDestroy {
|
|||||||
@Input() relationType: string;
|
@Input() relationType: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default options to start a search request with
|
* The amount to increment the list by when clicking "view more"
|
||||||
* Optional input, should you wish a different page size (or other options)
|
* 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)
|
* An i18n label to use as a title for the list (usually describes the relation)
|
||||||
*/
|
*/
|
||||||
@Input() label: string;
|
@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
|
* The view-mode we're currently on
|
||||||
* @type {ViewMode}
|
* @type {ViewMode}
|
||||||
*/
|
*/
|
||||||
viewMode = ViewMode.ListElement;
|
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) {
|
constructor(public relationshipService: RelationshipService) {
|
||||||
}
|
super();
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand the list to display all related items
|
* Get a specific page
|
||||||
|
* @param page The page to fetch
|
||||||
*/
|
*/
|
||||||
viewMore() {
|
getPage(page: number): Observable<RemoteData<PaginatedList<Item>>> {
|
||||||
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.allOptions);
|
return this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, Object.assign(this.options, { elementsPerPage: this.incrementBy, currentPage: page }));
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,20 @@
|
|||||||
<ds-metadata-field-wrapper *ngIf="(items$ | async)?.payload?.page?.length > 0" [label]="label">
|
<ds-metadata-field-wrapper [label]="label">
|
||||||
<ds-listable-object-component-loader *ngFor="let item of (items$ | async)?.payload?.page"
|
<ng-container *ngFor="let objectPage of objects; let i = index">
|
||||||
[object]="item" [viewMode]="viewMode">
|
<ng-container *ngVar="(objectPage | async) as itemsRD">
|
||||||
</ds-listable-object-component-loader>
|
<ds-listable-object-component-loader *ngFor="let item of itemsRD?.payload?.page"
|
||||||
<div *ngIf="(items$ | async)?.payload?.page?.length < (items$ | async)?.payload?.totalElements" class="mt-2" id="view-more">
|
[object]="item" [viewMode]="viewMode">
|
||||||
<a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a>
|
</ds-listable-object-component-loader>
|
||||||
</div>
|
<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 *ngIf="showingAll" class="mt-2" id="view-less">
|
<div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && itemsRD?.payload?.page?.length > 0">
|
||||||
<a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a>
|
<div *ngIf="itemsRD?.payload?.totalPages > objects.length" class="float-left" id="view-more">
|
||||||
</div>
|
<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>
|
</ds-metadata-field-wrapper>
|
||||||
|
@@ -9,6 +9,8 @@ import { createRelationshipsObservable } from '../item-types/shared/item.compone
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
||||||
import { RelationshipService } from '../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../core/data/relationship.service';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
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(), {
|
const parentItem: Item = Object.assign(new Item(), {
|
||||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
@@ -42,7 +44,7 @@ describe('RelatedItemsComponent', () => {
|
|||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [TranslateModule.forRoot()],
|
||||||
declarations: [RelatedItemsComponent],
|
declarations: [RelatedItemsComponent, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RelationshipService, useValue: relationshipService }
|
{ provide: RelationshipService, useValue: relationshipService }
|
||||||
],
|
],
|
||||||
@@ -65,31 +67,33 @@ describe('RelatedItemsComponent', () => {
|
|||||||
expect(fields.length).toBe(mockItems.length);
|
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(() => {
|
beforeEach(() => {
|
||||||
comp.viewMore();
|
comp.increase();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments', () => {
|
it('should add a new page to the list', () => {
|
||||||
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.allOptions);
|
expect(comp.objects.length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set showingAll to true', () => {
|
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments (second page)', () => {
|
||||||
expect(comp.showingAll).toEqual(true);
|
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(() => {
|
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', () => {
|
it('should decrease the list of pages', () => {
|
||||||
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.options);
|
expect(comp.objects.length).toEqual(1);
|
||||||
});
|
|
||||||
|
|
||||||
it('should set showingAll to false', () => {
|
|
||||||
expect(comp.showingAll).toEqual(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu
|
|||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { RouteService } from '../core/services/route.service';
|
import { RouteService } from '../core/services/route.service';
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a search page using a configuration as input.
|
* This component renders a search page using a configuration as input.
|
||||||
@@ -43,8 +44,9 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements
|
|||||||
protected sidebarService: SidebarService,
|
protected sidebarService: SidebarService,
|
||||||
protected windowService: HostWindowService,
|
protected windowService: HostWindowService,
|
||||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
protected routeService: RouteService) {
|
protected routeService: RouteService,
|
||||||
super(service, sidebarService, windowService, searchConfigService, routeService);
|
protected router: Router) {
|
||||||
|
super(service, sidebarService, windowService, searchConfigService, routeService, router);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { SearchComponent } from './search.component';
|
|
||||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||||
import { SearchPageComponent } from './search-page.component';
|
import { SearchPageComponent } from './search-page.component';
|
||||||
|
@@ -9,6 +9,7 @@ import { RouteService } from '../core/services/route.service';
|
|||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { SearchSuccessResponse } from '../core/cache/response.models';
|
import { SearchSuccessResponse } from '../core/cache/response.models';
|
||||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||||
import { SearchQueryResponse } from '../shared/search/search-query-response.model';
|
import { SearchQueryResponse } from '../shared/search/search-query-response.model';
|
||||||
@@ -30,14 +31,15 @@ import { SearchQueryResponse } from '../shared/search/search-query-response.mode
|
|||||||
export class SearchTrackerComponent extends SearchComponent implements OnInit {
|
export class SearchTrackerComponent extends SearchComponent implements OnInit {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected service:SearchService,
|
protected service: SearchService,
|
||||||
protected sidebarService:SidebarService,
|
protected sidebarService: SidebarService,
|
||||||
protected windowService:HostWindowService,
|
protected windowService: HostWindowService,
|
||||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService:SearchConfigurationService,
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
protected routeService:RouteService,
|
protected routeService: RouteService,
|
||||||
public angulartics2:Angulartics2
|
public angulartics2: Angulartics2,
|
||||||
|
protected router: Router
|
||||||
) {
|
) {
|
||||||
super(service, sidebarService, windowService, searchConfigService, routeService);
|
super(service, sidebarService, windowService, searchConfigService, routeService, router);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit():void {
|
ngOnInit():void {
|
||||||
@@ -58,9 +60,9 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.subscribe((entry) => {
|
.subscribe((entry) => {
|
||||||
const config:PaginatedSearchOptions = entry.searchOptions;
|
const config: PaginatedSearchOptions = entry.searchOptions;
|
||||||
const searchQueryResponse:SearchQueryResponse = entry.response;
|
const searchQueryResponse: SearchQueryResponse = entry.response;
|
||||||
const filters:Array<{ filter:string, operator:string, value:string, label:string; }> = [];
|
const filters:Array<{ filter: string, operator: string, value: string, label: string; }> = [];
|
||||||
const appliedFilters = searchQueryResponse.appliedFilters || [];
|
const appliedFilters = searchQueryResponse.appliedFilters || [];
|
||||||
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
|
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
|
||||||
const appliedFilter = appliedFilters[i];
|
const appliedFilter = appliedFilters[i];
|
||||||
|
@@ -46,5 +46,9 @@
|
|||||||
[scopes]="(scopeListRD$ | async)"
|
[scopes]="(scopeListRD$ | async)"
|
||||||
[inPlaceSearch]="inPlaceSearch">
|
[inPlaceSearch]="inPlaceSearch">
|
||||||
</ds-search-form>
|
</ds-search-form>
|
||||||
|
<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>
|
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
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 { startWith, switchMap, } from 'rxjs/operators';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
@@ -11,10 +11,12 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
|
|||||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
import { RouteService } from '../core/services/route.service';
|
import { RouteService } from '../core/services/route.service';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||||
import { SearchResult } from '../shared/search/search-result.model';
|
|
||||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
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 { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
|
import { currentPath } from '../shared/utils/route.utils';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search',
|
selector: 'ds-search',
|
||||||
@@ -96,7 +98,8 @@ export class SearchComponent implements OnInit {
|
|||||||
protected sidebarService: SidebarService,
|
protected sidebarService: SidebarService,
|
||||||
protected windowService: HostWindowService,
|
protected windowService: HostWindowService,
|
||||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
protected routeService: RouteService) {
|
protected routeService: RouteService,
|
||||||
|
protected router: Router) {
|
||||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +162,7 @@ export class SearchComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
private getSearchLink(): string {
|
private getSearchLink(): string {
|
||||||
if (this.inPlaceSearch) {
|
if (this.inPlaceSearch) {
|
||||||
return './';
|
return currentPath(this.router);
|
||||||
}
|
}
|
||||||
return this.service.getSearchLink();
|
return this.service.getSearchLink();
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { StoreEffects } from './store.effects';
|
import { StoreEffects } from './store.effects';
|
||||||
import { NotificationsEffects } from './shared/notifications/notifications.effects';
|
import { NotificationsEffects } from './shared/notifications/notifications.effects';
|
||||||
import { NavbarEffects } from './navbar/navbar.effects';
|
import { NavbarEffects } from './navbar/navbar.effects';
|
||||||
import { RelationshipEffects } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects';
|
|
||||||
import { SidebarEffects } from './shared/sidebar/sidebar-effects.service';
|
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 = [
|
export const appEffects = [
|
||||||
StoreEffects,
|
StoreEffects,
|
||||||
|
@@ -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 { 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 { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
||||||
import { NavbarModule } from './navbar/navbar.module';
|
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 { JournalEntitiesModule } from './entity-groups/journal-entities/journal-entities.module';
|
||||||
import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module';
|
import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module';
|
||||||
import { ClientCookieService } from './core/services/client-cookie.service';
|
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
return ENV_CONFIG;
|
return ENV_CONFIG;
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
||||||
import * as fromRouter from '@ngrx/router-store';
|
import * as fromRouter from '@ngrx/router-store';
|
||||||
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
|
|
||||||
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
||||||
|
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
|
||||||
import { formReducer, FormState } from './shared/form/form.reducer';
|
import { formReducer, FormState } from './shared/form/form.reducer';
|
||||||
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
||||||
import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer';
|
import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer';
|
||||||
|
@@ -128,8 +128,8 @@ import {
|
|||||||
MOCK_RESPONSE_MAP,
|
MOCK_RESPONSE_MAP,
|
||||||
MockResponseMap,
|
MockResponseMap,
|
||||||
mockResponseMap
|
mockResponseMap
|
||||||
} from './dspace-rest-v2/mocks/mock-response-map';
|
} from '../shared/mocks/dspace-rest-v2/mocks/mock-response-map';
|
||||||
import { EndpointMockingRestService } from './dspace-rest-v2/endpoint-mocking-rest.service';
|
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service';
|
||||||
import { ENV_CONFIG, GLOBAL_CONFIG, GlobalConfig } from '../../config';
|
import { ENV_CONFIG, GLOBAL_CONFIG, GlobalConfig } from '../../config';
|
||||||
import { SearchFilterService } from './shared/search/search-filter.service';
|
import { SearchFilterService } from './shared/search/search-filter.service';
|
||||||
import { SearchConfigurationService } from './shared/search/search-configuration.service';
|
import { SearchConfigurationService } from './shared/search/search-configuration.service';
|
||||||
@@ -141,6 +141,10 @@ import { NormalizedExternalSourceEntry } from './cache/models/normalized-externa
|
|||||||
import { ExternalSourceService } from './data/external-source.service';
|
import { ExternalSourceService } from './data/external-source.service';
|
||||||
import { LookupRelationService } from './data/lookup-relation.service';
|
import { LookupRelationService } from './data/lookup-relation.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) => {
|
export const restServiceFactory = (cfg: GlobalConfig, mocks: MockResponseMap, http: HttpClient) => {
|
||||||
if (ENV_CONFIG.production) {
|
if (ENV_CONFIG.production) {
|
||||||
return new DSpaceRESTv2Service(http);
|
return new DSpaceRESTv2Service(http);
|
||||||
|
@@ -12,8 +12,7 @@ import { Item } from '../shared/item.model';
|
|||||||
import { Relationship } from '../shared/item-relationships/relationship.model';
|
import { Relationship } from '../shared/item-relationships/relationship.model';
|
||||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest';
|
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { zip as observableZip } from 'rxjs';
|
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { ItemDataService } from './item-data.service';
|
import { ItemDataService } from './item-data.service';
|
||||||
import { compareArraysUsingIds, 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';
|
||||||
@@ -93,6 +92,14 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
addRelationship(typeId: string, item1: Item, item2: Item, leftwardValue?: string, rightwardValue?: string): Observable<RestResponse> {
|
addRelationship(typeId: string, item1: Item, item2: Item, leftwardValue?: string, rightwardValue?: string): Observable<RestResponse> {
|
||||||
const options: HttpOptions = Object.create({});
|
const options: HttpOptions = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
@@ -113,11 +120,15 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to remove two items of a relationship from the cache using the identifier of the relationship
|
||||||
|
* @param relationshipId The identifier of the relationship
|
||||||
|
*/
|
||||||
private removeRelationshipItemsFromCacheByRelationship(relationshipId: string) {
|
private removeRelationshipItemsFromCacheByRelationship(relationshipId: string) {
|
||||||
this.findById(relationshipId).pipe(
|
this.findById(relationshipId).pipe(
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
switchMap((relationship: Relationship) => observableCombineLatest(
|
switchMap((relationship: Relationship) => combineLatest(
|
||||||
relationship.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
|
relationship.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
|
||||||
relationship.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
|
relationship.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
|
||||||
)
|
)
|
||||||
@@ -129,10 +140,14 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to remove an item that's part of a relationship from the cache
|
||||||
|
* @param item The item to remove from the cache
|
||||||
|
*/
|
||||||
private removeRelationshipItemsFromCache(item) {
|
private removeRelationshipItemsFromCache(item) {
|
||||||
this.objectCache.remove(item.self);
|
this.objectCache.remove(item.self);
|
||||||
this.requestService.removeByHrefSubstring(item.self);
|
this.requestService.removeByHrefSubstring(item.self);
|
||||||
observableCombineLatest(
|
combineLatest(
|
||||||
this.objectCache.hasBySelfLinkObservable(item.self),
|
this.objectCache.hasBySelfLinkObservable(item.self),
|
||||||
this.requestService.hasByHrefObservable(item.self)
|
this.requestService.hasByHrefObservable(item.self)
|
||||||
).pipe(
|
).pipe(
|
||||||
@@ -259,6 +274,12 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> {
|
getRelationshipByItemsAndLabel(item1: Item, item2: Item, label: string): Observable<Relationship> {
|
||||||
return this.getItemRelationshipsByLabel(item1, label)
|
return this.getItemRelationshipsByLabel(item1, label)
|
||||||
.pipe(
|
.pipe(
|
||||||
@@ -289,19 +310,19 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a name variant for item with ID "itemID" part of list with ID "listID"
|
* Method to set the name variant for specific list and item
|
||||||
* @param listID ID of the list the item is a part of
|
* @param listID The list for which to save the name variant
|
||||||
* @param itemID ID of the item
|
* @param itemID The item ID for which to save the name variant
|
||||||
* @param nameVariant A name variant for the item
|
* @param nameVariant The name variant to save
|
||||||
*/
|
*/
|
||||||
public setNameVariant(listID: string, itemID: string, nameVariant: string) {
|
public setNameVariant(listID: string, itemID: string, nameVariant: string) {
|
||||||
this.appStore.dispatch(new SetNameVariantAction(listID, itemID, nameVariant));
|
this.appStore.dispatch(new SetNameVariantAction(listID, itemID, nameVariant));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the name variant for item with ID "itemID" part of list with ID "listID"
|
* Method to retrieve the name variant for a specific list and item
|
||||||
* @param listID ID of the list the item is a part of
|
* @param listID The list for which to retrieve the name variant
|
||||||
* @param itemID ID of the item
|
* @param itemID The item ID for which to retrieve the name variant
|
||||||
*/
|
*/
|
||||||
public getNameVariant(listID: string, itemID: string): Observable<string> {
|
public getNameVariant(listID: string, itemID: string): Observable<string> {
|
||||||
return this.appStore.pipe(
|
return this.appStore.pipe(
|
||||||
@@ -310,28 +331,28 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the name variant for item with ID "itemID" part of list with ID "listID"
|
* Method to remove the name variant for specific list and item
|
||||||
* @param listID ID of the list the item is a part of
|
* @param listID The list for which to remove the name variant
|
||||||
* @param itemID ID of the item
|
* @param itemID The item ID for which to remove the name variant
|
||||||
*/
|
*/
|
||||||
public removeNameVariant(listID: string, itemID: string) {
|
public removeNameVariant(listID: string, itemID: string) {
|
||||||
this.appStore.dispatch(new RemoveNameVariantAction(listID, itemID));
|
this.appStore.dispatch(new RemoveNameVariantAction(listID, itemID));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the name variants of all items part of list with ID "listID"
|
* Method to retrieve all name variants for a single list
|
||||||
* @param listID ID of the list the items are a part of
|
* @param listID The id of the list
|
||||||
*/
|
*/
|
||||||
public getNameVariantsByListID(listID: string) {
|
public getNameVariantsByListID(listID: string) {
|
||||||
return this.appStore.pipe(select(relationshipListStateSelector(listID)));
|
return this.appStore.pipe(select(relationshipListStateSelector(listID)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the relationship between two items with a name variant for the item on the opposite side of the relationship-label
|
* Method to update the name variant on the server
|
||||||
* @param item1 Related item
|
* @param item1 The first item of the relationship
|
||||||
* @param item2 Other related item
|
* @param item2 The second item of the relationship
|
||||||
* @param relationshipLabel The label describing the relationship between the two items
|
* @param relationshipLabel The leftward or rightward type of the relationship
|
||||||
* @param nameVariant The name variant to give the item on the opposite side of the relationship-label
|
* @param nameVariant The name variant to set for the matching relationship
|
||||||
*/
|
*/
|
||||||
public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> {
|
public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> {
|
||||||
return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
|
return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
|
||||||
|
@@ -206,7 +206,7 @@ export class RouteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current query and route parameters and add them
|
* Sets the current route parameters and query parameters in the store
|
||||||
*/
|
*/
|
||||||
public setCurrentRouteInfo() {
|
public setCurrentRouteInfo() {
|
||||||
combineLatest(this.getRouteParams(), this.route.queryParams)
|
combineLatest(this.getRouteParams(), this.route.queryParams)
|
||||||
|
@@ -118,7 +118,7 @@ export class SearchService implements OnDestroy {
|
|||||||
* @returns {Observable<RequestEntry>} Emits an observable with the request entries
|
* @returns {Observable<RequestEntry>} Emits an observable with the request entries
|
||||||
*/
|
*/
|
||||||
searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?:number)
|
searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?:number)
|
||||||
:Observable<{searchOptions:PaginatedSearchOptions, requestEntry:RequestEntry}> {
|
:Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> {
|
||||||
|
|
||||||
const hrefObs = this.getEndpoint(searchOptions);
|
const hrefObs = this.getEndpoint(searchOptions);
|
||||||
|
|
||||||
|
@@ -5,6 +5,10 @@ import { EquatableObject } from './equatable';
|
|||||||
const excludedFromEquals = new Map();
|
const excludedFromEquals = new Map();
|
||||||
const fieldsForEqualsMap = new Map();
|
const fieldsForEqualsMap = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorator function that adds the equatable settings from the given (parent) object
|
||||||
|
* @param parentCo The constructor of the parent object
|
||||||
|
*/
|
||||||
export function inheritEquatable(parentCo: GenericConstructor<EquatableObject<any>>) {
|
export function inheritEquatable(parentCo: GenericConstructor<EquatableObject<any>>) {
|
||||||
return function decorator(childCo: GenericConstructor<EquatableObject<any>>) {
|
return function decorator(childCo: GenericConstructor<EquatableObject<any>>) {
|
||||||
const parentExcludedFields = getExcludedFromEqualsFor(parentCo) || [];
|
const parentExcludedFields = getExcludedFromEqualsFor(parentCo) || [];
|
||||||
@@ -21,6 +25,11 @@ export function inheritEquatable(parentCo: GenericConstructor<EquatableObject<an
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to mark properties as excluded from the equals method
|
||||||
|
* @param object The object to exclude the property for
|
||||||
|
* @param propertyName The name of the property to exclude
|
||||||
|
*/
|
||||||
export function excludeFromEquals(object: any, propertyName: string): any {
|
export function excludeFromEquals(object: any, propertyName: string): any {
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return;
|
return;
|
||||||
@@ -37,6 +46,10 @@ export function getExcludedFromEqualsFor(constructor: Function): string[] {
|
|||||||
return excludedFromEquals.get(constructor) || [];
|
return excludedFromEquals.get(constructor) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to save the fields that are to be used for a certain property in the equals method for the given object
|
||||||
|
* @param fields The fields to use to equate the property of the object
|
||||||
|
*/
|
||||||
export function fieldsForEquals(...fields: string[]): any {
|
export function fieldsForEquals(...fields: string[]): any {
|
||||||
return function i(object: any, propertyName: string): any {
|
return function i(object: any, propertyName: string): any {
|
||||||
if (!object) {
|
if (!object) {
|
||||||
|
@@ -2,10 +2,10 @@ import { getExcludedFromEqualsFor, getFieldsForEquals } from './equals.decorator
|
|||||||
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two objects by comparing a given list of their properties
|
* Method to compare fields of two objects against each other
|
||||||
* @param object1 The first object
|
* @param object1 The first object for the comparison
|
||||||
* @param object2 The second object
|
* @param object2 The second object for the comparison
|
||||||
* @param fieldList A list of fields to compare the two objects by
|
* @param fieldList The list of property/field names to compare
|
||||||
*/
|
*/
|
||||||
function equalsByFields(object1, object2, fieldList): boolean {
|
function equalsByFields(object1, object2, fieldList): boolean {
|
||||||
const unequalProperty = fieldList.find((key) => {
|
const unequalProperty = fieldList.find((key) => {
|
||||||
@@ -34,7 +34,8 @@ function equalsByFields(object1, object2, fieldList): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object with a defined equals method
|
* Abstract class to represent objects that can be compared to each other
|
||||||
|
* It provides a default way of comparing
|
||||||
*/
|
*/
|
||||||
export abstract class EquatableObject<T> {
|
export abstract class EquatableObject<T> {
|
||||||
equals(other: T): boolean {
|
equals(other: T): boolean {
|
||||||
|
@@ -24,7 +24,7 @@ import { NameVariantModalComponent } from '../../name-variant-modal/name-variant
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The component for displaying a list element for an item search result of the type Person
|
* The component for displaying a list element for an item search result of the type OrgUnit
|
||||||
*/
|
*/
|
||||||
export class OrgUnitSearchResultListSubmissionElementComponent extends SearchResultListElementComponent<ItemSearchResult, Item> implements OnInit {
|
export class OrgUnitSearchResultListSubmissionElementComponent extends SearchResultListElementComponent<ItemSearchResult, Item> implements OnInit {
|
||||||
allSuggestions: string[];
|
allSuggestions: string[];
|
||||||
|
@@ -8,6 +8,6 @@
|
|||||||
{{'submission.sections.describe.relationship-lookup.name-variant.notification.content' | translate: { value: value } }}
|
{{'submission.sections.describe.relationship-lookup.name-variant.notification.content' | translate: { value: value } }}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer justify-content-between">
|
<div class="modal-footer justify-content-between">
|
||||||
<button type="button" class="btn btn-light" (click)="modal.close()">{{'submission.sections.describe.relationship-lookup.name-variant.notification.confirm' | translate }}</button>
|
<button type="button" class="btn btn-light confirm-button" (click)="modal.close()">{{'submission.sections.describe.relationship-lookup.name-variant.notification.confirm' | translate }}</button>
|
||||||
<button type="button" class="btn btn-light" (click)="modal.dismiss()">{{'submission.sections.describe.relationship-lookup.name-variant.notification.decline' | translate }}</button>
|
<button type="button" class="btn btn-light decline-button" (click)="modal.dismiss()">{{'submission.sections.describe.relationship-lookup.name-variant.notification.decline' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,16 +3,23 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
import { NameVariantModalComponent } from './name-variant-modal.component';
|
import { NameVariantModalComponent } from './name-variant-modal.component';
|
||||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
describe('NameVariantModalComponent', () => {
|
describe('NameVariantModalComponent', () => {
|
||||||
let component: NameVariantModalComponent;
|
let component: NameVariantModalComponent;
|
||||||
let fixture: ComponentFixture<NameVariantModalComponent>;
|
let fixture: ComponentFixture<NameVariantModalComponent>;
|
||||||
|
let debugElement;
|
||||||
|
let modal;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
modal = jasmine.createSpyObj('modal', ['close', 'dismiss']);
|
||||||
|
}
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [NameVariantModalComponent],
|
declarations: [NameVariantModalComponent],
|
||||||
imports: [NgbModule.forRoot(), TranslateModule.forRoot()],
|
imports: [NgbModule.forRoot(), TranslateModule.forRoot()],
|
||||||
providers: [NgbActiveModal]
|
providers: [{ provide: NgbActiveModal, useValue: modal }]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
}));
|
}));
|
||||||
@@ -20,10 +27,27 @@ describe('NameVariantModalComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(NameVariantModalComponent);
|
fixture = TestBed.createComponent(NameVariantModalComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
debugElement = fixture.debugElement;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('when close button is clicked, dismiss should be called on the modal', () => {
|
||||||
|
debugElement.query(By.css('button.close')).triggerEventHandler('click', {});
|
||||||
|
expect(modal.dismiss).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when confirm button is clicked, close should be called on the modal', () => {
|
||||||
|
debugElement.query(By.css('button.confirm-button')).triggerEventHandler('click', {});
|
||||||
|
expect(modal.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when decline button is clicked, dismiss should be called on the modal', () => {
|
||||||
|
debugElement.query(By.css('button.decline-button')).triggerEventHandler('click', {});
|
||||||
|
expect(modal.dismiss).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component a pop up for when the user selects a custom name variant during submission for a relationship$
|
||||||
|
* The user can either choose to decline or accept to save the name variant as a metadata in the entity
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-name-variant-modal',
|
selector: 'ds-name-variant-modal',
|
||||||
templateUrl: './name-variant-modal.component.html',
|
templateUrl: './name-variant-modal.component.html',
|
||||||
|
@@ -230,6 +230,9 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
super(componentFactoryResolver, layoutService, validationService);
|
super(componentFactoryResolver, layoutService, validationService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the necessary variables for when this control can be used to add relationships to the submitted item
|
||||||
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.hasRelationLookup = hasValue(this.model.relationship);
|
this.hasRelationLookup = hasValue(this.model.relationship);
|
||||||
if (this.hasRelationLookup) {
|
if (this.hasRelationLookup) {
|
||||||
@@ -314,6 +317,9 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
return this.model.value.pipe(map((list: Array<SearchResult<DSpaceObject>>) => isNotEmpty(list)));
|
return this.model.value.pipe(map((list: Array<SearchResult<DSpaceObject>>) => isNotEmpty(list)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a modal where the user can select relationships to be added to item being submitted
|
||||||
|
*/
|
||||||
openLookup() {
|
openLookup() {
|
||||||
this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, {
|
this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, {
|
||||||
size: 'lg'
|
size: 'lg'
|
||||||
@@ -327,12 +333,13 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
modalComp.item = this.item;
|
modalComp.item = this.item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to remove a selected relationship from the item
|
||||||
|
* @param object The second item in the relationship, the submitted item being the first
|
||||||
|
*/
|
||||||
removeSelection(object: SearchResult<Item>) {
|
removeSelection(object: SearchResult<Item>) {
|
||||||
this.selectableListService.deselectSingle(this.listId, object);
|
this.selectableListService.deselectSingle(this.listId, object);
|
||||||
this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject, this.model.relationship.relationshipType))
|
this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject, this.model.relationship.relationshipType))
|
||||||
|
|
||||||
// this.zone.runOutsideAngular(
|
|
||||||
// () => );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -0,0 +1,58 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { DsDynamicDisabledComponent } from './dynamic-disabled.component';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||||
|
import { DynamicDisabledModel } from './dynamic-disabled.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
describe('DsDynamicDisabledComponent', () => {
|
||||||
|
let comp: DsDynamicDisabledComponent;
|
||||||
|
let fixture: ComponentFixture<DsDynamicDisabledComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
let el: HTMLElement;
|
||||||
|
let model;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
model = new DynamicDisabledModel({ value: 'test', repeatable: false, metadataFields: [], submissionId: '1234', id: '1' });
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [DsDynamicDisabledComponent],
|
||||||
|
imports: [FormsModule, TranslateModule.forRoot()],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: DynamicFormLayoutService,
|
||||||
|
useValue: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: DynamicFormValidationService,
|
||||||
|
useValue: {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DsDynamicDisabledComponent);
|
||||||
|
comp = fixture.componentInstance; // DsDynamicDisabledComponent test instance
|
||||||
|
de = fixture.debugElement;
|
||||||
|
el = de.nativeElement;
|
||||||
|
comp.model = model;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(comp).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a disabled input', () => {
|
||||||
|
const input = de.query(By.css('input'));
|
||||||
|
console.log(input.nativeElement.getAttribute('disabled'));
|
||||||
|
expect(input.nativeElement.getAttribute('disabled')).toEqual('');
|
||||||
|
});
|
||||||
|
});
|
@@ -3,8 +3,10 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|||||||
import { DynamicFormControlComponent, DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
import { DynamicFormControlComponent, DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
import { DynamicDisabledModel } from './dynamic-disabled.model';
|
import { DynamicDisabledModel } from './dynamic-disabled.model';
|
||||||
import { RelationshipTypeService } from '../../../../../../core/data/relationship-type.service';
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component representing a simple disabled input field
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dynamic-disabled',
|
selector: 'ds-dynamic-disabled',
|
||||||
templateUrl: './dynamic-disabled.component.html'
|
templateUrl: './dynamic-disabled.component.html'
|
||||||
@@ -24,8 +26,7 @@ export class DsDynamicDisabledComponent extends DynamicFormControlComponent {
|
|||||||
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
|
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
|
||||||
|
|
||||||
constructor(protected layoutService: DynamicFormLayoutService,
|
constructor(protected layoutService: DynamicFormLayoutService,
|
||||||
protected validationService: DynamicFormValidationService,
|
protected validationService: DynamicFormValidationService
|
||||||
protected relationshipTypeService: RelationshipTypeService
|
|
||||||
) {
|
) {
|
||||||
super(layoutService, validationService);
|
super(layoutService, validationService);
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,9 @@ export interface DsDynamicDisabledModelConfig extends DsDynamicInputModelConfig
|
|||||||
value?: any;
|
value?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This model represents the data for a disabled input field
|
||||||
|
*/
|
||||||
export class DynamicDisabledModel extends DsDynamicInputModel {
|
export class DynamicDisabledModel extends DsDynamicInputModel {
|
||||||
|
|
||||||
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DISABLED;
|
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DISABLED;
|
||||||
@@ -14,7 +17,6 @@ export class DynamicDisabledModel extends DsDynamicInputModel {
|
|||||||
|
|
||||||
constructor(config: DsDynamicDisabledModelConfig, layout?: DynamicFormControlLayout) {
|
constructor(config: DsDynamicDisabledModelConfig, layout?: DynamicFormControlLayout) {
|
||||||
super(config, layout);
|
super(config, layout);
|
||||||
|
|
||||||
this.readOnly = true;
|
this.readOnly = true;
|
||||||
this.disabled = true;
|
this.disabled = true;
|
||||||
this.valueUpdates.next(config.value);
|
this.valueUpdates.next(config.value);
|
||||||
|
@@ -41,8 +41,9 @@ import { ExternalSourceService } from '../../../../../core/data/external-source.
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal component for looking up relations
|
* Represents a modal where the submitter can select items to be added as a certain relationship type to the object being submitted
|
||||||
*/
|
*/
|
||||||
export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy {
|
export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
|
@@ -11,7 +11,9 @@ export const NameVariantActionTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
/**
|
||||||
|
* Abstract class for actions that happen to name variants
|
||||||
|
*/
|
||||||
export abstract class NameVariantListAction implements Action {
|
export abstract class NameVariantListAction implements Action {
|
||||||
type;
|
type;
|
||||||
payload: {
|
payload: {
|
||||||
@@ -24,6 +26,9 @@ export abstract class NameVariantListAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for setting a new name on an item in a certain list
|
||||||
|
*/
|
||||||
export class SetNameVariantAction extends NameVariantListAction {
|
export class SetNameVariantAction extends NameVariantListAction {
|
||||||
type = NameVariantActionTypes.SET_NAME_VARIANT;
|
type = NameVariantActionTypes.SET_NAME_VARIANT;
|
||||||
payload: {
|
payload: {
|
||||||
@@ -38,6 +43,9 @@ export class SetNameVariantAction extends NameVariantListAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for removing a name on an item in a certain list
|
||||||
|
*/
|
||||||
export class RemoveNameVariantAction extends NameVariantListAction {
|
export class RemoveNameVariantAction extends NameVariantListAction {
|
||||||
type = NameVariantActionTypes.REMOVE_NAME_VARIANT;
|
type = NameVariantActionTypes.REMOVE_NAME_VARIANT;
|
||||||
constructor(listID: string, itemID: string) {
|
constructor(listID: string, itemID: string) {
|
||||||
|
@@ -72,6 +72,11 @@ export class RelationshipEffects {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the namevariant in a relationship
|
||||||
|
* If the relationship is currently being added or removed, it will add the name variant to an update map so it will be sent with the next add request instead
|
||||||
|
* Otherwise the update is done immediately
|
||||||
|
*/
|
||||||
@Effect({ dispatch: false }) updateNameVariantsActions$ = this.actions$
|
@Effect({ dispatch: false }) updateNameVariantsActions$ = this.actions$
|
||||||
.pipe(
|
.pipe(
|
||||||
ofType(RelationshipActionTypes.UPDATE_RELATIONSHIP),
|
ofType(RelationshipActionTypes.UPDATE_RELATIONSHIP),
|
||||||
|
@@ -33,8 +33,9 @@ import { LookupRelationService } from '../../../../../../core/data/lookup-relati
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tab for browsing local entities to add to the selection
|
* Tab for inside the lookup model that represents the items that can be used as a relationship in this submission
|
||||||
*/
|
*/
|
||||||
export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDestroy {
|
export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
@@ -121,6 +122,9 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the pagination and fixed query parameters
|
||||||
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.resetRoute();
|
this.resetRoute();
|
||||||
this.routeService.setParameter('fixedFilterQuery', this.relationship.filter);
|
this.routeService.setParameter('fixedFilterQuery', this.relationship.filter);
|
||||||
@@ -133,7 +137,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the route parameters
|
* Method to reset the route when the window is opened to make sure no strange pagination issues appears
|
||||||
*/
|
*/
|
||||||
resetRoute() {
|
resetRoute() {
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
@@ -142,8 +146,8 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select all results within the page provided
|
* Selects a page in the store
|
||||||
* @param page
|
* @param page The page to select
|
||||||
*/
|
*/
|
||||||
selectPage(page: Array<SearchResult<Item>>) {
|
selectPage(page: Array<SearchResult<Item>>) {
|
||||||
this.selection$
|
this.selection$
|
||||||
@@ -156,8 +160,8 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deselect all results within the page provided
|
* Deselects a page in the store
|
||||||
* @param page
|
* @param page the page to deselect
|
||||||
*/
|
*/
|
||||||
deselectPage(page: Array<SearchResult<Item>>) {
|
deselectPage(page: Array<SearchResult<Item>>) {
|
||||||
this.allSelected = false;
|
this.allSelected = false;
|
||||||
@@ -171,7 +175,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select all results
|
* Select all items that were found using the current search query
|
||||||
*/
|
*/
|
||||||
selectAll() {
|
selectAll() {
|
||||||
this.allSelected = true;
|
this.allSelected = true;
|
||||||
@@ -199,7 +203,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deselect all
|
* Deselect all items
|
||||||
*/
|
*/
|
||||||
deselectAll() {
|
deselectAll() {
|
||||||
this.allSelected = false;
|
this.allSelected = false;
|
||||||
|
@@ -24,8 +24,9 @@ import { Context } from '../../../../../../core/shared/context.model';
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tab displaying the currently selected relations to add
|
* Tab for inside the lookup model that represents the currently selected relationships
|
||||||
*/
|
*/
|
||||||
export class DsDynamicLookupRelationSelectionTabComponent {
|
export class DsDynamicLookupRelationSelectionTabComponent {
|
||||||
/**
|
/**
|
||||||
@@ -72,6 +73,9 @@ export class DsDynamicLookupRelationSelectionTabComponent {
|
|||||||
private searchConfigService: SearchConfigurationService) {
|
private searchConfigService: SearchConfigurationService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the selection and pagination on load
|
||||||
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.selectionRD$ = this.searchConfigService.paginatedSearchOptions
|
this.selectionRD$ = this.searchConfigService.paginatedSearchOptions
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const RELATION_METADATA_PREFIX = 'relation.'
|
const RELATION_METADATA_PREFIX = 'relation.'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extra options for displaying search results of relationships
|
* The submission options for fields that can represent relationships
|
||||||
*/
|
*/
|
||||||
export class RelationshipOptions {
|
export class RelationshipOptions {
|
||||||
relationshipType: string;
|
relationshipType: string;
|
||||||
|
@@ -0,0 +1,66 @@
|
|||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||||
|
import { ParserOptions } from './parser-options';
|
||||||
|
import { DisabledFieldParser } from './disabled-field-parser';
|
||||||
|
import { DynamicDisabledModel } from '../ds-dynamic-form-ui/models/disabled/dynamic-disabled.model';
|
||||||
|
|
||||||
|
describe('DisabledFieldParser test suite', () => {
|
||||||
|
let field: FormFieldModel;
|
||||||
|
let initFormValues: any = {};
|
||||||
|
|
||||||
|
const submissionId = '1234';
|
||||||
|
const parserOptions: ParserOptions = {
|
||||||
|
readOnly: false,
|
||||||
|
submissionScope: null,
|
||||||
|
authorityUuid: null
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
field = {
|
||||||
|
input: {
|
||||||
|
type: ''
|
||||||
|
},
|
||||||
|
label: 'Description',
|
||||||
|
mandatory: 'false',
|
||||||
|
repeatable: false,
|
||||||
|
hints: 'Enter a description.',
|
||||||
|
selectableMetadata: [
|
||||||
|
{
|
||||||
|
metadata: 'description'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
languageCodes: []
|
||||||
|
} as FormFieldModel;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should init parser properly', () => {
|
||||||
|
const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||||
|
|
||||||
|
expect(parser instanceof DisabledFieldParser).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a DynamicDisabledModel object when repeatable option is false', () => {
|
||||||
|
const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||||
|
|
||||||
|
const fieldModel = parser.parse();
|
||||||
|
|
||||||
|
expect(fieldModel instanceof DynamicDisabledModel).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set init value properly', () => {
|
||||||
|
initFormValues = {
|
||||||
|
description: [
|
||||||
|
new FormFieldMetadataValueObject('test description'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const expectedValue ='test description';
|
||||||
|
|
||||||
|
const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||||
|
|
||||||
|
const fieldModel = parser.parse();
|
||||||
|
console.log(fieldModel);
|
||||||
|
expect(fieldModel.value).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -2,10 +2,15 @@ import { FieldParser } from './field-parser';
|
|||||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||||
import { DsDynamicDisabledModelConfig, DynamicDisabledModel } from '../ds-dynamic-form-ui/models/disabled/dynamic-disabled.model';
|
import { DsDynamicDisabledModelConfig, DynamicDisabledModel } from '../ds-dynamic-form-ui/models/disabled/dynamic-disabled.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field parser for disabled fields
|
||||||
|
*/
|
||||||
export class DisabledFieldParser extends FieldParser {
|
export class DisabledFieldParser extends FieldParser {
|
||||||
|
|
||||||
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
|
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
|
||||||
|
console.log(fieldValue);
|
||||||
const emptyModelConfig: DsDynamicDisabledModelConfig = this.initModel(null, label);
|
const emptyModelConfig: DsDynamicDisabledModelConfig = this.initModel(null, label);
|
||||||
|
this.setValues(emptyModelConfig, fieldValue);
|
||||||
return new DynamicDisabledModel(emptyModelConfig)
|
return new DynamicDisabledModel(emptyModelConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,9 @@ const fieldParserDeps = [
|
|||||||
PARSER_OPTIONS,
|
PARSER_OPTIONS,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to retrieve a field parder with its providers based on the input type
|
||||||
|
*/
|
||||||
export class ParserFactory {
|
export class ParserFactory {
|
||||||
public static getProvider(type: ParserType): StaticProvider {
|
public static getProvider(type: ParserType): StaticProvider {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@@ -27,6 +27,10 @@ export const ROW_ID_PREFIX = 'df-row-group-config-';
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser the submission data for a single row
|
||||||
|
*/
|
||||||
export class RowParser {
|
export class RowParser {
|
||||||
constructor(private parentInjector: Injector) {
|
constructor(private parentInjector: Injector) {
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../../config/global-config.interface';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../../../core/data/rest-request-method';
|
||||||
import { EndpointMockingRestService } from './endpoint-mocking-rest.service';
|
import { EndpointMockingRestService } from './endpoint-mocking-rest.service';
|
||||||
import { MockResponseMap } from './mocks/mock-response-map';
|
import { MockResponseMap } from './mocks/mock-response-map';
|
||||||
|
|
@@ -1,12 +1,12 @@
|
|||||||
import { HttpClient, HttpHeaders } from '@angular/common/http'
|
import { HttpClient, HttpHeaders } from '@angular/common/http'
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
|
||||||
import { isEmpty } from '../../shared/empty.util';
|
import { isEmpty } from '../../empty.util';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../../../core/data/rest-request-method';
|
||||||
|
|
||||||
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../../../core/dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
import { DSpaceRESTv2Service, HttpOptions } from './dspace-rest-v2.service';
|
import { DSpaceRESTv2Service, HttpOptions } from '../../../core/dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { MOCK_RESPONSE_MAP, MockResponseMap } from './mocks/mock-response-map';
|
import { MOCK_RESPONSE_MAP, MockResponseMap } from './mocks/mock-response-map';
|
||||||
import * as URL from 'url-parse';
|
import * as URL from 'url-parse';
|
||||||
|
|
||||||
@@ -14,6 +14,8 @@ import * as URL from 'url-parse';
|
|||||||
* Service to access DSpace's REST API.
|
* Service to access DSpace's REST API.
|
||||||
*
|
*
|
||||||
* If a URL is found in this.mockResponseMap, it returns the mock response instead
|
* If a URL is found in this.mockResponseMap, it returns the mock response instead
|
||||||
|
* This service can be used for mocking REST responses when developing new features
|
||||||
|
* This is especially useful, when a REST endpoint is broken or does not exist yet
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EndpointMockingRestService extends DSpaceRESTv2Service {
|
export class EndpointMockingRestService extends DSpaceRESTv2Service {
|
@@ -1,5 +1,5 @@
|
|||||||
import { InjectionToken } from '@angular/core';
|
import { InjectionToken } from '@angular/core';
|
||||||
import mockSubmissionResponse from '../mocks/mock-submission-response.json';
|
import mockSubmissionResponse from './mock-submission-response.json';
|
||||||
|
|
||||||
export class MockResponseMap extends Map<string, any> {};
|
export class MockResponseMap extends Map<string, any> {};
|
||||||
|
|
@@ -3,10 +3,10 @@
|
|||||||
[name]="'checkbox' + index"
|
[name]="'checkbox' + index"
|
||||||
[id]="'object' + index"
|
[id]="'object' + index"
|
||||||
[ngModel]="selected$ | async"
|
[ngModel]="selected$ | async"
|
||||||
(ngModelChange)="selectCheckbox($event, object)">
|
(ngModelChange)="selectCheckbox($event)">
|
||||||
<input *ngIf="!selectionConfig.repeatable" class="form-check-input" type="radio"
|
<input *ngIf="!selectionConfig.repeatable" class="form-check-input" type="radio"
|
||||||
[name]="'radio' + index"
|
[name]="'radio' + index"
|
||||||
[id]="'object' + index"
|
[id]="'object' + index"
|
||||||
[checked]="selected$ | async"
|
[checked]="selected$ | async"
|
||||||
(click)="selectRadio(!checked, object)">
|
(click)="selectRadio(!checked)">
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -0,0 +1,91 @@
|
|||||||
|
import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core/testing';
|
||||||
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
|
||||||
|
import { SelectableListItemControlComponent } from './selectable-list-item-control.component';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { VarDirective } from '../../../utils/var.directive';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { ListableObject } from '../listable-object.model';
|
||||||
|
|
||||||
|
describe('SelectableListItemControlComponent', () => {
|
||||||
|
let comp: SelectableListItemControlComponent;
|
||||||
|
let fixture: ComponentFixture<SelectableListItemControlComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
let el: HTMLElement;
|
||||||
|
let object;
|
||||||
|
let otherObject;
|
||||||
|
let selectionConfig;
|
||||||
|
let listId;
|
||||||
|
let index;
|
||||||
|
let selectionService;
|
||||||
|
let selection: ListableObject[];
|
||||||
|
let uuid1: string;
|
||||||
|
let uuid2: string;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
uuid1 = '0beb44f8-d2ed-459a-a1e7-ffbe059089a9';
|
||||||
|
uuid2 = 'e1dc80aa-c269-4aa5-b6bd-008d98056247';
|
||||||
|
listId = 'Test List ID';
|
||||||
|
object = Object.assign(new Item(), {uuid: uuid1});
|
||||||
|
otherObject = Object.assign(new Item(), {uuid: uuid2});
|
||||||
|
selectionConfig = {repeatable: false, listId};
|
||||||
|
index = 0;
|
||||||
|
selection = [otherObject];
|
||||||
|
selectionService = jasmine.createSpyObj('selectionService', {
|
||||||
|
selectSingle: jasmine.createSpy('selectSingle'),
|
||||||
|
deselectSingle: jasmine.createSpy('deselectSingle'),
|
||||||
|
isObjectSelected: observableOf(true),
|
||||||
|
getSelectableList: observableOf({ selection })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [SelectableListItemControlComponent, VarDirective],
|
||||||
|
imports: [FormsModule],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SelectableListService,
|
||||||
|
useValue: selectionService
|
||||||
|
}
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SelectableListItemControlComponent);
|
||||||
|
comp = fixture.componentInstance; // SelectableListItemControlComponent test instance
|
||||||
|
de = fixture.debugElement;
|
||||||
|
el = de.nativeElement;
|
||||||
|
comp.object = object;
|
||||||
|
comp.selectionConfig = selectionConfig;
|
||||||
|
comp.index = index;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call deselectSingle on the service when the object when selectCheckbox is called with value false', () => {
|
||||||
|
comp.selectCheckbox(false);
|
||||||
|
expect(selectionService.deselectSingle).toHaveBeenCalledWith(listId, object);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call selectSingle on the service when the object when selectCheckbox is called with value false', () => {
|
||||||
|
comp.selectCheckbox(true);
|
||||||
|
expect(selectionService.selectSingle).toHaveBeenCalledWith(listId, object);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call selectSingle on the service when the object when selectRadio is called with value true and deselect all others in the selection', () => {
|
||||||
|
comp.selectRadio(true );
|
||||||
|
expect(selectionService.deselectSingle).toHaveBeenCalledWith(listId, selection[0]);
|
||||||
|
expect(selectionService.selectSingle).toHaveBeenCalledWith(listId, object);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call selectSingle on the service when the object when selectRadio is called with value false and not deselect all others in the selection', () => {
|
||||||
|
comp.selectRadio(false );
|
||||||
|
expect(selectionService.deselectSingle).not.toHaveBeenCalledWith(listId, selection[0]);
|
||||||
|
expect(selectionService.selectSingle).not.toHaveBeenCalledWith(listId, object);
|
||||||
|
});
|
||||||
|
});
|
@@ -49,29 +49,30 @@ export class SelectableListItemControlComponent implements OnInit {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
selectCheckbox(value: boolean, object: ListableObject) {
|
selectCheckbox(value: boolean) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.selectionService.selectSingle(this.selectionConfig.listId, object);
|
this.selectionService.selectSingle(this.selectionConfig.listId, this.object);
|
||||||
} else {
|
} else {
|
||||||
this.selectionService.deselectSingle(this.selectionConfig.listId, object);
|
this.selectionService.deselectSingle(this.selectionConfig.listId, this.object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectRadio(value: boolean, object: ListableObject) {
|
selectRadio(value: boolean) {
|
||||||
const selected$ = this.selectionService.getSelectableList(this.selectionConfig.listId);
|
if (value) {
|
||||||
selected$.pipe(
|
const selected$ = this.selectionService.getSelectableList(this.selectionConfig.listId);
|
||||||
take(1),
|
selected$.pipe(
|
||||||
map((selected) => selected ? selected.selection : [])
|
take(1),
|
||||||
).subscribe((selection) => {
|
map((selected) => selected ? selected.selection : [])
|
||||||
// First deselect any existing selections, this is a radio button
|
).subscribe((selection) => {
|
||||||
selection.forEach((selectedObject) => {
|
// First deselect any existing selections, this is a radio button
|
||||||
this.selectionService.deselectSingle(this.selectionConfig.listId, selectedObject);
|
selection.forEach((selectedObject) => {
|
||||||
this.deselectObject.emit(selectedObject);
|
this.selectionService.deselectSingle(this.selectionConfig.listId, selectedObject);
|
||||||
});
|
this.deselectObject.emit(selectedObject);
|
||||||
if (value) {
|
});
|
||||||
this.selectionService.selectSingle(this.selectionConfig.listId, object);
|
this.selectionService.selectSingle(this.selectionConfig.listId, this.object);
|
||||||
this.selectObject.emit(object);
|
this.selectObject.emit(this.object);
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,9 @@ export const SelectableListActionTypes = {
|
|||||||
DESELECT_ALL: type('dspace/selectable-lists/DESELECT_ALL')
|
DESELECT_ALL: type('dspace/selectable-lists/DESELECT_ALL')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract action class for actions on selectable lists
|
||||||
|
*/
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export abstract class SelectableListAction implements Action {
|
export abstract class SelectableListAction implements Action {
|
||||||
// tslint:disable-next-line:no-shadowed-variable
|
// tslint:disable-next-line:no-shadowed-variable
|
||||||
@@ -27,7 +30,7 @@ export abstract class SelectableListAction implements Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to select an item in a the selectable list
|
* Action to select objects in a the selectable list
|
||||||
*/
|
*/
|
||||||
export class SelectableListSelectAction extends SelectableListAction {
|
export class SelectableListSelectAction extends SelectableListAction {
|
||||||
payload: ListableObject[];
|
payload: ListableObject[];
|
||||||
@@ -37,7 +40,9 @@ export class SelectableListSelectAction extends SelectableListAction {
|
|||||||
this.payload = objects;
|
this.payload = objects;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Action to select a single object in a the selectable list
|
||||||
|
*/
|
||||||
export class SelectableListSelectSingleAction extends SelectableListAction {
|
export class SelectableListSelectSingleAction extends SelectableListAction {
|
||||||
payload: {
|
payload: {
|
||||||
object: ListableObject,
|
object: ListableObject,
|
||||||
@@ -49,6 +54,9 @@ export class SelectableListSelectSingleAction extends SelectableListAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to deselect objects in a the selectable list
|
||||||
|
*/
|
||||||
export class SelectableListDeselectSingleAction extends SelectableListAction {
|
export class SelectableListDeselectSingleAction extends SelectableListAction {
|
||||||
payload: ListableObject;
|
payload: ListableObject;
|
||||||
|
|
||||||
@@ -58,6 +66,9 @@ export class SelectableListDeselectSingleAction extends SelectableListAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to deselect a single object in a the selectable list
|
||||||
|
*/
|
||||||
export class SelectableListDeselectAction extends SelectableListAction {
|
export class SelectableListDeselectAction extends SelectableListAction {
|
||||||
payload: ListableObject[];
|
payload: ListableObject[];
|
||||||
|
|
||||||
@@ -67,6 +78,9 @@ export class SelectableListDeselectAction extends SelectableListAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to set a new or overwrite an existing selection
|
||||||
|
*/
|
||||||
export class SelectableListSetSelectionAction extends SelectableListAction {
|
export class SelectableListSetSelectionAction extends SelectableListAction {
|
||||||
payload: ListableObject[];
|
payload: ListableObject[];
|
||||||
|
|
||||||
@@ -76,6 +90,9 @@ export class SelectableListSetSelectionAction extends SelectableListAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to deselect all currently selected objects
|
||||||
|
*/
|
||||||
export class SelectableListDeselectAllAction extends SelectableListAction {
|
export class SelectableListDeselectAllAction extends SelectableListAction {
|
||||||
constructor(id: string) {
|
constructor(id: string) {
|
||||||
super(SelectableListActionTypes.DESELECT_ALL, id);
|
super(SelectableListActionTypes.DESELECT_ALL, id);
|
||||||
|
@@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
SelectableListAction,
|
||||||
|
SelectableListDeselectAction, SelectableListDeselectAllAction,
|
||||||
|
SelectableListDeselectSingleAction,
|
||||||
|
SelectableListSelectAction,
|
||||||
|
SelectableListSelectSingleAction,
|
||||||
|
SelectableListSetSelectionAction
|
||||||
|
} from './selectable-list.actions';
|
||||||
|
import { selectableListReducer } from './selectable-list.reducer';
|
||||||
|
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
||||||
|
import { hasValue } from '../../empty.util';
|
||||||
|
|
||||||
|
// tslint:disable:max-classes-per-file
|
||||||
|
class SelectableObject extends ListableObject {
|
||||||
|
constructor(private value: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
equals(other: SelectableObject): boolean {
|
||||||
|
return hasValue(this.value) && hasValue(other.value) && this.value === other.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRenderTypes() {
|
||||||
|
return ['selectable'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NullAction extends SelectableListAction {
|
||||||
|
type = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(undefined, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:enable:max-classes-per-file
|
||||||
|
const listID1 = 'id1';
|
||||||
|
const listID2 = 'id2';
|
||||||
|
const value1 = 'Selected object';
|
||||||
|
const value2 = 'Another selected object';
|
||||||
|
const value3 = 'Selection';
|
||||||
|
const value4 = 'Selected object numero 4';
|
||||||
|
|
||||||
|
const selected1 = new SelectableObject(value1);
|
||||||
|
const selected2 = new SelectableObject(value2);
|
||||||
|
const selected3 = new SelectableObject(value3);
|
||||||
|
const selected4 = new SelectableObject(value4);
|
||||||
|
const testState = { [listID1]: { id: listID1, selection: [selected1, selected2] } };
|
||||||
|
|
||||||
|
describe('selectableListReducer', () => {
|
||||||
|
|
||||||
|
it('should return the current state when no valid actions have been made', () => {
|
||||||
|
const state = {};
|
||||||
|
state[listID1] = {};
|
||||||
|
state[listID1] = { id: listID1, selection: [selected1, selected2] };
|
||||||
|
const action = new NullAction();
|
||||||
|
const newState = selectableListReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with an empty object', () => {
|
||||||
|
const state = {};
|
||||||
|
const action = new NullAction();
|
||||||
|
const newState = selectableListReducer(undefined, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add the payload to the existing list in response to the SELECT action for the given id', () => {
|
||||||
|
const action = new SelectableListSelectAction(listID1, [selected3, selected4]);
|
||||||
|
const newState = selectableListReducer(testState, action);
|
||||||
|
|
||||||
|
expect(newState[listID1].selection).toEqual([selected1, selected2, selected3, selected4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add the payload to the existing list in response to the SELECT_SINGLE action for the given id', () => {
|
||||||
|
const action = new SelectableListSelectSingleAction(listID1, selected4);
|
||||||
|
const newState = selectableListReducer(testState, action);
|
||||||
|
|
||||||
|
expect(newState[listID1].selection).toEqual([selected1, selected2, selected4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the payload from the existing list in response to the DESELECT action for the given id', () => {
|
||||||
|
const action = new SelectableListDeselectAction(listID1, [selected1, selected2]);
|
||||||
|
const newState = selectableListReducer(testState, action);
|
||||||
|
|
||||||
|
expect(newState[listID1].selection).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the payload from the existing list in response to the DESELECT_SINGLE action for the given id', () => {
|
||||||
|
const action = new SelectableListDeselectSingleAction(listID1, selected2);
|
||||||
|
const newState = selectableListReducer(testState, action);
|
||||||
|
|
||||||
|
expect(newState[listID1].selection).toEqual([selected1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the list to the payload in response to the SET_SELECTION action for the given id', () => {
|
||||||
|
const action = new SelectableListSetSelectionAction(listID2, [selected2, selected4]);
|
||||||
|
const newState = selectableListReducer(testState, action);
|
||||||
|
|
||||||
|
expect(newState[listID1].selection).toEqual(testState[listID1].selection);
|
||||||
|
expect(newState[listID2].selection).toEqual([selected2, selected4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the payload from the existing list in response to the DESELECT action for the given id', () => {
|
||||||
|
const action = new SelectableListDeselectAllAction(listID1);
|
||||||
|
const newState = selectableListReducer(testState, action);
|
||||||
|
|
||||||
|
expect(newState[listID1].selection).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
@@ -63,12 +63,22 @@ export function selectableListReducer(state: SelectableListsState = {}, action:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiple objects to the existing selection state
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform
|
||||||
|
*/
|
||||||
function select(state: SelectableListState, action: SelectableListSelectAction) {
|
function select(state: SelectableListState, action: SelectableListSelectAction) {
|
||||||
const filteredNewObjects = action.payload.filter((object) => !isObjectInSelection(state.selection, object));
|
const filteredNewObjects = action.payload.filter((object) => !isObjectInSelection(state.selection, object));
|
||||||
const newSelection = [...state.selection, ...filteredNewObjects];
|
const newSelection = [...state.selection, ...filteredNewObjects];
|
||||||
return Object.assign({}, state, { selection: newSelection });
|
return Object.assign({}, state, { selection: newSelection });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a single object to the existing selection state
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform
|
||||||
|
*/
|
||||||
function selectSingle(state: SelectableListState, action: SelectableListSelectSingleAction) {
|
function selectSingle(state: SelectableListState, action: SelectableListSelectSingleAction) {
|
||||||
let newSelection = state.selection;
|
let newSelection = state.selection;
|
||||||
if (!isObjectInSelection(state.selection, action.payload.object)) {
|
if (!isObjectInSelection(state.selection, action.payload.object)) {
|
||||||
@@ -77,11 +87,21 @@ function selectSingle(state: SelectableListState, action: SelectableListSelectSi
|
|||||||
return Object.assign({}, state, { selection: newSelection });
|
return Object.assign({}, state, { selection: newSelection });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes multiple objects in the existing selection state
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform
|
||||||
|
*/
|
||||||
function deselect(state: SelectableListState, action: SelectableListDeselectAction) {
|
function deselect(state: SelectableListState, action: SelectableListDeselectAction) {
|
||||||
const newSelection = state.selection.filter((selected) => hasNoValue(action.payload.find((object) => object.equals(selected))));
|
const newSelection = state.selection.filter((selected) => hasNoValue(action.payload.find((object) => object.equals(selected))));
|
||||||
return Object.assign({}, state, { selection: newSelection });
|
return Object.assign({}, state, { selection: newSelection });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Removes a single object from the existing selection state
|
||||||
|
*
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform
|
||||||
|
*/
|
||||||
function deselectSingle(state: SelectableListState, action: SelectableListDeselectSingleAction) {
|
function deselectSingle(state: SelectableListState, action: SelectableListDeselectSingleAction) {
|
||||||
const newSelection = state.selection.filter((selected) => {
|
const newSelection = state.selection.filter((selected) => {
|
||||||
return !selected.equals(action.payload);
|
return !selected.equals(action.payload);
|
||||||
@@ -89,14 +109,29 @@ function deselectSingle(state: SelectableListState, action: SelectableListDesele
|
|||||||
return Object.assign({}, state, { selection: newSelection });
|
return Object.assign({}, state, { selection: newSelection });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selection state of the list
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform
|
||||||
|
*/
|
||||||
function setList(state: SelectableListState, action: SelectableListSetSelectionAction) {
|
function setList(state: SelectableListState, action: SelectableListSetSelectionAction) {
|
||||||
return Object.assign({}, state, { selection: action.payload });
|
return Object.assign({}, state, { selection: action.payload });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the selection
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform
|
||||||
|
*/
|
||||||
function clearSelection(id: string) {
|
function clearSelection(id: string) {
|
||||||
return { id: id, selection: [] };
|
return { id: id, selection: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the object is in currently in the selection
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform
|
||||||
|
*/
|
||||||
function isObjectInSelection(selection: ListableObject[], object: ListableObject) {
|
function isObjectInSelection(selection: ListableObject[], object: ListableObject) {
|
||||||
return selection.findIndex((selected) => selected.equals(object)) >= 0
|
return selection.findIndex((selected) => selected.equals(object)) >= 0
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,98 @@
|
|||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
|
import { SelectableListService } from './selectable-list.service';
|
||||||
|
import { SelectableListsState } from './selectable-list.reducer';
|
||||||
|
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
||||||
|
import { hasValue } from '../../empty.util';
|
||||||
|
import { SelectableListDeselectAction, SelectableListDeselectSingleAction, SelectableListSelectAction, SelectableListSelectSingleAction } from './selectable-list.actions';
|
||||||
|
|
||||||
|
class SelectableObject extends ListableObject {
|
||||||
|
constructor(private value: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
equals(other: SelectableObject): boolean {
|
||||||
|
return hasValue(this.value) && hasValue(other.value) && this.value === other.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRenderTypes() {
|
||||||
|
return ['selectable'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SelectableListService', () => {
|
||||||
|
const listID1 = 'id1';
|
||||||
|
const value1 = 'Selected object';
|
||||||
|
const value2 = 'Another selected object';
|
||||||
|
const value3 = 'Selection';
|
||||||
|
const value4 = 'Selected object numero 4';
|
||||||
|
|
||||||
|
const selected1 = new SelectableObject(value1);
|
||||||
|
const selected2 = new SelectableObject(value2);
|
||||||
|
const selected3 = new SelectableObject(value3);
|
||||||
|
const selected4 = new SelectableObject(value4);
|
||||||
|
|
||||||
|
let service: SelectableListService;
|
||||||
|
const store: Store<SelectableListsState> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
});
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: Store, useValue: store
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new SelectableListService(store);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the selectSingle method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.selectSingle(listID1, selected3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SelectableListSelectSingleAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SelectableListSelectSingleAction(listID1, selected3));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the select method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.select(listID1, [selected1, selected4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SelectableListSelectAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SelectableListSelectAction(listID1, [selected1, selected4]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the deselectSingle method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.deselectSingle(listID1, selected4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SelectableListDeselectSingleAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SelectableListDeselectSingleAction(listID1, selected4));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the deselect method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.deselect(listID1, [selected2, selected4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SelectableListDeselectAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SelectableListDeselectAction(listID1, [selected2, selected4]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -3,7 +3,6 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { DebugElement } from '@angular/core';
|
import { DebugElement } from '@angular/core';
|
||||||
import { SearchFormComponent } from './search-form.component';
|
import { SearchFormComponent } from './search-form.component';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ResourceType } from '../../core/shared/resource-type';
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
@@ -15,24 +15,24 @@ import { SearchOptions } from '../shared/search/search-options.model';
|
|||||||
export class StatisticsService {
|
export class StatisticsService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService:RequestService,
|
protected requestService: RequestService,
|
||||||
protected halService:HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendEvent(linkPath:string, body:any) {
|
private sendEvent(linkPath: string, body: any) {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
this.halService.getEndpoint(linkPath).pipe(
|
this.halService.getEndpoint(linkPath).pipe(
|
||||||
map((endpoint:string) => new TrackRequest(requestId, endpoint, JSON.stringify(body))),
|
map((endpoint: string) => new TrackRequest(requestId, endpoint, JSON.stringify(body))),
|
||||||
take(1) // otherwise the previous events will fire again
|
take(1) // otherwise the previous events will fire again
|
||||||
).subscribe((request:RestRequest) => this.requestService.configure(request));
|
).subscribe((request: RestRequest) => this.requestService.configure(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To track a page view
|
* To track a page view
|
||||||
* @param dso: The dso which was viewed
|
* @param dso: The dso which was viewed
|
||||||
*/
|
*/
|
||||||
trackViewEvent(dso:DSpaceObject) {
|
trackViewEvent(dso: DSpaceObject) {
|
||||||
this.sendEvent('/statistics/viewevents', {
|
this.sendEvent('/statistics/viewevents', {
|
||||||
targetId: dso.uuid,
|
targetId: dso.uuid,
|
||||||
targetType: (dso as any).type
|
targetType: (dso as any).type
|
||||||
@@ -47,10 +47,10 @@ export class StatisticsService {
|
|||||||
* @param filters: An array of search filters used to filter the result set
|
* @param filters: An array of search filters used to filter the result set
|
||||||
*/
|
*/
|
||||||
trackSearchEvent(
|
trackSearchEvent(
|
||||||
searchOptions:SearchOptions,
|
searchOptions: SearchOptions,
|
||||||
page:{ size:number, totalElements:number, totalPages:number, number:number },
|
page: { size: number, totalElements: number, totalPages: number, number: number },
|
||||||
sort:{ by:string, order:string },
|
sort: { by: string, order: string },
|
||||||
filters?:Array<{ filter:string, operator:string, value:string, label:string }>
|
filters?: Array<{ filter: string, operator: string, value: string, label: string }>
|
||||||
) {
|
) {
|
||||||
const body = {
|
const body = {
|
||||||
query: searchOptions.query,
|
query: searchOptions.query,
|
||||||
@@ -66,13 +66,13 @@ export class StatisticsService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (hasValue(searchOptions.configuration)) {
|
if (hasValue(searchOptions.configuration)) {
|
||||||
Object.assign(body, {configuration: searchOptions.configuration})
|
Object.assign(body, { configuration: searchOptions.configuration })
|
||||||
}
|
}
|
||||||
if (hasValue(searchOptions.dsoType)) {
|
if (hasValue(searchOptions.dsoType)) {
|
||||||
Object.assign(body, {dsoType: searchOptions.dsoType.toLowerCase()})
|
Object.assign(body, { dsoType: searchOptions.dsoType.toLowerCase() })
|
||||||
}
|
}
|
||||||
if (hasValue(searchOptions.scope)) {
|
if (hasValue(searchOptions.scope)) {
|
||||||
Object.assign(body, {scope: searchOptions.scope})
|
Object.assign(body, { scope: searchOptions.scope })
|
||||||
}
|
}
|
||||||
if (isNotEmpty(filters)) {
|
if (isNotEmpty(filters)) {
|
||||||
const bodyFilters = [];
|
const bodyFilters = [];
|
||||||
@@ -85,7 +85,7 @@ export class StatisticsService {
|
|||||||
label: filter.label
|
label: filter.label
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Object.assign(body, {appliedFilters: bodyFilters})
|
Object.assign(body, { appliedFilters: bodyFilters })
|
||||||
}
|
}
|
||||||
this.sendEvent('/statistics/searchevents', body);
|
this.sendEvent('/statistics/searchevents', body);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user