Skip to content

Commit

Permalink
[sc-11269] Filter by label sets in resource list
Browse files Browse the repository at this point in the history
  • Loading branch information
operramon committed Jan 17, 2025
1 parent 977c20d commit 9ec007f
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 30 deletions.
17 changes: 14 additions & 3 deletions libs/common/src/lib/resources/resource-filters.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { OptionModel } from '@guillotinaweb/pastanaga-angular';
import { Classification, LabelSets, Search } from '@nuclia/core';
import { Classification, getLabelSetFromFilter, LabelSets, Search } from '@nuclia/core';
import mime from 'mime';

export const MIME_FACETS = ['/icon/application', '/icon/audio', '/icon/image', '/icon/text', '/icon/video', '/icon/message'];
Expand All @@ -12,6 +12,7 @@ export const HIDDEN_PREFIX = '/hidden/';

export interface Filters {
classification: OptionModel[];
labelSets: OptionModel[];
mainTypes: OptionModel[];
languages?: OptionModel[];
creation: {
Expand All @@ -22,15 +23,15 @@ export interface Filters {
}

export function getOptionFromFacet(
facet: { key: string; count: number },
facet: { key: string; count?: number },
label: string,
selected: boolean,
help?: string,
): OptionModel {
return new OptionModel({
id: facet.key,
value: facet.key,
label: `${label} (${facet.count})`,
label: facet.count ? `${label} (${facet.count})` : label,
help,
selected,
});
Expand All @@ -40,11 +41,13 @@ export function formatFiltersFromFacets(allFacets: Search.FacetsResult, queryPar
// Group facets by types
const facetGroups: {
classification: { key: string; count: number }[];
labelSets: { key: string }[];
mainTypes: { key: string; count: number }[];
languages: { key: string; count: number }[];
} = Object.entries(allFacets).reduce(
(groups, [facetId, values]) => {
if (facetId.startsWith('/classification.labels/')) {
groups.labelSets.push({ key: facetId });
Object.entries(values).forEach(([key, count]) => {
groups.classification.push({ key, count });
});
Expand All @@ -61,6 +64,7 @@ export function formatFiltersFromFacets(allFacets: Search.FacetsResult, queryPar
},
{
classification: [] as { key: string; count: number }[],
labelSets: [] as { key: string }[],
mainTypes: [] as { key: string; count: number }[],
languages: [] as { key: string; count: number }[],
},
Expand All @@ -69,6 +73,7 @@ export function formatFiltersFromFacets(allFacets: Search.FacetsResult, queryPar
// Create corresponding filter options
const filters: Filters = {
classification: [],
labelSets: [],
mainTypes: [],
languages: [],
creation: {},
Expand All @@ -80,6 +85,12 @@ export function formatFiltersFromFacets(allFacets: Search.FacetsResult, queryPar
});
filters.classification.sort((a, b) => a.label.localeCompare(b.label));
}
if (facetGroups.labelSets.length > 0) {
facetGroups.labelSets.forEach((facet) => {
const label = getLabelSetFromFilter(facet.key);
filters.labelSets.push(getOptionFromFacet(facet, label, queryParamsFilters.includes(facet.key)));
});
}
if (facetGroups.mainTypes.length > 0) {
facetGroups.mainTypes.forEach((facet) => {
let help: string | undefined = facet.key.substring(5);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
(selectionChange)="updateClassifications($event)"
[disabled]="filterOptions.classification.length === 0"
[fullWidth]="true"
[multiple]="true"
[forceMultiple]="true"
[selectAll]="true"
aspect="basic">
{{ 'resource.classification-column' | translate }}
{{ selectedClassifications.length ? '(' + selectedClassifications.length + ')' : '' }}
Expand Down Expand Up @@ -175,6 +176,9 @@
@if (isFiltering | async) {
<div class="filtered-by title-xxs">{{ 'resource.filtered-by' | translate }}</div>
}
@for (option of selectedLabelSetsOptions; track option.value) {
<pa-chip-closeable (closed)="clearFilter(option)">{{ option.label }}</pa-chip-closeable>
}
@for (option of selectedClassificationOptions; track option.value) {
<pa-chip-closeable (closed)="clearFilter(option)">{{ option.label }}</pa-chip-closeable>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ import {
MIME_FACETS,
trimLabelSets,
} from '../resource-filters.utils';
import { Classification, getFilterFromLabel, getLabelFromFilter, LabelSets, Search } from '@nuclia/core';
import {
Classification,
getFilterFromLabel,
getFilterFromLabelSet,
getLabelFromFilter,
getLabelSetFromFilter,
LabelSets,
Search,
} from '@nuclia/core';

@Component({
templateUrl: './resource-list.component.html',
Expand All @@ -43,7 +51,7 @@ export class ResourceListComponent implements OnDestroy {
labelSets = this.resourceListService.labelSets;
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 };
filterOptions: Filters = { classification: [], labelSets: [], mainTypes: [], creation: {}, hidden: undefined };
displayedLabelSets: LabelSets = {};

dateForm = new FormGroup({
Expand Down Expand Up @@ -93,7 +101,7 @@ export class ResourceListComponent implements OnDestroy {
if (this.isMainView || this.isProcessedView) {
return this.loadFilters();
} else {
this.filterOptions = { classification: [], mainTypes: [], creation: {}, hidden: undefined };
this.filterOptions = { classification: [], labelSets: [], mainTypes: [], creation: {}, hidden: undefined };
this.cdr.markForCheck();
return of(null);
}
Expand Down Expand Up @@ -139,9 +147,23 @@ export class ResourceListComponent implements OnDestroy {
}

updateClassifications(selection: Classification[]) {
const filters = selection.map((label) => getFilterFromLabel(label));
// If all labels from a label set are selected, a label set filter will be used
const labelSetFilters: string[] = [];
Object.entries(this.displayedLabelSets).forEach(([key, labelSet]) => {
const allSelected = labelSet.labels.every((label) =>
selection.some((item) => item.labelset === key && item.label === label.title),
);
if (allSelected) {
labelSetFilters.push(getFilterFromLabelSet(key));
selection = selection.filter((item) => item.labelset !== key);
}
});
this.filterOptions.labelSets.forEach((option) => {
option.selected = labelSetFilters.includes(option.id);
});
const labelFilters = selection.map((label) => getFilterFromLabel(label));
this.filterOptions.classification.forEach((option) => {
option.selected = filters.includes(option.id);
option.selected = labelFilters.includes(option.id);
});
this.cdr.markForCheck();
this.onToggleFilter();
Expand Down Expand Up @@ -193,6 +215,7 @@ export class ResourceListComponent implements OnDestroy {
this.router.navigate(['./'], { relativeTo: this.route, queryParams: {} });
this.resourceListService.filter([]);
this.filterOptions.classification.forEach((option) => (option.selected = false));
this.filterOptions.labelSets.forEach((option) => (option.selected = false));
this.filterOptions.mainTypes.forEach((option) => (option.selected = false));
this.filterOptions.hidden = undefined;
}
Expand All @@ -205,8 +228,23 @@ export class ResourceListComponent implements OnDestroy {
return this.filterOptions.classification.filter((option) => option.selected);
}

get selectedClassifications() {
return this.selectedClassificationOptions.map((option) => getLabelFromFilter(option.id));
get selectedLabelSetsOptions() {
return this.filterOptions.labelSets.filter((option) => option.selected);
}

get selectedClassifications(): Classification[] {
const selectedLabels = this.selectedClassificationOptions.map((option) => getLabelFromFilter(option.id));
const selectedLabelsets = this.selectedLabelSetsOptions.reduce((acc, curr) => {
const labelSet = getLabelSetFromFilter(curr.id);
acc = acc.concat(
(this.displayedLabelSets?.[labelSet]?.labels || []).map((label) => ({
labelset: labelSet,
label: label.title,
})),
);
return acc;
}, [] as Classification[]);
return selectedLabels.concat(selectedLabelsets);
}

get selectedVisibility() {
Expand All @@ -221,12 +259,13 @@ export class ResourceListComponent implements OnDestroy {

get selectedFilters(): string[] {
return this.getSelectionFor('classification')
.concat(this.getSelectionFor('labelSets'))
.concat(this.getSelectionFor('mainTypes'))
.concat(this.selectedDates)
.concat(this.selectedVisibility);
}

private getSelectionFor(type: 'classification' | 'mainTypes') {
private getSelectionFor(type: 'classification' | 'mainTypes' | 'labelSets') {
return this.filterOptions[type].filter((option) => option.selected).map((option) => option.value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<div class="labels-container">
<app-label-dropdown
aspect="basic"
single
forceSingle
[labelSets]="labelSets | async"
(selectionChange)="updateValueWithLabels($event)">
{{ 'search.configuration.search-box.preselected-filters.assistant.filter-value.field-label' | translate }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
icon="chevron-right"
iconOnRight
popupOnRight
[dontCloseOnSelect]="!labelSetSelection"
dontCloseOnSelect
[paPopup]="level2"
[popupVerticalOffset]="-40"
[selected]="selectedLabelSet === labelSet.key || labelSetExpanded === labelSet.key"
Expand Down Expand Up @@ -52,13 +52,26 @@
</pa-input>
</pa-option>
}
@if (
selectAll && (labelSets?.[labelSetExpanded]?.multiple || forceMultiple) && labelValues.length > 1 && !forceSingle
) {
<pa-option
class="level2-option"
dontCloseOnSelect>
<pa-checkbox
[value]="selectedLabelSets[labelSetExpanded]"
(change)="toggleLabelSet(labelSetExpanded)">
{{ 'generic.select_all' | translate }}
</pa-checkbox>
</pa-option>
<pa-separator></pa-separator>
}
@for (labelValue of filteredLabels || labelValues | slice: 0 : maxLabels; track labelValue) {
<pa-option
class="level2-option"
[disabled]="labelSetSelection"
[dontCloseOnSelect]="!single"
[dontCloseOnSelect]="!forceSingle"
(selectOption)="onOptionSelection(labelValue, $event)">
@if (single) {
@if (forceSingle) {
{{ labelValue.label }}
} @else {
<pa-checkbox
Expand Down
50 changes: 37 additions & 13 deletions libs/core/src/lib/label/label-dropdown/label-dropdown.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@ export class LabelDropdownComponent {
@Input() aspect: Aspect = 'solid';
@Input({ required: true }) labelSets?: LabelSets | null;
@Input({ transform: booleanAttribute }) disabled = false;
@Input({ transform: booleanAttribute }) labelSetSelection = false;
@Input({ transform: booleanAttribute }) single = false;
@Input({ transform: booleanAttribute }) multiple = false;
@Input({ transform: booleanAttribute }) forceSingle = false;
@Input({ transform: booleanAttribute }) forceMultiple = false;
@Input({ transform: booleanAttribute }) selectAll = false;
@Input({ transform: booleanAttribute }) fullWidth = false;
@Input() size: Size = 'medium';
@Input()
set selection(value: Classification[]) {
this._selection = [...value];
this.checkboxSelection = this._selection.map((labelValue) => `${labelValue.labelset}${labelValue.label}`);
if (this.selectAll) {
this.selectedLabelSets = this.getSelectedLabelSets();
}
}
get selection() {
return this._selection;
Expand All @@ -39,7 +42,6 @@ export class LabelDropdownComponent {
@Input() selectedLabelSet?: string;

@Output() selectionChange = new EventEmitter<Classification[]>();
@Output() labelSetSelected = new EventEmitter<{ id: string; labelSet: LabelSet }>();
@Output() close = new EventEmitter<void>();

@ViewChild('level2', { read: ElementRef }) level2Element?: ElementRef;
Expand All @@ -49,18 +51,15 @@ export class LabelDropdownComponent {
labelValues: Classification[] = [];
open = false;
checkboxSelection: string[] = [];
selectedLabelSets: { [key: string]: boolean } = {};
filter = '';
filteredLabels?: Classification[];
maxLabels = 100;

onLevel1Selection(labelSetType: string, labelSet: LabelSet) {
if (this.labelSetSelection && this.single) {
this.labelSetSelected.emit({ id: labelSetType, labelSet });
} else {
this.labelSetExpanded = labelSetType;
this.level2Popup?.close();
this.labelValues = labelSet.labels.map((label) => ({ labelset: labelSetType, label: label.title }));
}
this.labelSetExpanded = labelSetType;
this.level2Popup?.close();
this.labelValues = labelSet.labels.map((label) => ({ labelset: labelSetType, label: label.title }));
this.filter = '';
this.filteredLabels = undefined;
}
Expand All @@ -81,7 +80,7 @@ export class LabelDropdownComponent {
let newSelectedLabels;

if (!this.checkboxSelection.includes(checkboxValue)) {
const isMultiple = this.labelSets[labelValue.labelset]?.multiple || this.multiple;
const isMultiple = this.labelSets[labelValue.labelset]?.multiple || this.forceMultiple;
newSelectedLabels = isMultiple
? this.selection.concat([labelValue])
: this.selection.filter((item) => item.labelset !== labelValue.labelset).concat([labelValue]);
Expand All @@ -95,7 +94,7 @@ export class LabelDropdownComponent {
}

onOptionSelection(labelValue: Classification, event: MouseEvent | KeyboardEvent) {
if (this.single) {
if (this.forceSingle) {
this.selection = [labelValue];
this.selectionChange.emit(this.selection);
} else if ((event.target as HTMLElement).tagName === 'LI') {
Expand All @@ -112,4 +111,29 @@ export class LabelDropdownComponent {
.map((label) => ({ labelset, label: label.title }));
}
}

getSelectedLabelSets() {
return Object.entries(this.labelSets || {}).reduce(
(acc, [key, labelSet]) => {
acc[key] = labelSet.labels.every((label) => this.checkboxSelection.includes(`${key}${label.title}`));
return acc;
},
{} as { [key: string]: boolean },
);
}

toggleLabelSet(labelSet: string) {
if (!this.selectedLabelSets[labelSet]) {
let newLabels: Classification[] = [];
(this.labelSets?.[labelSet].labels || []).forEach((label) => {
if (!this.checkboxSelection.includes(`${labelSet}${label.title}`)) {
newLabels.push({ labelset: labelSet, label: label.title });
}
});
this.selection = this.selection.concat(newLabels);
} else {
this.selection = this.selection.filter((label) => label.labelset !== labelSet);
}
this.selectionChange.emit(this.selection);
}
}

0 comments on commit 9ec007f

Please sign in to comment.