mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1837: Fix - Filter Reset und Filter Sync - Removed unused code, logger performance
refactor: improve code formatting and readability in provide-filter.ts and filter-menu components fix: delay filter rollback on close in FilterMenuButtonComponent fix: update filter clear button text and method calls in filter-menu.component.html chore: update package-lock.json to remove unnecessary dev flags and add new dependencies Ref: #5125, #5076
This commit is contained in:
committed by
Nino Righi
parent
c322020c3f
commit
0d202ab97c
@@ -1,6 +1,258 @@
|
||||
# shared-filter
|
||||
# ISA Filter Library
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A powerful and flexible filtering library for Angular applications that provides a complete solution for implementing filters, search functionality, and sorting capabilities.
|
||||
|
||||
## Overview
|
||||
|
||||
The ISA Filter Library is designed to help developers quickly implement advanced filtering capabilities in their Angular applications. It provides a set of reusable components, schemas, and services to handle various types of filters, user inputs, and sorting options.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple Filter Types**: Support for text search, checkboxes, date ranges, and more
|
||||
- **Flexible Architecture**: Easily extendable to support custom filter types
|
||||
- **Modern Angular Patterns**: Built with Angular signals for reactive state management
|
||||
- **Schema-based Validation**: Type-safe filter models with zod schema validation
|
||||
- **Responsive Design**: Mobile-friendly filter UI components
|
||||
- **Declarative API**: Simple and intuitive API for configuring filters
|
||||
- **Sorting Capabilities**: Built-in support for sorting data by various criteria
|
||||
- **TypeScript Support**: Fully typed for excellent developer experience
|
||||
|
||||
## Installation
|
||||
|
||||
The library is part of the ISA Frontend monorepo and can be imported using:
|
||||
|
||||
```typescript
|
||||
import { ... } from '@isa/shared/filter';
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Filter Service
|
||||
|
||||
The `FilterService` is the central component of the library, responsible for managing filter state and providing methods to update and query filter values.
|
||||
|
||||
```typescript
|
||||
// Example of using FilterService
|
||||
export class MyComponent {
|
||||
constructor(private filterService: FilterService) {}
|
||||
|
||||
applyFilters() {
|
||||
// Apply filters and get the query
|
||||
const query = this.filterService.getQuery();
|
||||
// Use the query to fetch filtered data
|
||||
}
|
||||
|
||||
resetFilters() {
|
||||
this.filterService.reset();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Filter Types
|
||||
|
||||
The library supports several filter input types:
|
||||
|
||||
- **Text Filters** (`InputType.Text`): For search queries and text-based filtering
|
||||
- **Checkbox Filters** (`InputType.Checkbox`): For multi-select options
|
||||
- **Date Range Filters** (`InputType.DateRange`): For time-based filtering
|
||||
|
||||
### Query Settings
|
||||
|
||||
Filter configurations are defined using the `QuerySettings` interface, which includes filter groups, input fields, and sort options.
|
||||
|
||||
```typescript
|
||||
// Example QuerySettings configuration
|
||||
const settings: QuerySettings = {
|
||||
filter: [
|
||||
{
|
||||
group: 'products',
|
||||
label: 'Product Filters',
|
||||
input: [
|
||||
{
|
||||
key: 'category',
|
||||
label: 'Category',
|
||||
type: InputType.Checkbox,
|
||||
options: {
|
||||
values: [
|
||||
{ label: 'Electronics', value: 'electronics' },
|
||||
{ label: 'Clothing', value: 'clothing' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
input: [],
|
||||
orderBy: [
|
||||
{ by: 'price', label: 'Price (Low to High)', desc: false },
|
||||
{ by: 'price', label: 'Price (High to Low)', desc: true },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### Filter Menu
|
||||
|
||||
The `FilterMenuComponent` provides a UI for displaying and interacting with filters:
|
||||
|
||||
```html
|
||||
<filter-filter-menu (applied)="onApplyFilters()" (reseted)="onResetFilters()">
|
||||
</filter-filter-menu>
|
||||
```
|
||||
|
||||
### Order By Toolbar
|
||||
|
||||
The `OrderByToolbarComponent` allows users to sort data based on different criteria:
|
||||
|
||||
```html
|
||||
<filter-order-by-toolbar [commitOnToggle]="true" (toggled)="onSortChanged()">
|
||||
</filter-order-by-toolbar>
|
||||
```
|
||||
|
||||
### Input Components
|
||||
|
||||
The library includes specialized components for different input types:
|
||||
|
||||
- `CheckboxInputComponent`: For multi-select options
|
||||
- `DatepickerRangeInputComponent`: For date range selection
|
||||
- `SearchBarInputComponent`: For text-based search
|
||||
|
||||
### Input Renderer
|
||||
|
||||
The `InputRendererComponent` dynamically renders the appropriate input component based on the filter type:
|
||||
|
||||
```html
|
||||
<filter-input-renderer [filterInput]="myFilterInput"></filter-input-renderer>
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Filter Implementation
|
||||
|
||||
```typescript
|
||||
// Import filter library
|
||||
import { QuerySettings, InputType, provideFilter } from '@isa/shared/filter';
|
||||
|
||||
// Define filter settings
|
||||
const settings: QuerySettings = {
|
||||
filter: [
|
||||
{
|
||||
group: 'filter',
|
||||
label: 'Filters',
|
||||
input: [
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
type: InputType.Checkbox,
|
||||
options: {
|
||||
values: [
|
||||
{ label: 'Active', value: 'active' },
|
||||
{ label: 'Inactive', value: 'inactive' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
input: [],
|
||||
orderBy: [
|
||||
{ by: 'name', label: 'Name (A-Z)', desc: false },
|
||||
{ by: 'name', label: 'Name (Z-A)', desc: true },
|
||||
],
|
||||
};
|
||||
|
||||
// Provide filter in component/module
|
||||
@Component({
|
||||
// ...
|
||||
providers: [provideFilter(settings)],
|
||||
})
|
||||
export class MyFilterComponent {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Implementing Filter Logic
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
// ...
|
||||
})
|
||||
export class ProductListComponent {
|
||||
constructor(private filterService: FilterService) {}
|
||||
|
||||
products = computed(() => {
|
||||
const query = this.filterService.getQuery();
|
||||
return this.applyFilters(this.allProducts, query);
|
||||
});
|
||||
|
||||
private applyFilters(products: Product[], query: Query): Product[] {
|
||||
// Implement filtering logic based on query
|
||||
return filteredProducts;
|
||||
}
|
||||
|
||||
onApplyFilters() {
|
||||
// Trigger data refresh or update
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Schema Validation
|
||||
|
||||
The library uses Zod schemas to validate filter configurations and user inputs:
|
||||
|
||||
```typescript
|
||||
// Example of a filter schema
|
||||
export const CheckboxFilterInputSchema = CheckboxFilterInputBaseSchema.extend({
|
||||
options: z
|
||||
.object({
|
||||
values: z.array(CheckboxFilterInputOptionSchema).optional(),
|
||||
max: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
selected: z.array(z.string()).default([]),
|
||||
type: z.literal(InputType.Checkbox),
|
||||
});
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The Filter library is organized into several key modules:
|
||||
|
||||
### Core Module
|
||||
|
||||
- **Schemas**: Type definitions and validation using Zod
|
||||
- **Mappings**: Functions to transform between API and UI models
|
||||
- **Service**: The `FilterService` for state management
|
||||
- **Tokens**: Injection tokens for DI configuration
|
||||
|
||||
### Inputs Module
|
||||
|
||||
- **CheckboxInput**: Multi-select options component
|
||||
- **DatepickerRangeInput**: Date range selection component
|
||||
- **SearchBarInput**: Text-based search component
|
||||
- **InputRenderer**: Dynamic component renderer
|
||||
|
||||
### Menus Module
|
||||
|
||||
- **FilterMenu**: UI for displaying and interacting with filters
|
||||
- **InputMenu**: Specialized component for input configuration
|
||||
|
||||
### Actions Module
|
||||
|
||||
Components for filter operations (apply, reset, etc.)
|
||||
|
||||
### Order By Module
|
||||
|
||||
Components for sorting data based on different criteria
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Group Related Filters**: Use filter groups to organize related filters together
|
||||
2. **Provide Clear Labels**: Use descriptive labels for filters and options
|
||||
3. **Use Appropriate Filter Types**: Choose the right filter type for the data being filtered
|
||||
4. **Handle Filter State**: Properly manage filter state, especially for URL synchronization
|
||||
5. **Combine with NgRx**: For complex applications, consider using the library with NgRx for state management
|
||||
|
||||
## Running unit tests
|
||||
|
||||
|
||||
@@ -77,14 +77,7 @@ export class FilterActionsComponent {
|
||||
* Otherwise, all filter inputs are committed.
|
||||
*/
|
||||
onApply() {
|
||||
const inputKey = this.inputKey();
|
||||
|
||||
if (!inputKey) {
|
||||
this.filterService.commit();
|
||||
} else {
|
||||
this.filterService.commitInput(inputKey);
|
||||
}
|
||||
|
||||
this.filterService.commit();
|
||||
this.applied.emit();
|
||||
}
|
||||
|
||||
@@ -99,11 +92,11 @@ export class FilterActionsComponent {
|
||||
const inputKey = this.inputKey();
|
||||
|
||||
if (!inputKey) {
|
||||
this.filterInputs().forEach((input) => {
|
||||
this.filterService.resetInput(input.key);
|
||||
});
|
||||
this.filterService.resetInput(
|
||||
this.filterInputs().map((input) => input.key),
|
||||
);
|
||||
} else {
|
||||
this.filterService.resetInput(inputKey);
|
||||
this.filterService.resetInput([inputKey]);
|
||||
}
|
||||
|
||||
this.filterService.commit();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, inject, Injectable, signal } from '@angular/core';
|
||||
import { computed, effect, inject, Injectable, signal } from '@angular/core';
|
||||
import { InputType } from '../types';
|
||||
import { getState, patchState, signalState } from '@ngrx/signals';
|
||||
import { filterMapping } from './mappings';
|
||||
@@ -11,9 +11,15 @@ import {
|
||||
QuerySchema,
|
||||
} from './schemas';
|
||||
import { FILTER_ON_COMMIT, FILTER_ON_INIT, QUERY_SETTINGS } from './tokens';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
@Injectable()
|
||||
export class FilterService {
|
||||
#logger = logger(() => ({
|
||||
library: 'shared/filter',
|
||||
class: 'FilterService',
|
||||
}));
|
||||
|
||||
#onInit = inject(FILTER_ON_INIT, { optional: true })?.map((fn) => fn(this));
|
||||
#onCommit = inject(FILTER_ON_COMMIT, { optional: true })?.map((fn) =>
|
||||
fn(this),
|
||||
@@ -23,7 +29,8 @@ export class FilterService {
|
||||
|
||||
private readonly defaultState = filterMapping(this.settings);
|
||||
|
||||
#commitdState = signal(structuredClone(this.defaultState));
|
||||
// Use a more lightweight approach for creating the initial state
|
||||
#commitdState = signal({ ...this.defaultState });
|
||||
|
||||
#state = signalState(this.#commitdState());
|
||||
|
||||
@@ -39,11 +46,19 @@ export class FilterService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ordering field and direction for the filter.
|
||||
*
|
||||
* @param by - The field to order by
|
||||
* @param dir - The direction to order by (asc or desc)
|
||||
* @param options - Optional parameters
|
||||
* @param options.commit - If true, commits the changes immediately
|
||||
*/
|
||||
setOrderBy(
|
||||
by: string,
|
||||
dir: OrderByDirection | undefined,
|
||||
options?: { commit: boolean },
|
||||
) {
|
||||
): void {
|
||||
const orderByList = this.#state.orderBy().map((o) => {
|
||||
if (o.by === by && o.dir === dir) {
|
||||
return { ...o, selected: true };
|
||||
@@ -58,6 +73,14 @@ export class FilterService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text value for an input with the specified key.
|
||||
*
|
||||
* @param key - The key of the input to update
|
||||
* @param value - The new text value to set
|
||||
* @param options - Optional parameters
|
||||
* @param options.commit - If true, commits the changes immediately
|
||||
*/
|
||||
setInputTextValue(
|
||||
key: string,
|
||||
value: string | undefined,
|
||||
@@ -72,8 +95,7 @@ export class FilterService {
|
||||
return { ...input, value };
|
||||
}
|
||||
|
||||
console.warn(`Input type not supported: ${input.type}`);
|
||||
|
||||
this.logUnsupportedInputType(input, 'setInputTextValue');
|
||||
return input;
|
||||
});
|
||||
|
||||
@@ -84,6 +106,14 @@ export class FilterService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected values for a checkbox input with the specified key.
|
||||
*
|
||||
* @param key - The key of the checkbox input to update
|
||||
* @param selected - Array of selected values
|
||||
* @param options - Optional parameters
|
||||
* @param options.commit - If true, commits the changes immediately
|
||||
*/
|
||||
setInputCheckboxValue(
|
||||
key: string,
|
||||
selected: string[],
|
||||
@@ -98,8 +128,7 @@ export class FilterService {
|
||||
return { ...input, selected };
|
||||
}
|
||||
|
||||
console.warn(`Input type not supported: ${input.type}`);
|
||||
|
||||
this.logUnsupportedInputType(input, 'setInputCheckboxValue');
|
||||
return input;
|
||||
});
|
||||
|
||||
@@ -110,6 +139,15 @@ export class FilterService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the date range values for an input with the specified key.
|
||||
*
|
||||
* @param key - The key of the date range input to update
|
||||
* @param start - The start date as a string
|
||||
* @param stop - The end date as a string
|
||||
* @param options - Optional parameters
|
||||
* @param options.commit - If true, commits the changes immediately
|
||||
*/
|
||||
setInputDateRangeValue(
|
||||
key: string,
|
||||
start?: string,
|
||||
@@ -125,8 +163,7 @@ export class FilterService {
|
||||
return { ...input, start, stop };
|
||||
}
|
||||
|
||||
console.warn(`Input type not supported: ${input.type}`);
|
||||
|
||||
this.logUnsupportedInputType(input, 'setInputDateRangeValue');
|
||||
return input;
|
||||
});
|
||||
|
||||
@@ -137,6 +174,20 @@ export class FilterService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to consistently log unsupported input type warnings
|
||||
* @private
|
||||
* @param input - The input that has an unsupported type
|
||||
* @param method - The method name where the warning occurred
|
||||
*/
|
||||
private logUnsupportedInputType(input: FilterInput, method: string): void {
|
||||
this.#logger.warn(`Input type not supported`, () => ({
|
||||
inputType: input.type,
|
||||
inputKey: input.key,
|
||||
method,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the current state is the default state.
|
||||
* This computed property checks if the current state is equal to the default state.
|
||||
@@ -146,7 +197,13 @@ export class FilterService {
|
||||
return isEqual(currentState.inputs, this.defaultState.inputs);
|
||||
});
|
||||
|
||||
isDefaultFilterInput(filterInput: FilterInput) {
|
||||
/**
|
||||
* Checks if a specific filter input is in its default state.
|
||||
*
|
||||
* @param filterInput - The filter input to check
|
||||
* @returns True if the input is in its default state, false otherwise
|
||||
*/
|
||||
isDefaultFilterInput(filterInput: FilterInput): boolean {
|
||||
const currentInputState = this.#state
|
||||
.inputs()
|
||||
.find((i) => i.key === filterInput.key);
|
||||
@@ -160,7 +217,7 @@ export class FilterService {
|
||||
/**
|
||||
* Indicates whether the current state is empty.
|
||||
*/
|
||||
isEmptyFilter = computed(() => {
|
||||
isEmpty = computed(() => {
|
||||
const currentState = getState(this.#state);
|
||||
return currentState.inputs.every((input) => {
|
||||
if (input.type === InputType.Text) {
|
||||
@@ -175,31 +232,50 @@ export class FilterService {
|
||||
return !input.start && !input.stop;
|
||||
}
|
||||
|
||||
console.warn(`Input type not supported`);
|
||||
this.#logger.warn(`Input type not supported`, () => ({
|
||||
input,
|
||||
method: 'isEmptyFilter',
|
||||
}));
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
isEmptyFilterInput(filterInput: FilterInput) {
|
||||
/**
|
||||
* Checks if a specific filter input has an empty value.
|
||||
* For text inputs, checks if the value is falsy.
|
||||
* For checkbox inputs, checks if the selected array is empty.
|
||||
* For date range inputs, checks if both start and stop are falsy.
|
||||
*
|
||||
* @param filterInput - The filter input to check
|
||||
* @returns True if the filter input is empty, false otherwise
|
||||
*/
|
||||
isEmptyFilterInput(filterInput: FilterInput): boolean {
|
||||
const currentInputState = this.#state
|
||||
.inputs()
|
||||
.find((i) => i.key === filterInput.key);
|
||||
|
||||
if (currentInputState?.type === InputType.Text) {
|
||||
if (!currentInputState) {
|
||||
this.#logger.warn(`Input not found`, () => ({
|
||||
inputKey: filterInput.key,
|
||||
method: 'isEmptyFilterInput',
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentInputState.type === InputType.Text) {
|
||||
return !currentInputState.value;
|
||||
}
|
||||
|
||||
if (currentInputState?.type === InputType.Checkbox) {
|
||||
if (currentInputState.type === InputType.Checkbox) {
|
||||
return !currentInputState.selected?.length;
|
||||
}
|
||||
|
||||
if (currentInputState?.type === InputType.DateRange) {
|
||||
if (currentInputState.type === InputType.DateRange) {
|
||||
return !currentInputState.start && !currentInputState.stop;
|
||||
}
|
||||
|
||||
console.warn(`Input type not supported`);
|
||||
|
||||
this.logUnsupportedInputType(currentInputState, 'isEmptyFilterInput');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -207,36 +283,86 @@ export class FilterService {
|
||||
* Reverts the current state to the last committed state.
|
||||
* This method restores the state by applying the previously saved committed state.
|
||||
*/
|
||||
rollback() {
|
||||
rollback(): void {
|
||||
const currentState = getState(this.#state);
|
||||
const committedState = this.#commitdState();
|
||||
if (isEqual(currentState, committedState)) {
|
||||
this.#logger.debug('No changes to rollback', () => ({
|
||||
changes: false,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
this.#logger.debug('Rolling back filter state', () => ({
|
||||
changes: true,
|
||||
currentState,
|
||||
committedState,
|
||||
}));
|
||||
patchState(this.#state, this.#commitdState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolls back the input state for a specific key to its last committed state.
|
||||
* Rolls back the input state for specific keys to their last committed state.
|
||||
* If the input with the given key exists in the committed state, it replaces
|
||||
* the current input with the committed one. Otherwise, the input remains unchanged.
|
||||
*
|
||||
* @param key - The key of the input to roll back.
|
||||
* @param keys - The keys of the inputs to roll back
|
||||
*/
|
||||
rollbackInput(key: string) {
|
||||
rollbackInput(keys: string[]): void {
|
||||
// Find committed inputs for the specified keys
|
||||
const committedInputs = this.#commitdState().inputs;
|
||||
|
||||
// First check if there's anything to rollback
|
||||
const hasChangesToRollback = keys.some((key) => {
|
||||
const currentInput = this.#state.inputs().find((i) => i.key === key);
|
||||
const committedInput = committedInputs.find((i) => i.key === key);
|
||||
return committedInput && !isEqual(currentInput, committedInput);
|
||||
});
|
||||
|
||||
// Only proceed if there are changes to rollback
|
||||
if (!hasChangesToRollback) {
|
||||
this.#logger.debug('No changes to rollback for specified inputs', () => ({
|
||||
inputKeys: keys,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply rollback for changed inputs
|
||||
const inputs = this.#state.inputs().map((input) => {
|
||||
if (input.key !== key) {
|
||||
if (!keys.includes(input.key)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return this.#commitdState().inputs.find((i) => i.key === key) || input;
|
||||
// Get the committed version of this input
|
||||
const committedInput = committedInputs.find((i) => i.key === input.key);
|
||||
return committedInput || input;
|
||||
});
|
||||
|
||||
this.#logger.debug('Rolling back specified inputs', () => ({
|
||||
inputKeys: keys,
|
||||
}));
|
||||
|
||||
patchState(this.#state, { inputs });
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the current state by capturing a snapshot of the internal state.
|
||||
* This method updates the private `#commitdState` property with the result
|
||||
* of the `getState` function applied to the current `#state`.
|
||||
* This method updates the private `#commitdState` property with the current state
|
||||
* and triggers any registered commit callbacks.
|
||||
*/
|
||||
commit() {
|
||||
this.#commitdState.set(getState(this.#state));
|
||||
commit(): void {
|
||||
const currentState = getState(this.#state);
|
||||
const committedState = this.#commitdState();
|
||||
|
||||
if (!isEqual(currentState, committedState)) {
|
||||
this.#commitdState.set(currentState);
|
||||
this.#logger.debug('Filter state committed', () => ({
|
||||
changes: true,
|
||||
}));
|
||||
} else {
|
||||
this.#logger.debug('No changes to commit', () => ({
|
||||
changes: false,
|
||||
}));
|
||||
}
|
||||
|
||||
this.#onCommit?.forEach((commitFn) => {
|
||||
commitFn();
|
||||
@@ -244,50 +370,39 @@ export class FilterService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the input associated with the specified key to the committed state.
|
||||
* Clears all filter values without resetting to default values.
|
||||
* This sets text inputs to undefined, checkbox selections to empty arrays,
|
||||
* and date ranges to undefined for both start and stop.
|
||||
*
|
||||
* This method searches for an input in the current state that matches the given key.
|
||||
* If found, it clones the committed state, updates the corresponding input in the
|
||||
* cloned state with the found input, and then replaces the committed state with the
|
||||
* updated clone.
|
||||
*
|
||||
* @param key - The unique identifier of the input to commit.
|
||||
* @param options - Optional parameters
|
||||
* @param options.commit - If true, commits the changes immediately after clearing
|
||||
*/
|
||||
commitInput(key: string) {
|
||||
const inputToCommit = this.#state.inputs().find((i) => i.key === key);
|
||||
clear(options?: { commit: boolean }): void {
|
||||
// First check if there's anything to clear
|
||||
const hasDataToClear = this.#state.inputs().some((input) => {
|
||||
if (input.type === InputType.Text) {
|
||||
return !!input.value;
|
||||
}
|
||||
|
||||
if (!inputToCommit) {
|
||||
console.warn(`No input found with key: ${key}`);
|
||||
if (input.type === InputType.Checkbox) {
|
||||
return input.selected?.length > 0;
|
||||
}
|
||||
|
||||
if (input.type === InputType.DateRange) {
|
||||
return !!input.start || !!input.stop;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Only proceed if there's data to clear
|
||||
if (!hasDataToClear) {
|
||||
this.#logger.debug('No filter data to clear', () => ({
|
||||
changes: false,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const inputIndex = this.#commitdState().inputs.findIndex(
|
||||
(i) => i.key === key,
|
||||
);
|
||||
|
||||
if (inputIndex === -1) {
|
||||
console.warn(`No committed input found with key: ${key}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#commitdState.set({
|
||||
...this.#commitdState(),
|
||||
inputs: this.#commitdState().inputs.map((input, index) =>
|
||||
index === inputIndex ? inputToCommit : input,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
commitOrderBy() {
|
||||
const orderBy = this.#state.orderBy();
|
||||
|
||||
this.#commitdState.set({
|
||||
...this.#commitdState(),
|
||||
orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
clear(options?: { commit: boolean }) {
|
||||
const inputs = this.#state.inputs().map((input) => {
|
||||
if (input.type === InputType.Text) {
|
||||
return { ...input, value: undefined };
|
||||
@@ -304,6 +419,10 @@ export class FilterService {
|
||||
return input;
|
||||
});
|
||||
|
||||
this.#logger.debug('Clearing filter state', () => ({
|
||||
changes: true,
|
||||
}));
|
||||
|
||||
patchState(this.#state, { inputs });
|
||||
if (options?.commit) {
|
||||
this.commit();
|
||||
@@ -316,49 +435,71 @@ export class FilterService {
|
||||
* @param options - Optional parameters for the reset operation.
|
||||
* @param options.commit - If `true`, the changes will be committed after resetting the state.
|
||||
*/
|
||||
reset(options?: { commit: boolean }) {
|
||||
patchState(this.#state, structuredClone(this.defaultState));
|
||||
reset(options?: { commit: boolean }): void {
|
||||
// Use a more lightweight approach than structuredClone
|
||||
patchState(this.#state, { ...this.defaultState });
|
||||
if (options?.commit) {
|
||||
this.commit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the input with the specified key to its default state.
|
||||
* Resets one or more inputs to their default state.
|
||||
*
|
||||
* @param key - The key of the input to reset.
|
||||
* @param keys - The key or array of keys of the inputs to reset.
|
||||
* @param options - Optional parameters for the reset operation.
|
||||
* @param options.commit - If `true`, commits the changes after resetting the input.
|
||||
* @param options.commit - If `true`, commits the changes after resetting the input(s).
|
||||
*
|
||||
* @remarks
|
||||
* - If no input is found with the specified key, a warning is logged, and the method exits.
|
||||
* - The method updates the state by replacing the input with its default configuration.
|
||||
* - If no input is found with the specified key, a warning is logged for that key.
|
||||
* - The method updates the state by replacing the input(s) with their default configuration.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Reset a single input
|
||||
* filterService.resetInput('exampleKey', { commit: true });
|
||||
*
|
||||
* // Reset multiple inputs
|
||||
* filterService.resetInput(['key1', 'key2'], { commit: true });
|
||||
* ```
|
||||
*/
|
||||
resetInput(key: string, options?: { commit: boolean }) {
|
||||
const defaultFilter = structuredClone(this.defaultState);
|
||||
const inputToReset = defaultFilter.inputs.find((i) => i.key === key);
|
||||
resetInput(keys: string[], options?: { commit: boolean }): void {
|
||||
// Use a more lightweight approach than structuredClone
|
||||
const defaultFilter = { ...this.defaultState };
|
||||
|
||||
if (!inputToReset) {
|
||||
console.warn(`No input found with key: ${key}`);
|
||||
// Find all default inputs that match the provided keys
|
||||
const inputsToReset = keys
|
||||
.map((key) => {
|
||||
const inputToReset = defaultFilter.inputs.find((i) => i.key === key);
|
||||
|
||||
if (!inputToReset) {
|
||||
this.#logger.warn(`No input found with key`, () => ({
|
||||
key,
|
||||
method: 'resetInput',
|
||||
}));
|
||||
}
|
||||
|
||||
return { key, defaultInput: inputToReset };
|
||||
})
|
||||
.filter((item) => item.defaultInput !== undefined);
|
||||
|
||||
if (inputsToReset.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputIndex = this.#state.inputs().findIndex((i) => i.key === key);
|
||||
// Create a set of keys for faster lookups
|
||||
const keysToReset = new Set(inputsToReset.map((item) => item.key));
|
||||
|
||||
if (inputIndex === -1) {
|
||||
console.warn(`No input found with key: ${key}`);
|
||||
return;
|
||||
}
|
||||
const inputs = this.#state.inputs().map((input) => {
|
||||
if (!keysToReset.has(input.key)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const resetItem = inputsToReset.find((item) => item.key === input.key);
|
||||
return resetItem?.defaultInput ?? input;
|
||||
});
|
||||
|
||||
patchState(this.#state, {
|
||||
inputs: this.#state
|
||||
.inputs()
|
||||
.map((input, index) => (index === inputIndex ? inputToReset : input)),
|
||||
inputs,
|
||||
});
|
||||
|
||||
if (options?.commit) {
|
||||
@@ -366,9 +507,15 @@ export class FilterService {
|
||||
}
|
||||
}
|
||||
|
||||
resetOrderBy(options?: { commit: boolean }) {
|
||||
const defaultOrderBy = structuredClone(this.defaultState.orderBy);
|
||||
patchState(this.#state, { orderBy: defaultOrderBy });
|
||||
/**
|
||||
* Resets the orderBy state to its default values.
|
||||
*
|
||||
* @param options - Optional parameters for the reset operation.
|
||||
* @param options.commit - If `true`, the changes will be committed after resetting.
|
||||
*/
|
||||
resetOrderBy(options?: { commit: boolean }): void {
|
||||
// Use a more lightweight approach than structuredClone
|
||||
patchState(this.#state, { orderBy: [...this.defaultState.orderBy] });
|
||||
|
||||
if (options?.commit) {
|
||||
this.commit();
|
||||
@@ -384,11 +531,15 @@ export class FilterService {
|
||||
case InputType.Text:
|
||||
if (input.value) {
|
||||
result[input.key] = input.value;
|
||||
} else {
|
||||
result[input.key] = '';
|
||||
}
|
||||
break;
|
||||
case InputType.Checkbox:
|
||||
if (input.selected) {
|
||||
result[input.key] = input.selected.join(';');
|
||||
} else {
|
||||
result[input.key] = '';
|
||||
}
|
||||
break;
|
||||
case InputType.DateRange:
|
||||
@@ -398,6 +549,8 @@ export class FilterService {
|
||||
result[input.key] = `"${input.start}"-`;
|
||||
} else if (input.stop) {
|
||||
result[input.key] = `-"${input.stop}"`;
|
||||
} else {
|
||||
result[input.key] = '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -406,12 +559,21 @@ export class FilterService {
|
||||
const orderBy = commited.orderBy.find((o) => o.selected);
|
||||
|
||||
if (orderBy) {
|
||||
result['orderBy'] = `${orderBy.by}:${orderBy.dir}`;
|
||||
result['order_by'] = `${orderBy.by}:${orderBy.dir}`;
|
||||
} else {
|
||||
// Ensure 'orderBy' is not included if no order is selected
|
||||
result['order_by'] = '';
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks if the current query parameters match the provided parameters.
|
||||
*
|
||||
* @param params - The parameters to compare with the current query parameters
|
||||
* @returns True if the parameters match, false otherwise
|
||||
*/
|
||||
isQueryParamsEqual(params: Record<string, string>): boolean {
|
||||
const currentParams = this.queryParams();
|
||||
return this.queryParamKeys().every(
|
||||
@@ -424,7 +586,7 @@ export class FilterService {
|
||||
const orderBy = this.orderBy().find((o) => o.dir);
|
||||
|
||||
if (orderBy) {
|
||||
keys.push('orderBy');
|
||||
keys.push('order_by');
|
||||
}
|
||||
|
||||
return keys;
|
||||
@@ -483,29 +645,46 @@ export class FilterService {
|
||||
});
|
||||
});
|
||||
|
||||
parseQueryParams(
|
||||
/**
|
||||
* Parses query parameters into filter state.
|
||||
*
|
||||
* @param params - Record of query parameters to parse
|
||||
* @param options - Optional parameters
|
||||
* @param options.commit - If true, commits the changes immediately after parsing
|
||||
*/ parseQueryParams(
|
||||
params: Record<string, string>,
|
||||
options?: { commit: boolean },
|
||||
): void {
|
||||
this.reset();
|
||||
|
||||
for (const key in params) {
|
||||
if (key === 'orderBy') {
|
||||
if (key === 'order_by') {
|
||||
const [by, dir] = params[key].split(':');
|
||||
const orderBy = this.orderBy().some(
|
||||
(o) => o.by === by && o.dir === dir,
|
||||
);
|
||||
|
||||
if (orderBy) {
|
||||
console.warn(`OrderBy already exists: ${by}:${dir}`);
|
||||
this.#logger.warn(`OrderBy already exists`, () => ({
|
||||
by,
|
||||
dir,
|
||||
method: 'parseQueryParams',
|
||||
}));
|
||||
this.setOrderBy(by, OrderByDirectionSchema.parse(dir));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const inputType = this.inputs().find((i) => i.key === key)?.type;
|
||||
const input = this.inputs().find((i) => i.key === key);
|
||||
if (!input) {
|
||||
this.#logger.warn(`Input not found for key`, () => ({
|
||||
key,
|
||||
method: 'parseQueryParams',
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (inputType) {
|
||||
switch (input.type) {
|
||||
case InputType.Text:
|
||||
this.setInputTextValue(key, params[key]);
|
||||
break;
|
||||
@@ -521,7 +700,7 @@ export class FilterService {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.warn(`Input type not supported: ${inputType}`);
|
||||
this.logUnsupportedInputType(input, 'parseQueryParams');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { DestroyRef, EnvironmentProviders, inject, Provider } from '@angular/core';
|
||||
import {
|
||||
DestroyRef,
|
||||
EnvironmentProviders,
|
||||
inject,
|
||||
Provider,
|
||||
} from '@angular/core';
|
||||
import { QuerySettings } from '../types';
|
||||
import { FilterService, QUERY_SETTINGS } from './filter.service';
|
||||
import { NavigationEnd, NavigationExtras, Router } from '@angular/router';
|
||||
@@ -35,7 +40,9 @@ export function withQuerySettings(querySettings: QuerySettings): FilterFeature {
|
||||
]);
|
||||
}
|
||||
|
||||
export function withQuerySettingsFactory(querySettingsFactory: () => QuerySettings): FilterFeature {
|
||||
export function withQuerySettingsFactory(
|
||||
querySettingsFactory: () => QuerySettings,
|
||||
): FilterFeature {
|
||||
return filterFeature(ProvideFilterKind.QuerySettings, [
|
||||
{ provide: QUERY_SETTINGS, useFactory: querySettingsFactory },
|
||||
]);
|
||||
@@ -44,7 +51,10 @@ export function withQuerySettingsFactory(querySettingsFactory: () => QuerySettin
|
||||
export function withQueryParamsSync({
|
||||
replaceUrl = true,
|
||||
queryParamsHandling = 'merge',
|
||||
}: Pick<NavigationExtras, 'replaceUrl' | 'queryParamsHandling'> = {}): FilterFeature {
|
||||
}: Pick<
|
||||
NavigationExtras,
|
||||
'replaceUrl' | 'queryParamsHandling'
|
||||
> = {}): FilterFeature {
|
||||
function onCommitFactory() {
|
||||
const router = inject(Router);
|
||||
return (filterService: FilterService) => () => {
|
||||
|
||||
@@ -89,7 +89,9 @@ export class FilterMenuButtonComponent {
|
||||
*/
|
||||
this.closed.subscribe(() => {
|
||||
if (this.rollbackOnClose()) {
|
||||
this.#filter.rollback();
|
||||
setTimeout(() => {
|
||||
this.#filter.rollback();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
(click)="filter.clear()"
|
||||
type="button"
|
||||
>
|
||||
<span class="isa-text-body-2-bold" [class.active]="!filter.isEmptyFilter()">
|
||||
<span class="isa-text-body-2-bold" [class.active]="!filter.isEmpty()">
|
||||
Alle abwählen
|
||||
</span>
|
||||
</button>
|
||||
@@ -25,7 +25,11 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-icon class="text-isa-neutral-900" name="isaActionChevronRight" size="1.5rem"></ng-icon>
|
||||
<ng-icon
|
||||
class="text-isa-neutral-900"
|
||||
name="isaActionChevronRight"
|
||||
size="1.5rem"
|
||||
></ng-icon>
|
||||
</button>
|
||||
}
|
||||
} @else {
|
||||
@@ -39,7 +43,10 @@
|
||||
<span> {{ input!.label }} </span>
|
||||
</button>
|
||||
|
||||
<filter-input-renderer class="overflow-scroll" [filterInput]="input!"></filter-input-renderer>
|
||||
<filter-input-renderer
|
||||
class="overflow-scroll"
|
||||
[filterInput]="input!"
|
||||
></filter-input-renderer>
|
||||
}
|
||||
|
||||
<filter-actions
|
||||
|
||||
Reference in New Issue
Block a user