mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-12 04:23:04 +00:00
Merge branch 'master' into w2p-61493_Configurable-entities-master-merge
Conflicts: src/app/+item-page/item-page.module.ts src/app/+search-page/search-filters/search-filter/search-filter.service.ts src/app/+search-page/search-page.component.ts src/app/+search-page/search-page.module.ts src/app/core/shared/dspace-object.model.ts src/app/core/shared/item.model.ts src/app/core/shared/metadata.models.ts src/app/core/shared/resource-type.ts src/app/shared/services/route.service.spec.ts src/app/shared/services/route.service.ts src/app/thumbnail/thumbnail.component.html
This commit is contained in:
@@ -48,6 +48,68 @@ module.exports = {
|
||||
// NOTE: 'fade' | 'fromTop' | 'fromRight' | 'fromBottom' | 'fromLeft' | 'rotate' | 'scale'
|
||||
animate: 'scale'
|
||||
},
|
||||
// Submission settings
|
||||
submission: {
|
||||
autosave: {
|
||||
// NOTE: which metadata trigger an autosave
|
||||
metadata: ['dc.title', 'dc.identifier.doi', 'dc.identifier.pmid', 'dc.identifier.arxiv'],
|
||||
// NOTE: every how many minutes submission is saved automatically
|
||||
timer: 5
|
||||
},
|
||||
icons: {
|
||||
metadata: [
|
||||
/**
|
||||
* NOTE: example of configuration
|
||||
* {
|
||||
* // NOTE: metadata name
|
||||
* name: 'dc.author',
|
||||
* // NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
|
||||
* style: 'fa-user'
|
||||
* }
|
||||
*/
|
||||
{
|
||||
name: 'dc.author',
|
||||
style: 'fas fa-user'
|
||||
},
|
||||
// default configuration
|
||||
{
|
||||
name: 'default',
|
||||
style: ''
|
||||
}
|
||||
],
|
||||
authority: {
|
||||
confidence: [
|
||||
/**
|
||||
* NOTE: example of configuration
|
||||
* {
|
||||
* // NOTE: confidence value
|
||||
* value: 'dc.author',
|
||||
* // NOTE: fontawesome (v4.x) icon classes and bootstrap utility classes can be used
|
||||
* style: 'fa-user'
|
||||
* }
|
||||
*/
|
||||
{
|
||||
value: 600,
|
||||
style: 'text-success'
|
||||
},
|
||||
{
|
||||
value: 500,
|
||||
style: 'text-info'
|
||||
},
|
||||
{
|
||||
value: 400,
|
||||
style: 'text-warning'
|
||||
},
|
||||
// default configuration
|
||||
{
|
||||
value: 'default',
|
||||
style: 'text-muted'
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
// Angular Universal settings
|
||||
universal: {
|
||||
preboot: true,
|
||||
|
@@ -75,8 +75,8 @@
|
||||
"@angular/router": "^6.1.4",
|
||||
"@angularclass/bootloader": "1.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^2.0.0",
|
||||
"@ng-dynamic-forms/core": "6.0.9",
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": "6.0.9",
|
||||
"@ng-dynamic-forms/core": "6.2.0",
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": "6.2.0",
|
||||
"@ngrx/effects": "^6.1.0",
|
||||
"@ngrx/router-store": "^6.1.0",
|
||||
"@ngrx/store": "^6.1.0",
|
||||
@@ -97,6 +97,7 @@
|
||||
"express": "4.16.2",
|
||||
"express-session": "1.15.6",
|
||||
"fast-json-patch": "^2.0.7",
|
||||
"file-saver": "^1.3.8",
|
||||
"font-awesome": "4.7.0",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.10",
|
||||
"http-server": "0.11.1",
|
||||
@@ -119,6 +120,7 @@
|
||||
"pem": "1.12.3",
|
||||
"reflect-metadata": "0.1.12",
|
||||
"rxjs": "6.2.2",
|
||||
"rxjs-spy": "^7.5.1",
|
||||
"sortablejs": "1.7.0",
|
||||
"text-mask-core": "5.0.1",
|
||||
"ts-loader": "^5.2.1",
|
||||
@@ -142,6 +144,7 @@
|
||||
"@types/deep-freeze": "0.1.1",
|
||||
"@types/express": "^4.11.1",
|
||||
"@types/express-serve-static-core": "4.16.0",
|
||||
"@types/file-saver": "^1.3.0",
|
||||
"@types/hammerjs": "2.0.35",
|
||||
"@types/jasmine": "^2.8.6",
|
||||
"@types/js-cookie": "2.1.0",
|
||||
|
@@ -369,7 +369,7 @@
|
||||
"results-per-page": "Results Per Page",
|
||||
"sort-direction": "Sort Options",
|
||||
"showing": {
|
||||
"label": "Now showing items ",
|
||||
"label": "Now showing ",
|
||||
"detail": "{{ range }} of {{ total }}"
|
||||
}
|
||||
},
|
||||
@@ -726,13 +726,25 @@
|
||||
"license": {
|
||||
"notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission."
|
||||
}
|
||||
},
|
||||
"submission": {
|
||||
"sections": {
|
||||
"init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below : <br> <br>"
|
||||
}
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"search-help": "Click here to looking for an existing correspondence",
|
||||
"remove": "Remove",
|
||||
"clear": "Clear",
|
||||
"clear-help": "Click here to remove the selected value",
|
||||
"edit": "Edit",
|
||||
"edit-help": "Click here to edit the selected value",
|
||||
"save": "Save",
|
||||
"save-help": "Save changes",
|
||||
"first-name": "First name",
|
||||
"last-name": "Last name",
|
||||
"loading": "Loading...",
|
||||
@@ -741,7 +753,9 @@
|
||||
"group-collapse": "Collapse",
|
||||
"group-expand": "Expand",
|
||||
"group-collapse-help": "Click here to collapse",
|
||||
"group-expand-help": "Click here to expand and add more elements"
|
||||
"group-expand-help": "Click here to expand and add more elements",
|
||||
"other-information": {
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "Login",
|
||||
@@ -799,5 +813,97 @@
|
||||
},
|
||||
"placeholder": "Search for a {{ type }}",
|
||||
"no-results": "No {{ type }} found"
|
||||
},
|
||||
"submission": {
|
||||
"general":{
|
||||
"cannot_submit": "You have not the privilege to make a new submission.",
|
||||
"deposit": "Deposit",
|
||||
"discard": {
|
||||
"submit": "Discard",
|
||||
"confirm": {
|
||||
"cancel": "Cancel",
|
||||
"submit": "Yes, I'm sure",
|
||||
"title": "Discard submission",
|
||||
"info": "This operation can't be undone. Are you sure?"
|
||||
}
|
||||
},
|
||||
"save": "Save",
|
||||
"save-later": "Save for later"
|
||||
},
|
||||
"submit": {
|
||||
"title": "Submission"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit Submission"
|
||||
},
|
||||
"mydspace": {
|
||||
|
||||
},
|
||||
"sections": {
|
||||
|
||||
"general": {
|
||||
"add-more": "Add more",
|
||||
"no-sections": "No options available",
|
||||
"sections_not_valid": "There are incomplete sections.",
|
||||
"collection": "Collection",
|
||||
"no-collection": "No collection found",
|
||||
"search-collection": "Search for a collection",
|
||||
"save_error_notice": "There was an issue when saving the item, please try again later.",
|
||||
"deposit_success_notice": "Submission deposited successfully.",
|
||||
"deposit_error_notice": "There was an issue when submitting the item, please try again later.",
|
||||
"discard_success_notice": "Submission discarded successfully.",
|
||||
"discard_error_notice": "There was an issue when discarding the item, please try again later.",
|
||||
"save_success_notice": "Submission saved successfully.",
|
||||
"metadata-extracted": "New metadata have been extracted and added to the <strong>{{sectionId}}</strong> section.",
|
||||
"metadata-extracted-new-section": "New <strong>{{sectionId}}</strong> section has been added to submission."
|
||||
},
|
||||
"submit.progressbar.describe.stepone": "Describe",
|
||||
"submit.progressbar.describe.steptwo": "Describe",
|
||||
"submit.progressbar.describe.stepcustom": "Describe",
|
||||
"submit.progressbar.describe.recycle": "Recycle",
|
||||
"submit.progressbar.upload": "Upload files",
|
||||
"submit.progressbar.license": "Deposit license",
|
||||
"submit.progressbar.cclicense": "Creative commons license",
|
||||
"submit.progressbar.detect-duplicate": "Potential duplicates",
|
||||
|
||||
"upload": {
|
||||
"no-entry": "No",
|
||||
"no-file-uploaded": "No file uploaded yet.",
|
||||
"info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or <strong>upload additional files just dragging & dropping them everywhere in the page</strong>",
|
||||
"drop-message": "Drop files to attach them to the item",
|
||||
"upload-successful": "Upload successful",
|
||||
"upload-failed": "Upload failed",
|
||||
"header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):",
|
||||
"header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):",
|
||||
"form": {
|
||||
"access-condition-label": "Access condition type",
|
||||
"from-label": "Access grant from",
|
||||
"from-placeholder": "From",
|
||||
"until-label": "Access grant until",
|
||||
"until-placeholder": "Until",
|
||||
"group-label": "Group",
|
||||
"group-required": "Group is required.",
|
||||
"date-required": "Date is required."
|
||||
},
|
||||
"save-metadata": "Save metadata",
|
||||
"undo": "Cancel",
|
||||
"delete": {
|
||||
"submit": "Delete",
|
||||
"confirm": {
|
||||
"cancel": "Cancel",
|
||||
"submit": "Yes, I'm sure",
|
||||
"title": "Delete bitstream",
|
||||
"info": "This operation can't be undone. Are you sure?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"uploader": {
|
||||
"drag-message": "Drag & Drop your files here",
|
||||
"or": ", or",
|
||||
"browse": "browse",
|
||||
"queue-lenght": "Queue length",
|
||||
"processing": "Processing"
|
||||
}
|
||||
}
|
||||
|
@@ -40,8 +40,8 @@
|
||||
"description": "Beschrijving:"
|
||||
},
|
||||
"link": {
|
||||
"simple": "Eenvoudige item weergave",
|
||||
"full": "Volledige item weergave"
|
||||
"simple": "Eenvoudige itemweergave",
|
||||
"full": "Volledige itemweergave"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -52,10 +52,10 @@
|
||||
},
|
||||
"pagination": {
|
||||
"results-per-page": "Resultaten per pagina",
|
||||
"sort-direction": "Sorteer mogelijkheden",
|
||||
"sort-direction": "Sorteermogelijkheden",
|
||||
"showing": {
|
||||
"label": "Getoonde items ",
|
||||
"detail": "{{ range }} tot {{ total }}"
|
||||
"label": "Resultaten ",
|
||||
"detail": "{{ range }} van {{ total }}"
|
||||
}
|
||||
},
|
||||
"sorting": {
|
||||
@@ -116,8 +116,8 @@
|
||||
"reset": "Filters verwijderen",
|
||||
"applied": {
|
||||
"f.author": "Auteur",
|
||||
"f.dateIssued.min": "Start datum",
|
||||
"f.dateIssued.max": "Eind datum",
|
||||
"f.dateIssued.min": "Startdatum",
|
||||
"f.dateIssued.max": "Einddatum",
|
||||
"f.subject": "Sleutelwoord",
|
||||
"f.has_content_in_original_bundle": "Heeft bestanden"
|
||||
},
|
||||
@@ -129,7 +129,7 @@
|
||||
"head": "Auteur"
|
||||
},
|
||||
"scope": {
|
||||
"placeholder": "Bereik filter",
|
||||
"placeholder": "Bereikfilter",
|
||||
"head": "Bereik"
|
||||
},
|
||||
"subject": {
|
||||
@@ -159,27 +159,27 @@
|
||||
"metadata": {
|
||||
"title": "DSpace Angular :: Metadata Register",
|
||||
"head": "Metadata Register",
|
||||
"description": "Het metadata register omvat de lijst van alle metadata velden die beschikbaar zijn in het systeem. Deze velden kunnen verspreid zijn over verschillende metadata schema's. Het qualified Dublin Core schema (dc) is een verplicht schema en kan niet worden verwijderd.",
|
||||
"description": "Het metadataregister omvat de lijst van alle metadatavelden die beschikbaar zijn in het systeem. Deze velden kunnen verspreid zijn over verschillende metadataschema's. Het qualified Dublin Core schema (dc) is een verplicht schema en kan niet worden verwijderd.",
|
||||
"schemas": {
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"namespace": "Naamruimte",
|
||||
"name": "Naam"
|
||||
},
|
||||
"no-items": "Er kunnen geen metadata schema's getoond worden."
|
||||
"no-items": "Er kunnen geen metadataschema's getoond worden."
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"title": "DSpace Angular :: Metadata Schema Register",
|
||||
"head": "Metadata Schema",
|
||||
"description": "Dit is het metadata schema voor \"{{namespace}}\".",
|
||||
"description": "Dit is het metadataschema voor \"{{namespace}}\".",
|
||||
"fields": {
|
||||
"head": "Schema metadata velden",
|
||||
"head": "Schema metadatavelden",
|
||||
"table": {
|
||||
"field": "Veld",
|
||||
"scopenote": "Opmerking over bereik"
|
||||
},
|
||||
"no-items": "Er kunnen geen metadata velden getoond worden."
|
||||
"no-items": "Er kunnen geen metadatavelden getoond worden."
|
||||
}
|
||||
},
|
||||
"bitstream-formats": {
|
||||
@@ -198,7 +198,7 @@
|
||||
},
|
||||
"internal": "intern"
|
||||
},
|
||||
"no-items": "Er kunnen geen bitstream formaten getoond worden."
|
||||
"no-items": "Er kunnen geen bitstreamformaten getoond worden."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,7 @@
|
||||
"validation": {
|
||||
"pattern": "Deze invoer is niet toegelaten volgens dit patroon: {{ pattern }}.",
|
||||
"license": {
|
||||
"notgranted": "U moet de invoerlicentie goedkeuren om de invoer af te werken. Indien u deze licentie momenteel niet kan of mag goedkeuren, kan u uw werk opslaan en de invoer later afwerken. U kan dit nieuwe item ook verwijderen indien u niet voldoet aan de vereisten van de invoer licentie."
|
||||
"notgranted": "U moet de invoerlicentie goedkeuren om de invoer af te werken. Indien u deze licentie momenteel niet kan of mag goedkeuren, kan u uw werk opslaan en de invoer later afwerken. U kunt dit nieuwe item ook verwijderen indien u niet voldoet aan de vereisten van de invoerlicentie."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -271,7 +271,7 @@
|
||||
"expired": "Uw sessie is vervallen. Gelieve opnieuw aan te melden."
|
||||
},
|
||||
"errors": {
|
||||
"invalid-user": "Ongeldig email adres of wachtwoord."
|
||||
"invalid-user": "Ongeldig e-mailadres of wachtwoord."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { MetadataRegistryComponent } from './metadata-registry.component';
|
||||
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
@@ -44,7 +44,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
getSelectedMetadataSchemas: () => observableOf([]),
|
||||
editMetadataSchema: (schema) => {},
|
||||
cancelEditMetadataSchema: () => {},
|
||||
deleteMetadataSchema: () => observableOf(new RestResponse(true, '200')),
|
||||
deleteMetadataSchema: () => observableOf(new RestResponse(true, 200, 'OK')),
|
||||
deselectAllMetadataSchema: () => {},
|
||||
clearMetadataSchemaRequests: () => observableOf(undefined)
|
||||
};
|
||||
|
@@ -84,7 +84,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
getSelectedMetadataFields: () => observableOf([]),
|
||||
editMetadataField: (schema) => {},
|
||||
cancelEditMetadataField: () => {},
|
||||
deleteMetadataField: () => observableOf(new RestResponse(true, '200')),
|
||||
deleteMetadataField: () => observableOf(new RestResponse(true, 200, 'OK')),
|
||||
deselectAllMetadataField: () => {},
|
||||
clearMetadataFieldRequests: () => observableOf(undefined)
|
||||
};
|
||||
|
@@ -32,7 +32,7 @@
|
||||
</ds-comcol-page-content>
|
||||
<!-- Licence -->
|
||||
<ds-comcol-page-content
|
||||
[content]="collection.license"
|
||||
[content]="collection.dcLicense"
|
||||
[title]="'collection.page.license'">
|
||||
</ds-comcol-page-content>
|
||||
</div>
|
||||
|
@@ -1,12 +1,13 @@
|
||||
<ng-container *ngVar="(communitiesRDObs | async) as communitiesRD">
|
||||
<div *ngIf="communitiesRD?.hasSucceeded " @fadeInOut>
|
||||
<ng-container *ngVar="(communitiesRD$ | async) as communitiesRD">
|
||||
<div *ngIf="communitiesRD?.hasSucceeded ">
|
||||
<h2>{{'home.top-level-communities.head' | translate}}</h2>
|
||||
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
|
||||
<ds-viewable-collection
|
||||
[config]="config"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="communitiesRD"
|
||||
(paginationChange)="updatePage($event)">
|
||||
[objects]="communitiesRD$ | async"
|
||||
[hideGear]="true"
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="communitiesRD?.hasFailed " message="{{'error.top-level-communites' | translate}}"></ds-error>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
@@ -9,7 +9,11 @@ import { Community } from '../../core/shared/community.model';
|
||||
|
||||
import { fadeInOut } from '../../shared/animations/fade';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* this component renders the Top-Level Community list
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-top-level-community-list',
|
||||
styleUrls: ['./top-level-community-list.component.scss'],
|
||||
@@ -18,9 +22,20 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
||||
animations: [fadeInOut]
|
||||
})
|
||||
|
||||
export class TopLevelCommunityListComponent {
|
||||
communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
||||
export class TopLevelCommunityListComponent implements OnInit {
|
||||
/**
|
||||
* A list of remote data objects of all top communities
|
||||
*/
|
||||
communitiesRD$: BehaviorSubject<RemoteData<PaginatedList<Community>>> = new BehaviorSubject<RemoteData<PaginatedList<Community>>>({} as any);
|
||||
|
||||
/**
|
||||
* The pagination configuration
|
||||
*/
|
||||
config: PaginationComponentOptions;
|
||||
|
||||
/**
|
||||
* The sorting configuration
|
||||
*/
|
||||
sortConfig: SortOptions;
|
||||
|
||||
constructor(private cds: CommunityDataService) {
|
||||
@@ -29,20 +44,34 @@ export class TopLevelCommunityListComponent {
|
||||
this.config.pageSize = 5;
|
||||
this.config.currentPage = 1;
|
||||
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||
|
||||
this.updatePage({
|
||||
page: this.config.currentPage,
|
||||
pageSize: this.config.pageSize,
|
||||
sortField: this.sortConfig.field,
|
||||
direction: this.sortConfig.direction
|
||||
});
|
||||
}
|
||||
|
||||
updatePage(data) {
|
||||
this.communitiesRDObs = this.cds.findTop({
|
||||
currentPage: data.page,
|
||||
elementsPerPage: data.pageSize,
|
||||
sort: { field: data.sortField, direction: data.sortDirection }
|
||||
ngOnInit() {
|
||||
this.updatePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the pagination settings is changed
|
||||
* @param event The new pagination data
|
||||
*/
|
||||
onPaginationChange(event) {
|
||||
this.config.currentPage = event.page;
|
||||
this.config.pageSize = event.pageSize;
|
||||
this.sortConfig.field = event.sortField;
|
||||
this.sortConfig.direction = event.sortDirection;
|
||||
this.updatePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of top communities
|
||||
*/
|
||||
updatePage() {
|
||||
this.cds.findTop({
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize,
|
||||
sort: { field: this.sortConfig.field, direction: this.sortConfig.direction }
|
||||
}).pipe(take(1)).subscribe((results) => {
|
||||
this.communitiesRD$.next(results);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,20 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Item} from '../../../core/shared/item.model';
|
||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
||||
import {of as observableOf} from 'rxjs';
|
||||
import {RemoteData} from '../../../core/data/remote-data';
|
||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {ItemPrivateComponent} from './item-private.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ItemPrivateComponent } from './item-private.component';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
|
||||
let comp: ItemPrivateComponent;
|
||||
@@ -44,8 +44,8 @@ describe('ItemPrivateComponent', () => {
|
||||
url: `${itemPageUrl}/edit`
|
||||
});
|
||||
|
||||
mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
|
||||
setDiscoverable: observableOf(new RestResponse(true, '200'))
|
||||
mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
|
||||
setDiscoverable: observableOf(new RestResponse(true, 200, 'OK'))
|
||||
});
|
||||
|
||||
routeStub = {
|
||||
@@ -62,10 +62,10 @@ describe('ItemPrivateComponent', () => {
|
||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [ItemPrivateComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: Router, useValue: routerStub},
|
||||
{provide: ItemDataService, useValue: mockItemDataService},
|
||||
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||
], schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
@@ -73,8 +73,8 @@ describe('ItemPrivateComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
successfulRestResponse = new RestResponse(true, '200');
|
||||
failRestResponse = new RestResponse(false, '500');
|
||||
successfulRestResponse = new RestResponse(true, 200, 'OK');
|
||||
failRestResponse = new RestResponse(false, 500, 'Internal Server Error');
|
||||
|
||||
fixture = TestBed.createComponent(ItemPrivateComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
@@ -44,8 +44,8 @@ describe('ItemPublicComponent', () => {
|
||||
url: `${itemPageUrl}/edit`
|
||||
});
|
||||
|
||||
mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
|
||||
setDiscoverable: observableOf(new RestResponse(true, '200'))
|
||||
mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
|
||||
setDiscoverable: observableOf(new RestResponse(true, 200, 'OK'))
|
||||
});
|
||||
|
||||
routeStub = {
|
||||
@@ -62,10 +62,10 @@ describe('ItemPublicComponent', () => {
|
||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [ItemPublicComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: Router, useValue: routerStub},
|
||||
{provide: ItemDataService, useValue: mockItemDataService},
|
||||
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||
], schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
@@ -73,8 +73,8 @@ describe('ItemPublicComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
successfulRestResponse = new RestResponse(true, '200');
|
||||
failRestResponse = new RestResponse(false, '500');
|
||||
successfulRestResponse = new RestResponse(true, 200, 'OK');
|
||||
failRestResponse = new RestResponse(false, 500, 'Internal Server Error');
|
||||
|
||||
fixture = TestBed.createComponent(ItemPublicComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
@@ -44,8 +44,8 @@ describe('ItemReinstateComponent', () => {
|
||||
url: `${itemPageUrl}/edit`
|
||||
});
|
||||
|
||||
mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
|
||||
setWithDrawn: observableOf(new RestResponse(true, '200'))
|
||||
mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
|
||||
setWithDrawn: observableOf(new RestResponse(true, 200, 'OK'))
|
||||
});
|
||||
|
||||
routeStub = {
|
||||
@@ -62,10 +62,10 @@ describe('ItemReinstateComponent', () => {
|
||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [ItemReinstateComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: Router, useValue: routerStub},
|
||||
{provide: ItemDataService, useValue: mockItemDataService},
|
||||
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||
], schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
@@ -73,8 +73,8 @@ describe('ItemReinstateComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
successfulRestResponse = new RestResponse(true, '200');
|
||||
failRestResponse = new RestResponse(false, '500');
|
||||
successfulRestResponse = new RestResponse(true, 200, 'OK');
|
||||
failRestResponse = new RestResponse(false, 500, 'Internal Server Error');
|
||||
|
||||
fixture = TestBed.createComponent(ItemReinstateComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
@@ -44,8 +44,8 @@ describe('ItemWithdrawComponent', () => {
|
||||
url: `${itemPageUrl}/edit`
|
||||
});
|
||||
|
||||
mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
|
||||
setWithDrawn: observableOf(new RestResponse(true, '200'))
|
||||
mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
|
||||
setWithDrawn: observableOf(new RestResponse(true, 200, 'OK'))
|
||||
});
|
||||
|
||||
routeStub = {
|
||||
@@ -62,10 +62,10 @@ describe('ItemWithdrawComponent', () => {
|
||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot(),],
|
||||
declarations: [ItemWithdrawComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: Router, useValue: routerStub},
|
||||
{provide: ItemDataService, useValue: mockItemDataService},
|
||||
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||
], schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
@@ -73,8 +73,8 @@ describe('ItemWithdrawComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
successfulRestResponse = new RestResponse(true, '200');
|
||||
failRestResponse = new RestResponse(false, '500');
|
||||
successfulRestResponse = new RestResponse(true, 200, 'OK');
|
||||
failRestResponse = new RestResponse(false, 500, 'Internal Server Error');
|
||||
|
||||
fixture = TestBed.createComponent(ItemWithdrawComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
@@ -82,10 +82,10 @@ describe('AbstractSimpleItemActionComponent', () => {
|
||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [MySimpleItemActionComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: Router, useValue: routerStub},
|
||||
{provide: ItemDataService, useValue: mockItemDataService},
|
||||
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||
], schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
@@ -93,14 +93,19 @@ describe('AbstractSimpleItemActionComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
successfulRestResponse = new RestResponse(true, '200');
|
||||
failRestResponse = new RestResponse(false, '500');
|
||||
successfulRestResponse = new RestResponse(true, 200, 'OK');
|
||||
failRestResponse = new RestResponse(false, 500, 'Internal Server Error');
|
||||
|
||||
fixture = TestBed.createComponent(MySimpleItemActionComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
comp = null;
|
||||
});
|
||||
|
||||
it('should render a page with messages based on the provided messageKey', () => {
|
||||
const header = fixture.debugElement.query(By.css('h2')).nativeElement;
|
||||
expect(header.innerHTML).toContain('item.edit.myEditAction.header');
|
||||
@@ -124,7 +129,6 @@ describe('AbstractSimpleItemActionComponent', () => {
|
||||
});
|
||||
|
||||
it('should process a RestResponse to navigate and display success notification', () => {
|
||||
spyOn(notificationsServiceStub, 'success');
|
||||
comp.processRestResponse(successfulRestResponse);
|
||||
|
||||
expect(notificationsServiceStub.success).toHaveBeenCalled();
|
||||
@@ -132,7 +136,6 @@ describe('AbstractSimpleItemActionComponent', () => {
|
||||
});
|
||||
|
||||
it('should process a RestResponse to navigate and display success notification', () => {
|
||||
spyOn(notificationsServiceStub, 'error');
|
||||
comp.processRestResponse(failRestResponse);
|
||||
|
||||
expect(notificationsServiceStub.error).toHaveBeenCalled();
|
||||
|
@@ -6,7 +6,7 @@ import { LoginPageComponent } from './login-page.component';
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: '', component: LoginPageComponent, data: { title: 'login.title' } }
|
||||
{ path: '', pathMatch: 'full', component: LoginPageComponent, data: { title: 'login.title' } }
|
||||
])
|
||||
]
|
||||
})
|
||||
|
@@ -1,15 +1,20 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { LoginPageComponent } from './login-page.component';
|
||||
import { ActivatedRouteStub } from '../shared/testing/active-router-stub';
|
||||
|
||||
describe('LoginPageComponent', () => {
|
||||
let comp: LoginPageComponent;
|
||||
let fixture: ComponentFixture<LoginPageComponent>;
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({})
|
||||
});
|
||||
|
||||
const store: Store<LoginPageComponent> = jasmine.createSpyObj('store', {
|
||||
/* tslint:disable:no-empty */
|
||||
@@ -25,9 +30,8 @@ describe('LoginPageComponent', () => {
|
||||
],
|
||||
declarations: [LoginPageComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: Store, useValue: store
|
||||
}
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: Store, useValue: store }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -1,20 +1,81 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { combineLatest as observableCombineLatest, Subscription } from 'rxjs';
|
||||
import { filter, take } from 'rxjs/operators';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
import { AppState } from '../app.reducer';
|
||||
import { ResetAuthenticationMessagesAction } from '../core/auth/auth.actions';
|
||||
import {
|
||||
AddAuthenticationMessageAction,
|
||||
AuthenticatedAction,
|
||||
AuthenticationSuccessAction,
|
||||
ResetAuthenticationMessagesAction
|
||||
} from '../core/auth/auth.actions';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
|
||||
import { isAuthenticated } from '../core/auth/selectors';
|
||||
|
||||
/**
|
||||
* This component represents the login page
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-login-page',
|
||||
styleUrls: ['./login-page.component.scss'],
|
||||
templateUrl: './login-page.component.html'
|
||||
})
|
||||
export class LoginPageComponent implements OnDestroy {
|
||||
export class LoginPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
constructor(private store: Store<AppState>) {}
|
||||
/**
|
||||
* Subscription to unsubscribe onDestroy
|
||||
* @type {Subscription}
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {ActivatedRoute} route
|
||||
* @param {Store<AppState>} store
|
||||
*/
|
||||
constructor(private route: ActivatedRoute,
|
||||
private store: Store<AppState>) {}
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
const queryParamsObs = this.route.queryParams;
|
||||
const authenticated = this.store.select(isAuthenticated);
|
||||
this.sub = observableCombineLatest(queryParamsObs, authenticated).pipe(
|
||||
filter(([params, auth]) => isNotEmpty(params.token) || isNotEmpty(params.expired)),
|
||||
take(1)
|
||||
).subscribe(([params, auth]) => {
|
||||
const token = params.token;
|
||||
let authToken: AuthTokenInfo;
|
||||
if (!auth) {
|
||||
if (isNotEmpty(token)) {
|
||||
authToken = new AuthTokenInfo(token);
|
||||
this.store.dispatch(new AuthenticatedAction(authToken));
|
||||
} else if (isNotEmpty(params.expired)) {
|
||||
this.store.dispatch(new AddAuthenticationMessageAction('auth.messages.expired'));
|
||||
}
|
||||
} else {
|
||||
if (isNotEmpty(token)) {
|
||||
authToken = new AuthTokenInfo(token);
|
||||
this.store.dispatch(new AuthenticationSuccessAction(authToken));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from subscription
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
// Clear all authentication messages when leaving login page
|
||||
this.store.dispatch(new ResetAuthenticationMessagesAction());
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
@@ -33,10 +32,9 @@ export class FilteredSearchPageComponent extends SearchPageComponent {
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SearchSidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
protected filterService: SearchFilterService,
|
||||
protected searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
super(service, sidebarService, windowService, filterService, searchConfigService, routeService);
|
||||
super(service, sidebarService, windowService, searchConfigService, routeService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,24 +1,9 @@
|
||||
<div>
|
||||
<div class="filters py-2">
|
||||
<a *ngFor="let value of (selectedValues | async)" class="d-flex flex-row"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="getRemoveParams(value) | async" queryParamsHandling="merge">
|
||||
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
|
||||
<span class="filter-value pl-1">{{value}}</span>
|
||||
</a>
|
||||
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$"></ds-search-facet-selected-option>
|
||||
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||
<div [@facetLoad]="animationState">
|
||||
<ng-container *ngFor="let value of page.page; let i=index">
|
||||
<a *ngIf="!(selectedValues | async).includes(value.value)" class="d-flex flex-row"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="getAddParams(value.value) | async" queryParamsHandling="merge">
|
||||
<input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/>
|
||||
<span class="filter-value px-1">{{value.value}}</span>
|
||||
<span class="float-right filter-value-count ml-auto">
|
||||
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
|
||||
</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$"></ds-search-facet-option>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="clearfix toggle-more-filters">
|
||||
|
@@ -0,0 +1,9 @@
|
||||
<a *ngIf="isVisible | async" class="d-flex flex-row"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="addQueryParams" queryParamsHandling="merge">
|
||||
<input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/>
|
||||
<span class="filter-value px-1">{{filterValue.value}}</span>
|
||||
<span class="float-right filter-value-count ml-auto">
|
||||
<span class="badge badge-secondary badge-pill">{{filterValue.count}}</span>
|
||||
</span>
|
||||
</a>
|
@@ -0,0 +1,121 @@
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { SearchFacetOptionComponent } from './search-facet-option.component';
|
||||
import { SearchFilterConfig } from '../../../../search-service/search-filter-config.model';
|
||||
import { FilterType } from '../../../../search-service/filter-type.model';
|
||||
import { FacetValue } from '../../../../search-service/facet-value.model';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { SearchService } from '../../../../search-service/search.service';
|
||||
import { SearchServiceStub } from '../../../../../shared/testing/search-service-stub';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterStub } from '../../../../../shared/testing/router-stub';
|
||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||
import { SearchFilterService } from '../../search-filter.service';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('SearchFacetOptionComponent', () => {
|
||||
let comp: SearchFacetOptionComponent;
|
||||
let fixture: ComponentFixture<SearchFacetOptionComponent>;
|
||||
const filterName1 = 'test name';
|
||||
const value1 = 'testvalue1';
|
||||
const value2 = 'test2';
|
||||
const value3 = 'another value3';
|
||||
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||
name: filterName1,
|
||||
type: FilterType.range,
|
||||
hasFacets: false,
|
||||
isOpenByDefault: false,
|
||||
pageSize: 2,
|
||||
minValue: 200,
|
||||
maxValue: 3000,
|
||||
});
|
||||
const value: FacetValue = {
|
||||
value: value2,
|
||||
count: 20,
|
||||
search: ''
|
||||
};
|
||||
|
||||
const searchLink = '/search';
|
||||
const selectedValues = [value1];
|
||||
const selectedValues$ = observableOf(selectedValues);
|
||||
let filterService;
|
||||
let searchService;
|
||||
let router;
|
||||
const page = observableOf(0);
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
|
||||
declarations: [SearchFacetOptionComponent],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{
|
||||
provide: SearchConfigurationService, useValue: {
|
||||
searchOptions: observableOf({})
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: SearchFilterService, useValue: {
|
||||
getSelectedValuesForFilter: () => selectedValues,
|
||||
isFilterActiveWithValue: (paramName: string, filterValue: string) => observableOf(true),
|
||||
getPage: (paramName: string) => page,
|
||||
/* tslint:disable:no-empty */
|
||||
incrementPage: (filterName: string) => {
|
||||
},
|
||||
resetPage: (filterName: string) => {
|
||||
}
|
||||
/* tslint:enable:no-empty */
|
||||
}
|
||||
}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(SearchFacetOptionComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchFacetOptionComponent);
|
||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||
filterService = (comp as any).filterService;
|
||||
searchService = (comp as any).searchService;
|
||||
router = (comp as any).router;
|
||||
comp.filterValue = value;
|
||||
comp.selectedValues$ = selectedValues$;
|
||||
comp.filterConfig = mockFilterConfig;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when the updateAddParams method is called wih a value', () => {
|
||||
it('should update the addQueryParams with the new parameter values', () => {
|
||||
comp.addQueryParams = {};
|
||||
(comp as any).updateAddParams(selectedValues);
|
||||
expect(comp.addQueryParams).toEqual({
|
||||
[mockFilterConfig.paramName]: [value1, value.value],
|
||||
page: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when isVisible emits true', () => {
|
||||
it('the facet option should be visible', () => {
|
||||
comp.isVisible = observableOf(true);
|
||||
fixture.detectChanges();
|
||||
const linkEl = fixture.debugElement.query(By.css('a'));
|
||||
expect(linkEl).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when isVisible emits false', () => {
|
||||
it('the facet option should not be visible', () => {
|
||||
comp.isVisible = observableOf(false);
|
||||
fixture.detectChanges();
|
||||
const linkEl = fixture.debugElement.query(By.css('a'));
|
||||
expect(linkEl).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,102 @@
|
||||
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { FacetValue } from '../../../../search-service/facet-value.model';
|
||||
import { SearchFilterConfig } from '../../../../search-service/search-filter-config.model';
|
||||
import { SearchService } from '../../../../search-service/search.service';
|
||||
import { SearchFilterService } from '../../search-filter.service';
|
||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||
import { hasValue } from '../../../../../shared/empty.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-facet-option',
|
||||
templateUrl: './search-facet-option.component.html',
|
||||
})
|
||||
|
||||
/**
|
||||
* Represents a single option in a filter facet
|
||||
*/
|
||||
export class SearchFacetOptionComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* A single value for this component
|
||||
*/
|
||||
@Input() filterValue: FacetValue;
|
||||
|
||||
/**
|
||||
* The filter configuration for this facet option
|
||||
*/
|
||||
@Input() filterConfig: SearchFilterConfig;
|
||||
|
||||
/**
|
||||
* Emits the active values for this filter
|
||||
*/
|
||||
@Input() selectedValues$: Observable<string[]>;
|
||||
|
||||
/**
|
||||
* Emits true when this option should be visible and false when it should be invisible
|
||||
*/
|
||||
isVisible: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* UI parameters when this filter is added
|
||||
*/
|
||||
addQueryParams;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from on destroy
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
constructor(protected searchService: SearchService,
|
||||
protected filterService: SearchFilterService,
|
||||
protected searchConfigService: SearchConfigurationService,
|
||||
protected router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all observable instance variables and starts listening to them
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.isVisible = this.isChecked().pipe(map((checked: boolean) => !checked));
|
||||
this.sub = observableCombineLatest(this.selectedValues$, this.searchConfigService.searchOptions)
|
||||
.subscribe(([selectedValues, searchOptions]) => {
|
||||
this.updateAddParams(selectedValues)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value for this filter is currently active
|
||||
*/
|
||||
private isChecked(): Observable<boolean> {
|
||||
return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.filterValue.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The base path to the search page
|
||||
*/
|
||||
getSearchLink() {
|
||||
return this.searchService.getSearchLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the parameters that should change if a given value for this filter would be added to the active filters
|
||||
* @param {string[]} selectedValues The values that are currently selected for this filter
|
||||
*/
|
||||
private updateAddParams(selectedValues: string[]): void {
|
||||
this.addQueryParams = {
|
||||
[this.filterConfig.paramName]: [...selectedValues, this.filterValue.value],
|
||||
page: 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the subscription is unsubscribed from when this component is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
<a *ngIf="isVisible | async" class="d-flex flex-row"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="changeQueryParams" queryParamsHandling="merge">
|
||||
<span class="filter-value px-1">{{filterValue.value}}</span>
|
||||
<span class="float-right filter-value-count ml-auto">
|
||||
<span class="badge badge-secondary badge-pill">{{filterValue.count}}</span>
|
||||
</span>
|
||||
</a>
|
@@ -0,0 +1,125 @@
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { SearchFilterConfig } from '../../../../search-service/search-filter-config.model';
|
||||
import { FilterType } from '../../../../search-service/filter-type.model';
|
||||
import { FacetValue } from '../../../../search-service/facet-value.model';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { SearchService } from '../../../../search-service/search.service';
|
||||
import { SearchServiceStub } from '../../../../../shared/testing/search-service-stub';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterStub } from '../../../../../shared/testing/router-stub';
|
||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||
import { SearchFilterService } from '../../search-filter.service';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SearchFacetRangeOptionComponent } from './search-facet-range-option.component';
|
||||
import {
|
||||
RANGE_FILTER_MAX_SUFFIX,
|
||||
RANGE_FILTER_MIN_SUFFIX
|
||||
} from '../../search-range-filter/search-range-filter.component';
|
||||
|
||||
describe('SearchFacetRangeOptionComponent', () => {
|
||||
let comp: SearchFacetRangeOptionComponent;
|
||||
let fixture: ComponentFixture<SearchFacetRangeOptionComponent>;
|
||||
const filterName1 = 'test name';
|
||||
const value2 = '20 - 30';
|
||||
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||
name: filterName1,
|
||||
type: FilterType.range,
|
||||
hasFacets: false,
|
||||
isOpenByDefault: false,
|
||||
pageSize: 2,
|
||||
minValue: 200,
|
||||
maxValue: 3000,
|
||||
});
|
||||
const value: FacetValue = {
|
||||
value: value2,
|
||||
count: 20,
|
||||
search: ''
|
||||
};
|
||||
|
||||
const searchLink = '/search';
|
||||
let filterService;
|
||||
let searchService;
|
||||
let router;
|
||||
const page = observableOf(0);
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
|
||||
declarations: [SearchFacetRangeOptionComponent],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{
|
||||
provide: SearchConfigurationService, useValue: {
|
||||
searchOptions: observableOf({})
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: SearchFilterService, useValue: {
|
||||
isFilterActiveWithValue: (paramName: string, filterValue: string) => observableOf(true),
|
||||
getPage: (paramName: string) => page,
|
||||
/* tslint:disable:no-empty */
|
||||
incrementPage: (filterName: string) => {
|
||||
},
|
||||
resetPage: (filterName: string) => {
|
||||
}
|
||||
/* tslint:enable:no-empty */
|
||||
}
|
||||
}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(SearchFacetRangeOptionComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchFacetRangeOptionComponent);
|
||||
comp = fixture.componentInstance; // SearchFacetRangeOptionComponent test instance
|
||||
filterService = (comp as any).filterService;
|
||||
searchService = (comp as any).searchService;
|
||||
router = (comp as any).router;
|
||||
comp.filterValue = value;
|
||||
comp.filterConfig = mockFilterConfig;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when the updateChangeParams method is called wih a value', () => {
|
||||
it('should update the changeQueryParams with the new parameter values', () => {
|
||||
comp.changeQueryParams = {};
|
||||
comp.filterValue = {
|
||||
value: '50-60',
|
||||
count: 20,
|
||||
search: ''
|
||||
};
|
||||
(comp as any).updateChangeParams();
|
||||
expect(comp.changeQueryParams).toEqual({
|
||||
[mockFilterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: ['50'],
|
||||
[mockFilterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: ['60'],
|
||||
page: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when isVisible emits true', () => {
|
||||
it('the facet option should be visible', () => {
|
||||
comp.isVisible = observableOf(true);
|
||||
fixture.detectChanges();
|
||||
const linkEl = fixture.debugElement.query(By.css('a'));
|
||||
expect(linkEl).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when isVisible emits false', () => {
|
||||
it('the facet option should not be visible', () => {
|
||||
comp.isVisible = observableOf(false);
|
||||
fixture.detectChanges();
|
||||
const linkEl = fixture.debugElement.query(By.css('a'));
|
||||
expect(linkEl).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,105 @@
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { FacetValue } from '../../../../search-service/facet-value.model';
|
||||
import { SearchFilterConfig } from '../../../../search-service/search-filter-config.model';
|
||||
import { SearchService } from '../../../../search-service/search.service';
|
||||
import { SearchFilterService } from '../../search-filter.service';
|
||||
import {
|
||||
RANGE_FILTER_MAX_SUFFIX,
|
||||
RANGE_FILTER_MIN_SUFFIX
|
||||
} from '../../search-range-filter/search-range-filter.component';
|
||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||
import { hasValue } from '../../../../../shared/empty.util';
|
||||
|
||||
const rangeDelimiter = '-';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-facet-range-option',
|
||||
templateUrl: './search-facet-range-option.component.html',
|
||||
})
|
||||
|
||||
/**
|
||||
* Represents a single option in a range filter facet
|
||||
*/
|
||||
export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* A single value for this component
|
||||
*/
|
||||
@Input() filterValue: FacetValue;
|
||||
|
||||
/**
|
||||
* The filter configuration for this facet option
|
||||
*/
|
||||
@Input() filterConfig: SearchFilterConfig;
|
||||
|
||||
/**
|
||||
* Emits true when this option should be visible and false when it should be invisible
|
||||
*/
|
||||
isVisible: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* UI parameters when this filter is changed
|
||||
*/
|
||||
changeQueryParams;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from on destroy
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
constructor(protected searchService: SearchService,
|
||||
protected filterService: SearchFilterService,
|
||||
protected searchConfigService: SearchConfigurationService,
|
||||
protected router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all observable instance variables and starts listening to them
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.isVisible = this.isChecked().pipe(map((checked: boolean) => !checked));
|
||||
this.sub = this.searchConfigService.searchOptions.subscribe(() => {
|
||||
this.updateChangeParams()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value for this filter is currently active
|
||||
*/
|
||||
private isChecked(): Observable<boolean> {
|
||||
return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.filterValue.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The base path to the search page
|
||||
*/
|
||||
getSearchLink() {
|
||||
return this.searchService.getSearchLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the parameters that should change if a given values for this range filter would be changed
|
||||
*/
|
||||
private updateChangeParams(): void {
|
||||
const parts = this.filterValue.value.split(rangeDelimiter);
|
||||
const min = parts.length > 1 ? parts[0].trim() : this.filterValue.value;
|
||||
const max = parts.length > 1 ? parts[1].trim() : this.filterValue.value;
|
||||
this.changeQueryParams = {
|
||||
[this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: [min],
|
||||
[this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: [max],
|
||||
page: 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the subscription is unsubscribed from when this component is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
<a class="d-flex flex-row"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="removeQueryParams" queryParamsHandling="merge">
|
||||
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
|
||||
<span class="filter-value pl-1">{{selectedValue}}</span>
|
||||
</a>
|
@@ -0,0 +1,95 @@
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { SearchFilterConfig } from '../../../../search-service/search-filter-config.model';
|
||||
import { FilterType } from '../../../../search-service/filter-type.model';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { SearchService } from '../../../../search-service/search.service';
|
||||
import { SearchServiceStub } from '../../../../../shared/testing/search-service-stub';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterStub } from '../../../../../shared/testing/router-stub';
|
||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||
import { SearchFilterService } from '../../search-filter.service';
|
||||
import { SearchFacetSelectedOptionComponent } from './search-facet-selected-option.component';
|
||||
|
||||
describe('SearchFacetSelectedOptionComponent', () => {
|
||||
let comp: SearchFacetSelectedOptionComponent;
|
||||
let fixture: ComponentFixture<SearchFacetSelectedOptionComponent>;
|
||||
const filterName1 = 'test name';
|
||||
const value1 = 'testvalue1';
|
||||
const value2 = 'test2';
|
||||
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||
name: filterName1,
|
||||
type: FilterType.range,
|
||||
hasFacets: false,
|
||||
isOpenByDefault: false,
|
||||
pageSize: 2,
|
||||
minValue: 200,
|
||||
maxValue: 3000,
|
||||
});
|
||||
|
||||
const searchLink = '/search';
|
||||
const selectedValues = [value1, value2];
|
||||
const selectedValues$ = observableOf(selectedValues);
|
||||
let filterService;
|
||||
let searchService;
|
||||
let router;
|
||||
const page = observableOf(0);
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
|
||||
declarations: [SearchFacetSelectedOptionComponent],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{
|
||||
provide: SearchConfigurationService, useValue: {
|
||||
searchOptions: observableOf({})
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: SearchFilterService, useValue: {
|
||||
getSelectedValuesForFilter: () => selectedValues,
|
||||
isFilterActiveWithValue: (paramName: string, filterValue: string) => observableOf(true),
|
||||
getPage: (paramName: string) => page,
|
||||
/* tslint:disable:no-empty */
|
||||
incrementPage: (filterName: string) => {
|
||||
},
|
||||
resetPage: (filterName: string) => {
|
||||
}
|
||||
/* tslint:enable:no-empty */
|
||||
}
|
||||
}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(SearchFacetSelectedOptionComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchFacetSelectedOptionComponent);
|
||||
comp = fixture.componentInstance; // SearchFacetSelectedOptionComponent test instance
|
||||
filterService = (comp as any).filterService;
|
||||
searchService = (comp as any).searchService;
|
||||
router = (comp as any).router;
|
||||
comp.selectedValue = value2;
|
||||
comp.selectedValues$ = selectedValues$;
|
||||
comp.filterConfig = mockFilterConfig;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when the updateRemoveParams method is called wih a value', () => {
|
||||
it('should update the removeQueryParams with the new parameter values', () => {
|
||||
comp.removeQueryParams = {};
|
||||
(comp as any).updateRemoveParams(selectedValues);
|
||||
expect(comp.removeQueryParams).toEqual({
|
||||
[mockFilterConfig.paramName]: [value1],
|
||||
page: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,87 @@
|
||||
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { SearchFilterConfig } from '../../../../search-service/search-filter-config.model';
|
||||
import { SearchService } from '../../../../search-service/search.service';
|
||||
import { SearchFilterService } from '../../search-filter.service';
|
||||
import { hasValue } from '../../../../../shared/empty.util';
|
||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-facet-selected-option',
|
||||
templateUrl: './search-facet-selected-option.component.html',
|
||||
})
|
||||
|
||||
/**
|
||||
* Represents a single selected option in a filter facet
|
||||
*/
|
||||
export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* The value for this component
|
||||
*/
|
||||
@Input() selectedValue: string;
|
||||
|
||||
/**
|
||||
* The filter configuration for this facet option
|
||||
*/
|
||||
@Input() filterConfig: SearchFilterConfig;
|
||||
|
||||
/**
|
||||
* Emits the active values for this filter
|
||||
*/
|
||||
@Input() selectedValues$: Observable<string[]>;
|
||||
|
||||
/**
|
||||
* UI parameters when this filter is removed
|
||||
*/
|
||||
removeQueryParams;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from on destroy
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
constructor(protected searchService: SearchService,
|
||||
protected filterService: SearchFilterService,
|
||||
protected searchConfigService: SearchConfigurationService,
|
||||
protected router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all observable instance variables and starts listening to them
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.sub = observableCombineLatest(this.selectedValues$, this.searchConfigService.searchOptions)
|
||||
.subscribe(([selectedValues, searchOptions]) => {
|
||||
this.updateRemoveParams(selectedValues)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The base path to the search page
|
||||
*/
|
||||
getSearchLink() {
|
||||
return this.searchService.getSearchLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the parameters that should change if a given value for this filter would be removed from the active filters
|
||||
* @param {string[]} selectedValues The values that are currently selected for this filter
|
||||
*/
|
||||
private updateRemoveParams(selectedValues: string[]): void {
|
||||
this.removeQueryParams = {
|
||||
[this.filterConfig.paramName]: selectedValues.filter((v) => v !== this.selectedValue),
|
||||
page: 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the subscription is unsubscribed from when this component is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1 +1 @@
|
||||
<ng-container *ngComponentOutlet="getSearchFilter(); injector: objectInjector;"></ng-container>
|
||||
<ng-container *ngComponentOutlet="searchFilter injector: objectInjector;"></ng-container>
|
@@ -3,6 +3,8 @@ import { renderFilterType } from '../search-filter-type-decorator';
|
||||
import { FilterType } from '../../../search-service/filter-type.model';
|
||||
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
||||
import { FILTER_CONFIG } from '../search-filter.service';
|
||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||
import { SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-facet-filter-wrapper',
|
||||
@@ -18,6 +20,10 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
|
||||
*/
|
||||
@Input() filterConfig: SearchFilterConfig;
|
||||
|
||||
/**
|
||||
* The constructor of the search facet filter that should be rendered, based on the filter config's type
|
||||
*/
|
||||
searchFilter: GenericConstructor<SearchFacetFilterComponent>;
|
||||
/**
|
||||
* Injector to inject a child component with the @Input parameters
|
||||
*/
|
||||
@@ -30,6 +36,7 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
|
||||
* Initialize and add the filter config to the injector
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.searchFilter = this.getSearchFilter();
|
||||
this.objectInjector = Injector.create({
|
||||
providers: [
|
||||
{ provide: FILTER_CONFIG, useFactory: () => (this.filterConfig), deps: [] }
|
||||
@@ -41,7 +48,7 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
|
||||
/**
|
||||
* Find the correct component based on the filter config's type
|
||||
*/
|
||||
getSearchFilter() {
|
||||
private getSearchFilter() {
|
||||
const type: FilterType = this.filterConfig.type;
|
||||
return renderFilterType(type);
|
||||
}
|
||||
|
@@ -120,20 +120,6 @@ describe('SearchFacetFilterComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the getAddParams method is called wih a value', () => {
|
||||
it('should return the selectedValue list with the new parameter value', () => {
|
||||
const result = comp.getAddParams(value3);
|
||||
result.subscribe((r) => expect(r[mockFilterConfig.paramName]).toEqual([value1, value2, value3]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the getRemoveParams method is called wih a value', () => {
|
||||
it('should return the selectedValue list with the parameter value left out', () => {
|
||||
const result = comp.getRemoveParams(value1);
|
||||
result.subscribe((r) => expect(r[mockFilterConfig.paramName]).toEqual([value2]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the showMore method is called', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(filterService, 'incrementPage');
|
||||
|
@@ -22,6 +22,7 @@ import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
|
||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
||||
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||
import { SearchOptions } from '../../../search-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-facet-filter',
|
||||
@@ -65,7 +66,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Emits the active values for this filter
|
||||
*/
|
||||
selectedValues: Observable<string[]>;
|
||||
selectedValues$: Observable<string[]>;
|
||||
private collapseNextUpdate = true;
|
||||
|
||||
/**
|
||||
@@ -73,6 +74,11 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
animationState = 'loading';
|
||||
|
||||
/**
|
||||
* Emits all current search options available in the search URL
|
||||
*/
|
||||
searchOptions$: Observable<SearchOptions>;
|
||||
|
||||
constructor(protected searchService: SearchService,
|
||||
protected filterService: SearchFilterService,
|
||||
protected searchConfigService: SearchConfigurationService,
|
||||
@@ -87,10 +93,11 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
ngOnInit(): void {
|
||||
this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined));
|
||||
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
|
||||
this.selectedValues = this.filterService.getSelectedValuesForFilter(this.filterConfig);
|
||||
const searchOptions = this.searchConfigService.searchOptions;
|
||||
this.subs.push(this.searchConfigService.searchOptions.subscribe(() => this.updateFilterValueList()));
|
||||
const facetValues = observableCombineLatest(searchOptions, this.currentPage).pipe(
|
||||
|
||||
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig);
|
||||
this.searchOptions$ = this.searchConfigService.searchOptions;
|
||||
this.subs.push(this.searchOptions$.subscribe(() => this.updateFilterValueList()));
|
||||
const facetValues = observableCombineLatest(this.searchOptions$, this.currentPage).pipe(
|
||||
map(([options, page]) => {
|
||||
return { options, page }
|
||||
}),
|
||||
@@ -190,7 +197,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
* @param data The string from the input field
|
||||
*/
|
||||
onSubmit(data: any) {
|
||||
this.selectedValues.pipe(take(1)).subscribe((selectedValues) => {
|
||||
this.selectedValues$.pipe(take(1)).subscribe((selectedValues) => {
|
||||
if (isNotEmpty(data)) {
|
||||
this.router.navigate([this.getSearchLink()], {
|
||||
queryParams:
|
||||
@@ -204,6 +211,10 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* On click, set the input's value to the clicked data
|
||||
* @param data The value of the option that was clicked
|
||||
*/
|
||||
onClick(data: any) {
|
||||
this.filter = data;
|
||||
}
|
||||
@@ -215,34 +226,6 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
return hasValue(o);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the parameters that should change if a given value for this filter would be removed from the active filters
|
||||
* @param {string} value The value that is removed for this filter
|
||||
* @returns {Observable<any>} The changed filter parameters
|
||||
*/
|
||||
getRemoveParams(value: string): Observable<any> {
|
||||
return this.selectedValues.pipe(map((selectedValues) => {
|
||||
return {
|
||||
[this.filterConfig.paramName]: selectedValues.filter((v) => v !== value),
|
||||
page: 1
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the parameters that should change if a given value for this filter would be added to the active filters
|
||||
* @param {string} value The value that is added for this filter
|
||||
* @returns {Observable<any>} The changed filter parameters
|
||||
*/
|
||||
getAddParams(value: string): Observable<any> {
|
||||
return this.selectedValues.pipe(map((selectedValues) => {
|
||||
return {
|
||||
[this.filterConfig.paramName]: [...selectedValues, value],
|
||||
page: 1
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions
|
||||
*/
|
||||
@@ -259,7 +242,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
findSuggestions(data): void {
|
||||
if (isNotEmpty(data)) {
|
||||
this.searchConfigService.searchOptions.pipe(take(1)).subscribe(
|
||||
this.searchOptions$.pipe(take(1)).subscribe(
|
||||
(options) => {
|
||||
this.filterSearchResults = this.searchService.getFacetValuesFor(this.filterConfig, 1, options, data.toLowerCase())
|
||||
.pipe(
|
||||
@@ -290,6 +273,13 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
getDisplayValue(facet: FacetValue, query: string): string {
|
||||
return new EmphasizePipe().transform(facet.value, query) + ' (' + facet.count + ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent unnecessary rerendering
|
||||
*/
|
||||
trackUpdate(index, value: FacetValue) {
|
||||
return value ? value.search : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const facetLoad = trigger('facetLoad', [
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
import { type } from '../../../shared/ngrx/type';
|
||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
@@ -12,9 +13,8 @@ import { type } from '../../../shared/ngrx/type';
|
||||
*/
|
||||
export const SearchFilterActionTypes = {
|
||||
COLLAPSE: type('dspace/search-filter/COLLAPSE'),
|
||||
INITIAL_COLLAPSE: type('dspace/search-filter/INITIAL_COLLAPSE'),
|
||||
INITIALIZE: type('dspace/search-filter/INITIALIZE'),
|
||||
EXPAND: type('dspace/search-filter/EXPAND'),
|
||||
INITIAL_EXPAND: type('dspace/search-filter/INITIAL_EXPAND'),
|
||||
TOGGLE: type('dspace/search-filter/TOGGLE'),
|
||||
DECREMENT_PAGE: type('dspace/search-filter/DECREMENT_PAGE'),
|
||||
INCREMENT_PAGE: type('dspace/search-filter/INCREMENT_PAGE'),
|
||||
@@ -64,17 +64,15 @@ export class SearchFilterToggleAction extends SearchFilterAction {
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to set the initial state of a filter to collapsed
|
||||
* Used to set the initial state of a filter
|
||||
*/
|
||||
export class SearchFilterInitialCollapseAction extends SearchFilterAction {
|
||||
type = SearchFilterActionTypes.INITIAL_COLLAPSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to set the initial state of a filter to expanded
|
||||
*/
|
||||
export class SearchFilterInitialExpandAction extends SearchFilterAction {
|
||||
type = SearchFilterActionTypes.INITIAL_EXPAND;
|
||||
export class SearchFilterInitializeAction extends SearchFilterAction {
|
||||
type = SearchFilterActionTypes.INITIALIZE;
|
||||
initiallyExpanded;
|
||||
constructor(filter: SearchFilterConfig) {
|
||||
super(filter.name);
|
||||
this.initiallyExpanded = filter.isOpenByDefault;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div>
|
||||
<div class="facet-filter d-block mb-3 p-3" *ngIf="active$ | async">
|
||||
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fas float-right"
|
||||
[ngClass]="(isCollapsed() | async) ? 'fa-plus' : 'fa-minus'"></span></div>
|
||||
<div [@slide]="(isCollapsed() | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : collapsed}">
|
||||
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'"></span></div>
|
||||
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : closed}">
|
||||
<ds-search-facet-filter-wrapper [filterConfig]="filter"></ds-search-facet-filter-wrapper>
|
||||
</div>
|
||||
</div>
|
@@ -1,7 +1,7 @@
|
||||
@import '../../../../styles/variables.scss';
|
||||
@import '../../../../styles/mixins.scss';
|
||||
|
||||
:host {
|
||||
:host .facet-filter {
|
||||
border: 1px solid map-get($theme-colors, light);
|
||||
cursor: pointer;
|
||||
.search-filter-wrapper.closed {
|
||||
|
@@ -10,6 +10,7 @@ import { SearchService } from '../../search-service/search.service';
|
||||
import { SearchFilterComponent } from './search-filter.component';
|
||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||
import { FilterType } from '../../search-service/filter-type.model';
|
||||
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
||||
|
||||
describe('SearchFilterComponent', () => {
|
||||
let comp: SearchFilterComponent;
|
||||
@@ -33,9 +34,7 @@ describe('SearchFilterComponent', () => {
|
||||
},
|
||||
expand: (filter) => {
|
||||
},
|
||||
initialCollapse: (filter) => {
|
||||
},
|
||||
initialExpand: (filter) => {
|
||||
initializeFilter: (filter) => {
|
||||
},
|
||||
getSelectedValuesForFilter: (filter) => {
|
||||
return observableOf([filterName1, filterName2, filterName3])
|
||||
@@ -55,6 +54,8 @@ describe('SearchFilterComponent', () => {
|
||||
getFacetValuesFor: (filter) => mockResults
|
||||
};
|
||||
|
||||
const searchConfigServiceStub = {};
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
||||
@@ -65,6 +66,7 @@ describe('SearchFilterComponent', () => {
|
||||
provide: SearchFilterService,
|
||||
useValue: mockFilterService
|
||||
},
|
||||
{ provide: SearchConfigurationService, useValue: searchConfigServiceStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(SearchFilterComponent, {
|
||||
@@ -91,32 +93,21 @@ describe('SearchFilterComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the initialCollapse method is triggered', () => {
|
||||
describe('when the initializeFilter method is triggered', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(filterService, 'initialCollapse');
|
||||
comp.initialCollapse();
|
||||
spyOn(filterService, 'initializeFilter');
|
||||
comp.initializeFilter();
|
||||
});
|
||||
|
||||
it('should call initialCollapse with the correct filter configuration name', () => {
|
||||
expect(filterService.initialCollapse).toHaveBeenCalledWith(mockFilterConfig.name)
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the initialExpand method is triggered', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(filterService, 'initialExpand');
|
||||
comp.initialExpand();
|
||||
});
|
||||
|
||||
it('should call initialCollapse with the correct filter configuration name', () => {
|
||||
expect(filterService.initialExpand).toHaveBeenCalledWith(mockFilterConfig.name)
|
||||
expect(filterService.initializeFilter).toHaveBeenCalledWith(mockFilterConfig)
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getSelectedValues is called', () => {
|
||||
let valuesObservable: Observable<string[]>;
|
||||
beforeEach(() => {
|
||||
valuesObservable = comp.getSelectedValues();
|
||||
valuesObservable = (comp as any).getSelectedValues();
|
||||
});
|
||||
|
||||
it('should return an observable containing the existing filters', () => {
|
||||
@@ -141,7 +132,7 @@ describe('SearchFilterComponent', () => {
|
||||
let isActive: Observable<boolean>;
|
||||
beforeEach(() => {
|
||||
filterService.isCollapsed = () => observableOf(true);
|
||||
isActive = comp.isCollapsed();
|
||||
isActive = (comp as any).isCollapsed();
|
||||
});
|
||||
|
||||
it('should return an observable containing true', () => {
|
||||
@@ -156,7 +147,7 @@ describe('SearchFilterComponent', () => {
|
||||
let isActive: Observable<boolean>;
|
||||
beforeEach(() => {
|
||||
filterService.isCollapsed = () => observableOf(false);
|
||||
isActive = comp.isCollapsed();
|
||||
isActive = (comp as any).isCollapsed();
|
||||
});
|
||||
|
||||
it('should return an observable containing false', () => {
|
||||
|
@@ -1,11 +1,12 @@
|
||||
|
||||
import { take } from 'rxjs/operators';
|
||||
import { filter, first, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||
import { SearchFilterService } from './search-filter.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { slide } from '../../../shared/animations/slide';
|
||||
import { isNotEmpty } from '../../../shared/empty.util';
|
||||
import { SearchService } from '../../search-service/search.service';
|
||||
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-filter',
|
||||
@@ -26,9 +27,24 @@ export class SearchFilterComponent implements OnInit {
|
||||
/**
|
||||
* True when the filter is 100% collapsed in the UI
|
||||
*/
|
||||
collapsed;
|
||||
closed = true;
|
||||
|
||||
constructor(private filterService: SearchFilterService) {
|
||||
/**
|
||||
* Emits true when the filter is currently collapsed in the store
|
||||
*/
|
||||
collapsed$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Emits all currently selected values for this filter
|
||||
*/
|
||||
selectedValues$: Observable<string[]>;
|
||||
|
||||
/**
|
||||
* Emits true when the current filter is supposed to be shown
|
||||
*/
|
||||
active$: Observable<boolean>;
|
||||
|
||||
constructor(private filterService: SearchFilterService, private searchService: SearchService, private searchConfigService: SearchConfigurationService) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,11 +53,13 @@ export class SearchFilterComponent implements OnInit {
|
||||
* Else, the filter should initially be collapsed
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.getSelectedValues().pipe(take(1)).subscribe((isActive) => {
|
||||
if (this.filter.isOpenByDefault || isNotEmpty(isActive)) {
|
||||
this.initialExpand();
|
||||
} else {
|
||||
this.initialCollapse();
|
||||
this.selectedValues$ = this.getSelectedValues();
|
||||
this.active$ = this.isActive();
|
||||
this.collapsed$ = this.isCollapsed();
|
||||
this.initializeFilter();
|
||||
this.selectedValues$.pipe(take(1)).subscribe((selectedValues) => {
|
||||
if (isNotEmpty(selectedValues)) {
|
||||
this.filterService.expand(this.filter.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -57,30 +75,21 @@ export class SearchFilterComponent implements OnInit {
|
||||
* Checks if the filter is currently collapsed
|
||||
* @returns {Observable<boolean>} Emits true when the current state of the filter is collapsed, false when it's expanded
|
||||
*/
|
||||
isCollapsed(): Observable<boolean> {
|
||||
private isCollapsed(): Observable<boolean> {
|
||||
return this.filterService.isCollapsed(this.filter.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the initial state to collapsed
|
||||
* Sets the initial state of the filter
|
||||
*/
|
||||
initialCollapse() {
|
||||
this.filterService.initialCollapse(this.filter.name);
|
||||
this.collapsed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the initial state to expanded
|
||||
*/
|
||||
initialExpand() {
|
||||
this.filterService.initialExpand(this.filter.name);
|
||||
this.collapsed = false;
|
||||
initializeFilter() {
|
||||
this.filterService.initializeFilter(this.filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Observable<string[]>} Emits a list of all values that are currently active for this filter
|
||||
*/
|
||||
getSelectedValues(): Observable<string[]> {
|
||||
private getSelectedValues(): Observable<string[]> {
|
||||
return this.filterService.getSelectedValuesForFilter(this.filter);
|
||||
}
|
||||
|
||||
@@ -90,7 +99,7 @@ export class SearchFilterComponent implements OnInit {
|
||||
*/
|
||||
finishSlide(event: any): void {
|
||||
if (event.fromState === 'collapsed') {
|
||||
this.collapsed = false;
|
||||
this.closed = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +109,31 @@ export class SearchFilterComponent implements OnInit {
|
||||
*/
|
||||
startSlide(event: any): void {
|
||||
if (event.toState === 'collapsed') {
|
||||
this.collapsed = true;
|
||||
this.closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given filter is supposed to be shown or not
|
||||
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
|
||||
*/
|
||||
private isActive(): Observable<boolean> {
|
||||
return this.selectedValues$.pipe(
|
||||
switchMap((isActive) => {
|
||||
if (isNotEmpty(isActive)) {
|
||||
return observableOf(true);
|
||||
} else {
|
||||
return this.searchConfigService.searchOptions.pipe(
|
||||
switchMap((options) => {
|
||||
return this.searchService.getFacetValuesFor(this.filter, 1, options).pipe(
|
||||
filter((RD) => !RD.isLoading),
|
||||
map((valuesRD) => {
|
||||
return valuesRD.payload.totalElements > 0
|
||||
}),)
|
||||
}
|
||||
))
|
||||
}
|
||||
}),
|
||||
startWith(true));
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,8 @@
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
import {
|
||||
SearchFilterCollapseAction, SearchFilterExpandAction, SearchFilterIncrementPageAction,
|
||||
SearchFilterInitialCollapseAction,
|
||||
SearchFilterInitialExpandAction,
|
||||
SearchFilterToggleAction,
|
||||
SearchFilterDecrementPageAction, SearchFilterResetPageAction
|
||||
SearchFilterDecrementPageAction, SearchFilterResetPageAction, SearchFilterInitializeAction
|
||||
} from './search-filter.actions';
|
||||
import { filterReducer } from './search-filter.reducer';
|
||||
|
||||
@@ -98,35 +96,39 @@ describe('filterReducer', () => {
|
||||
filterReducer(state, action);
|
||||
});
|
||||
|
||||
it('should set filterCollapsed to true in response to the INITIAL_COLLAPSE action when no state has been set for this filter', () => {
|
||||
it('should set filterCollapsed to true in response to the INITIALIZE action with isOpenByDefault to false when no state has been set for this filter', () => {
|
||||
const state = {};
|
||||
state[filterName2] = { filterCollapsed: false, page: 1 };
|
||||
const action = new SearchFilterInitialCollapseAction(filterName1);
|
||||
const filterConfig = {isOpenByDefault: false, name: filterName1} as any;
|
||||
const action = new SearchFilterInitializeAction(filterConfig);
|
||||
const newState = filterReducer(state, action);
|
||||
|
||||
expect(newState[filterName1].filterCollapsed).toEqual(true);
|
||||
});
|
||||
|
||||
it('should set filterCollapsed to true in response to the INITIAL_EXPAND action when no state has been set for this filter', () => {
|
||||
it('should set filterCollapsed to false in response to the INITIALIZE action with isOpenByDefault to true when no state has been set for this filter', () => {
|
||||
const state = {};
|
||||
state[filterName2] = { filterCollapsed: true, page: 1 };
|
||||
const action = new SearchFilterInitialExpandAction(filterName1);
|
||||
const filterConfig = {isOpenByDefault: true, name: filterName1} as any;
|
||||
const action = new SearchFilterInitializeAction(filterConfig);
|
||||
const newState = filterReducer(state, action);
|
||||
expect(newState[filterName1].filterCollapsed).toEqual(false);
|
||||
});
|
||||
|
||||
it('should not change the state in response to the INITIAL_COLLAPSE action when the state has already been set for this filter', () => {
|
||||
it('should not change the state in response to the INITIALIZE action with isOpenByDefault to false when the state has already been set for this filter', () => {
|
||||
const state = {};
|
||||
state[filterName1] = { filterCollapsed: false, page: 1 };
|
||||
const action = new SearchFilterInitialCollapseAction(filterName1);
|
||||
const filterConfig = { isOpenByDefault: true, name: filterName1 } as any;
|
||||
const action = new SearchFilterInitializeAction(filterConfig);
|
||||
const newState = filterReducer(state, action);
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should not change the state in response to the INITIAL_EXPAND action when the state has already been set for this filter', () => {
|
||||
it('should not change the state in response to the INITIALIZE action with isOpenByDefault to true when the state has already been set for this filter', () => {
|
||||
const state = {};
|
||||
state[filterName1] = { filterCollapsed: true, page: 1 };
|
||||
const action = new SearchFilterInitialExpandAction(filterName1);
|
||||
const filterConfig = { isOpenByDefault: false, name: filterName1 } as any;
|
||||
const action = new SearchFilterInitializeAction(filterConfig);
|
||||
const newState = filterReducer(state, action);
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
@@ -1,5 +1,9 @@
|
||||
import { SearchFilterAction, SearchFilterActionTypes } from './search-filter.actions';
|
||||
import { isEmpty } from '../../../shared/empty.util';
|
||||
import {
|
||||
SearchFilterAction,
|
||||
SearchFilterActionTypes,
|
||||
SearchFilterInitializeAction
|
||||
} from './search-filter.actions';
|
||||
import { isEmpty, isNotUndefined } from '../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Interface that represents the state for a single filters
|
||||
@@ -28,27 +32,14 @@ export function filterReducer(state = initialState, action: SearchFilterAction):
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case SearchFilterActionTypes.INITIAL_COLLAPSE: {
|
||||
if (isEmpty(state) || isEmpty(state[action.filterName])) {
|
||||
return Object.assign({}, state, {
|
||||
[action.filterName]: {
|
||||
filterCollapsed: true,
|
||||
page: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case SearchFilterActionTypes.INITIAL_EXPAND: {
|
||||
if (isEmpty(state) || isEmpty(state[action.filterName])) {
|
||||
return Object.assign({}, state, {
|
||||
[action.filterName]: {
|
||||
filterCollapsed: false,
|
||||
page: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
case SearchFilterActionTypes.INITIALIZE: {
|
||||
const initAction = (action as SearchFilterInitializeAction);
|
||||
return Object.assign({}, state, {
|
||||
[action.filterName]: {
|
||||
filterCollapsed: !initAction.initiallyExpanded,
|
||||
page: 1
|
||||
}
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
|
@@ -5,8 +5,7 @@ import {
|
||||
SearchFilterDecrementPageAction,
|
||||
SearchFilterExpandAction,
|
||||
SearchFilterIncrementPageAction,
|
||||
SearchFilterInitialCollapseAction,
|
||||
SearchFilterInitialExpandAction,
|
||||
SearchFilterInitializeAction,
|
||||
SearchFilterResetPageAction,
|
||||
SearchFilterToggleAction
|
||||
} from './search-filter.actions';
|
||||
@@ -74,23 +73,13 @@ describe('SearchFilterService', () => {
|
||||
service = new SearchFilterService(store, routeServiceStub, mockFixedFilterService);
|
||||
});
|
||||
|
||||
describe('when the initialCollapse method is triggered', () => {
|
||||
describe('when the initializeFilter method is triggered', () => {
|
||||
beforeEach(() => {
|
||||
service.initialCollapse(mockFilterConfig.name);
|
||||
service.initializeFilter(mockFilterConfig);
|
||||
});
|
||||
|
||||
it('SearchFilterInitialCollapseAction should be dispatched to the store', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitialCollapseAction(mockFilterConfig.name));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the initialExpand method is triggered', () => {
|
||||
beforeEach(() => {
|
||||
service.initialExpand(mockFilterConfig.name);
|
||||
});
|
||||
|
||||
it('SearchFilterInitialExpandAction should be dispatched to the store', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitialExpandAction(mockFilterConfig.name));
|
||||
it('SearchFilterInitializeAction should be dispatched to the store', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitializeAction(mockFilterConfig));
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -8,8 +8,7 @@ import {
|
||||
SearchFilterDecrementPageAction,
|
||||
SearchFilterExpandAction,
|
||||
SearchFilterIncrementPageAction,
|
||||
SearchFilterInitialCollapseAction,
|
||||
SearchFilterInitialExpandAction,
|
||||
SearchFilterInitializeAction,
|
||||
SearchFilterResetPageAction,
|
||||
SearchFilterToggleAction
|
||||
} from './search-filter.actions';
|
||||
@@ -22,7 +21,7 @@ import { SearchOptions } from '../../search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../paginated-search-options.model';
|
||||
import { SearchFixedFilterService } from './search-fixed-filter.service';
|
||||
import { Params } from '@angular/router';
|
||||
|
||||
// const spy = create();
|
||||
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
||||
|
||||
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
|
||||
@@ -197,7 +196,7 @@ export class SearchFilterService {
|
||||
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
|
||||
const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName);
|
||||
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe(
|
||||
map((params: Params) => [].concat(...Object.values(params)))
|
||||
map((params: Params) => [].concat(...Object.values(params))),
|
||||
);
|
||||
|
||||
return observableCombineLatest(values$, prefixValues$).pipe(
|
||||
@@ -225,13 +224,14 @@ export class SearchFilterService {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
}),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the current page of a given filter
|
||||
* @param {string} filterName The filtername for which the page state is checked
|
||||
* @param {string} filterName The filter name for which the page state is checked
|
||||
* @returns {Observable<boolean>} Emits the current page state of the given filter, if it's unavailable, return 1
|
||||
*/
|
||||
getPage(filterName: string): Observable<number> {
|
||||
@@ -243,7 +243,8 @@ export class SearchFilterService {
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}));
|
||||
}),
|
||||
distinctUntilChanged());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,19 +272,11 @@ export class SearchFilterService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an initial collapse action to the store for a given filter
|
||||
* @param {string} filterName The filter for which the action is dispatched
|
||||
* Dispatches an initialize action to the store for a given filter
|
||||
* @param {SearchFilterConfig} filter The filter for which the action is dispatched
|
||||
*/
|
||||
public initialCollapse(filterName: string): void {
|
||||
this.store.dispatch(new SearchFilterInitialCollapseAction(filterName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an initial expand action to the store for a given filter
|
||||
* @param {string} filterName The filter for which the action is dispatched
|
||||
*/
|
||||
public initialExpand(filterName: string): void {
|
||||
this.store.dispatch(new SearchFilterInitialExpandAction(filterName));
|
||||
public initializeFilter(filter: SearchFilterConfig): void {
|
||||
this.store.dispatch(new SearchFilterInitializeAction(filter));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -18,7 +18,7 @@ describe('SearchFixedFilterService', () => {
|
||||
/* tslint:enable:no-empty */
|
||||
generateRequestId: () => 'fake-id',
|
||||
getByUUID: () => observableOf(Object.assign(new RequestEntry(), {
|
||||
response: new FilteredDiscoveryQueryResponse(filterQuery, '200')
|
||||
response: new FilteredDiscoveryQueryResponse(filterQuery, 200, 'OK')
|
||||
}))
|
||||
}) as RequestService;
|
||||
const halServiceStub = Object.assign(new HALEndpointService(requestServiceStub, undefined), {
|
||||
|
@@ -1,24 +1,9 @@
|
||||
<div>
|
||||
<div class="filters py-2">
|
||||
<a *ngFor="let value of (selectedValues | async)" class="d-flex flex-row"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="getRemoveParams(value) | async" queryParamsHandling="merge">
|
||||
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
|
||||
<span class="filter-value pl-1">{{value}}</span>
|
||||
</a>
|
||||
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$"></ds-search-facet-selected-option>
|
||||
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||
<div [@facetLoad]="animationState">
|
||||
<ng-container *ngFor="let value of page.page; let i=index">
|
||||
<a *ngIf="!(selectedValues | async).includes(value.value)" class="d-flex flex-row"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="getAddParams(value.value) | async" queryParamsHandling="merge" >
|
||||
<input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/>
|
||||
<span class="filter-value px-1">{{value.value}}</span>
|
||||
<span class="float-right filter-value-count ml-auto">
|
||||
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
|
||||
</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$"></ds-search-facet-option>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="clearfix toggle-more-filters">
|
||||
|
@@ -24,16 +24,7 @@
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||
<div [@facetLoad]="animationState">
|
||||
<ng-container *ngFor="let value of page.page; let i=index">
|
||||
<a *ngIf="!(selectedValues | async).includes(value.value)" class="d-flex flex-row"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="getChangeParams(value.value) | async" queryParamsHandling="merge">
|
||||
<span class="filter-value px-1">{{value.value}}</span>
|
||||
<span class="float-right filter-value-count ml-auto">
|
||||
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
|
||||
</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ds-search-facet-range-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value"></ds-search-facet-range-option>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@@ -106,16 +106,6 @@ describe('SearchRangeFilterComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when the getChangeParams method is called wih a value', () => {
|
||||
it('should return the selectedValue list with the new parameter value', () => {
|
||||
const result$ = comp.getChangeParams(value3);
|
||||
result$.subscribe((result) => {
|
||||
expect(result[mockFilterConfig.paramName + minSuffix]).toEqual(['1990']);
|
||||
expect(result[mockFilterConfig.paramName + maxSuffix]).toEqual(['1992']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the onSubmit method is called with data', () => {
|
||||
const searchUrl = '/search/path';
|
||||
// const data = { [mockFilterConfig.paramName + minSuffix]: '1900', [mockFilterConfig.paramName + maxSuffix]: '1950' };
|
||||
|
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
of as observableOf,
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
Subscription
|
||||
} from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, Subscription } from 'rxjs';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
@@ -23,16 +18,26 @@ import { RouteService } from '../../../../shared/services/route.service';
|
||||
import { hasValue } from '../../../../shared/empty.util';
|
||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
||||
|
||||
/**
|
||||
* The suffix for a range filters' minimum in the frontend URL
|
||||
*/
|
||||
export const RANGE_FILTER_MIN_SUFFIX = '.min';
|
||||
|
||||
/**
|
||||
* The suffix for a range filters' maximum in the frontend URL
|
||||
*/
|
||||
export const RANGE_FILTER_MAX_SUFFIX = '.max';
|
||||
|
||||
/**
|
||||
* The date formats that are possible to appear in a date filter
|
||||
*/
|
||||
const dateFormats = ['YYYY', 'YYYY-MM', 'YYYY-MM-DD'];
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
* The route parameter 'id' is used to request the item it represents.
|
||||
* All fields of the item that should be displayed, are defined in its template.
|
||||
*/
|
||||
const minSuffix = '.min';
|
||||
const maxSuffix = '.max';
|
||||
const dateFormats = ['YYYY', 'YYYY-MM', 'YYYY-MM-DD'];
|
||||
const rangeDelimiter = '-';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-range-filter',
|
||||
styleUrls: ['./search-range-filter.component.scss'],
|
||||
@@ -85,8 +90,8 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
||||
super.ngOnInit();
|
||||
this.min = moment(this.filterConfig.minValue, dateFormats).year() || this.min;
|
||||
this.max = moment(this.filterConfig.maxValue, dateFormats).year() || this.max;
|
||||
const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + minSuffix).pipe(startWith(undefined));
|
||||
const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + maxSuffix).pipe(startWith(undefined));
|
||||
const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined));
|
||||
const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined));
|
||||
this.sub = observableCombineLatest(iniMin, iniMax).pipe(
|
||||
map(([min, max]) => {
|
||||
const minimum = hasValue(min) ? min : this.min;
|
||||
@@ -96,23 +101,6 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
||||
).subscribe((minmax) => this.range = minmax);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the parameters that should change if a given values for this range filter would be changed
|
||||
* @param {string} value The values that are changed for this filter
|
||||
* @returns {Observable<any>} The changed filter parameters
|
||||
*/
|
||||
getChangeParams(value: string) {
|
||||
const parts = value.split(rangeDelimiter);
|
||||
const min = parts.length > 1 ? parts[0].trim() : value;
|
||||
const max = parts.length > 1 ? parts[1].trim() : value;
|
||||
return observableOf(
|
||||
{
|
||||
[this.filterConfig.paramName + minSuffix]: [min],
|
||||
[this.filterConfig.paramName + maxSuffix]: [max],
|
||||
page: 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits new custom range values to the range filter from the widget
|
||||
*/
|
||||
@@ -122,8 +110,8 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
||||
this.router.navigate([this.getSearchLink()], {
|
||||
queryParams:
|
||||
{
|
||||
[this.filterConfig.paramName + minSuffix]: newMin,
|
||||
[this.filterConfig.paramName + maxSuffix]: newMax
|
||||
[this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: newMin,
|
||||
[this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: newMax
|
||||
},
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
@@ -148,8 +136,4 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
out(call) {
|
||||
console.log(call);
|
||||
}
|
||||
}
|
||||
|
@@ -1,26 +1,9 @@
|
||||
<div>
|
||||
<div class="filters py-2">
|
||||
<a *ngFor="let value of (selectedValues | async)" class="d-flex flex-row"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="getRemoveParams(value) | async" queryParamsHandling="merge">
|
||||
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
|
||||
<span class="filter-value pl-1">{{value}}</span>
|
||||
</a>
|
||||
<ng-container *ngVar="(filterValues$ | async) as filterValuesRD">
|
||||
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$"></ds-search-facet-selected-option>
|
||||
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||
<div [@facetLoad]="animationState">
|
||||
<ng-container *ngFor="let page of filterValuesRD?.payload">
|
||||
<ng-container *ngFor="let value of page.page; let i=index">
|
||||
<a *ngIf="!(selectedValues | async).includes(value.value)" class="d-flex flex-row"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="getAddParams(value.value) | async" queryParamsHandling="merge" >
|
||||
<input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/>
|
||||
<span class="filter-value px-1">{{value.value}}</span>
|
||||
<span class="float-right filter-value-count ml-auto">
|
||||
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
|
||||
</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$"></ds-search-facet-option>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="clearfix toggle-more-filters">
|
||||
@@ -40,6 +23,5 @@
|
||||
(submitSuggestion)="onSubmit($event)"
|
||||
(clickSuggestion)="onClick($event)"
|
||||
(findSuggestions)="findSuggestions($event)"
|
||||
ngDefaultControl
|
||||
></ds-input-suggestions>
|
||||
ngDefaultControl></ds-input-suggestions>
|
||||
</div>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<h3>{{"search.filters.head" | translate}}</h3>
|
||||
<div *ngIf="(filters | async)?.hasSucceeded">
|
||||
<div *ngFor="let filter of (filters | async)?.payload">
|
||||
<ds-search-filter *ngIf="isActive(filter) | async" class="d-block mb-3 p-3" [filter]="filter"></ds-search-filter>
|
||||
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
|
||||
<ds-search-filter [filter]="filter"></ds-search-filter>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button">{{"search.filters.reset" | translate}}</a>
|
@@ -1,12 +1,11 @@
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { filter, map, mergeMap, startWith, switchMap } from 'rxjs/operators';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Component } from '@angular/core';
|
||||
import { SearchService } from '../search-service/search.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
|
||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { SearchFilterService } from './search-filter/search-filter.service';
|
||||
import { getSucceededRemoteData } from '../../core/shared/operators';
|
||||
|
||||
@@ -53,26 +52,9 @@ export class SearchFiltersComponent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given filter is supposed to be shown or not
|
||||
* @param {SearchFilterConfig} filter The filter to check for
|
||||
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
|
||||
* Prevent unnecessary rerendering
|
||||
*/
|
||||
isActive(filterConfig: SearchFilterConfig): Observable<boolean> {
|
||||
return this.filterService.getSelectedValuesForFilter(filterConfig).pipe(
|
||||
mergeMap((isActive) => {
|
||||
if (isNotEmpty(isActive)) {
|
||||
return observableOf(true);
|
||||
} else {
|
||||
return this.searchConfigService.searchOptions.pipe(
|
||||
switchMap((options) => {
|
||||
return this.searchService.getFacetValuesFor(filterConfig, 1, options).pipe(
|
||||
filter((RD) => !RD.isLoading),
|
||||
map((valuesRD) => {
|
||||
return valuesRD.payload.totalElements > 0
|
||||
}),)
|
||||
}
|
||||
))
|
||||
}
|
||||
}),startWith(true),);
|
||||
trackUpdate(index, config: SearchFilterConfig) {
|
||||
return config ? config.name : undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -76,7 +76,6 @@ export class SearchPageComponent implements OnInit {
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SearchSidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
protected filterService: SearchFilterService,
|
||||
protected searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
|
@@ -30,6 +30,9 @@ import { SearchFacetFilterWrapperComponent } from './search-filters/search-filte
|
||||
import { SearchBooleanFilterComponent } from './search-filters/search-filter/search-boolean-filter/search-boolean-filter.component';
|
||||
import { SearchHierarchyFilterComponent } from './search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { SearchFacetOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component';
|
||||
import { SearchFacetSelectedOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component';
|
||||
import { SearchFacetRangeOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component';
|
||||
|
||||
const effects = [
|
||||
SearchSidebarEffects
|
||||
@@ -62,6 +65,9 @@ const effects = [
|
||||
SearchTextFilterComponent,
|
||||
SearchHierarchyFilterComponent,
|
||||
SearchBooleanFilterComponent,
|
||||
SearchFacetOptionComponent,
|
||||
SearchFacetSelectedOptionComponent,
|
||||
SearchFacetRangeOptionComponent
|
||||
],
|
||||
providers: [
|
||||
SearchSidebarService,
|
||||
@@ -83,6 +89,9 @@ const effects = [
|
||||
SearchTextFilterComponent,
|
||||
SearchHierarchyFilterComponent,
|
||||
SearchBooleanFilterComponent,
|
||||
SearchFacetOptionComponent,
|
||||
SearchFacetSelectedOptionComponent,
|
||||
SearchFacetRangeOptionComponent
|
||||
],
|
||||
exports: [
|
||||
FilteredSearchPageComponent,
|
||||
|
@@ -121,7 +121,7 @@ describe('SearchConfigurationService', () => {
|
||||
|
||||
describe('when subscribeToSearchOptions is called', () => {
|
||||
beforeEach(() => {
|
||||
service.subscribeToSearchOptions(defaults)
|
||||
(service as any).subscribeToSearchOptions(defaults)
|
||||
});
|
||||
it('should call all getters it needs, but not call any others', () => {
|
||||
expect(service.getCurrentPagination).not.toHaveBeenCalled();
|
||||
@@ -135,7 +135,7 @@ describe('SearchConfigurationService', () => {
|
||||
|
||||
describe('when subscribeToPaginatedSearchOptions is called', () => {
|
||||
beforeEach(() => {
|
||||
service.subscribeToPaginatedSearchOptions(defaults);
|
||||
(service as any).subscribeToPaginatedSearchOptions(defaults);
|
||||
});
|
||||
it('should call all getters it needs', () => {
|
||||
expect(service.getCurrentPagination).toHaveBeenCalled();
|
||||
|
@@ -197,7 +197,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
* @param {SearchOptions} defaults Default values for when no parameters are available
|
||||
* @returns {Subscription} The subscription to unsubscribe from
|
||||
*/
|
||||
subscribeToSearchOptions(defaults: SearchOptions): Subscription {
|
||||
private subscribeToSearchOptions(defaults: SearchOptions): Subscription {
|
||||
return observableMerge(
|
||||
this.getScopePart(defaults.scope),
|
||||
this.getQueryPart(defaults.query),
|
||||
@@ -216,7 +216,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
* @param {PaginatedSearchOptions} defaults Default values for when no parameters are available
|
||||
* @returns {Subscription} The subscription to unsubscribe from
|
||||
*/
|
||||
subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription {
|
||||
private subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription {
|
||||
return observableMerge(
|
||||
this.getPaginationPart(defaults.pagination),
|
||||
this.getSortPart(defaults.sort),
|
||||
|
@@ -158,7 +158,7 @@ describe('SearchService', () => {
|
||||
const endPoint = 'http://endpoint.com/test/test';
|
||||
const searchOptions = new PaginatedSearchOptions({});
|
||||
const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] });
|
||||
const response = new SearchSuccessResponse(queryResponse, '200');
|
||||
const response = new SearchSuccessResponse(queryResponse, 200, 'OK');
|
||||
beforeEach(() => {
|
||||
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
||||
/* tslint:disable:no-empty */
|
||||
@@ -183,7 +183,7 @@ describe('SearchService', () => {
|
||||
describe('when getConfig is called without a scope', () => {
|
||||
const endPoint = 'http://endpoint.com/test/config';
|
||||
const filterConfig = [new SearchFilterConfig()];
|
||||
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
||||
const response = new FacetConfigSuccessResponse(filterConfig, 200, 'OK');
|
||||
beforeEach(() => {
|
||||
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
||||
/* tslint:disable:no-empty */
|
||||
@@ -210,7 +210,7 @@ describe('SearchService', () => {
|
||||
const scope = 'test';
|
||||
const requestUrl = endPoint + '?scope=' + scope;
|
||||
const filterConfig = [new SearchFilterConfig()];
|
||||
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
||||
const response = new FacetConfigSuccessResponse(filterConfig, 200, 'OK');
|
||||
beforeEach(() => {
|
||||
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
||||
/* tslint:disable:no-empty */
|
||||
|
23
src/app/+submit-page/submit-page-routing.module.ts
Normal file
23
src/app/+submit-page/submit-page-routing.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { SubmissionSubmitComponent } from '../submission/submit/submission-submit.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
canActivate: [AuthenticatedGuard],
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
component: SubmissionSubmitComponent,
|
||||
data: { title: 'submission.submit.title' }
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
/**
|
||||
* This module defines the default component to load when navigating to the submit page path.
|
||||
*/
|
||||
export class SubmitPageRoutingModule { }
|
20
src/app/+submit-page/submit-page.module.ts
Normal file
20
src/app/+submit-page/submit-page.module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { SubmitPageRoutingModule } from './submit-page-routing.module';
|
||||
import { SubmissionModule } from '../submission/submission.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SubmitPageRoutingModule,
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
SubmissionModule,
|
||||
],
|
||||
})
|
||||
/**
|
||||
* This module handles all modules that need to access the submit page.
|
||||
*/
|
||||
export class SubmitPageModule {
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { SubmissionEditComponent } from '../submission/edit/submission-edit.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{
|
||||
canActivate: [AuthenticatedGuard],
|
||||
path: ':id/edit',
|
||||
component: SubmissionEditComponent,
|
||||
data: { title: 'submission.edit.title' }
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
/**
|
||||
* This module defines the default component to load when navigating to the workflowitems edit page path.
|
||||
*/
|
||||
export class WorkflowitemsEditPageRoutingModule { }
|
@@ -0,0 +1,21 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { WorkflowitemsEditPageRoutingModule } from './workflowitems-edit-page-routing.module';
|
||||
import { SubmissionModule } from '../submission/submission.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
WorkflowitemsEditPageRoutingModule,
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
SubmissionModule,
|
||||
],
|
||||
declarations: []
|
||||
})
|
||||
/**
|
||||
* This module handles all modules that need to access the workflowitems edit page.
|
||||
*/
|
||||
export class WorkflowitemsEditPageModule {
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { SubmissionEditComponent } from '../submission/edit/submission-edit.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{
|
||||
canActivate: [AuthenticatedGuard],
|
||||
path: ':id/edit',
|
||||
component: SubmissionEditComponent,
|
||||
data: { title: 'submission.edit.title' }
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
/**
|
||||
* This module defines the default component to load when navigating to the workspaceitems edit page path
|
||||
*/
|
||||
export class WorkspaceitemsEditPageRoutingModule { }
|
@@ -0,0 +1,21 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { WorkspaceitemsEditPageRoutingModule } from './workspaceitems-edit-page-routing.module';
|
||||
import { SubmissionModule } from '../submission/submission.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
WorkspaceitemsEditPageRoutingModule,
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
SubmissionModule,
|
||||
],
|
||||
declarations: []
|
||||
})
|
||||
/**
|
||||
* This module handles all modules that need to access the workspaceitems edit page.
|
||||
*/
|
||||
export class WorkspaceitemsEditPageModule {
|
||||
|
||||
}
|
@@ -29,6 +29,9 @@ export function getCommunityModulePath() {
|
||||
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
||||
{ path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' },
|
||||
{ path: 'workflowitems', loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowitemsEditPageModule' },
|
||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||
])
|
||||
],
|
||||
|
@@ -30,11 +30,16 @@ body {
|
||||
|
||||
.main-content {
|
||||
z-index: $main-z-index;
|
||||
flex: 1 0 auto;
|
||||
flex: 1 1 100%;
|
||||
margin-top: $content-spacing;
|
||||
margin-bottom: $content-spacing;
|
||||
}
|
||||
|
||||
.alert.hide {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ds-header-navbar-wrapper {
|
||||
z-index: $nav-z-index;
|
||||
}
|
||||
|
@@ -34,13 +34,16 @@ import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
import { AngularticsMock } from './shared/mocks/mock-angulartics.service';
|
||||
import { AuthServiceMock } from './shared/mocks/mock-auth.service';
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { MenuService } from './shared/menu/menu.service';
|
||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableServiceStub } from './shared/testing/css-variable-service-stub';
|
||||
import { MenuServiceStub } from './shared/testing/menu-service-stub';
|
||||
import { HostWindowService } from './shared/host-window.service';
|
||||
import { HostWindowServiceStub } from './shared/testing/host-window-service-stub';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouteService } from './shared/services/route.service';
|
||||
import { MockActivatedRoute } from './shared/mocks/mock-active-router';
|
||||
import { MockRouter } from './shared/mocks/mock-router';
|
||||
|
||||
let comp: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
@@ -70,11 +73,13 @@ describe('App component', () => {
|
||||
{ provide: MetadataService, useValue: new MockMetadataService() },
|
||||
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: Router, useValue: {} },
|
||||
{ provide: Router, useValue: new MockRouter() },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||
AppComponent
|
||||
AppComponent,
|
||||
RouteService
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
|
@@ -23,6 +23,7 @@ import { NativeWindowRef, NativeWindowService } from './shared/services/window.s
|
||||
import { isAuthenticated } from './core/auth/selectors';
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
import { RouteService } from './shared/services/route.service';
|
||||
import variables from '../styles/_exposed_variables.scss';
|
||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||
import { MenuService } from './shared/menu/menu.service';
|
||||
@@ -56,6 +57,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private routeService: RouteService,
|
||||
private cssService: CSSVariableService,
|
||||
private menuService: MenuService,
|
||||
private windowService: HostWindowService
|
||||
@@ -75,6 +77,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
|
||||
metadata.listenForRouteChange();
|
||||
|
||||
routeService.saveRouting();
|
||||
|
||||
if (config.debug) {
|
||||
console.info(config);
|
||||
}
|
||||
@@ -83,7 +87,6 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
const env: string = this.config.production ? 'Production' : 'Development';
|
||||
const color: string = this.config.production ? 'red' : 'green';
|
||||
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
||||
|
@@ -31,6 +31,7 @@ import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-s
|
||||
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
||||
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
|
||||
import { AdminSidebarComponent } from './+admin/admin-sidebar/admin-sidebar.component';
|
||||
import { AdminSidebarSectionComponent } from './+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
||||
@@ -57,6 +58,7 @@ const IMPORTS = [
|
||||
HttpClientModule,
|
||||
AppRoutingModule,
|
||||
CoreModule.forRoot(),
|
||||
ScrollToModule.forRoot(),
|
||||
NgbModule.forRoot(),
|
||||
TranslateModule.forRoot(),
|
||||
EffectsModule.forRoot(appEffects),
|
||||
|
@@ -22,9 +22,11 @@ import {
|
||||
import { hasValue } from './shared/empty.util';
|
||||
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
||||
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
|
||||
import { historyReducer, HistoryState } from './shared/history/history.reducer';
|
||||
|
||||
export interface AppState {
|
||||
router: fromRouter.RouterReducerState;
|
||||
history: HistoryState;
|
||||
hostWindow: HostWindowState;
|
||||
forms: FormState;
|
||||
metadataRegistry: MetadataRegistryState;
|
||||
@@ -38,6 +40,7 @@ export interface AppState {
|
||||
|
||||
export const appReducers: ActionReducerMap<AppState> = {
|
||||
router: fromRouter.routerReducer,
|
||||
history: historyReducer,
|
||||
hostWindow: hostWindowReducer,
|
||||
forms: formReducer,
|
||||
metadataRegistry: metadataRegistryReducer,
|
||||
|
@@ -1,20 +1,36 @@
|
||||
import { AuthStatusResponse } from '../cache/response.models';
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { AuthStatusResponse } from '../cache/response.models';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthResponseParsingService } from './auth-response-parsing.service';
|
||||
import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
|
||||
import { MockStore } from '../../shared/testing/mock-store';
|
||||
import { ObjectCacheState } from '../cache/object-cache.reducer';
|
||||
|
||||
describe('AuthResponseParsingService', () => {
|
||||
let service: AuthResponseParsingService;
|
||||
|
||||
const EnvConfig = { cache: { msToLive: 1000 } } as any;
|
||||
const store = new MockStore<ObjectCacheState>({});
|
||||
const objectCacheService = new ObjectCacheService(store as any);
|
||||
const EnvConfig: GlobalConfig = { cache: { msToLive: 1000 } } as any;
|
||||
let store: any;
|
||||
let objectCacheService: ObjectCacheService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({}),
|
||||
],
|
||||
providers: [
|
||||
{ provide: Store, useClass: MockStore }
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
store = TestBed.get(Store);
|
||||
objectCacheService = new ObjectCacheService(store as any);
|
||||
service = new AuthResponseParsingService(EnvConfig, objectCacheService);
|
||||
});
|
||||
|
||||
@@ -38,12 +54,14 @@ describe('AuthResponseParsingService', () => {
|
||||
expires: 1526318322000
|
||||
},
|
||||
} as AuthStatus,
|
||||
statusCode: '200'
|
||||
statusCode: 200,
|
||||
statusText: '200'
|
||||
};
|
||||
|
||||
const validResponse1 = {
|
||||
payload: {},
|
||||
statusCode: '404'
|
||||
statusCode: 404,
|
||||
statusText: '404'
|
||||
};
|
||||
|
||||
const validResponse2 = {
|
||||
@@ -102,7 +120,9 @@ describe('AuthResponseParsingService', () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
statusCode: '200'
|
||||
statusCode: 200,
|
||||
statusText: '200'
|
||||
|
||||
};
|
||||
|
||||
it('should return a AuthStatusResponse if data contains a valid AuthStatus object as payload', () => {
|
||||
|
@@ -26,11 +26,11 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple
|
||||
}
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 200)) {
|
||||
const response = this.process<NormalizedAuthStatus, AuthType>(data.payload, request.uuid);
|
||||
return new AuthStatusResponse(response, data.statusCode);
|
||||
return new AuthStatusResponse(response, data.statusCode, data.statusText);
|
||||
} else {
|
||||
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
|
||||
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode, data.statusText);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -22,7 +22,7 @@ import {
|
||||
} from './auth.actions';
|
||||
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||
import { AuthService } from './auth.service';
|
||||
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
|
||||
import { AuthState } from './auth.reducer';
|
||||
|
||||
import { EPersonMock } from '../../shared/testing/eperson-mock';
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('AuthEffects', () => {
|
||||
let authEffects: AuthEffects;
|
||||
let actions: Observable<any>;
|
||||
let authServiceStub;
|
||||
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
|
||||
const store: Store<AuthState> = jasmine.createSpyObj('store', {
|
||||
/* tslint:disable:no-empty */
|
||||
dispatch: {},
|
||||
/* tslint:enable:no-empty */
|
||||
|
@@ -17,7 +17,7 @@ import { AppState } from '../../app.reducer';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { isNotEmpty, isUndefined } from '../../shared/empty.util';
|
||||
import { isNotEmpty, isUndefined, isNotNull } from '../../shared/empty.util';
|
||||
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Router } from '@angular/router';
|
||||
@@ -142,7 +142,7 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
url: error.url
|
||||
});
|
||||
return observableOf(authResponse);
|
||||
} else if (this.isUnauthorized(error)) {
|
||||
} else if (this.isUnauthorized(error) && isNotNull(token) && authService.isTokenExpired()) {
|
||||
// The access token provided is expired, revoked, malformed, or invalid for other reasons
|
||||
// Redirect to the login route
|
||||
this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
|
||||
|
@@ -73,7 +73,7 @@ describe('AuthService test', () => {
|
||||
{ provide: REQUEST, useValue: {} },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{provide: Store, useValue: mockStore},
|
||||
{ provide: Store, useValue: mockStore },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
CookieService,
|
||||
AuthService
|
||||
|
@@ -114,7 +114,7 @@ describe('BrowseService', () => {
|
||||
scheduler.schedule(() => service.getBrowseDefinitions().subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
||||
});
|
||||
|
||||
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||
@@ -155,7 +155,7 @@ describe('BrowseService', () => {
|
||||
scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
||||
});
|
||||
|
||||
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||
@@ -174,7 +174,7 @@ describe('BrowseService', () => {
|
||||
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
||||
});
|
||||
|
||||
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||
@@ -190,7 +190,7 @@ describe('BrowseService', () => {
|
||||
it('should throw an Error', () => {
|
||||
|
||||
const definitionID = 'invalidID';
|
||||
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
|
||||
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`));
|
||||
|
||||
expect(service.getBrowseEntriesFor(new BrowseEntrySearchOptions(definitionID))).toBeObservable(expected);
|
||||
});
|
||||
@@ -303,7 +303,7 @@ describe('BrowseService', () => {
|
||||
scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
||||
});
|
||||
|
||||
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||
|
@@ -1,25 +1,15 @@
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
race as observableRace
|
||||
} from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import {
|
||||
hasValue,
|
||||
hasValueOperator,
|
||||
isEmpty,
|
||||
isNotEmpty,
|
||||
isNotUndefined
|
||||
} from '../../../shared/empty.util';
|
||||
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs';
|
||||
import { distinctUntilChanged, flatMap, map, startWith, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util';
|
||||
import { PaginatedList } from '../../data/paginated-list';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
import { RemoteDataError } from '../../data/remote-data-error';
|
||||
import { GetRequest } from '../../data/request.models';
|
||||
import { RequestEntry } from '../../data/request.reducer';
|
||||
import { RequestService } from '../../data/request.service';
|
||||
|
||||
import { NormalizedObject } from '../models/normalized-object.model';
|
||||
import { ObjectCacheService } from '../object-cache.service';
|
||||
import { DSOSuccessResponse, ErrorResponse } from '../response.models';
|
||||
@@ -99,7 +89,11 @@ export class RemoteDataBuildService {
|
||||
isSuccessful = reqEntry.response.isSuccessful;
|
||||
const errorMessage = isSuccessful === false ? (reqEntry.response as ErrorResponse).errorMessage : undefined;
|
||||
if (hasValue(errorMessage)) {
|
||||
error = new RemoteDataError(reqEntry.response.statusCode, errorMessage);
|
||||
error = new RemoteDataError(
|
||||
(reqEntry.response as ErrorResponse).statusCode,
|
||||
(reqEntry.response as ErrorResponse).statusText,
|
||||
errorMessage
|
||||
);
|
||||
}
|
||||
}
|
||||
return new RemoteData(
|
||||
@@ -232,16 +226,25 @@ export class RemoteDataBuildService {
|
||||
}).filter((e: string) => hasValue(e))
|
||||
.join(', ');
|
||||
|
||||
const statusCode: string = arr
|
||||
const statusText: string = arr
|
||||
.map((d: RemoteData<T>) => d.error)
|
||||
.map((e: RemoteDataError, idx: number) => {
|
||||
if (hasValue(e)) {
|
||||
return `[${idx}]: ${e.statusCode}`;
|
||||
return `[${idx}]: ${e.statusText}`;
|
||||
}
|
||||
}).filter((c: string) => hasValue(c))
|
||||
.join(', ');
|
||||
|
||||
const error = new RemoteDataError(statusCode, errorMessage);
|
||||
const statusCode: number = arr
|
||||
.map((d: RemoteData<T>) => d.error)
|
||||
.map((e: RemoteDataError, idx: number) => {
|
||||
if (hasValue(e)) {
|
||||
return e.statusCode;
|
||||
}
|
||||
}).filter((c: number) => hasValue(c))
|
||||
.reduce((acc, status) => status, undefined);
|
||||
|
||||
const error = new RemoteDataError(statusCode, statusText, errorMessage);
|
||||
|
||||
const payload: T[] = arr.map((d: RemoteData<T>) => d.payload);
|
||||
|
||||
@@ -260,8 +263,10 @@ export class RemoteDataBuildService {
|
||||
map((rd: RemoteData<T[] | PaginatedList<T>>) => {
|
||||
if (Array.isArray(rd.payload)) {
|
||||
return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload) })
|
||||
} else {
|
||||
} else if (isNotUndefined(rd.payload)) {
|
||||
return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload.page) });
|
||||
} else {
|
||||
return Object.assign(rd, { payload: new PaginatedList(pageInfo, []) });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@@ -18,6 +18,20 @@ export class NormalizedCollection extends NormalizedDSpaceObject<Collection> {
|
||||
@autoserialize
|
||||
handle: string;
|
||||
|
||||
/**
|
||||
* The Bitstream that represents the license of this Collection
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.License, false)
|
||||
license: string;
|
||||
|
||||
/**
|
||||
* The Bitstream that represents the default Access Conditions of this Collection
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.ResourcePolicy, false)
|
||||
defaultAccessConditions: string;
|
||||
|
||||
/**
|
||||
* The Bitstream that represents the logo of this Collection
|
||||
*/
|
||||
|
@@ -31,9 +31,6 @@ export class NormalizedDSpaceObject<T extends DSpaceObject> extends NormalizedOb
|
||||
|
||||
/**
|
||||
* The universally unique identifier of this DSpaceObject
|
||||
*
|
||||
* Repeated here to make the serialization work,
|
||||
* inheritSerialization doesn't seem to work for more than one level
|
||||
*/
|
||||
@autoserializeAs(String)
|
||||
uuid: string;
|
||||
|
24
src/app/core/cache/models/normalized-license.model.ts
vendored
Normal file
24
src/app/core/cache/models/normalized-license.model.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { mapsTo } from '../builders/build-decorators';
|
||||
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
|
||||
import { License } from '../../shared/license.model';
|
||||
|
||||
/**
|
||||
* Normalized model class for a Collection License
|
||||
*/
|
||||
@mapsTo(License)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
export class NormalizedLicense extends NormalizedDSpaceObject<License> {
|
||||
|
||||
/**
|
||||
* A boolean representing if this License is custom or not
|
||||
*/
|
||||
@autoserialize
|
||||
custom: boolean;
|
||||
|
||||
/**
|
||||
* The text of the license
|
||||
*/
|
||||
@autoserialize
|
||||
text: string;
|
||||
}
|
@@ -9,12 +9,18 @@ import { GenericConstructor } from '../../shared/generic-constructor';
|
||||
import { NormalizedCommunity } from './normalized-community.model';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { NormalizedObject } from './normalized-object.model';
|
||||
import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
|
||||
import { NormalizedLicense } from './normalized-license.model';
|
||||
import { NormalizedResourcePolicy } from './normalized-resource-policy.model';
|
||||
import { NormalizedWorkspaceItem } from '../../submission/models/normalized-workspaceitem.model';
|
||||
import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model';
|
||||
import { NormalizedGroup } from '../../eperson/models/normalized-group.model';
|
||||
import { NormalizedWorkflowItem } from '../../submission/models/normalized-workflowitem.model';
|
||||
import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
|
||||
import { NormalizedMetadataSchema } from '../../metadata/normalized-metadata-schema.model';
|
||||
import { CacheableObject } from '../object-cache.reducer';
|
||||
import { NormalizedSubmissionDefinitionsModel } from '../../config/models/normalized-config-submission-definitions.model';
|
||||
import { NormalizedSubmissionFormsModel } from '../../config/models/normalized-config-submission-forms.model';
|
||||
import { NormalizedSubmissionSectionModel } from '../../config/models/normalized-config-submission-section.model';
|
||||
|
||||
export class NormalizedObjectFactory {
|
||||
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject<CacheableObject>> {
|
||||
@@ -37,6 +43,9 @@ export class NormalizedObjectFactory {
|
||||
case ResourceType.BitstreamFormat: {
|
||||
return NormalizedBitstreamFormat
|
||||
}
|
||||
case ResourceType.License: {
|
||||
return NormalizedLicense
|
||||
}
|
||||
case ResourceType.ResourcePolicy: {
|
||||
return NormalizedResourcePolicy
|
||||
}
|
||||
@@ -61,6 +70,24 @@ export class NormalizedObjectFactory {
|
||||
case ResourceType.MetadataField: {
|
||||
return NormalizedGroup
|
||||
}
|
||||
case ResourceType.Workspaceitem: {
|
||||
return NormalizedWorkspaceItem
|
||||
}
|
||||
case ResourceType.Workflowitem: {
|
||||
return NormalizedWorkflowItem
|
||||
}
|
||||
case ResourceType.SubmissionDefinition:
|
||||
case ResourceType.SubmissionDefinitions: {
|
||||
return NormalizedSubmissionDefinitionsModel
|
||||
}
|
||||
case ResourceType.SubmissionForm:
|
||||
case ResourceType.SubmissionForms: {
|
||||
return NormalizedSubmissionFormsModel
|
||||
}
|
||||
case ResourceType.SubmissionSection:
|
||||
case ResourceType.SubmissionSections: {
|
||||
return NormalizedSubmissionSectionModel
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -13,11 +13,8 @@ export abstract class NormalizedObject<T extends CacheableObject> implements Cac
|
||||
self: string;
|
||||
|
||||
/**
|
||||
* The universally unique identifier of this Object
|
||||
* A string representing the kind of DSpaceObject, e.g. community, item, …
|
||||
*/
|
||||
@autoserialize
|
||||
uuid: string;
|
||||
|
||||
@autoserialize
|
||||
type: ResourceType;
|
||||
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { ResourcePolicy } from '../../shared/resource-policy.model';
|
||||
|
||||
import { mapsTo, relationship } from '../builders/build-decorators';
|
||||
import { mapsTo } from '../builders/build-decorators';
|
||||
import { NormalizedObject } from './normalized-object.model';
|
||||
import { IDToUUIDSerializer } from '../id-to-uuid-serializer';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { ActionType } from './action-type.model';
|
||||
|
||||
/**
|
||||
@@ -17,6 +16,7 @@ export class NormalizedResourcePolicy extends NormalizedObject<ResourcePolicy> {
|
||||
/**
|
||||
* The action that is allowed by this Resource Policy
|
||||
*/
|
||||
@autoserialize
|
||||
action: ActionType;
|
||||
|
||||
/**
|
||||
@@ -28,9 +28,8 @@ export class NormalizedResourcePolicy extends NormalizedObject<ResourcePolicy> {
|
||||
/**
|
||||
* The uuid of the Group this Resource Policy applies to
|
||||
*/
|
||||
@relationship(ResourceType.Group, false)
|
||||
@autoserializeAs(String, 'groupUUID')
|
||||
group: string;
|
||||
@autoserialize
|
||||
groupUUID: string;
|
||||
|
||||
/**
|
||||
* Identifier for this Resource Policy
|
||||
@@ -46,4 +45,5 @@ export class NormalizedResourcePolicy extends NormalizedObject<ResourcePolicy> {
|
||||
*/
|
||||
@autoserializeAs(new IDToUUIDSerializer('resource-policy'), 'id')
|
||||
uuid: string;
|
||||
|
||||
}
|
||||
|
9
src/app/core/cache/models/search-param.model.ts
vendored
Normal file
9
src/app/core/cache/models/search-param.model.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
/**
|
||||
* Class representing a query parameter (query?fieldName=fieldValue) used in FindAllOptions object
|
||||
*/
|
||||
export class SearchParam {
|
||||
constructor(public fieldName: string, public fieldValue: any) {
|
||||
|
||||
}
|
||||
}
|
48
src/app/core/cache/object-cache.service.ts
vendored
48
src/app/core/cache/object-cache.service.ts
vendored
@@ -1,34 +1,44 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { applyPatch, Operation } from 'fast-json-patch';
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
|
||||
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { IndexName } from '../index/index.reducer';
|
||||
|
||||
import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer';
|
||||
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
import { selfLinkFromUuidSelector } from '../index/index.selectors';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { NormalizedObjectFactory } from './models/normalized-object-factory';
|
||||
import { NormalizedObject } from './models/normalized-object.model';
|
||||
import {
|
||||
AddPatchObjectCacheAction,
|
||||
AddToObjectCacheAction,
|
||||
ApplyPatchObjectCacheAction,
|
||||
RemoveFromObjectCacheAction
|
||||
} from './object-cache.actions';
|
||||
import { hasNoValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { coreSelector, CoreState } from '../core.reducers';
|
||||
import { pathSelector } from '../shared/selectors';
|
||||
import { NormalizedObjectFactory } from './models/normalized-object-factory';
|
||||
import { NormalizedObject } from './models/normalized-object.model';
|
||||
import { applyPatch, Operation } from 'fast-json-patch';
|
||||
|
||||
import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer';
|
||||
import { AddToSSBAction } from './server-sync-buffer.actions';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
|
||||
function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
|
||||
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.OBJECT, uuid);
|
||||
}
|
||||
/**
|
||||
* The base selector function to select the object cache in the store
|
||||
*/
|
||||
const objectCacheSelector = createSelector(
|
||||
coreSelector,
|
||||
(state: CoreState) => state['cache/object']
|
||||
);
|
||||
|
||||
function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector<CoreState, ObjectCacheEntry> {
|
||||
return pathSelector<CoreState, ObjectCacheEntry>(coreSelector, 'cache/object', selfLink);
|
||||
}
|
||||
/**
|
||||
* Selector function to select an object entry by self link from the cache
|
||||
* @param selfLink The self link of the object
|
||||
*/
|
||||
const entryFromSelfLinkSelector =
|
||||
(selfLink: string): MemoizedSelector<CoreState, ObjectCacheEntry> => createSelector(
|
||||
objectCacheSelector,
|
||||
(state: ObjectCacheState) => state[selfLink],
|
||||
);
|
||||
|
||||
/**
|
||||
* A service to interact with the object cache
|
||||
|
128
src/app/core/cache/response.models.ts
vendored
128
src/app/core/cache/response.models.ts
vendored
@@ -1,7 +1,7 @@
|
||||
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
||||
import { RequestError } from '../data/request.models';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { ConfigObject } from '../shared/config/config.model';
|
||||
import { ConfigObject } from '../config/models/config.model';
|
||||
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
|
||||
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
|
||||
import { IntegrationModel } from '../integration/models/integration.model';
|
||||
@@ -11,14 +11,19 @@ import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstream
|
||||
import { AuthStatus } from '../auth/models/auth-status.model';
|
||||
import { MetadataSchema } from '../metadata/metadataschema.model';
|
||||
import { MetadataField } from '../metadata/metadatafield.model';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { SubmissionObject } from '../submission/models/submission-object.model';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export class RestResponse {
|
||||
public toCache = true;
|
||||
public timeAdded: number;
|
||||
|
||||
constructor(
|
||||
public isSuccessful: boolean,
|
||||
public statusCode: string,
|
||||
public statusCode: number,
|
||||
public statusText: string
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -26,10 +31,11 @@ export class RestResponse {
|
||||
export class DSOSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public resourceSelfLinks: string[],
|
||||
public statusCode: string,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +45,11 @@ export class DSOSuccessResponse extends RestResponse {
|
||||
export class RegistryMetadataschemasSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public metadataschemasResponse: RegistryMetadataschemasResponse,
|
||||
public statusCode: string,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,10 +59,11 @@ export class RegistryMetadataschemasSuccessResponse extends RestResponse {
|
||||
export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public metadatafieldsResponse: RegistryMetadatafieldsResponse,
|
||||
public statusCode: string,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +73,11 @@ export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
|
||||
export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public bitstreamformatsResponse: RegistryBitstreamformatsResponse,
|
||||
public statusCode: string,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,9 +87,10 @@ export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
|
||||
export class MetadataschemaSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public metadataschema: MetadataSchema,
|
||||
public statusCode: string
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,28 +100,31 @@ export class MetadataschemaSuccessResponse extends RestResponse {
|
||||
export class MetadatafieldSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public metadatafield: MetadataField,
|
||||
public statusCode: string
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public results: SearchQueryResponse,
|
||||
public statusCode: string,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
export class FacetConfigSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public results: SearchFilterConfig[],
|
||||
public statusCode: string
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,18 +135,20 @@ export class FacetValueMap {
|
||||
export class FacetValueSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public results: FacetValue[],
|
||||
public statusCode: string,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
export class FacetValueMapSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public results: FacetValueMap,
|
||||
public statusCode: string,
|
||||
public statusCode: number,
|
||||
public statusText: string
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,19 +159,21 @@ export class EndpointMap {
|
||||
export class EndpointMapSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public endpointMap: EndpointMap,
|
||||
public statusCode: string,
|
||||
public statusCode: number,
|
||||
public statusText: string
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
export class GenericSuccessResponse<T> extends RestResponse {
|
||||
constructor(
|
||||
public payload: T,
|
||||
public statusCode: string,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +181,7 @@ export class ErrorResponse extends RestResponse {
|
||||
errorMessage: string;
|
||||
|
||||
constructor(error: RequestError) {
|
||||
super(false, error.statusText);
|
||||
super(false, error.statusCode, error.statusText);
|
||||
console.error(error);
|
||||
this.errorMessage = error.message;
|
||||
}
|
||||
@@ -172,11 +189,12 @@ export class ErrorResponse extends RestResponse {
|
||||
|
||||
export class ConfigSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public configDefinition: ConfigObject[],
|
||||
public statusCode: string,
|
||||
public configDefinition: ConfigObject,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,29 +203,65 @@ export class AuthStatusResponse extends RestResponse {
|
||||
|
||||
constructor(
|
||||
public response: AuthStatus,
|
||||
public statusCode: string
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
export class IntegrationSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public dataDefinition: IntegrationModel[],
|
||||
public statusCode: string,
|
||||
public dataDefinition: PaginatedList<IntegrationModel>,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
export class PostPatchSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public dataDefinition: any,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
export class SubmissionSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public dataDefinition: Array<SubmissionObject | ConfigObject | string>,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
export class EpersonSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public epersonDefinition: DSpaceObject[],
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
export class FilteredDiscoveryQueryResponse extends RestResponse {
|
||||
constructor(
|
||||
public filterQuery: string,
|
||||
public statusCode: string,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
@@ -1,20 +1,17 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
|
||||
import { ServerSyncBufferEffects } from './server-sync-buffer.effects';
|
||||
import { GLOBAL_CONFIG } from '../../../config';
|
||||
import {
|
||||
CommitSSBAction,
|
||||
EmptySSBAction,
|
||||
ServerSyncBufferActionTypes
|
||||
} from './server-sync-buffer.actions';
|
||||
import { CommitSSBAction, EmptySSBAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { ObjectCacheService } from './object-cache.service';
|
||||
import { MockStore } from '../../shared/testing/mock-store';
|
||||
import { ObjectCacheState } from './object-cache.reducer';
|
||||
import * as operators from 'rxjs/operators';
|
||||
import { spyOnOperator } from '../../shared/testing/utils';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
@@ -38,8 +35,10 @@ describe('ServerSyncBufferEffects', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new MockStore<ObjectCacheState>({});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({}),
|
||||
],
|
||||
providers: [
|
||||
ServerSyncBufferEffects,
|
||||
provideMockActions(() => actions),
|
||||
@@ -54,11 +53,12 @@ describe('ServerSyncBufferEffects', () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
{ provide: Store, useValue: store }
|
||||
{ provide: Store, useClass: MockStore }
|
||||
// other providers
|
||||
],
|
||||
});
|
||||
|
||||
store = TestBed.get(Store);
|
||||
ssbEffects = TestBed.get(ServerSyncBufferEffects);
|
||||
});
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
import {
|
||||
AddToSSBAction,
|
||||
CommitSSBAction,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
} from './server-sync-buffer.actions';
|
||||
import { GLOBAL_CONFIG } from '../../../config';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { coreSelector, CoreState } from '../core.reducers';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
|
||||
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { ConfigObject } from '../shared/config/config.model';
|
||||
import { ConfigObject } from './models/config.model';
|
||||
|
||||
/**
|
||||
* A class to represent the data retrieved by a configuration service
|
||||
@@ -7,7 +7,7 @@ import { ConfigObject } from '../shared/config/config.model';
|
||||
export class ConfigData {
|
||||
constructor(
|
||||
public pageInfo: PageInfo,
|
||||
public payload: ConfigObject[]
|
||||
public payload: ConfigObject
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
@@ -2,13 +2,14 @@ import { ConfigSuccessResponse, ErrorResponse } from '../cache/response.models';
|
||||
import { ConfigResponseParsingService } from './config-response-parsing.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { ConfigRequest } from './request.models';
|
||||
import { ConfigRequest } from '../data/request.models';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { SubmissionDefinitionsModel } from '../shared/config/config-submission-definitions.model';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { NormalizedSubmissionDefinitionsModel } from './models/normalized-config-submission-definitions.model';
|
||||
import { NormalizedSubmissionSectionModel } from './models/normalized-config-submission-section.model';
|
||||
|
||||
describe('ConfigResponseParsingService', () => {
|
||||
let service: ConfigResponseParsingService;
|
||||
@@ -119,7 +120,8 @@ describe('ConfigResponseParsingService', () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
statusCode: '200'
|
||||
statusCode: 200,
|
||||
statusText: 'OK'
|
||||
};
|
||||
});
|
||||
|
||||
@@ -128,7 +130,8 @@ describe('ConfigResponseParsingService', () => {
|
||||
|
||||
const invalidResponse1 = {
|
||||
payload: {},
|
||||
statusCode: '200'
|
||||
statusCode: 200,
|
||||
statusText: 'OK'
|
||||
};
|
||||
|
||||
const invalidResponse2 = {
|
||||
@@ -152,14 +155,15 @@ describe('ConfigResponseParsingService', () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
statusCode: '200'
|
||||
statusCode: 200,
|
||||
statusText: 'OK'
|
||||
};
|
||||
|
||||
const invalidResponse3 = {
|
||||
payload: {
|
||||
_links: { self: { href: 'https://rest.api/config/submissiondefinitions/traditional' } },
|
||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||
}, statusCode: '500'
|
||||
}, statusCode: 500, statusText: 'Internal Server Error'
|
||||
};
|
||||
const pageinfo = Object.assign(new PageInfo(), {
|
||||
elementsPerPage: 4,
|
||||
@@ -169,7 +173,7 @@ describe('ConfigResponseParsingService', () => {
|
||||
self: 'https://rest.api/config/submissiondefinitions/traditional/sections'
|
||||
});
|
||||
const definitions =
|
||||
Object.assign(new SubmissionDefinitionsModel(), {
|
||||
Object.assign(new NormalizedSubmissionDefinitionsModel(), {
|
||||
isDefault: true,
|
||||
name: 'traditional',
|
||||
type: 'submissiondefinition',
|
||||
@@ -179,10 +183,65 @@ describe('ConfigResponseParsingService', () => {
|
||||
},
|
||||
self: 'https://rest.api/config/submissiondefinitions/traditional',
|
||||
sections: new PaginatedList(pageinfo, [
|
||||
'https://rest.api/config/submissionsections/traditionalpageone',
|
||||
'https://rest.api/config/submissionsections/traditionalpagetwo',
|
||||
'https://rest.api/config/submissionsections/upload',
|
||||
'https://rest.api/config/submissionsections/license'
|
||||
Object.assign(new NormalizedSubmissionSectionModel(), {
|
||||
header: 'submit.progressbar.describe.stepone',
|
||||
mandatory: true,
|
||||
sectionType: 'submission-form',
|
||||
visibility:{
|
||||
main:null,
|
||||
other:'READONLY'
|
||||
},
|
||||
type: 'submissionsection',
|
||||
_links: {
|
||||
self: 'https://rest.api/config/submissionsections/traditionalpageone',
|
||||
config: 'https://rest.api/config/submissionforms/traditionalpageone'
|
||||
},
|
||||
self: 'https://rest.api/config/submissionsections/traditionalpageone',
|
||||
}),
|
||||
Object.assign(new NormalizedSubmissionSectionModel(), {
|
||||
header: 'submit.progressbar.describe.steptwo',
|
||||
mandatory: true,
|
||||
sectionType: 'submission-form',
|
||||
visibility:{
|
||||
main:null,
|
||||
other:'READONLY'
|
||||
},
|
||||
type: 'submissionsection',
|
||||
_links: {
|
||||
self: 'https://rest.api/config/submissionsections/traditionalpagetwo',
|
||||
config: 'https://rest.api/config/submissionforms/traditionalpagetwo'
|
||||
},
|
||||
self: 'https://rest.api/config/submissionsections/traditionalpagetwo',
|
||||
}),
|
||||
Object.assign(new NormalizedSubmissionSectionModel(), {
|
||||
header: 'submit.progressbar.upload',
|
||||
mandatory: false,
|
||||
sectionType: 'upload',
|
||||
visibility:{
|
||||
main:null,
|
||||
other:'READONLY'
|
||||
},
|
||||
type: 'submissionsection',
|
||||
_links: {
|
||||
self: 'https://rest.api/config/submissionsections/upload',
|
||||
config: 'https://rest.api/config/submissionuploads/upload'
|
||||
},
|
||||
self: 'https://rest.api/config/submissionsections/upload',
|
||||
}),
|
||||
Object.assign(new NormalizedSubmissionSectionModel(), {
|
||||
header: 'submit.progressbar.license',
|
||||
mandatory: true,
|
||||
sectionType: 'license',
|
||||
visibility:{
|
||||
main:null,
|
||||
other:'READONLY'
|
||||
},
|
||||
type: 'submissionsection',
|
||||
_links: {
|
||||
self: 'https://rest.api/config/submissionsections/license'
|
||||
},
|
||||
self: 'https://rest.api/config/submissionsections/license',
|
||||
})
|
||||
])
|
||||
});
|
||||
|
@@ -1,15 +1,15 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
import { ResponseParsingService } from '../data/parsing.service';
|
||||
import { RestRequest } from '../data/request.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { ConfigObjectFactory } from '../shared/config/config-object-factory';
|
||||
import { ConfigObjectFactory } from './models/config-object-factory';
|
||||
|
||||
import { ConfigObject } from '../shared/config/config.model';
|
||||
import { ConfigType } from '../shared/config/config-type';
|
||||
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||
import { ConfigObject } from './models/config.model';
|
||||
import { ConfigType } from './models/config-type';
|
||||
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
|
||||
import { GLOBAL_CONFIG } from '../../../config';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
@@ -27,14 +27,14 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp
|
||||
}
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '201' || data.statusCode === '200' || data.statusCode === 'OK')) {
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 201 || data.statusCode === 200)) {
|
||||
const configDefinition = this.process<ConfigObject,ConfigType>(data.payload, request.uuid);
|
||||
return new ConfigSuccessResponse(configDefinition, data.statusCode, this.processPageInfo(data.payload));
|
||||
return new ConfigSuccessResponse(configDefinition, data.statusCode, data.statusText, this.processPageInfo(data.payload));
|
||||
} else {
|
||||
return new ErrorResponse(
|
||||
Object.assign(
|
||||
new Error('Unexpected response from config endpoint'),
|
||||
{statusText: data.statusCode}
|
||||
{ statusCode: data.statusCode, statusText: data.statusText }
|
||||
)
|
||||
);
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||
import { ConfigService } from './config.service';
|
||||
|
@@ -6,7 +6,6 @@ import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.mode
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { ConfigData } from './config-data';
|
||||
import { RequestEntry } from '../data/request.reducer';
|
||||
import { getResponseFromEntry } from '../shared/operators';
|
||||
|
||||
export abstract class ConfigService {
|
||||
|
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Model class for an Access Condition
|
||||
*/
|
||||
export class AccessConditionOption {
|
||||
|
||||
/**
|
||||
* The name for this Access Condition
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The uuid of the Group this Access Condition applies to
|
||||
*/
|
||||
groupUUID: string;
|
||||
|
||||
/**
|
||||
* The uuid of the Group that contains set of groups this Resource Policy applies to
|
||||
*/
|
||||
selectGroupUUID: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if this Access Condition has a start date
|
||||
*/
|
||||
hasStartDate: boolean;
|
||||
|
||||
/**
|
||||
* A boolean representing if this Access Condition has an end date
|
||||
*/
|
||||
hasEndDate: boolean;
|
||||
|
||||
/**
|
||||
* Maximum value of the start date
|
||||
*/
|
||||
maxStartDate: string;
|
||||
|
||||
/**
|
||||
* Maximum value of the end date
|
||||
*/
|
||||
maxEndDate: string;
|
||||
}
|
36
src/app/core/config/models/config-object-factory.ts
Normal file
36
src/app/core/config/models/config-object-factory.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||
import { ConfigType } from './config-type';
|
||||
import { ConfigObject } from './config.model';
|
||||
import { NormalizedSubmissionDefinitionsModel } from './normalized-config-submission-definitions.model';
|
||||
import { NormalizedSubmissionFormsModel } from './normalized-config-submission-forms.model';
|
||||
import { NormalizedSubmissionSectionModel } from './normalized-config-submission-section.model';
|
||||
import { NormalizedSubmissionUploadsModel } from './normalized-config-submission-uploads.model';
|
||||
|
||||
/**
|
||||
* Class to return normalized models for config objects
|
||||
*/
|
||||
export class ConfigObjectFactory {
|
||||
public static getConstructor(type): GenericConstructor<ConfigObject> {
|
||||
switch (type) {
|
||||
case ConfigType.SubmissionDefinition:
|
||||
case ConfigType.SubmissionDefinitions: {
|
||||
return NormalizedSubmissionDefinitionsModel
|
||||
}
|
||||
case ConfigType.SubmissionForm:
|
||||
case ConfigType.SubmissionForms: {
|
||||
return NormalizedSubmissionFormsModel
|
||||
}
|
||||
case ConfigType.SubmissionSection:
|
||||
case ConfigType.SubmissionSections: {
|
||||
return NormalizedSubmissionSectionModel
|
||||
}
|
||||
case ConfigType.SubmissionUpload:
|
||||
case ConfigType.SubmissionUploads: {
|
||||
return NormalizedSubmissionUploadsModel
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,15 +1,17 @@
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { ConfigObject } from './config.model';
|
||||
import { SubmissionSectionModel } from './config-submission-section.model';
|
||||
import { PaginatedList } from '../../data/paginated-list';
|
||||
|
||||
@inheritSerialization(ConfigObject)
|
||||
export class SubmissionDefinitionsModel extends ConfigObject {
|
||||
|
||||
@autoserialize
|
||||
/**
|
||||
* A boolean representing if this submission definition is the default or not
|
||||
*/
|
||||
isDefault: boolean;
|
||||
|
||||
@autoserializeAs(SubmissionSectionModel)
|
||||
/**
|
||||
* A list of SubmissionSectionModel that are present in this submission definition
|
||||
*/
|
||||
sections: PaginatedList<SubmissionSectionModel>;
|
||||
|
||||
}
|
@@ -1,14 +1,20 @@
|
||||
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { ConfigObject } from './config.model';
|
||||
import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model';
|
||||
|
||||
/**
|
||||
* An interface that define a form row and its properties.
|
||||
*/
|
||||
export interface FormRowModel {
|
||||
fields: FormFieldModel[];
|
||||
}
|
||||
|
||||
@inheritSerialization(ConfigObject)
|
||||
/**
|
||||
* A model class for a NormalizedObject.
|
||||
*/
|
||||
export class SubmissionFormsModel extends ConfigObject {
|
||||
|
||||
@autoserialize
|
||||
/**
|
||||
* An array of [FormRowModel] that are present in this form
|
||||
*/
|
||||
rows: FormRowModel[];
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
import { ConfigObject } from './config.model';
|
||||
import { SectionsType } from '../../../submission/sections/sections-type';
|
||||
|
||||
/**
|
||||
* An interface that define section visibility and its properties.
|
||||
*/
|
||||
export interface SubmissionSectionVisibility {
|
||||
main: any,
|
||||
other: any
|
||||
}
|
||||
|
||||
export class SubmissionSectionModel extends ConfigObject {
|
||||
|
||||
/**
|
||||
* The header for this section
|
||||
*/
|
||||
header: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if this submission section is the mandatory or not
|
||||
*/
|
||||
mandatory: boolean;
|
||||
|
||||
/**
|
||||
* A string representing the kind of section object
|
||||
*/
|
||||
sectionType: SectionsType;
|
||||
|
||||
/**
|
||||
* The [SubmissionSectionVisibility] object for this section
|
||||
*/
|
||||
visibility: SubmissionSectionVisibility
|
||||
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import { ConfigObject } from './config.model';
|
||||
import { AccessConditionOption } from './config-access-condition-option.model';
|
||||
import { SubmissionFormsModel } from './config-submission-forms.model';
|
||||
|
||||
export class SubmissionUploadsModel extends ConfigObject {
|
||||
|
||||
/**
|
||||
* A list of available bitstream access conditions
|
||||
*/
|
||||
accessConditionOptions: AccessConditionOption[];
|
||||
|
||||
/**
|
||||
* An object representing the configuration describing the bistream metadata form
|
||||
*/
|
||||
metadata: SubmissionFormsModel;
|
||||
|
||||
required: boolean;
|
||||
|
||||
maxSize: number;
|
||||
|
||||
}
|
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* TODO replace with actual string enum after upgrade to TypeScript 2.4:
|
||||
* https://github.com/Microsoft/TypeScript/pull/15486
|
||||
*/
|
||||
import { ResourceType } from '../resource-type';
|
||||
|
||||
export enum ConfigType {
|
||||
SubmissionDefinitions = 'submissiondefinitions',
|
||||
SubmissionDefinition = 'submissiondefinition',
|
||||
@@ -11,5 +5,6 @@ export enum ConfigType {
|
||||
SubmissionForms = 'submissionforms',
|
||||
SubmissionSections = 'submissionsections',
|
||||
SubmissionSection = 'submissionsection',
|
||||
Authority = 'authority'
|
||||
SubmissionUploads = 'submissionuploads',
|
||||
SubmissionUpload = 'submissionupload',
|
||||
}
|
27
src/app/core/config/models/config.model.ts
Normal file
27
src/app/core/config/models/config.model.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { CacheableObject } from '../../cache/object-cache.reducer';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
|
||||
export abstract class ConfigObject implements CacheableObject {
|
||||
|
||||
/**
|
||||
* The name for this configuration
|
||||
*/
|
||||
public name: string;
|
||||
|
||||
/**
|
||||
* A string representing the kind of config object
|
||||
*/
|
||||
public type: ResourceType;
|
||||
|
||||
/**
|
||||
* The links to all related resources returned by the rest api.
|
||||
*/
|
||||
public _links: {
|
||||
[name: string]: string
|
||||
};
|
||||
|
||||
/**
|
||||
* The link to the rest endpoint where this config object can be found
|
||||
*/
|
||||
self: string;
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { SubmissionSectionModel } from './config-submission-section.model';
|
||||
import { PaginatedList } from '../../data/paginated-list';
|
||||
import { NormalizedConfigObject } from './normalized-config.model';
|
||||
import { SubmissionDefinitionsModel } from './config-submission-definitions.model';
|
||||
|
||||
/**
|
||||
* Normalized class for the configuration describing the submission
|
||||
*/
|
||||
@inheritSerialization(NormalizedConfigObject)
|
||||
export class NormalizedSubmissionDefinitionsModel extends NormalizedConfigObject<SubmissionDefinitionsModel> {
|
||||
|
||||
/**
|
||||
* A boolean representing if this submission definition is the default or not
|
||||
*/
|
||||
@autoserialize
|
||||
isDefault: boolean;
|
||||
|
||||
/**
|
||||
* A list of SubmissionSectionModel that are present in this submission definition
|
||||
*/
|
||||
@autoserializeAs(SubmissionSectionModel)
|
||||
sections: PaginatedList<SubmissionSectionModel>;
|
||||
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { NormalizedConfigObject } from './normalized-config.model';
|
||||
import { FormRowModel, SubmissionFormsModel } from './config-submission-forms.model';
|
||||
|
||||
/**
|
||||
* Normalized class for the configuration describing the submission form
|
||||
*/
|
||||
@inheritSerialization(NormalizedConfigObject)
|
||||
export class NormalizedSubmissionFormsModel extends NormalizedConfigObject<SubmissionFormsModel> {
|
||||
|
||||
/**
|
||||
* An array of [FormRowModel] that are present in this form
|
||||
*/
|
||||
@autoserialize
|
||||
rows: FormRowModel[];
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { SectionsType } from '../../../submission/sections/sections-type';
|
||||
import { NormalizedConfigObject } from './normalized-config.model';
|
||||
import { SubmissionFormsModel } from './config-submission-forms.model';
|
||||
import { SubmissionSectionVisibility } from './config-submission-section.model';
|
||||
|
||||
/**
|
||||
* Normalized class for the configuration describing the submission section
|
||||
*/
|
||||
@inheritSerialization(NormalizedConfigObject)
|
||||
export class NormalizedSubmissionSectionModel extends NormalizedConfigObject<SubmissionFormsModel> {
|
||||
|
||||
/**
|
||||
* The header for this section
|
||||
*/
|
||||
@autoserialize
|
||||
header: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if this submission section is the mandatory or not
|
||||
*/
|
||||
@autoserialize
|
||||
mandatory: boolean;
|
||||
|
||||
/**
|
||||
* A string representing the kind of section object
|
||||
*/
|
||||
@autoserialize
|
||||
sectionType: SectionsType;
|
||||
|
||||
/**
|
||||
* The [SubmissionSectionVisibility] object for this section
|
||||
*/
|
||||
@autoserialize
|
||||
visibility: SubmissionSectionVisibility
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user