Compare commits
2 Commits
master
...
RED-6408-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c78b00e21a | ||
|
|
4fede31e8c |
59
src/lib/search-screen-filtering/filter.service.ts
Normal file
59
src/lib/search-screen-filtering/filter.service.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { Injectable, signal } from '@angular/core';
|
||||||
|
|
||||||
|
interface Filter {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
expanded: boolean;
|
||||||
|
indeterminate: boolean;
|
||||||
|
checked: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
meta: Record<string, unknown> | undefined;
|
||||||
|
children: Record<string, Omit<Filter, 'children' | 'indeterminate' | 'expanded'>> | undefined;
|
||||||
|
}
|
||||||
|
interface FilterGroup {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
filters: Record<string, Filter>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeObj = <T>(obj: T | undefined) => obj ?? ({} as T);
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SearcnScreenFilterService {
|
||||||
|
readonly #filterGroups = signal<Record<string, FilterGroup> | undefined>(undefined);
|
||||||
|
readonly filterGroups = this.#filterGroups.asReadonly();
|
||||||
|
|
||||||
|
addFilterGroup(newFilterGroup: FilterGroup) {
|
||||||
|
this.#filterGroups.update(filterGroups => ({ ...safeObj(filterGroups), [newFilterGroup.name]: newFilterGroup }));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilterGroup(name: string, updates: Partial<Omit<FilterGroup, 'name'>>) {
|
||||||
|
this.#filterGroups.update(filterGroups => {
|
||||||
|
const current = safeObj(filterGroups)[name];
|
||||||
|
if (!current) {
|
||||||
|
console.warn('[Filter service] Cannot update non-existent group: ', name);
|
||||||
|
return filterGroups;
|
||||||
|
}
|
||||||
|
return { ...filterGroups, [name]: { ...current, ...updates } };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFilterGroup(name: string) {
|
||||||
|
this.#filterGroups.update(filterGroups => {
|
||||||
|
if (!filterGroups) return undefined;
|
||||||
|
const { [name]: _, ...rest } = filterGroups;
|
||||||
|
return Object.keys(rest) ? rest : undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addFilterToGroup(groupName: string, filter: Filter) {
|
||||||
|
this.#filterGroups.update(filterGroups => {
|
||||||
|
const current = safeObj(filterGroups)[groupName];
|
||||||
|
if (!current) {
|
||||||
|
console.warn('[Filter service] Cannot add filter to non-existent group: ', groupName);
|
||||||
|
return filterGroups;
|
||||||
|
}
|
||||||
|
return { ...filterGroups, [groupName]: { ...current, filters: { ...current.filters, [filter.id]: filter } } };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
<div class="page-header">
|
||||||
|
<div *ngIf="pageLabel" class="breadcrumb">{{ pageLabel }}</div>
|
||||||
|
|
||||||
|
<div *ngIf="filters$ | async as filters" class="filters">
|
||||||
|
<ng-content select="[slot=beforeFilters]"></ng-content>
|
||||||
|
|
||||||
|
<div
|
||||||
|
*ngIf="filters.length && searchPosition !== searchPositions.beforeFilters"
|
||||||
|
class="text-muted"
|
||||||
|
translate="filters.filter-by"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="searchPosition === searchPositions.beforeFilters" [ngTemplateOutlet]="searchBar"></ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngFor="let filter of filters; trackBy: trackByLabel">
|
||||||
|
<iqser-popup-filter
|
||||||
|
*ngIf="!filter.hide"
|
||||||
|
[primaryFiltersSlug]="filter.slug"
|
||||||
|
[attr.help-mode-key]="filterHelpModeKey"
|
||||||
|
></iqser-popup-filter>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngFor="let filter$ of filterService.singleFilters">
|
||||||
|
<iqser-single-filter *ngIf="filter$ | async as filter" [filter]="filter"></iqser-single-filter>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="searchPosition === searchPositions.afterFilters" [ngTemplateOutlet]="searchBar"></ng-container>
|
||||||
|
|
||||||
|
<div
|
||||||
|
(click)="resetFilters()"
|
||||||
|
*ngIf="!hideResetButton && (showResetFilters$ | async) === true"
|
||||||
|
[attr.help-mode-key]="'filter_' + helpModeKey + '_list'"
|
||||||
|
class="reset-filters"
|
||||||
|
translate="reset-filters"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="showCloseButton || actionConfigs || buttonConfigs || viewModeSelection" class="actions">
|
||||||
|
<ng-container *ngIf="searchPosition === searchPositions.withActions" [ngTemplateOutlet]="searchBar"></ng-container>
|
||||||
|
|
||||||
|
<ng-container [ngTemplateOutlet]="viewModeSelection"></ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngFor="let config of buttonConfigs; trackBy: trackByLabel">
|
||||||
|
<iqser-icon-button
|
||||||
|
(action)="config.action($event)"
|
||||||
|
*ngIf="!config.hide"
|
||||||
|
[buttonId]="config.label.replace('.', '-')"
|
||||||
|
[icon]="config.icon"
|
||||||
|
[label]="config.label | translate"
|
||||||
|
[type]="config.type"
|
||||||
|
[attr.help-mode-key]="config.helpModeKey"
|
||||||
|
></iqser-icon-button>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<ng-container *ngFor="let config of actionConfigs; trackBy: trackByLabel">
|
||||||
|
<iqser-circle-button
|
||||||
|
(action)="config.action($event)"
|
||||||
|
*ngIf="!config.hide"
|
||||||
|
[buttonId]="config.id"
|
||||||
|
[disabled]="config.disabled$ && (config.disabled$ | async)"
|
||||||
|
[icon]="config.icon"
|
||||||
|
[tooltip]="config.label"
|
||||||
|
[attr.help-mode-key]="config.helpModeKey"
|
||||||
|
></iqser-circle-button>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Extra custom actions here -->
|
||||||
|
<ng-content select="[slot=right]"></ng-content>
|
||||||
|
|
||||||
|
<iqser-circle-button
|
||||||
|
buttonId="close-view-btn"
|
||||||
|
(action)="closeAction.emit()"
|
||||||
|
*ngIf="showCloseButton"
|
||||||
|
[class.ml-6]="actionConfigs"
|
||||||
|
[icon]="'iqser:close'"
|
||||||
|
[attr.help-mode-key]="'edit_dossier_in_dossier'"
|
||||||
|
[tooltip]="'common.close' | translate"
|
||||||
|
></iqser-circle-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #searchBar>
|
||||||
|
<iqser-input-with-action
|
||||||
|
[inputId]="searchInputId"
|
||||||
|
(valueChange)="searchService.searchValue = $event"
|
||||||
|
*ngIf="searchPlaceholder && searchService"
|
||||||
|
[class.mr-8]="searchPosition === searchPositions.beforeFilters"
|
||||||
|
[placeholder]="searchPlaceholder"
|
||||||
|
[value]="searchService.valueChanges$ | async"
|
||||||
|
[width]="searchWidth"
|
||||||
|
></iqser-input-with-action>
|
||||||
|
</ng-template>
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
.actions iqser-input-with-action {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Optional, Output, TemplateRef } from '@angular/core';
|
||||||
|
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||||
|
import { combineLatest, Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
import { CircleButtonComponent, IconButtonComponent, IconButtonTypes } from '../../buttons';
|
||||||
|
import { SearchService } from '../../search';
|
||||||
|
import { FilterService } from '../../filtering';
|
||||||
|
import { filterEach, List } from '../../utils';
|
||||||
|
import { ActionConfig, ButtonConfig, IListable, SearchPosition, SearchPositions } from '../../listing';
|
||||||
|
import { AsyncPipe, NgFor, NgTemplateOutlet } from '@angular/common';
|
||||||
|
import { IqserTranslateModule } from '../../translations';
|
||||||
|
import { InputWithActionComponent } from '../../inputs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'iqser-page-header',
|
||||||
|
templateUrl: './page-header.component.html',
|
||||||
|
styleUrls: ['./page-header.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgTemplateOutlet,
|
||||||
|
AsyncPipe,
|
||||||
|
NgFor,
|
||||||
|
IqserTranslateModule,
|
||||||
|
IconButtonComponent,
|
||||||
|
InputWithActionComponent,
|
||||||
|
CircleButtonComponent,
|
||||||
|
],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class SearchScreenPageHeaderComponent<T extends IListable> {
|
||||||
|
readonly searchPositions = SearchPositions;
|
||||||
|
readonly iconButtonTypes = IconButtonTypes;
|
||||||
|
|
||||||
|
@Input() pageLabel?: string;
|
||||||
|
@Input() searchInputId?: string;
|
||||||
|
@Input() showCloseButton = false;
|
||||||
|
@Input() hideResetButton = false;
|
||||||
|
@Input() actionConfigs?: List<ActionConfig>;
|
||||||
|
@Input() buttonConfigs?: List<ButtonConfig>;
|
||||||
|
@Input() viewModeSelection?: TemplateRef<unknown>;
|
||||||
|
@Input() searchPlaceholder?: string;
|
||||||
|
@Input() searchWidth?: number | 'full';
|
||||||
|
@Input() helpModeKey?: 'dossier' | 'document';
|
||||||
|
@Input() searchPosition: SearchPosition = SearchPositions.afterFilters;
|
||||||
|
@Output() readonly closeAction = new EventEmitter();
|
||||||
|
|
||||||
|
readonly filters$ = this.filterService?.filterGroups$.pipe(filterEach(f => !!f.icon));
|
||||||
|
readonly showResetFilters$ = this.#showResetFilters$;
|
||||||
|
|
||||||
|
constructor(@Optional() readonly filterService: FilterService, @Optional() readonly searchService: SearchService<T>) {}
|
||||||
|
|
||||||
|
get filterHelpModeKey() {
|
||||||
|
return this.helpModeKey ? `filter_${this.helpModeKey}_list` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get #showResetFilters$(): Observable<boolean> {
|
||||||
|
if (!this.filterService) {
|
||||||
|
return of(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return combineLatest([this.filterService.showResetFilters$, this.searchService.valueChanges$]).pipe(
|
||||||
|
map(([showResetFilters, searchValue]) => showResetFilters || !!searchValue),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetFilters(): void {
|
||||||
|
this.filterService.reset();
|
||||||
|
this.searchService.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByLabel<K extends { label?: string }>(_index: number, item: K): string | undefined {
|
||||||
|
return item.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,7 +22,8 @@
|
|||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@biesbjerg/ngx-translate-extract-marker": ["src/lib/translations/ngx-translate-extract-marker"]
|
"@biesbjerg/ngx-translate-extract-marker": ["src/lib/translations/ngx-translate-extract-marker"]
|
||||||
}
|
},
|
||||||
|
"noUncheckedIndexedAccess": true
|
||||||
},
|
},
|
||||||
"include": ["./**/*"],
|
"include": ["./**/*"],
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user