mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-18 15:33:04 +00:00
Merge remote-tracking branch 'remotes/origin/master' into submission
This commit is contained in:
@@ -120,6 +120,7 @@
|
|||||||
"pem": "1.12.3",
|
"pem": "1.12.3",
|
||||||
"reflect-metadata": "0.1.12",
|
"reflect-metadata": "0.1.12",
|
||||||
"rxjs": "6.2.2",
|
"rxjs": "6.2.2",
|
||||||
|
"rxjs-spy": "^7.5.1",
|
||||||
"sortablejs": "1.7.0",
|
"sortablejs": "1.7.0",
|
||||||
"text-mask-core": "5.0.1",
|
"text-mask-core": "5.0.1",
|
||||||
"ts-loader": "^5.2.1",
|
"ts-loader": "^5.2.1",
|
||||||
|
@@ -289,7 +289,7 @@
|
|||||||
"results-per-page": "Results Per Page",
|
"results-per-page": "Results Per Page",
|
||||||
"sort-direction": "Sort Options",
|
"sort-direction": "Sort Options",
|
||||||
"showing": {
|
"showing": {
|
||||||
"label": "Now showing items ",
|
"label": "Now showing ",
|
||||||
"detail": "{{ range }} of {{ total }}"
|
"detail": "{{ range }} of {{ total }}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -40,8 +40,8 @@
|
|||||||
"description": "Beschrijving:"
|
"description": "Beschrijving:"
|
||||||
},
|
},
|
||||||
"link": {
|
"link": {
|
||||||
"simple": "Eenvoudige item weergave",
|
"simple": "Eenvoudige itemweergave",
|
||||||
"full": "Volledige item weergave"
|
"full": "Volledige itemweergave"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -52,10 +52,10 @@
|
|||||||
},
|
},
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"results-per-page": "Resultaten per pagina",
|
"results-per-page": "Resultaten per pagina",
|
||||||
"sort-direction": "Sorteer mogelijkheden",
|
"sort-direction": "Sorteermogelijkheden",
|
||||||
"showing": {
|
"showing": {
|
||||||
"label": "Getoonde items ",
|
"label": "Resultaten ",
|
||||||
"detail": "{{ range }} tot {{ total }}"
|
"detail": "{{ range }} van {{ total }}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sorting": {
|
"sorting": {
|
||||||
@@ -116,8 +116,8 @@
|
|||||||
"reset": "Filters verwijderen",
|
"reset": "Filters verwijderen",
|
||||||
"applied": {
|
"applied": {
|
||||||
"f.author": "Auteur",
|
"f.author": "Auteur",
|
||||||
"f.dateIssued.min": "Start datum",
|
"f.dateIssued.min": "Startdatum",
|
||||||
"f.dateIssued.max": "Eind datum",
|
"f.dateIssued.max": "Einddatum",
|
||||||
"f.subject": "Sleutelwoord",
|
"f.subject": "Sleutelwoord",
|
||||||
"f.has_content_in_original_bundle": "Heeft bestanden"
|
"f.has_content_in_original_bundle": "Heeft bestanden"
|
||||||
},
|
},
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
"head": "Auteur"
|
"head": "Auteur"
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"placeholder": "Bereik filter",
|
"placeholder": "Bereikfilter",
|
||||||
"head": "Bereik"
|
"head": "Bereik"
|
||||||
},
|
},
|
||||||
"subject": {
|
"subject": {
|
||||||
@@ -159,27 +159,27 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"title": "DSpace Angular :: Metadata Register",
|
"title": "DSpace Angular :: Metadata Register",
|
||||||
"head": "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": {
|
"schemas": {
|
||||||
"table": {
|
"table": {
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"namespace": "Naamruimte",
|
"namespace": "Naamruimte",
|
||||||
"name": "Naam"
|
"name": "Naam"
|
||||||
},
|
},
|
||||||
"no-items": "Er kunnen geen metadata schema's getoond worden."
|
"no-items": "Er kunnen geen metadataschema's getoond worden."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": {
|
"schema": {
|
||||||
"title": "DSpace Angular :: Metadata Schema Register",
|
"title": "DSpace Angular :: Metadata Schema Register",
|
||||||
"head": "Metadata Schema",
|
"head": "Metadata Schema",
|
||||||
"description": "Dit is het metadata schema voor \"{{namespace}}\".",
|
"description": "Dit is het metadataschema voor \"{{namespace}}\".",
|
||||||
"fields": {
|
"fields": {
|
||||||
"head": "Schema metadata velden",
|
"head": "Schema metadatavelden",
|
||||||
"table": {
|
"table": {
|
||||||
"field": "Veld",
|
"field": "Veld",
|
||||||
"scopenote": "Opmerking over bereik"
|
"scopenote": "Opmerking over bereik"
|
||||||
},
|
},
|
||||||
"no-items": "Er kunnen geen metadata velden getoond worden."
|
"no-items": "Er kunnen geen metadatavelden getoond worden."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bitstream-formats": {
|
"bitstream-formats": {
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
},
|
},
|
||||||
"internal": "intern"
|
"internal": "intern"
|
||||||
},
|
},
|
||||||
"no-items": "Er kunnen geen bitstream formaten getoond worden."
|
"no-items": "Er kunnen geen bitstreamformaten getoond worden."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"pattern": "Deze invoer is niet toegelaten volgens dit patroon: {{ pattern }}.",
|
"pattern": "Deze invoer is niet toegelaten volgens dit patroon: {{ pattern }}.",
|
||||||
"license": {
|
"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."
|
"expired": "Uw sessie is vervallen. Gelieve opnieuw aan te melden."
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"invalid-user": "Ongeldig email adres of wachtwoord."
|
"invalid-user": "Ongeldig e-mailadres of wachtwoord."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
<ng-container *ngVar="(communitiesRDObs | async) as communitiesRD">
|
<ng-container *ngVar="(communitiesRD$ | async) as communitiesRD">
|
||||||
<div *ngIf="communitiesRD?.hasSucceeded " @fadeInOut>
|
<div *ngIf="communitiesRD?.hasSucceeded ">
|
||||||
<h2>{{'home.top-level-communities.head' | translate}}</h2>
|
<h2>{{'home.top-level-communities.head' | translate}}</h2>
|
||||||
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
|
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
|
||||||
<ds-viewable-collection
|
<ds-viewable-collection
|
||||||
[config]="config"
|
[config]="config"
|
||||||
[sortConfig]="sortConfig"
|
[sortConfig]="sortConfig"
|
||||||
[objects]="communitiesRD"
|
[objects]="communitiesRD$ | async"
|
||||||
(paginationChange)="updatePage($event)">
|
[hideGear]="true"
|
||||||
|
(paginationChange)="onPaginationChange($event)">
|
||||||
</ds-viewable-collection>
|
</ds-viewable-collection>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="communitiesRD?.hasFailed " message="{{'error.top-level-communites' | translate}}"></ds-error>
|
<ds-error *ngIf="communitiesRD?.hasFailed " message="{{'error.top-level-communites' | translate}}"></ds-error>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
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 { fadeInOut } from '../../shared/animations/fade';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this component renders the Top-Level Community list
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-top-level-community-list',
|
selector: 'ds-top-level-community-list',
|
||||||
styleUrls: ['./top-level-community-list.component.scss'],
|
styleUrls: ['./top-level-community-list.component.scss'],
|
||||||
@@ -18,9 +22,20 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
|||||||
animations: [fadeInOut]
|
animations: [fadeInOut]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class TopLevelCommunityListComponent {
|
export class TopLevelCommunityListComponent implements OnInit {
|
||||||
communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
/**
|
||||||
|
* 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;
|
config: PaginationComponentOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sorting configuration
|
||||||
|
*/
|
||||||
sortConfig: SortOptions;
|
sortConfig: SortOptions;
|
||||||
|
|
||||||
constructor(private cds: CommunityDataService) {
|
constructor(private cds: CommunityDataService) {
|
||||||
@@ -29,20 +44,34 @@ export class TopLevelCommunityListComponent {
|
|||||||
this.config.pageSize = 5;
|
this.config.pageSize = 5;
|
||||||
this.config.currentPage = 1;
|
this.config.currentPage = 1;
|
||||||
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
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) {
|
ngOnInit() {
|
||||||
this.communitiesRDObs = this.cds.findTop({
|
this.updatePage();
|
||||||
currentPage: data.page,
|
}
|
||||||
elementsPerPage: data.pageSize,
|
|
||||||
sort: { field: data.sortField, direction: data.sortDirection }
|
/**
|
||||||
|
* 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,24 +1,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="filters py-2">
|
<div class="filters py-2">
|
||||||
<a *ngFor="let value of (selectedValues | async)" class="d-flex flex-row"
|
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$"></ds-search-facet-selected-option>
|
||||||
[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 *ngFor="let page of (filterValues$ | async)?.payload">
|
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||||
<div [@facetLoad]="animationState">
|
<div [@facetLoad]="animationState">
|
||||||
<ng-container *ngFor="let value of page.page; let i=index">
|
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$"></ds-search-facet-option>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="clearfix toggle-more-filters">
|
<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 { FilterType } from '../../../search-service/filter-type.model';
|
||||||
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
||||||
import { FILTER_CONFIG } from '../search-filter.service';
|
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({
|
@Component({
|
||||||
selector: 'ds-search-facet-filter-wrapper',
|
selector: 'ds-search-facet-filter-wrapper',
|
||||||
@@ -18,6 +20,10 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() filterConfig: SearchFilterConfig;
|
@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
|
* 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
|
* Initialize and add the filter config to the injector
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.searchFilter = this.getSearchFilter();
|
||||||
this.objectInjector = Injector.create({
|
this.objectInjector = Injector.create({
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: FILTER_CONFIG, useFactory: () => (this.filterConfig), deps: [] }
|
{ 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
|
* Find the correct component based on the filter config's type
|
||||||
*/
|
*/
|
||||||
getSearchFilter() {
|
private getSearchFilter() {
|
||||||
const type: FilterType = this.filterConfig.type;
|
const type: FilterType = this.filterConfig.type;
|
||||||
return renderFilterType(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', () => {
|
describe('when the showMore method is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(filterService, 'incrementPage');
|
spyOn(filterService, 'incrementPage');
|
||||||
|
@@ -22,6 +22,7 @@ import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
|
|||||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
||||||
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
|
import { SearchOptions } from '../../../search-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-filter',
|
selector: 'ds-search-facet-filter',
|
||||||
@@ -65,7 +66,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Emits the active values for this filter
|
* Emits the active values for this filter
|
||||||
*/
|
*/
|
||||||
selectedValues: Observable<string[]>;
|
selectedValues$: Observable<string[]>;
|
||||||
private collapseNextUpdate = true;
|
private collapseNextUpdate = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,6 +74,11 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
animationState = 'loading';
|
animationState = 'loading';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits all current search options available in the search URL
|
||||||
|
*/
|
||||||
|
searchOptions$: Observable<SearchOptions>;
|
||||||
|
|
||||||
constructor(protected searchService: SearchService,
|
constructor(protected searchService: SearchService,
|
||||||
protected filterService: SearchFilterService,
|
protected filterService: SearchFilterService,
|
||||||
protected searchConfigService: SearchConfigurationService,
|
protected searchConfigService: SearchConfigurationService,
|
||||||
@@ -87,10 +93,11 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined));
|
this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined));
|
||||||
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
|
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
|
||||||
this.selectedValues = this.filterService.getSelectedValuesForFilter(this.filterConfig);
|
|
||||||
const searchOptions = this.searchConfigService.searchOptions;
|
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig);
|
||||||
this.subs.push(this.searchConfigService.searchOptions.subscribe(() => this.updateFilterValueList()));
|
this.searchOptions$ = this.searchConfigService.searchOptions;
|
||||||
const facetValues = observableCombineLatest(searchOptions, this.currentPage).pipe(
|
this.subs.push(this.searchOptions$.subscribe(() => this.updateFilterValueList()));
|
||||||
|
const facetValues = observableCombineLatest(this.searchOptions$, this.currentPage).pipe(
|
||||||
map(([options, page]) => {
|
map(([options, page]) => {
|
||||||
return { options, page }
|
return { options, page }
|
||||||
}),
|
}),
|
||||||
@@ -190,7 +197,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
* @param data The string from the input field
|
* @param data The string from the input field
|
||||||
*/
|
*/
|
||||||
onSubmit(data: any) {
|
onSubmit(data: any) {
|
||||||
this.selectedValues.pipe(take(1)).subscribe((selectedValues) => {
|
this.selectedValues$.pipe(take(1)).subscribe((selectedValues) => {
|
||||||
if (isNotEmpty(data)) {
|
if (isNotEmpty(data)) {
|
||||||
this.router.navigate([this.getSearchLink()], {
|
this.router.navigate([this.getSearchLink()], {
|
||||||
queryParams:
|
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) {
|
onClick(data: any) {
|
||||||
this.filter = data;
|
this.filter = data;
|
||||||
}
|
}
|
||||||
@@ -215,34 +226,6 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
return hasValue(o);
|
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
|
* Unsubscribe from all subscriptions
|
||||||
*/
|
*/
|
||||||
@@ -259,7 +242,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
findSuggestions(data): void {
|
findSuggestions(data): void {
|
||||||
if (isNotEmpty(data)) {
|
if (isNotEmpty(data)) {
|
||||||
this.searchConfigService.searchOptions.pipe(take(1)).subscribe(
|
this.searchOptions$.pipe(take(1)).subscribe(
|
||||||
(options) => {
|
(options) => {
|
||||||
this.filterSearchResults = this.searchService.getFacetValuesFor(this.filterConfig, 1, options, data.toLowerCase())
|
this.filterSearchResults = this.searchService.getFacetValuesFor(this.filterConfig, 1, options, data.toLowerCase())
|
||||||
.pipe(
|
.pipe(
|
||||||
@@ -290,6 +273,13 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
getDisplayValue(facet: FacetValue, query: string): string {
|
getDisplayValue(facet: FacetValue, query: string): string {
|
||||||
return new EmphasizePipe().transform(facet.value, query) + ' (' + facet.count + ')';
|
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', [
|
export const facetLoad = trigger('facetLoad', [
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Action } from '@ngrx/store';
|
import { Action } from '@ngrx/store';
|
||||||
|
|
||||||
import { type } from '../../../shared/ngrx/type';
|
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
|
* For each action type in an action group, make a simple
|
||||||
@@ -12,9 +13,8 @@ import { type } from '../../../shared/ngrx/type';
|
|||||||
*/
|
*/
|
||||||
export const SearchFilterActionTypes = {
|
export const SearchFilterActionTypes = {
|
||||||
COLLAPSE: type('dspace/search-filter/COLLAPSE'),
|
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'),
|
EXPAND: type('dspace/search-filter/EXPAND'),
|
||||||
INITIAL_EXPAND: type('dspace/search-filter/INITIAL_EXPAND'),
|
|
||||||
TOGGLE: type('dspace/search-filter/TOGGLE'),
|
TOGGLE: type('dspace/search-filter/TOGGLE'),
|
||||||
DECREMENT_PAGE: type('dspace/search-filter/DECREMENT_PAGE'),
|
DECREMENT_PAGE: type('dspace/search-filter/DECREMENT_PAGE'),
|
||||||
INCREMENT_PAGE: type('dspace/search-filter/INCREMENT_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 {
|
export class SearchFilterInitializeAction extends SearchFilterAction {
|
||||||
type = SearchFilterActionTypes.INITIAL_COLLAPSE;
|
type = SearchFilterActionTypes.INITIALIZE;
|
||||||
}
|
initiallyExpanded;
|
||||||
|
constructor(filter: SearchFilterConfig) {
|
||||||
/**
|
super(filter.name);
|
||||||
* Used to set the initial state of a filter to expanded
|
this.initiallyExpanded = filter.isOpenByDefault;
|
||||||
*/
|
}
|
||||||
export class SearchFilterInitialExpandAction extends SearchFilterAction {
|
|
||||||
type = SearchFilterActionTypes.INITIAL_EXPAND;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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"
|
<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>
|
[ngClass]="(collapsed$ | 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}">
|
<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>
|
<ds-search-facet-filter-wrapper [filterConfig]="filter"></ds-search-facet-filter-wrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -1,7 +1,7 @@
|
|||||||
@import '../../../../styles/variables.scss';
|
@import '../../../../styles/variables.scss';
|
||||||
@import '../../../../styles/mixins.scss';
|
@import '../../../../styles/mixins.scss';
|
||||||
|
|
||||||
:host {
|
:host .facet-filter {
|
||||||
border: 1px solid map-get($theme-colors, light);
|
border: 1px solid map-get($theme-colors, light);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
.search-filter-wrapper.closed {
|
.search-filter-wrapper.closed {
|
||||||
|
@@ -10,6 +10,7 @@ import { SearchService } from '../../search-service/search.service';
|
|||||||
import { SearchFilterComponent } from './search-filter.component';
|
import { SearchFilterComponent } from './search-filter.component';
|
||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { FilterType } from '../../search-service/filter-type.model';
|
import { FilterType } from '../../search-service/filter-type.model';
|
||||||
|
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
||||||
|
|
||||||
describe('SearchFilterComponent', () => {
|
describe('SearchFilterComponent', () => {
|
||||||
let comp: SearchFilterComponent;
|
let comp: SearchFilterComponent;
|
||||||
@@ -33,9 +34,7 @@ describe('SearchFilterComponent', () => {
|
|||||||
},
|
},
|
||||||
expand: (filter) => {
|
expand: (filter) => {
|
||||||
},
|
},
|
||||||
initialCollapse: (filter) => {
|
initializeFilter: (filter) => {
|
||||||
},
|
|
||||||
initialExpand: (filter) => {
|
|
||||||
},
|
},
|
||||||
getSelectedValuesForFilter: (filter) => {
|
getSelectedValuesForFilter: (filter) => {
|
||||||
return observableOf([filterName1, filterName2, filterName3])
|
return observableOf([filterName1, filterName2, filterName3])
|
||||||
@@ -55,6 +54,8 @@ describe('SearchFilterComponent', () => {
|
|||||||
getFacetValuesFor: (filter) => mockResults
|
getFacetValuesFor: (filter) => mockResults
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchConfigServiceStub = {};
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
||||||
@@ -65,6 +66,7 @@ describe('SearchFilterComponent', () => {
|
|||||||
provide: SearchFilterService,
|
provide: SearchFilterService,
|
||||||
useValue: mockFilterService
|
useValue: mockFilterService
|
||||||
},
|
},
|
||||||
|
{ provide: SearchConfigurationService, useValue: searchConfigServiceStub },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(SearchFilterComponent, {
|
}).overrideComponent(SearchFilterComponent, {
|
||||||
@@ -91,32 +93,21 @@ describe('SearchFilterComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the initialCollapse method is triggered', () => {
|
describe('when the initializeFilter method is triggered', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(filterService, 'initialCollapse');
|
spyOn(filterService, 'initializeFilter');
|
||||||
comp.initialCollapse();
|
comp.initializeFilter();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call initialCollapse with the correct filter configuration name', () => {
|
it('should call initialCollapse with the correct filter configuration name', () => {
|
||||||
expect(filterService.initialCollapse).toHaveBeenCalledWith(mockFilterConfig.name)
|
expect(filterService.initializeFilter).toHaveBeenCalledWith(mockFilterConfig)
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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)
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when getSelectedValues is called', () => {
|
describe('when getSelectedValues is called', () => {
|
||||||
let valuesObservable: Observable<string[]>;
|
let valuesObservable: Observable<string[]>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
valuesObservable = comp.getSelectedValues();
|
valuesObservable = (comp as any).getSelectedValues();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an observable containing the existing filters', () => {
|
it('should return an observable containing the existing filters', () => {
|
||||||
@@ -141,7 +132,7 @@ describe('SearchFilterComponent', () => {
|
|||||||
let isActive: Observable<boolean>;
|
let isActive: Observable<boolean>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
filterService.isCollapsed = () => observableOf(true);
|
filterService.isCollapsed = () => observableOf(true);
|
||||||
isActive = comp.isCollapsed();
|
isActive = (comp as any).isCollapsed();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an observable containing true', () => {
|
it('should return an observable containing true', () => {
|
||||||
@@ -156,7 +147,7 @@ describe('SearchFilterComponent', () => {
|
|||||||
let isActive: Observable<boolean>;
|
let isActive: Observable<boolean>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
filterService.isCollapsed = () => observableOf(false);
|
filterService.isCollapsed = () => observableOf(false);
|
||||||
isActive = comp.isCollapsed();
|
isActive = (comp as any).isCollapsed();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an observable containing false', () => {
|
it('should return an observable containing false', () => {
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
|
import { filter, first, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { SearchFilterService } from './search-filter.service';
|
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 { slide } from '../../../shared/animations/slide';
|
||||||
import { isNotEmpty } from '../../../shared/empty.util';
|
import { isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
import { SearchService } from '../../search-service/search.service';
|
||||||
|
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-filter',
|
selector: 'ds-search-filter',
|
||||||
@@ -26,9 +27,24 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* True when the filter is 100% collapsed in the UI
|
* 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
|
* Else, the filter should initially be collapsed
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.getSelectedValues().pipe(take(1)).subscribe((isActive) => {
|
this.selectedValues$ = this.getSelectedValues();
|
||||||
if (this.filter.isOpenByDefault || isNotEmpty(isActive)) {
|
this.active$ = this.isActive();
|
||||||
this.initialExpand();
|
this.collapsed$ = this.isCollapsed();
|
||||||
} else {
|
this.initializeFilter();
|
||||||
this.initialCollapse();
|
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
|
* 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
|
* @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);
|
return this.filterService.isCollapsed(this.filter.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the initial state to collapsed
|
* Sets the initial state of the filter
|
||||||
*/
|
*/
|
||||||
initialCollapse() {
|
initializeFilter() {
|
||||||
this.filterService.initialCollapse(this.filter.name);
|
this.filterService.initializeFilter(this.filter);
|
||||||
this.collapsed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the initial state to expanded
|
|
||||||
*/
|
|
||||||
initialExpand() {
|
|
||||||
this.filterService.initialExpand(this.filter.name);
|
|
||||||
this.collapsed = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Observable<string[]>} Emits a list of all values that are currently active for 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);
|
return this.filterService.getSelectedValuesForFilter(this.filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +99,7 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
finishSlide(event: any): void {
|
finishSlide(event: any): void {
|
||||||
if (event.fromState === 'collapsed') {
|
if (event.fromState === 'collapsed') {
|
||||||
this.collapsed = false;
|
this.closed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +109,31 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
startSlide(event: any): void {
|
startSlide(event: any): void {
|
||||||
if (event.toState === 'collapsed') {
|
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 * as deepFreeze from 'deep-freeze';
|
||||||
import {
|
import {
|
||||||
SearchFilterCollapseAction, SearchFilterExpandAction, SearchFilterIncrementPageAction,
|
SearchFilterCollapseAction, SearchFilterExpandAction, SearchFilterIncrementPageAction,
|
||||||
SearchFilterInitialCollapseAction,
|
|
||||||
SearchFilterInitialExpandAction,
|
|
||||||
SearchFilterToggleAction,
|
SearchFilterToggleAction,
|
||||||
SearchFilterDecrementPageAction, SearchFilterResetPageAction
|
SearchFilterDecrementPageAction, SearchFilterResetPageAction, SearchFilterInitializeAction
|
||||||
} from './search-filter.actions';
|
} from './search-filter.actions';
|
||||||
import { filterReducer } from './search-filter.reducer';
|
import { filterReducer } from './search-filter.reducer';
|
||||||
|
|
||||||
@@ -98,35 +96,39 @@ describe('filterReducer', () => {
|
|||||||
filterReducer(state, action);
|
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 = {};
|
const state = {};
|
||||||
state[filterName2] = { filterCollapsed: false, page: 1 };
|
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);
|
const newState = filterReducer(state, action);
|
||||||
|
|
||||||
expect(newState[filterName1].filterCollapsed).toEqual(true);
|
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 = {};
|
const state = {};
|
||||||
state[filterName2] = { filterCollapsed: true, page: 1 };
|
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);
|
const newState = filterReducer(state, action);
|
||||||
expect(newState[filterName1].filterCollapsed).toEqual(false);
|
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 = {};
|
const state = {};
|
||||||
state[filterName1] = { filterCollapsed: false, page: 1 };
|
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);
|
const newState = filterReducer(state, action);
|
||||||
expect(newState).toEqual(state);
|
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 = {};
|
const state = {};
|
||||||
state[filterName1] = { filterCollapsed: true, page: 1 };
|
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);
|
const newState = filterReducer(state, action);
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
import { SearchFilterAction, SearchFilterActionTypes } from './search-filter.actions';
|
import {
|
||||||
import { isEmpty } from '../../../shared/empty.util';
|
SearchFilterAction,
|
||||||
|
SearchFilterActionTypes,
|
||||||
|
SearchFilterInitializeAction
|
||||||
|
} from './search-filter.actions';
|
||||||
|
import { isEmpty, isNotUndefined } from '../../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface that represents the state for a single filters
|
* Interface that represents the state for a single filters
|
||||||
@@ -28,27 +32,14 @@ export function filterReducer(state = initialState, action: SearchFilterAction):
|
|||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
|
||||||
case SearchFilterActionTypes.INITIAL_COLLAPSE: {
|
case SearchFilterActionTypes.INITIALIZE: {
|
||||||
if (isEmpty(state) || isEmpty(state[action.filterName])) {
|
const initAction = (action as SearchFilterInitializeAction);
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
[action.filterName]: {
|
[action.filterName]: {
|
||||||
filterCollapsed: true,
|
filterCollapsed: !initAction.initiallyExpanded,
|
||||||
page: 1
|
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,8 +5,7 @@ import {
|
|||||||
SearchFilterDecrementPageAction,
|
SearchFilterDecrementPageAction,
|
||||||
SearchFilterExpandAction,
|
SearchFilterExpandAction,
|
||||||
SearchFilterIncrementPageAction,
|
SearchFilterIncrementPageAction,
|
||||||
SearchFilterInitialCollapseAction,
|
SearchFilterInitializeAction,
|
||||||
SearchFilterInitialExpandAction,
|
|
||||||
SearchFilterResetPageAction,
|
SearchFilterResetPageAction,
|
||||||
SearchFilterToggleAction
|
SearchFilterToggleAction
|
||||||
} from './search-filter.actions';
|
} from './search-filter.actions';
|
||||||
@@ -62,23 +61,13 @@ describe('SearchFilterService', () => {
|
|||||||
service = new SearchFilterService(store, routeServiceStub);
|
service = new SearchFilterService(store, routeServiceStub);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the initialCollapse method is triggered', () => {
|
describe('when the initializeFilter method is triggered', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.initialCollapse(mockFilterConfig.name);
|
service.initializeFilter(mockFilterConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('SearchFilterInitialCollapseAction should be dispatched to the store', () => {
|
it('SearchFilterInitializeAction should be dispatched to the store', () => {
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitialCollapseAction(mockFilterConfig.name));
|
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitializeAction(mockFilterConfig));
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { Injectable, InjectionToken } from '@angular/core';
|
import { Injectable, InjectionToken } from '@angular/core';
|
||||||
import { map } from 'rxjs/operators';
|
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||||
import { SearchFiltersState, SearchFilterState } from './search-filter.reducer';
|
import { SearchFiltersState, SearchFilterState } from './search-filter.reducer';
|
||||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import {
|
import {
|
||||||
@@ -8,8 +8,7 @@ import {
|
|||||||
SearchFilterDecrementPageAction,
|
SearchFilterDecrementPageAction,
|
||||||
SearchFilterExpandAction,
|
SearchFilterExpandAction,
|
||||||
SearchFilterIncrementPageAction,
|
SearchFilterIncrementPageAction,
|
||||||
SearchFilterInitialCollapseAction,
|
SearchFilterInitializeAction,
|
||||||
SearchFilterInitialExpandAction,
|
|
||||||
SearchFilterResetPageAction,
|
SearchFilterResetPageAction,
|
||||||
SearchFilterToggleAction
|
SearchFilterToggleAction
|
||||||
} from './search-filter.actions';
|
} from './search-filter.actions';
|
||||||
@@ -17,7 +16,8 @@ import { hasValue, isNotEmpty, } from '../../../shared/empty.util';
|
|||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { RouteService } from '../../../shared/services/route.service';
|
import { RouteService } from '../../../shared/services/route.service';
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
|
import { SearchOptions } from '../../search-options.model';
|
||||||
|
// const spy = create();
|
||||||
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
||||||
|
|
||||||
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
|
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
|
||||||
@@ -60,7 +60,7 @@ export class SearchFilterService {
|
|||||||
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
|
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
|
||||||
const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName);
|
const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName);
|
||||||
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe(
|
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(
|
return observableCombineLatest(values$, prefixValues$).pipe(
|
||||||
@@ -88,13 +88,14 @@ export class SearchFilterService {
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the current page of a given filter
|
* 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
|
* @returns {Observable<boolean>} Emits the current page state of the given filter, if it's unavailable, return 1
|
||||||
*/
|
*/
|
||||||
getPage(filterName: string): Observable<number> {
|
getPage(filterName: string): Observable<number> {
|
||||||
@@ -106,7 +107,8 @@ export class SearchFilterService {
|
|||||||
} else {
|
} else {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}));
|
}),
|
||||||
|
distinctUntilChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,19 +136,11 @@ export class SearchFilterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches an initial collapse action to the store for a given filter
|
* Dispatches an initialize action to the store for a given filter
|
||||||
* @param {string} filterName The filter for which the action is dispatched
|
* @param {SearchFilterConfig} filter The filter for which the action is dispatched
|
||||||
*/
|
*/
|
||||||
public initialCollapse(filterName: string): void {
|
public initializeFilter(filter: SearchFilterConfig): void {
|
||||||
this.store.dispatch(new SearchFilterInitialCollapseAction(filterName));
|
this.store.dispatch(new SearchFilterInitializeAction(filter));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,24 +1,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="filters py-2">
|
<div class="filters py-2">
|
||||||
<a *ngFor="let value of (selectedValues | async)" class="d-flex flex-row"
|
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$"></ds-search-facet-selected-option>
|
||||||
[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 *ngFor="let page of (filterValues$ | async)?.payload">
|
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||||
<div [@facetLoad]="animationState">
|
<div [@facetLoad]="animationState">
|
||||||
<ng-container *ngFor="let value of page.page; let i=index">
|
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$"></ds-search-facet-option>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="clearfix toggle-more-filters">
|
<div class="clearfix toggle-more-filters">
|
||||||
|
@@ -24,16 +24,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||||
<div [@facetLoad]="animationState">
|
<div [@facetLoad]="animationState">
|
||||||
<ng-container *ngFor="let value of page.page; let i=index">
|
<ds-search-facet-range-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value"></ds-search-facet-range-option>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -106,16 +106,6 @@ describe('SearchRangeFilterComponent', () => {
|
|||||||
fixture.detectChanges();
|
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', () => {
|
describe('when the onSubmit method is called with data', () => {
|
||||||
const searchUrl = '/search/path';
|
const searchUrl = '/search/path';
|
||||||
// const data = { [mockFilterConfig.paramName + minSuffix]: '1900', [mockFilterConfig.paramName + maxSuffix]: '1950' };
|
// const data = { [mockFilterConfig.paramName + minSuffix]: '1900', [mockFilterConfig.paramName + maxSuffix]: '1950' };
|
||||||
|
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { combineLatest as observableCombineLatest, Subscription } from 'rxjs';
|
||||||
of as observableOf,
|
|
||||||
combineLatest as observableCombineLatest,
|
|
||||||
Observable,
|
|
||||||
Subscription
|
|
||||||
} from 'rxjs';
|
|
||||||
import { map, startWith } from 'rxjs/operators';
|
import { map, startWith } from 'rxjs/operators';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
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 { hasValue } from '../../../../shared/empty.util';
|
||||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
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.
|
* This component renders a simple item page.
|
||||||
* The route parameter 'id' is used to request the item it represents.
|
* 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.
|
* 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({
|
@Component({
|
||||||
selector: 'ds-search-range-filter',
|
selector: 'ds-search-range-filter',
|
||||||
styleUrls: ['./search-range-filter.component.scss'],
|
styleUrls: ['./search-range-filter.component.scss'],
|
||||||
@@ -85,8 +90,8 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.min = moment(this.filterConfig.minValue, dateFormats).year() || this.min;
|
this.min = moment(this.filterConfig.minValue, dateFormats).year() || this.min;
|
||||||
this.max = moment(this.filterConfig.maxValue, dateFormats).year() || this.max;
|
this.max = moment(this.filterConfig.maxValue, dateFormats).year() || this.max;
|
||||||
const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + minSuffix).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 + maxSuffix).pipe(startWith(undefined));
|
const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined));
|
||||||
this.sub = observableCombineLatest(iniMin, iniMax).pipe(
|
this.sub = observableCombineLatest(iniMin, iniMax).pipe(
|
||||||
map(([min, max]) => {
|
map(([min, max]) => {
|
||||||
const minimum = hasValue(min) ? min : this.min;
|
const minimum = hasValue(min) ? min : this.min;
|
||||||
@@ -96,23 +101,6 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
).subscribe((minmax) => this.range = minmax);
|
).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
|
* 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()], {
|
this.router.navigate([this.getSearchLink()], {
|
||||||
queryParams:
|
queryParams:
|
||||||
{
|
{
|
||||||
[this.filterConfig.paramName + minSuffix]: newMin,
|
[this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: newMin,
|
||||||
[this.filterConfig.paramName + maxSuffix]: newMax
|
[this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: newMax
|
||||||
},
|
},
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
@@ -148,8 +136,4 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
this.sub.unsubscribe();
|
this.sub.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
out(call) {
|
|
||||||
console.log(call);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,26 +1,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="filters py-2">
|
<div class="filters py-2">
|
||||||
<a *ngFor="let value of (selectedValues | async)" class="d-flex flex-row"
|
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$"></ds-search-facet-selected-option>
|
||||||
[routerLink]="[getSearchLink()]"
|
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||||
[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">
|
|
||||||
<div [@facetLoad]="animationState">
|
<div [@facetLoad]="animationState">
|
||||||
<ng-container *ngFor="let page of filterValuesRD?.payload">
|
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$"></ds-search-facet-option>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="clearfix toggle-more-filters">
|
<div class="clearfix toggle-more-filters">
|
||||||
@@ -40,6 +23,5 @@
|
|||||||
(submitSuggestion)="onSubmit($event)"
|
(submitSuggestion)="onSubmit($event)"
|
||||||
(clickSuggestion)="onClick($event)"
|
(clickSuggestion)="onClick($event)"
|
||||||
(findSuggestions)="findSuggestions($event)"
|
(findSuggestions)="findSuggestions($event)"
|
||||||
ngDefaultControl
|
ngDefaultControl></ds-input-suggestions>
|
||||||
></ds-input-suggestions>
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<h3>{{"search.filters.head" | translate}}</h3>
|
<h3>{{"search.filters.head" | translate}}</h3>
|
||||||
<div *ngIf="(filters | async)?.hasSucceeded">
|
<div *ngIf="(filters | async)?.hasSucceeded">
|
||||||
<div *ngFor="let filter of (filters | async)?.payload">
|
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
|
||||||
<ds-search-filter *ngIf="isActive(filter) | async" class="d-block mb-3 p-3" [filter]="filter"></ds-search-filter>
|
<ds-search-filter [filter]="filter"></ds-search-filter>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button">{{"search.filters.reset" | translate}}</a>
|
<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 { Component } from '@angular/core';
|
||||||
import { SearchService } from '../search-service/search.service';
|
import { SearchService } from '../search-service/search.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
|
||||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
|
||||||
import { SearchFilterService } from './search-filter/search-filter.service';
|
import { SearchFilterService } from './search-filter/search-filter.service';
|
||||||
import { getSucceededRemoteData } from '../../core/shared/operators';
|
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
|
* Prevent unnecessary rerendering
|
||||||
* @param {SearchFilterConfig} filter The filter to check for
|
|
||||||
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
|
|
||||||
*/
|
*/
|
||||||
isActive(filterConfig: SearchFilterConfig): Observable<boolean> {
|
trackUpdate(index, config: SearchFilterConfig) {
|
||||||
return this.filterService.getSelectedValuesForFilter(filterConfig).pipe(
|
return config ? config.name : undefined;
|
||||||
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),);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -62,7 +62,6 @@ export class SearchPageComponent implements OnInit {
|
|||||||
constructor(private service: SearchService,
|
constructor(private service: SearchService,
|
||||||
private sidebarService: SearchSidebarService,
|
private sidebarService: SearchSidebarService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
private filterService: SearchFilterService,
|
|
||||||
private searchConfigService: SearchConfigurationService) {
|
private searchConfigService: SearchConfigurationService) {
|
||||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,9 @@ import { SearchFacetFilterWrapperComponent } from './search-filters/search-filte
|
|||||||
import { SearchBooleanFilterComponent } from './search-filters/search-filter/search-boolean-filter/search-boolean-filter.component';
|
import { SearchBooleanFilterComponent } from './search-filters/search-filter/search-boolean-filter/search-boolean-filter.component';
|
||||||
import { SearchHierarchyFilterComponent } from './search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component';
|
import { SearchHierarchyFilterComponent } from './search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component';
|
||||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
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 = [
|
const effects = [
|
||||||
SearchSidebarEffects
|
SearchSidebarEffects
|
||||||
@@ -58,6 +61,9 @@ const effects = [
|
|||||||
SearchTextFilterComponent,
|
SearchTextFilterComponent,
|
||||||
SearchHierarchyFilterComponent,
|
SearchHierarchyFilterComponent,
|
||||||
SearchBooleanFilterComponent,
|
SearchBooleanFilterComponent,
|
||||||
|
SearchFacetOptionComponent,
|
||||||
|
SearchFacetSelectedOptionComponent,
|
||||||
|
SearchFacetRangeOptionComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SearchSidebarService,
|
SearchSidebarService,
|
||||||
@@ -76,6 +82,9 @@ const effects = [
|
|||||||
SearchTextFilterComponent,
|
SearchTextFilterComponent,
|
||||||
SearchHierarchyFilterComponent,
|
SearchHierarchyFilterComponent,
|
||||||
SearchBooleanFilterComponent,
|
SearchBooleanFilterComponent,
|
||||||
|
SearchFacetOptionComponent,
|
||||||
|
SearchFacetSelectedOptionComponent,
|
||||||
|
SearchFacetRangeOptionComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -117,7 +117,7 @@ describe('SearchConfigurationService', () => {
|
|||||||
|
|
||||||
describe('when subscribeToSearchOptions is called', () => {
|
describe('when subscribeToSearchOptions is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.subscribeToSearchOptions(defaults)
|
(service as any).subscribeToSearchOptions(defaults)
|
||||||
});
|
});
|
||||||
it('should call all getters it needs, but not call any others', () => {
|
it('should call all getters it needs, but not call any others', () => {
|
||||||
expect(service.getCurrentPagination).not.toHaveBeenCalled();
|
expect(service.getCurrentPagination).not.toHaveBeenCalled();
|
||||||
@@ -131,7 +131,7 @@ describe('SearchConfigurationService', () => {
|
|||||||
|
|
||||||
describe('when subscribeToPaginatedSearchOptions is called', () => {
|
describe('when subscribeToPaginatedSearchOptions is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.subscribeToPaginatedSearchOptions(defaults);
|
(service as any).subscribeToPaginatedSearchOptions(defaults);
|
||||||
});
|
});
|
||||||
it('should call all getters it needs', () => {
|
it('should call all getters it needs', () => {
|
||||||
expect(service.getCurrentPagination).toHaveBeenCalled();
|
expect(service.getCurrentPagination).toHaveBeenCalled();
|
||||||
|
@@ -186,7 +186,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
* @param {SearchOptions} defaults Default values for when no parameters are available
|
* @param {SearchOptions} defaults Default values for when no parameters are available
|
||||||
* @returns {Subscription} The subscription to unsubscribe from
|
* @returns {Subscription} The subscription to unsubscribe from
|
||||||
*/
|
*/
|
||||||
subscribeToSearchOptions(defaults: SearchOptions): Subscription {
|
private subscribeToSearchOptions(defaults: SearchOptions): Subscription {
|
||||||
return observableMerge(
|
return observableMerge(
|
||||||
this.getScopePart(defaults.scope),
|
this.getScopePart(defaults.scope),
|
||||||
this.getQueryPart(defaults.query),
|
this.getQueryPart(defaults.query),
|
||||||
@@ -204,7 +204,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
* @param {PaginatedSearchOptions} defaults Default values for when no parameters are available
|
* @param {PaginatedSearchOptions} defaults Default values for when no parameters are available
|
||||||
* @returns {Subscription} The subscription to unsubscribe from
|
* @returns {Subscription} The subscription to unsubscribe from
|
||||||
*/
|
*/
|
||||||
subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription {
|
private subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription {
|
||||||
return observableMerge(
|
return observableMerge(
|
||||||
this.getPaginationPart(defaults.pagination),
|
this.getPaginationPart(defaults.pagination),
|
||||||
this.getSortPart(defaults.sort),
|
this.getSortPart(defaults.sort),
|
||||||
|
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 { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
import { CoreState } from '../core.reducers';
|
||||||
import { IndexName } from '../index/index.reducer';
|
import { coreSelector } from '../core.selectors';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer';
|
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 {
|
import {
|
||||||
AddPatchObjectCacheAction,
|
AddPatchObjectCacheAction,
|
||||||
AddToObjectCacheAction,
|
AddToObjectCacheAction,
|
||||||
ApplyPatchObjectCacheAction,
|
ApplyPatchObjectCacheAction,
|
||||||
RemoveFromObjectCacheAction
|
RemoveFromObjectCacheAction
|
||||||
} from './object-cache.actions';
|
} from './object-cache.actions';
|
||||||
import { hasNoValue, isNotEmpty } from '../../shared/empty.util';
|
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer';
|
||||||
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 { AddToSSBAction } from './server-sync-buffer.actions';
|
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
|
* A service to interact with the object cache
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators';
|
import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
|
import { coreSelector } from '../core.selectors';
|
||||||
import {
|
import {
|
||||||
AddToSSBAction,
|
AddToSSBAction,
|
||||||
CommitSSBAction,
|
CommitSSBAction,
|
||||||
@@ -9,7 +10,7 @@ import {
|
|||||||
} from './server-sync-buffer.actions';
|
} from './server-sync-buffer.actions';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
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 { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
|
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
|
||||||
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||||
|
@@ -4,7 +4,7 @@ import {
|
|||||||
} from '@ngrx/store';
|
} from '@ngrx/store';
|
||||||
|
|
||||||
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
|
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
|
||||||
import { indexReducer, IndexState } from './index/index.reducer';
|
import { indexReducer, MetaIndexState } from './index/index.reducer';
|
||||||
import { requestReducer, RequestState } from './data/request.reducer';
|
import { requestReducer, RequestState } from './data/request.reducer';
|
||||||
import { authReducer, AuthState } from './auth/auth.reducer';
|
import { authReducer, AuthState } from './auth/auth.reducer';
|
||||||
import { jsonPatchOperationsReducer, JsonPatchOperationsState } from './json-patch/json-patch-operations.reducer';
|
import { jsonPatchOperationsReducer, JsonPatchOperationsState } from './json-patch/json-patch-operations.reducer';
|
||||||
@@ -19,7 +19,7 @@ export interface CoreState {
|
|||||||
'cache/syncbuffer': ServerSyncBufferState,
|
'cache/syncbuffer': ServerSyncBufferState,
|
||||||
'cache/object-updates': ObjectUpdatesState
|
'cache/object-updates': ObjectUpdatesState
|
||||||
'data/request': RequestState,
|
'data/request': RequestState,
|
||||||
'index': IndexState,
|
'index': MetaIndexState,
|
||||||
'auth': AuthState,
|
'auth': AuthState,
|
||||||
'json/patch': JsonPatchOperationsState
|
'json/patch': JsonPatchOperationsState
|
||||||
}
|
}
|
||||||
@@ -33,5 +33,3 @@ export const coreReducers: ActionReducerMap<CoreState> = {
|
|||||||
'auth': authReducer,
|
'auth': authReducer,
|
||||||
'json/patch': jsonPatchOperationsReducer
|
'json/patch': jsonPatchOperationsReducer
|
||||||
};
|
};
|
||||||
|
|
||||||
export const coreSelector = createFeatureSelector<CoreState>('core');
|
|
||||||
|
7
src/app/core/core.selectors.ts
Normal file
7
src/app/core/core.selectors.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createFeatureSelector } from '@ngrx/store';
|
||||||
|
import { CoreState } from './core.reducers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base selector to select the core state from the store
|
||||||
|
*/
|
||||||
|
export const coreSelector = createFeatureSelector<CoreState>('core');
|
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import { coreSelector, CoreState } from '../../core.reducers';
|
import { CoreState } from '../../core.reducers';
|
||||||
|
import { coreSelector } from '../../core.selectors';
|
||||||
import {
|
import {
|
||||||
FieldState,
|
FieldState,
|
||||||
FieldUpdates,
|
FieldUpdates,
|
||||||
|
@@ -2,6 +2,7 @@ import * as ngrx from '@ngrx/store';
|
|||||||
import { ActionsSubject, Store } from '@ngrx/store';
|
import { ActionsSubject, Store } from '@ngrx/store';
|
||||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
import { BehaviorSubject, EMPTY, of as observableOf } from 'rxjs';
|
import { BehaviorSubject, EMPTY, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
|
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
|
||||||
import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service';
|
import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
@@ -4,20 +4,75 @@ import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
|||||||
import { Observable, race as observableRace } from 'rxjs';
|
import { Observable, race as observableRace } from 'rxjs';
|
||||||
import { filter, mergeMap, take } from 'rxjs/operators';
|
import { filter, mergeMap, take } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { coreSelector, CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { IndexName, IndexState } from '../index/index.reducer';
|
import { IndexName, IndexState, MetaIndexState } from '../index/index.reducer';
|
||||||
import { pathSelector } from '../shared/selectors';
|
import {
|
||||||
|
originalRequestUUIDFromRequestUUIDSelector,
|
||||||
|
requestIndexSelector,
|
||||||
|
uuidFromHrefSelector
|
||||||
|
} from '../index/index.selectors';
|
||||||
import { UUIDService } from '../shared/uuid.service';
|
import { UUIDService } from '../shared/uuid.service';
|
||||||
import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions';
|
import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions';
|
||||||
import { GetRequest, RestRequest } from './request.models';
|
import { GetRequest, RestRequest } from './request.models';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry, RequestState } from './request.reducer';
|
||||||
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
|
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
import { AddToIndexAction, RemoveFromIndexBySubstringAction } from '../index/index.actions';
|
import { AddToIndexAction, RemoveFromIndexBySubstringAction } from '../index/index.actions';
|
||||||
|
import { coreSelector } from '../core.selectors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base selector function to select the request state in the store
|
||||||
|
*/
|
||||||
|
const requestCacheSelector = createSelector(
|
||||||
|
coreSelector,
|
||||||
|
(state: CoreState) => state['data/request']
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selector function to select a request entry by uuid from the cache
|
||||||
|
* @param uuid The uuid of the request
|
||||||
|
*/
|
||||||
|
const entryFromUUIDSelector = (uuid: string): MemoizedSelector<CoreState, RequestEntry> => createSelector(
|
||||||
|
requestCacheSelector,
|
||||||
|
(state: RequestState) => {
|
||||||
|
return hasValue(state) ? state[uuid] : undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a selector that fetches a list of request UUIDs from a given index substate of which the request href
|
||||||
|
* contains a given substring
|
||||||
|
* @param selector MemoizedSelector to start from
|
||||||
|
* @param name The name of the index substate we're fetching request UUIDs from
|
||||||
|
* @param href Substring that the request's href should contain
|
||||||
|
*/
|
||||||
|
const uuidsFromHrefSubstringSelector =
|
||||||
|
(selector: MemoizedSelector<AppState, IndexState>, href: string): MemoizedSelector<AppState, string[]> => createSelector(
|
||||||
|
selector,
|
||||||
|
(state: IndexState) => getUuidsFromHrefSubstring(state, href)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a list of request UUIDs from a given index substate of which the request href contains a given substring
|
||||||
|
* @param state The IndexState
|
||||||
|
* @param href Substring that the request's href should contain
|
||||||
|
*/
|
||||||
|
const getUuidsFromHrefSubstring = (state: IndexState, href: string): string[] => {
|
||||||
|
let result = [];
|
||||||
|
if (isNotEmpty(state)) {
|
||||||
|
result = Object.values(state)
|
||||||
|
.filter((value: string) => value.startsWith(href));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service to interact with the request state in the store
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RequestService {
|
export class RequestService {
|
||||||
private requestsOnTheirWayToTheStore: string[] = [];
|
private requestsOnTheirWayToTheStore: string[] = [];
|
||||||
@@ -25,51 +80,7 @@ export class RequestService {
|
|||||||
constructor(private objectCache: ObjectCacheService,
|
constructor(private objectCache: ObjectCacheService,
|
||||||
private uuidService: UUIDService,
|
private uuidService: UUIDService,
|
||||||
private store: Store<CoreState>,
|
private store: Store<CoreState>,
|
||||||
private indexStore: Store<IndexState>) {
|
private indexStore: Store<MetaIndexState>) {
|
||||||
}
|
|
||||||
|
|
||||||
private entryFromUUIDSelector(uuid: string): MemoizedSelector<CoreState, RequestEntry> {
|
|
||||||
return pathSelector<CoreState, RequestEntry>(coreSelector, 'data/request', uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
private uuidFromHrefSelector(href: string): MemoizedSelector<CoreState, string> {
|
|
||||||
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.REQUEST, href);
|
|
||||||
}
|
|
||||||
|
|
||||||
private originalUUIDFromUUIDSelector(uuid: string): MemoizedSelector<CoreState, string> {
|
|
||||||
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.UUID_MAPPING, uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a selector that fetches a list of request UUIDs from a given index substate of which the request href
|
|
||||||
* contains a given substring
|
|
||||||
* @param selector MemoizedSelector to start from
|
|
||||||
* @param name The name of the index substate we're fetching request UUIDs from
|
|
||||||
* @param href Substring that the request's href should contain
|
|
||||||
*/
|
|
||||||
private uuidsFromHrefSubstringSelector(selector: MemoizedSelector<any, IndexState>, name: string, href: string): MemoizedSelector<any, string[]> {
|
|
||||||
return createSelector(selector, (state: IndexState) => this.getUuidsFromHrefSubstring(state, name, href));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a list of request UUIDs from a given index substate of which the request href contains a given substring
|
|
||||||
* @param state The IndexState
|
|
||||||
* @param name The name of the index substate we're fetching request UUIDs from
|
|
||||||
* @param href Substring that the request's href should contain
|
|
||||||
*/
|
|
||||||
private getUuidsFromHrefSubstring(state: IndexState, name: string, href: string): string[] {
|
|
||||||
let result = [];
|
|
||||||
if (isNotEmpty(state)) {
|
|
||||||
const subState = state[name];
|
|
||||||
if (isNotEmpty(subState)) {
|
|
||||||
for (const value in subState) {
|
|
||||||
if (value.indexOf(href) > -1) {
|
|
||||||
result = [...result, subState[value]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
generateRequestId(): string {
|
generateRequestId(): string {
|
||||||
@@ -100,11 +111,11 @@ export class RequestService {
|
|||||||
*/
|
*/
|
||||||
getByUUID(uuid: string): Observable<RequestEntry> {
|
getByUUID(uuid: string): Observable<RequestEntry> {
|
||||||
return observableRace(
|
return observableRace(
|
||||||
this.store.pipe(select(this.entryFromUUIDSelector(uuid))),
|
this.store.pipe(select(entryFromUUIDSelector(uuid))),
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
select(this.originalUUIDFromUUIDSelector(uuid)),
|
select(originalRequestUUIDFromRequestUUIDSelector(uuid)),
|
||||||
mergeMap((originalUUID) => {
|
mergeMap((originalUUID) => {
|
||||||
return this.store.pipe(select(this.entryFromUUIDSelector(originalUUID)))
|
return this.store.pipe(select(entryFromUUIDSelector(originalUUID)))
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
@@ -115,7 +126,7 @@ export class RequestService {
|
|||||||
*/
|
*/
|
||||||
getByHref(href: string): Observable<RequestEntry> {
|
getByHref(href: string): Observable<RequestEntry> {
|
||||||
return this.store.pipe(
|
return this.store.pipe(
|
||||||
select(this.uuidFromHrefSelector(href)),
|
select(uuidFromHrefSelector(href)),
|
||||||
mergeMap((uuid: string) => this.getByUUID(uuid))
|
mergeMap((uuid: string) => this.getByUUID(uuid))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -152,7 +163,7 @@ export class RequestService {
|
|||||||
*/
|
*/
|
||||||
removeByHrefSubstring(href: string) {
|
removeByHrefSubstring(href: string) {
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
select(this.uuidsFromHrefSubstringSelector(pathSelector<CoreState, IndexState>(coreSelector, 'index'), IndexName.REQUEST, href)),
|
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
||||||
take(1)
|
take(1)
|
||||||
).subscribe((uuids: string[]) => {
|
).subscribe((uuids: string[]) => {
|
||||||
for (const uuid of uuids) {
|
for (const uuid of uuids) {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import * as deepFreeze from 'deep-freeze';
|
import * as deepFreeze from 'deep-freeze';
|
||||||
|
|
||||||
import { IndexName, indexReducer, IndexState } from './index.reducer';
|
import { IndexName, indexReducer, MetaIndexState } from './index.reducer';
|
||||||
import { AddToIndexAction, RemoveFromIndexBySubstringAction, RemoveFromIndexByValueAction } from './index.actions';
|
import { AddToIndexAction, RemoveFromIndexBySubstringAction, RemoveFromIndexByValueAction } from './index.actions';
|
||||||
|
|
||||||
class NullAction extends AddToIndexAction {
|
class NullAction extends AddToIndexAction {
|
||||||
@@ -17,7 +17,7 @@ describe('requestReducer', () => {
|
|||||||
const key2 = '1911e8a4-6939-490c-b58b-a5d70f8d91fb';
|
const key2 = '1911e8a4-6939-490c-b58b-a5d70f8d91fb';
|
||||||
const val1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8';
|
const val1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8';
|
||||||
const val2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb';
|
const val2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb';
|
||||||
const testState: IndexState = {
|
const testState: MetaIndexState = {
|
||||||
[IndexName.OBJECT]: {
|
[IndexName.OBJECT]: {
|
||||||
[key1]: val1
|
[key1]: val1
|
||||||
},[IndexName.REQUEST]: {
|
},[IndexName.REQUEST]: {
|
||||||
|
@@ -1,26 +1,57 @@
|
|||||||
import {
|
import {
|
||||||
|
AddToIndexAction,
|
||||||
IndexAction,
|
IndexAction,
|
||||||
IndexActionTypes,
|
IndexActionTypes,
|
||||||
AddToIndexAction,
|
RemoveFromIndexBySubstringAction,
|
||||||
RemoveFromIndexByValueAction, RemoveFromIndexBySubstringAction
|
RemoveFromIndexByValueAction
|
||||||
} from './index.actions';
|
} from './index.actions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An enum containing all index names
|
||||||
|
*/
|
||||||
export enum IndexName {
|
export enum IndexName {
|
||||||
|
// Contains all objects in the object cache indexed by UUID
|
||||||
OBJECT = 'object/uuid-to-self-link',
|
OBJECT = 'object/uuid-to-self-link',
|
||||||
|
|
||||||
|
// contains all requests in the request cache indexed by UUID
|
||||||
REQUEST = 'get-request/href-to-uuid',
|
REQUEST = 'get-request/href-to-uuid',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the UUIDs of requests that were sent to the server and
|
||||||
|
* have their responses cached, indexed by the UUIDs of requests that
|
||||||
|
* weren't sent because the response they requested was already cached
|
||||||
|
*/
|
||||||
UUID_MAPPING = 'get-request/configured-to-cache-uuid'
|
UUID_MAPPING = 'get-request/configured-to-cache-uuid'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IndexState = {
|
/**
|
||||||
[name in IndexName]: {
|
* The state of a single index
|
||||||
[key: string]: string
|
*/
|
||||||
}
|
export interface IndexState {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state that contains all indices
|
||||||
|
*/
|
||||||
|
export type MetaIndexState = {
|
||||||
|
[name in IndexName]: IndexState
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||||
const initialState: IndexState = Object.create(null);
|
const initialState: MetaIndexState = Object.create(null);
|
||||||
|
|
||||||
export function indexReducer(state = initialState, action: IndexAction): IndexState {
|
/**
|
||||||
|
* The Index Reducer
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* the action to perform on the state
|
||||||
|
* @return MetaIndexState
|
||||||
|
* the new state
|
||||||
|
*/
|
||||||
|
export function indexReducer(state = initialState, action: IndexAction): MetaIndexState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
|
||||||
case IndexActionTypes.ADD: {
|
case IndexActionTypes.ADD: {
|
||||||
@@ -41,7 +72,17 @@ export function indexReducer(state = initialState, action: IndexAction): IndexSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToIndex(state: IndexState, action: AddToIndexAction): IndexState {
|
/**
|
||||||
|
* Add an entry to a given index
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* The MetaIndexState that contains all indices
|
||||||
|
* @param action
|
||||||
|
* The AddToIndexAction containing the value to add, and the index to add it to
|
||||||
|
* @return MetaIndexState
|
||||||
|
* the new state
|
||||||
|
*/
|
||||||
|
function addToIndex(state: MetaIndexState, action: AddToIndexAction): MetaIndexState {
|
||||||
const subState = state[action.payload.name];
|
const subState = state[action.payload.name];
|
||||||
const newSubState = Object.assign({}, subState, {
|
const newSubState = Object.assign({}, subState, {
|
||||||
[action.payload.key]: action.payload.value
|
[action.payload.key]: action.payload.value
|
||||||
@@ -52,7 +93,17 @@ function addToIndex(state: IndexState, action: AddToIndexAction): IndexState {
|
|||||||
return obs;
|
return obs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValueAction): IndexState {
|
/**
|
||||||
|
* Remove a entries that contain a given value from a given index
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* The MetaIndexState that contains all indices
|
||||||
|
* @param action
|
||||||
|
* The RemoveFromIndexByValueAction containing the value to remove, and the index to remove it from
|
||||||
|
* @return MetaIndexState
|
||||||
|
* the new state
|
||||||
|
*/
|
||||||
|
function removeFromIndexByValue(state: MetaIndexState, action: RemoveFromIndexByValueAction): MetaIndexState {
|
||||||
const subState = state[action.payload.name];
|
const subState = state[action.payload.name];
|
||||||
const newSubState = Object.create(null);
|
const newSubState = Object.create(null);
|
||||||
for (const value in subState) {
|
for (const value in subState) {
|
||||||
@@ -66,11 +117,16 @@ function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValu
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove values from the IndexState's substate that contain a given substring
|
* Remove entries that contain a given substring from a given index
|
||||||
* @param state The IndexState to remove values from
|
*
|
||||||
* @param action The RemoveFromIndexByValueAction containing the necessary information to remove the values
|
* @param state
|
||||||
|
* The MetaIndexState that contains all indices
|
||||||
|
* @param action
|
||||||
|
* The RemoveFromIndexByValueAction the substring to remove, and the index to remove it from
|
||||||
|
* @return MetaIndexState
|
||||||
|
* the new state
|
||||||
*/
|
*/
|
||||||
function removeFromIndexBySubstring(state: IndexState, action: RemoveFromIndexByValueAction): IndexState {
|
function removeFromIndexBySubstring(state: MetaIndexState, action: RemoveFromIndexByValueAction): MetaIndexState {
|
||||||
const subState = state[action.payload.name];
|
const subState = state[action.payload.name];
|
||||||
const newSubState = Object.create(null);
|
const newSubState = Object.create(null);
|
||||||
for (const value in subState) {
|
for (const value in subState) {
|
||||||
|
94
src/app/core/index/index.selectors.ts
Normal file
94
src/app/core/index/index.selectors.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { createSelector, MemoizedSelector } from '@ngrx/store';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { coreSelector } from '../core.selectors';
|
||||||
|
import { IndexName, IndexState, MetaIndexState } from './index.reducer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the MetaIndexState based on the CoreSate
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
* a MemoizedSelector to select the MetaIndexState
|
||||||
|
*/
|
||||||
|
export const metaIndexSelector: MemoizedSelector<AppState, MetaIndexState> = createSelector(
|
||||||
|
coreSelector,
|
||||||
|
(state: CoreState) => state.index
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the object index based on the MetaIndexState
|
||||||
|
* It contains all objects in the object cache indexed by UUID
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
* a MemoizedSelector to select the object index
|
||||||
|
*/
|
||||||
|
export const objectIndexSelector: MemoizedSelector<AppState, IndexState> = createSelector(
|
||||||
|
metaIndexSelector,
|
||||||
|
(state: MetaIndexState) => state[IndexName.OBJECT]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the request index based on the MetaIndexState
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
* a MemoizedSelector to select the request index
|
||||||
|
*/
|
||||||
|
export const requestIndexSelector: MemoizedSelector<AppState, IndexState> = createSelector(
|
||||||
|
metaIndexSelector,
|
||||||
|
(state: MetaIndexState) => state[IndexName.REQUEST]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the request UUID mapping index based on the MetaIndexState
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
* a MemoizedSelector to select the request UUID mapping
|
||||||
|
*/
|
||||||
|
export const requestUUIDIndexSelector: MemoizedSelector<AppState, IndexState> = createSelector(
|
||||||
|
metaIndexSelector,
|
||||||
|
(state: MetaIndexState) => state[IndexName.UUID_MAPPING]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the self link of an object in the object-cache based on its UUID
|
||||||
|
*
|
||||||
|
* @param uuid
|
||||||
|
* the UUID for which you want to find the matching self link
|
||||||
|
* @returns
|
||||||
|
* a MemoizedSelector to select the self link
|
||||||
|
*/
|
||||||
|
export const selfLinkFromUuidSelector =
|
||||||
|
(uuid: string): MemoizedSelector<AppState, string> => createSelector(
|
||||||
|
objectIndexSelector,
|
||||||
|
(state: IndexState) => hasValue(state) ? state[uuid] : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the UUID of a GET request based on its href
|
||||||
|
*
|
||||||
|
* @param href
|
||||||
|
* the href of the GET request
|
||||||
|
* @returns
|
||||||
|
* a MemoizedSelector to select the UUID
|
||||||
|
*/
|
||||||
|
export const uuidFromHrefSelector =
|
||||||
|
(href: string): MemoizedSelector<AppState, string> => createSelector(
|
||||||
|
requestIndexSelector,
|
||||||
|
(state: IndexState) => hasValue(state) ? state[href] : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the UUID of a cached request based on the UUID of a request
|
||||||
|
* that wasn't sent because the response was already cached
|
||||||
|
*
|
||||||
|
* @param uuid
|
||||||
|
* The UUID of the new request
|
||||||
|
* @returns
|
||||||
|
* a MemoizedSelector to select the UUID of the cached request
|
||||||
|
*/
|
||||||
|
export const originalRequestUUIDFromRequestUUIDSelector =
|
||||||
|
(uuid: string): MemoizedSelector<AppState, string> => createSelector(
|
||||||
|
requestUUIDIndexSelector,
|
||||||
|
(state: IndexState) => hasValue(state) ? state[uuid] : undefined
|
||||||
|
);
|
@@ -1,17 +0,0 @@
|
|||||||
import { createSelector, MemoizedSelector } from '@ngrx/store';
|
|
||||||
import { hasNoValue, isEmpty } from '../../shared/empty.util';
|
|
||||||
|
|
||||||
export function pathSelector<From, To>(selector: MemoizedSelector<any, From>, ...path: string[]): MemoizedSelector<any, To> {
|
|
||||||
return createSelector(selector, (state: any) => getSubState(state, path));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSubState(state: any, path: string[]) {
|
|
||||||
const current = path[0];
|
|
||||||
const remainingPath = path.slice(1);
|
|
||||||
const subState = state[current];
|
|
||||||
if (hasNoValue(subState) || isEmpty(remainingPath)) {
|
|
||||||
return subState;
|
|
||||||
} else {
|
|
||||||
return getSubState(subState, remainingPath);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -32,5 +32,4 @@ export class ErrorComponent {
|
|||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,11 @@
|
|||||||
[sortConfig]="sortConfig"
|
[sortConfig]="sortConfig"
|
||||||
[objects]="objects"
|
[objects]="objects"
|
||||||
[hideGear]="hideGear"
|
[hideGear]="hideGear"
|
||||||
|
(paginationChange)="onPaginationChange($event)"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||||
|
(sortFieldChange)="onSortFieldChange($event)"
|
||||||
*ngIf="getViewMode()===viewModeEnum.List">
|
*ngIf="getViewMode()===viewModeEnum.List">
|
||||||
</ds-object-list>
|
</ds-object-list>
|
||||||
|
|
||||||
@@ -9,6 +14,11 @@
|
|||||||
[sortConfig]="sortConfig"
|
[sortConfig]="sortConfig"
|
||||||
[objects]="objects"
|
[objects]="objects"
|
||||||
[hideGear]="hideGear"
|
[hideGear]="hideGear"
|
||||||
|
(paginationChange)="onPaginationChange($event)"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||||
|
(sortFieldChange)="onSortFieldChange($event)"
|
||||||
*ngIf="getViewMode()===viewModeEnum.Grid">
|
*ngIf="getViewMode()===viewModeEnum.Grid">
|
||||||
</ds-object-grid>
|
</ds-object-grid>
|
||||||
|
|
||||||
|
@@ -4,24 +4,36 @@ import { ActivatedRoute, NavigationEnd, Params, Router, } from '@angular/router'
|
|||||||
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
|
import { detect } from 'rxjs-spy';
|
||||||
|
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { AddUrlToHistoryAction } from '../history/history.actions';
|
import { AddUrlToHistoryAction } from '../history/history.actions';
|
||||||
import { historySelector } from '../history/selectors';
|
import { historySelector } from '../history/selectors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to keep track of the current query parameters
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RouteService {
|
export class RouteService {
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private router: Router, private store: Store<AppState>) {
|
constructor(private route: ActivatedRoute, private router: Router, private store: Store<AppState>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all query parameter values based on a parameter name
|
||||||
|
* @param paramName The name of the parameter to look for
|
||||||
|
*/
|
||||||
getQueryParameterValues(paramName: string): Observable<string[]> {
|
getQueryParameterValues(paramName: string): Observable<string[]> {
|
||||||
return this.route.queryParamMap.pipe(
|
return this.route.queryParamMap.pipe(
|
||||||
map((params) => [...params.getAll(paramName)]),
|
map((params) => [...params.getAll(paramName)]),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single query parameter values based on a parameter name
|
||||||
|
* @param paramName The name of the parameter to look for
|
||||||
|
*/
|
||||||
getQueryParameterValue(paramName: string): Observable<string> {
|
getQueryParameterValue(paramName: string): Observable<string> {
|
||||||
return this.route.queryParamMap.pipe(
|
return this.route.queryParamMap.pipe(
|
||||||
map((params) => params.get(paramName)),
|
map((params) => params.get(paramName)),
|
||||||
@@ -29,6 +41,10 @@ export class RouteService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the query parameter currently exists in the route
|
||||||
|
* @param paramName The name of the parameter to look for
|
||||||
|
*/
|
||||||
hasQueryParam(paramName: string): Observable<boolean> {
|
hasQueryParam(paramName: string): Observable<boolean> {
|
||||||
return this.route.queryParamMap.pipe(
|
return this.route.queryParamMap.pipe(
|
||||||
map((params) => params.has(paramName)),
|
map((params) => params.has(paramName)),
|
||||||
@@ -36,6 +52,11 @@ export class RouteService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the query parameter with a specific value currently exists in the route
|
||||||
|
* @param paramName The name of the parameter to look for
|
||||||
|
* @param paramValue The value of the parameter to look for
|
||||||
|
*/
|
||||||
hasQueryParamWithValue(paramName: string, paramValue: string): Observable<boolean> {
|
hasQueryParamWithValue(paramName: string, paramValue: string): Observable<boolean> {
|
||||||
return this.route.queryParamMap.pipe(
|
return this.route.queryParamMap.pipe(
|
||||||
map((params) => params.getAll(paramName).indexOf(paramValue) > -1),
|
map((params) => params.getAll(paramName).indexOf(paramValue) > -1),
|
||||||
@@ -43,6 +64,10 @@ export class RouteService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all query parameters of which the parameter name starts with the given prefix
|
||||||
|
* @param prefix The prefix of the parameter name to look for
|
||||||
|
*/
|
||||||
getQueryParamsWithPrefix(prefix: string): Observable<Params> {
|
getQueryParamsWithPrefix(prefix: string): Observable<Params> {
|
||||||
return this.route.queryParamMap.pipe(
|
return this.route.queryParamMap.pipe(
|
||||||
map((qparams) => {
|
map((qparams) => {
|
||||||
@@ -54,13 +79,14 @@ export class RouteService {
|
|||||||
});
|
});
|
||||||
return params;
|
return params;
|
||||||
}),
|
}),
|
||||||
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)));
|
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public saveRouting(): void {
|
public saveRouting(): void {
|
||||||
this.router.events
|
this.router.events
|
||||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||||
.subscribe(({urlAfterRedirects}: NavigationEnd) => {
|
.subscribe(({ urlAfterRedirects }: NavigationEnd) => {
|
||||||
this.store.dispatch(new AddUrlToHistoryAction(urlAfterRedirects))
|
this.store.dispatch(new AddUrlToHistoryAction(urlAfterRedirects))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user