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:
Lorenz Hilpert
2025-05-26 15:02:43 +00:00
committed by Nino Righi
parent c322020c3f
commit 0d202ab97c
17 changed files with 1050 additions and 263 deletions

View File

@@ -61,9 +61,8 @@ export function logger(ctxFn?: () => LoggerContext): LoggerApi {
* @param message The log message.
* @param additionalContext Optional context data specific to this log message.
*/
trace: (message: string, additionalContext?: LoggerContext): void => {
loggingService.trace(
message,
trace: (message: string, additionalContext?: () => LoggerContext): void => {
loggingService.trace(message, () =>
mergeContexts(context, ctxFn, additionalContext),
);
},
@@ -72,9 +71,8 @@ export function logger(ctxFn?: () => LoggerContext): LoggerApi {
* @param message The log message.
* @param additionalContext Optional context data specific to this log message.
*/
debug: (message: string, additionalContext?: LoggerContext): void => {
loggingService.debug(
message,
debug: (message: string, additionalContext?: () => LoggerContext): void => {
loggingService.debug(message, () =>
mergeContexts(context, ctxFn, additionalContext),
);
},
@@ -83,9 +81,8 @@ export function logger(ctxFn?: () => LoggerContext): LoggerApi {
* @param message The log message.
* @param additionalContext Optional context data specific to this log message.
*/
info: (message: string, additionalContext?: LoggerContext): void => {
loggingService.info(
message,
info: (message: string, additionalContext?: () => LoggerContext): void => {
loggingService.info(message, () =>
mergeContexts(context, ctxFn, additionalContext),
);
},
@@ -94,9 +91,8 @@ export function logger(ctxFn?: () => LoggerContext): LoggerApi {
* @param message The log message.
* @param additionalContext Optional context data specific to this log message.
*/
warn: (message: string, additionalContext?: LoggerContext): void => {
loggingService.warn(
message,
warn: (message: string, additionalContext?: () => LoggerContext): void => {
loggingService.warn(message, () =>
mergeContexts(context, ctxFn, additionalContext),
);
},
@@ -109,11 +105,9 @@ export function logger(ctxFn?: () => LoggerContext): LoggerApi {
error: (
message: string,
error?: Error,
additionalContext?: LoggerContext,
additionalContext?: () => LoggerContext,
): void => {
loggingService.error(
message,
error,
loggingService.error(message, error, () =>
mergeContexts(context, ctxFn, additionalContext),
);
},
@@ -135,7 +129,7 @@ export function logger(ctxFn?: () => LoggerContext): LoggerApi {
function mergeContexts(
baseContext: LoggerContext[] | null,
injectorContext?: () => LoggerContext,
additionalContext?: LoggerContext,
additionalContext?: () => LoggerContext,
): LoggerContext {
const contextArray = Array.isArray(baseContext) ? baseContext : [];
@@ -145,9 +139,7 @@ function mergeContexts(
contextArray.push(injectorCtx);
}
if (typeof additionalContext === 'object') {
contextArray.push(additionalContext);
}
contextArray.push(additionalContext ? additionalContext() : {});
if (!contextArray.length) {
return {};

View File

@@ -106,7 +106,7 @@ export class LoggingService implements LoggerApi {
* @param message - The message to log
* @param context - Optional metadata or structured data to include
*/
trace(message: string, context?: LoggerContext): void {
trace(message: string, context?: () => LoggerContext): void {
this.log(LogLevel.Trace, message, context);
}
@@ -117,7 +117,7 @@ export class LoggingService implements LoggerApi {
* @param message - The message to log
* @param context - Optional metadata or structured data to include
*/
debug(message: string, context?: LoggerContext): void {
debug(message: string, context?: () => LoggerContext): void {
this.log(LogLevel.Debug, message, context);
}
@@ -128,7 +128,7 @@ export class LoggingService implements LoggerApi {
* @param message - The message to log
* @param context - Optional metadata or structured data to include
*/
info(message: string, context?: LoggerContext): void {
info(message: string, context?: () => LoggerContext): void {
this.log(LogLevel.Info, message, context);
}
@@ -139,7 +139,7 @@ export class LoggingService implements LoggerApi {
* @param message - The message to log
* @param context - Optional metadata or structured data to include
*/
warn(message: string, context?: LoggerContext): void {
warn(message: string, context?: () => LoggerContext): void {
this.log(LogLevel.Warn, message, context);
}
@@ -164,7 +164,7 @@ export class LoggingService implements LoggerApi {
* }
* ```
*/
error(message: string, error?: Error, context?: LoggerContext): void {
error(message: string, error?: Error, context?: () => LoggerContext): void {
this.log(LogLevel.Error, message, context, error);
}
@@ -178,7 +178,7 @@ export class LoggingService implements LoggerApi {
private log(
level: LogLevel,
message: string,
context?: LoggerContext,
context?: () => LoggerContext,
error?: Error,
): void {
// Short-circuit if logging is disabled or level is too low (performance optimization)
@@ -192,7 +192,7 @@ export class LoggingService implements LoggerApi {
}
// Merge global context with the provided context
const mergedContext = this.mergeContext(context);
const mergedContext = this.mergeContext(context?.());
// Send to all sinks
for (const sink of this.sinks) {

View File

@@ -108,25 +108,25 @@ export interface LoggerApi {
* Logs a trace message with optional context.
* Use for fine-grained debugging information.
*/
trace(message: string, context?: LoggerContext): void;
trace(message: string, context?: () => LoggerContext): void;
/**
* Logs a debug message with optional context.
* Use for development-time debugging information.
*/
debug(message: string, context?: LoggerContext): void;
debug(message: string, context?: () => LoggerContext): void;
/**
* Logs an info message with optional context.
* Use for general runtime information.
*/
info(message: string, context?: LoggerContext): void;
info(message: string, context?: () => LoggerContext): void;
/**
* Logs a warning message with optional context.
* Use for potentially harmful situations.
*/
warn(message: string, context?: LoggerContext): void;
warn(message: string, context?: () => LoggerContext): void;
/**
* Logs an error message with an optional error object and context.
@@ -136,7 +136,7 @@ export interface LoggerApi {
* @param error - Any error object or value that caused this error condition
* @param context - Optional context data associated with the error
*/
error(message: string, error: unknown, context?: LoggerContext): void;
error(message: string, error: unknown, context?: () => LoggerContext): void;
}
/**

View File

@@ -126,10 +126,10 @@ export class ReturnDetailsOrderGroupItemControlsComponent {
this.changeProductCategory(category || 'unknown');
this.showProductCategoryDropdownLoading.set(false);
} catch (error) {
this.#logger.error('Failed to setProductCategory', error, {
this.#logger.error('Failed to setProductCategory', error, () => ({
itemId: this.item().id,
category,
});
}));
this.canReturn.emit(undefined);
this.showProductCategoryDropdownLoading.set(false);
}

View File

@@ -157,9 +157,13 @@ export class ReturnProcessItemComponent {
await this.#returnCanReturnService.canReturn(returnProcess);
this.canReturn.set(canReturnResponse);
} catch (error) {
this.#logger.error('Failed to validate return process', error, {
returnProcessId: returnProcess.id,
});
this.#logger.error(
'Failed to validate return process',
error,
() => ({
returnProcessId: returnProcess.id,
}),
);
this.canReturn.set(undefined);
}
} else {

View File

@@ -160,9 +160,13 @@ export class ReturnProcessComponent {
canReturnResults.push(Boolean(canReturnResponse?.result));
} catch (error) {
this.#logger.error('Failed to check canReturn for process', error, {
processId: returnProcess.processId,
});
this.#logger.error(
'Failed to check canReturn for process',
error,
() => ({
processId: returnProcess.processId,
}),
);
canReturnResults.push(false);
}
}

View File

@@ -183,6 +183,7 @@ export class ReturnSearchResultComponent implements AfterViewInit {
const processId = this.processId();
if (processId) {
this.#filterService.commit();
this.returnSearchStore.search({
processId,
query: this.#filterService.query(),
@@ -200,7 +201,7 @@ export class ReturnSearchResultComponent implements AfterViewInit {
searchCb = ({ data }: CallbackResult<ListResponseArgs<ReceiptListItem>>) => {
if (data) {
if (data.result.length === 1) {
this.navigate(['receipt', data.result[0].id]);
this.navigate(['../receipt', data.result[0].id]);
}
}
};

View File

@@ -1,19 +1,9 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ActivatedRoute, RouterOutlet } from '@angular/router';
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
untracked,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { injectActivatedProcessId } from '@isa/core/process';
import { ReturnSearchStore } from '@isa/oms/data-access';
import {
FilterService,
provideFilter,
withQueryParamsSync,
withQuerySettingsFactory,
withQueryParamsSync,
} from '@isa/shared/filter';
function querySettingsFactory() {
@@ -36,67 +26,4 @@ function querySettingsFactory() {
'"flex flex-col gap-5 isa-desktop:gap-6 items-center overflow-x-hidden"',
},
})
export class ReturnSearchComponent {
#route = inject(ActivatedRoute);
#router = inject(Router);
#filterService = inject(FilterService);
#returnSearchStore = inject(ReturnSearchStore);
#processId = injectActivatedProcessId();
#queryParams = toSignal(inject(ActivatedRoute).queryParams);
parseFilterParamsEffectFn = () =>
effect(() => {
const processId = this.#processId();
untracked(() => {
if (processId) {
const params = this.#queryParams();
const entity = this.#returnSearchStore.getEntity(processId);
// TODO: Caching der Entities einbauen - Aktuell kommt er sonst in den Filter Reset bei F5/Refresh Page
if (!entity || !params) {
this.#filterService.reset({ commit: true });
return;
}
this.#filterService.parseQueryParams(params, { commit: true });
}
});
});
searchResultsNavigationEffectFn = () =>
effect(() => {
// TODO: Wenn er hier beim Prozesswechsel nicht sofort navigieren soll, dann muss auf ReturnSearchStatus.Success geprüft werden und nach der Navigation auf ReturnSearchStatus.Idle gesetzt werden
const processId = this.#processId();
if (processId) {
const entity = this.#returnSearchStore.getEntity(processId);
if (entity && entity.status !== 'error') {
untracked(async () => {
const items = entity?.items;
if (items) {
if (items?.length === 1) {
return await this._navigateTo([
'receipts',
items[0].id.toString(),
]);
}
if (items?.length >= 0) {
return await this._navigateTo(['receipts']);
}
}
return;
});
}
}
});
private async _navigateTo(url: string[]) {
return await this.#router.navigate(url, {
queryParams: this.#filterService.queryParams(),
relativeTo: this.#route,
});
}
}
export class ReturnSearchComponent {}

View File

@@ -94,9 +94,9 @@ export class ReturnSummaryComponent {
*/
async returnItemsAndPrintRecipt() {
if (this.returnItemsAndPrintReciptStatus() === 'pending') {
this.#logger.warn('Return process already in progress', {
this.#logger.warn('Return process already in progress', () => ({
function: 'returnItemsAndPrintRecipt',
});
}));
return;
}
@@ -108,18 +108,18 @@ export class ReturnSummaryComponent {
this.returnProcesses(),
);
this.#logger.info('Return receipts created', {
this.#logger.info('Return receipts created', () => ({
count: returnReceipts.length,
});
}));
this.returnItemsAndPrintReciptStatus.set('success');
await this.#router.navigate(['../', 'review'], {
relativeTo: this.#activatedRoute,
});
} catch (error) {
this.#logger.error('Error completing return process', error, {
this.#logger.error('Error completing return process', error, () => ({
function: 'returnItemsAndPrintRecipt',
});
}));
this.returnItemsAndPrintReciptStatus.set('error');
}
}

View File

@@ -130,9 +130,9 @@ export class ReturnTaskListComponent {
});
}
} catch (error) {
this.#logger.error('Error completing task', error, {
this.#logger.error('Error completing task', error, () => ({
function: 'completeTask',
});
}));
}
}
@@ -160,9 +160,10 @@ export class ReturnTaskListComponent {
});
}
} catch (error) {
this.#logger.error('Error updating task', error, {
this.#logger.error('Error updating task', error, () => ({
taskId,
function: 'updateTask',
});
}));
}
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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;
}
}

View File

@@ -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) => () => {

View File

@@ -89,7 +89,9 @@ export class FilterMenuButtonComponent {
*/
this.closed.subscribe(() => {
if (this.rollbackOnClose()) {
this.#filter.rollback();
setTimeout(() => {
this.#filter.rollback();
});
}
});

View File

@@ -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

447
package-lock.json generated
View File

@@ -1331,7 +1331,6 @@
"version": "19.1.7",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.1.7.tgz",
"integrity": "sha512-Uu/TxfIcE1lStlCLmOPbghG1Y5o83odES89sr7bYNJz2mcG7TEonatf6GTOMzbJNil9FBJt6qnJkDkSjn4nUKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "7.26.0",
@@ -1360,7 +1359,6 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
@@ -1391,14 +1389,12 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
"node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -1667,7 +1663,6 @@
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
@@ -1698,14 +1693,12 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -12492,6 +12485,306 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz",
"integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz",
"integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz",
"integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz",
"integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz",
"integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz",
"integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz",
"integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz",
"integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz",
"integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz",
"integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz",
"integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz",
"integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz",
"integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz",
"integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz",
"integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz",
"integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz",
"integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz",
"integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz",
"integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz",
"integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rspack/binding": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.3.2.tgz",
@@ -17658,7 +17951,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
@@ -18143,7 +18435,6 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
@@ -19470,7 +19761,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.2"
@@ -22025,7 +22316,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -29494,6 +29785,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -29533,7 +29835,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
@@ -29588,7 +29889,6 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/regenerate": {
@@ -30162,6 +30462,47 @@
"node": ">=8.0"
}
},
"node_modules/rollup": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz",
"integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.7"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.41.0",
"@rollup/rollup-android-arm64": "4.41.0",
"@rollup/rollup-darwin-arm64": "4.41.0",
"@rollup/rollup-darwin-x64": "4.41.0",
"@rollup/rollup-freebsd-arm64": "4.41.0",
"@rollup/rollup-freebsd-x64": "4.41.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.41.0",
"@rollup/rollup-linux-arm-musleabihf": "4.41.0",
"@rollup/rollup-linux-arm64-gnu": "4.41.0",
"@rollup/rollup-linux-arm64-musl": "4.41.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.41.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.41.0",
"@rollup/rollup-linux-riscv64-gnu": "4.41.0",
"@rollup/rollup-linux-riscv64-musl": "4.41.0",
"@rollup/rollup-linux-s390x-gnu": "4.41.0",
"@rollup/rollup-linux-x64-gnu": "4.41.0",
"@rollup/rollup-linux-x64-musl": "4.41.0",
"@rollup/rollup-win32-arm64-msvc": "4.41.0",
"@rollup/rollup-win32-ia32-msvc": "4.41.0",
"@rollup/rollup-win32-x64-msvc": "4.41.0",
"fsevents": "~2.3.2"
}
},
"node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
@@ -30266,7 +30607,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/sass": {
@@ -30879,7 +31220,6 @@
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"devOptional": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -33496,7 +33836,6 @@
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -33887,6 +34226,82 @@
"node": ">= 0.8"
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",