From b9beb08f7a7007efe1e3acbfda7fd175c61bf9fd Mon Sep 17 00:00:00 2001 From: oriol Date: Tue, 21 Jan 2025 16:13:55 +0100 Subject: [PATCH] [sc-11385] Allow selection of label filtering logic --- libs/common/common.babel | 46 +++++++++++++++++++ libs/common/src/assets/i18n/ca.json | 2 + libs/common/src/assets/i18n/en.json | 2 + libs/common/src/assets/i18n/es.json | 2 + libs/common/src/assets/i18n/fr.json | 2 + .../resource-list.component.html | 16 ++++++- .../resource-list.component.scss | 14 ++++++ .../resource-list/resource-list.component.ts | 28 ++++++++--- .../resource-list/resource-list.model.ts | 7 ++- .../resource-list/resource-list.service.ts | 14 +++++- 10 files changed, 124 insertions(+), 9 deletions(-) diff --git a/libs/common/common.babel b/libs/common/common.babel index d54a8856c..161590614 100644 --- a/libs/common/common.babel +++ b/libs/common/common.babel @@ -22312,6 +22312,29 @@ filter + + and + + + + + ca-ES + false + + + en-US + false + + + es-ES + false + + + fr-FR + false + + + apply @@ -22404,6 +22427,29 @@ + + or + + + + + ca-ES + false + + + en-US + false + + + es-ES + false + + + fr-FR + false + + + resource-type diff --git a/libs/common/src/assets/i18n/ca.json b/libs/common/src/assets/i18n/ca.json index ff118eaed..809c71fa9 100644 --- a/libs/common/src/assets/i18n/ca.json +++ b/libs/common/src/assets/i18n/ca.json @@ -923,10 +923,12 @@ "resource.field-text": "Camp de Text", "resource.field-text-body-label": "Cos del text", "resource.field-text-format-label": "Format de text", + "resource.filter.and": "L'operador AND mostra recursos que coincideixen amb totes les etiquetes seleccionades", "resource.filter.apply": "Aplicar", "resource.filter.clear": "Esborra tot", "resource.filter.date-added": "Data de creació", "resource.filter.from": "Des de", + "resource.filter.or": "L'operador OR mostra recursos que coincideixen almenys amb una de les etiquetes seleccionades", "resource.filter.resource-type": "Tipus de recurs", "resource.filter.until": "Fins a", "resource.filtered-by": "Filtrat per", diff --git a/libs/common/src/assets/i18n/en.json b/libs/common/src/assets/i18n/en.json index e993ec714..940a9434f 100644 --- a/libs/common/src/assets/i18n/en.json +++ b/libs/common/src/assets/i18n/en.json @@ -923,10 +923,12 @@ "resource.field-text": "Text Field", "resource.field-text-body-label": "Text body", "resource.field-text-format-label": "Text format", + "resource.filter.and": "AND operator displays resources that match all of the selected labels", "resource.filter.apply": "Apply", "resource.filter.clear": "Clear all", "resource.filter.date-added": "Date added", "resource.filter.from": "From", + "resource.filter.or": "OR operator displays resources that match at least one of the selected labels", "resource.filter.resource-type": "Resource type", "resource.filter.until": "Until", "resource.filtered-by": "Filtered by", diff --git a/libs/common/src/assets/i18n/es.json b/libs/common/src/assets/i18n/es.json index b552c4471..493d82c35 100644 --- a/libs/common/src/assets/i18n/es.json +++ b/libs/common/src/assets/i18n/es.json @@ -923,10 +923,12 @@ "resource.field-text": "Campo de Texto", "resource.field-text-body-label": "Cuerpo de texto", "resource.field-text-format-label": "Formato de texto", + "resource.filter.and": "El operador AND muestra los recursos que coinciden con todas las etiquetas seleccionadas", "resource.filter.apply": "Aplicar", "resource.filter.clear": "Limpiar todo", "resource.filter.date-added": "Fecha de creación", "resource.filter.from": "De", + "resource.filter.or": "El operador OR muestra los recursos que coinciden con al menos una de las etiquetas seleccionadas", "resource.filter.resource-type": "Tipo de recurso", "resource.filter.until": "Hasta", "resource.filtered-by": "Filtrado por", diff --git a/libs/common/src/assets/i18n/fr.json b/libs/common/src/assets/i18n/fr.json index cd2fe5fd6..09f1dbb8a 100644 --- a/libs/common/src/assets/i18n/fr.json +++ b/libs/common/src/assets/i18n/fr.json @@ -923,10 +923,12 @@ "resource.field-text": "Champ de texte", "resource.field-text-body-label": "Corps du texte", "resource.field-text-format-label": "Format texte", + "resource.filter.and": "L'opérateur AND affiche les ressources qui correspondent à toutes les étiquettes sélectionnées", "resource.filter.apply": "Appliquer", "resource.filter.clear": "Tout effacer", "resource.filter.date-added": "Fecha de création", "resource.filter.from": "Depuis", + "resource.filter.or": "L'opérateur OU affiche les ressources qui correspondent à au moins une des étiquettes sélectionnées", "resource.filter.resource-type": "Type de ressource", "resource.filter.until": "Jusqu’à", "resource.filtered-by": "Filtre par", diff --git a/libs/common/src/lib/resources/resource-list/resource-list.component.html b/libs/common/src/lib/resources/resource-list/resource-list.component.html index 811e1c938..97bcfd3eb 100644 --- a/libs/common/src/lib/resources/resource-list/resource-list.component.html +++ b/libs/common/src/lib/resources/resource-list/resource-list.component.html @@ -41,7 +41,7 @@ } -
+
+ @if (selectedClassificationOptions.length > 1) { +
+ + {{ andLogicForLabels ? 'AND' : 'OR' }} + + + {{ (andLogicForLabels ? 'resource.filter.and' : 'resource.filter.or') | translate }} + +
+ }
diff --git a/libs/common/src/lib/resources/resource-list/resource-list.component.scss b/libs/common/src/lib/resources/resource-list/resource-list.component.scss index 9a43feaba..283829c05 100644 --- a/libs/common/src/lib/resources/resource-list/resource-list.component.scss +++ b/libs/common/src/lib/resources/resource-list/resource-list.component.scss @@ -34,6 +34,20 @@ gap: rhythm(1.5); } + .label-filters { + align-items: center; + display: flex; + ::ng-deep .pa-button { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .labels-logic { + border: 1px solid $color-neutral-light; + border-left: 0; + height: rhythm(5); + padding: rhythm(1) rhythm(1.5); + } + } .filtered-by { color: $color-neutral-regular; } diff --git a/libs/common/src/lib/resources/resource-list/resource-list.component.ts b/libs/common/src/lib/resources/resource-list/resource-list.component.ts index a07341e24..932665fe7 100644 --- a/libs/common/src/lib/resources/resource-list/resource-list.component.ts +++ b/libs/common/src/lib/resources/resource-list/resource-list.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Params, Router } from '@angular/router'; import { FormControl, FormGroup } from '@angular/forms'; import { distinctUntilChanged, filter, forkJoin, merge, Observable, of, Subject, take } from 'rxjs'; import { map, switchMap, takeUntil } from 'rxjs/operators'; @@ -44,6 +44,7 @@ export class ResourceListComponent implements OnDestroy { isFiltering = this.resourceListService.filters.pipe(map((filters) => filters.length > 0)); showClearButton = this.resourceListService.filters.pipe(map((filters) => filters.length > 2)); filterOptions: Filters = { classification: [], mainTypes: [], creation: {}, hidden: undefined }; + andLogicForLabels: boolean = false; displayedLabelSets: LabelSets = {}; dateForm = new FormGroup({ @@ -174,6 +175,11 @@ export class ResourceListComponent implements OnDestroy { this.onToggleFilter(); } + updateLabelsLogic(value: boolean) { + this.andLogicForLabels = value; + this.onToggleFilter(); + } + clearFilter(option: OptionModel) { option.selected = !option.selected; this.onToggleFilter(); @@ -182,8 +188,12 @@ export class ResourceListComponent implements OnDestroy { onToggleFilter() { const filters = this.selectedFilters; if (filters.length > 0) { - this.router.navigate(['./'], { relativeTo: this.route, queryParams: { filters } }); - this.resourceListService.filter(filters); + const queryParams: Params = { filters }; + if (this.andLogicForLabels) { + queryParams['labelsLogic'] = 'AND'; + } + this.router.navigate(['./'], { relativeTo: this.route, queryParams }); + this.resourceListService.filter(filters, this.andLogicForLabels ? 'AND' : 'OR'); } else { this.clearFilters(); } @@ -235,13 +245,19 @@ export class ResourceListComponent implements OnDestroy { this.labelSets.pipe(take(1)), this.route.queryParamMap.pipe(take(1)), this.resourceListService.prevFilters.pipe(take(1)), + this.resourceListService.prevLabelsLogic.pipe(take(1)), ]).pipe( - switchMap(([labelSets, queryParams, prevFilters]) => { + switchMap(([labelSets, queryParams, prevFilters, prevLabelsLogic]) => { const faceted = MIME_FACETS.concat(Object.keys(labelSets).map((setId) => `/l/${setId}`)); - return this.getFacets(faceted).pipe(map((facets) => ({ facets, labelSets, queryParams, prevFilters }))); + return this.getFacets(faceted).pipe( + map((facets) => ({ facets, labelSets, queryParams, prevFilters, prevLabelsLogic })), + ); }), - map(({ facets, labelSets, queryParams, prevFilters }) => { + map(({ facets, labelSets, queryParams, prevFilters, prevLabelsLogic }) => { const previousFilters = queryParams.get('preserveFilters') ? prevFilters : queryParams.getAll('filters'); + this.andLogicForLabels = queryParams.get('preserveFilters') + ? prevLabelsLogic === 'AND' + : queryParams.get('labelsLogic') === 'AND'; this.formatFiltersFromFacets(facets, labelSets, previousFilters); if (previousFilters.length > 0) { this.onToggleFilter(); diff --git a/libs/common/src/lib/resources/resource-list/resource-list.model.ts b/libs/common/src/lib/resources/resource-list/resource-list.model.ts index 6274d05e7..e941a61be 100644 --- a/libs/common/src/lib/resources/resource-list/resource-list.model.ts +++ b/libs/common/src/lib/resources/resource-list/resource-list.model.ts @@ -43,6 +43,9 @@ export const DEFAULT_PAGE_SIZE = 25; export const PAGE_SIZES = [25, 50, 100]; export const DEFAULT_SORTING: SortOption = { field: SortField.created, order: 'desc' }; +export type LabelsLogic = 'OR' | 'AND'; +export const DEFAULT_LABELS_LOGIC = 'OR'; + export const RESOURCE_LIST_PREFERENCES = 'NUCLIA_RESOURCE_LIST_PREFERENCES'; export interface BulkAction { @@ -60,11 +63,13 @@ export interface ResourceListParams { sort: SortOption; query: string; filters: string[]; + labelsLogic?: LabelsLogic; } export function getSearchOptions(params: ResourceListParams): CatalogOptions { + const labelsOperator = params.labelsLogic === 'AND' ? FilterOperator.all : FilterOperator.any; const filters: Filter[] = [ { [FilterOperator.any]: params.filters.filter((filter) => filter.startsWith('/icon/')) }, - { [FilterOperator.any]: params.filters.filter((filter) => filter.startsWith('/classification.labels/')) }, + { [labelsOperator]: params.filters.filter((filter) => filter.startsWith('/classification.labels/')) }, { [FilterOperator.any]: params.status ? [`/n/s/${params.status}`] : [] }, ].filter((item) => (Object.values(item)[0] || []).length > 0); const start = params.filters.find((filter) => filter.startsWith(CREATION_START_PREFIX)); diff --git a/libs/common/src/lib/resources/resource-list/resource-list.service.ts b/libs/common/src/lib/resources/resource-list/resource-list.service.ts index ebfa24447..c0a76c9f8 100644 --- a/libs/common/src/lib/resources/resource-list/resource-list.service.ts +++ b/libs/common/src/lib/resources/resource-list/resource-list.service.ts @@ -21,9 +21,11 @@ import { tap, } from 'rxjs'; import { + DEFAULT_LABELS_LOGIC, DEFAULT_PAGE_SIZE, DEFAULT_SORTING, getSearchOptions, + LabelsLogic, ResourceListParams, ResourceWithLabels, searchResources, @@ -71,6 +73,7 @@ export class ResourceListService { query = this._query.asObservable(); private _headerHeight = new BehaviorSubject(0); headerHeight = this._headerHeight.asObservable(); + private _labelsLogic = new BehaviorSubject(DEFAULT_LABELS_LOGIC); private _page = new BehaviorSubject(0); page = this._page.asObservable(); @@ -98,6 +101,12 @@ export class ResourceListService { map(([prev]) => prev), shareReplay(1), ); + prevLabelsLogic = this._labelsLogic.pipe( + startWith(DEFAULT_LABELS_LOGIC), + pairwise(), + map(([prev]) => prev), + shareReplay(1), + ); private _sort: SortOption = DEFAULT_SORTING; private _cursor?: string; @@ -127,15 +136,17 @@ export class ResourceListService { this._sort = DEFAULT_SORTING; this._query.next(''); this._filters.next([]); + this._labelsLogic.next(DEFAULT_LABELS_LOGIC); } get sort(): SortOption { return this._sort; } - filter(filters: string[]) { + filter(filters: string[], labelsLogic: LabelsLogic = DEFAULT_LABELS_LOGIC) { this._page.next(0); this._filters.next(filters); + this._labelsLogic.next(labelsLogic); this._triggerResourceLoad.next({ replaceData: true, updateCount: false }); } @@ -238,6 +249,7 @@ export class ResourceListService { sort: this._sort, query: this._query.value.trim().replace('.', '\\.'), filters: this._filters.value, + labelsLogic: this._labelsLogic.value, }; return forkJoin([ this.labelSets.pipe(take(1)),