Merge branch 'develop' into feature/5032-Filter-Menu-Refinement

This commit is contained in:
Lorenz Hilpert
2025-04-11 16:36:57 +02:00
93 changed files with 3779 additions and 193 deletions

View File

@@ -15,7 +15,9 @@ import { FILTER_ON_COMMIT, FILTER_ON_INIT, QUERY_SETTINGS } from './tokens';
@Injectable()
export class FilterService {
#onInit = inject(FILTER_ON_INIT, { optional: true })?.map((fn) => fn(this));
#onCommit = inject(FILTER_ON_COMMIT, { optional: true })?.map((fn) => fn(this));
#onCommit = inject(FILTER_ON_COMMIT, { optional: true })?.map((fn) =>
fn(this),
);
readonly settings = inject(QUERY_SETTINGS);
@@ -37,7 +39,11 @@ export class FilterService {
});
}
setOrderBy(by: string, dir: OrderByDirection | undefined, options?: { commit: boolean }) {
setOrderBy(
by: string,
dir: OrderByDirection | undefined,
options?: { commit: boolean },
) {
const orderByList = this.#state.orderBy().map((o) => {
if (o.by === by && o.dir === dir) {
return { ...o, selected: true };
@@ -52,7 +58,11 @@ export class FilterService {
}
}
setInputTextValue(key: string, value: string | undefined, options?: { commit: boolean }): void {
setInputTextValue(
key: string,
value: string | undefined,
options?: { commit: boolean },
): void {
const inputs = this.#state.inputs().map((input) => {
if (input.key !== key) {
return input;
@@ -74,7 +84,11 @@ export class FilterService {
}
}
setInputCheckboxValue(key: string, selected: string[], options?: { commit: boolean }): void {
setInputCheckboxValue(
key: string,
selected: string[],
options?: { commit: boolean },
): void {
const inputs = this.#state.inputs().map((input) => {
if (input.key !== key) {
return input;
@@ -96,6 +110,33 @@ export class FilterService {
}
}
setInputDateRangeValue(
key: string,
start?: string,
stop?: string,
options?: { commit: boolean },
): void {
const inputs = this.#state.inputs().map((input) => {
if (input.key !== key) {
return input;
}
if (input.type === InputType.DateRange) {
return { ...input, start, stop };
}
console.warn(`Input type not supported: ${input.type}`);
return input;
});
patchState(this.#state, { inputs });
if (options?.commit) {
this.commit();
}
}
/**
* Indicates whether the current state is the default state.
* This computed property checks if the current state is equal to the default state.
@@ -106,8 +147,12 @@ export class FilterService {
});
isDefaultFilterInput(filterInput: FilterInput) {
const currentInputState = this.#state.inputs().find((i) => i.key === filterInput.key);
const defaultInputState = this.defaultState.inputs.find((i) => i.key === filterInput.key);
const currentInputState = this.#state
.inputs()
.find((i) => i.key === filterInput.key);
const defaultInputState = this.defaultState.inputs.find(
(i) => i.key === filterInput.key,
);
return isEqual(currentInputState, defaultInputState);
}
@@ -126,14 +171,20 @@ export class FilterService {
return !input.selected?.length;
}
console.warn(`Input type not supported: ${input.type}`);
if (input.type === InputType.DateRange) {
return !input.start && !input.stop;
}
console.warn(`Input type not supported`);
return true;
});
});
isEmptyFilterInput(filterInput: FilterInput) {
const currentInputState = this.#state.inputs().find((i) => i.key === filterInput.key);
const currentInputState = this.#state
.inputs()
.find((i) => i.key === filterInput.key);
if (currentInputState?.type === InputType.Text) {
return !currentInputState.value;
@@ -143,7 +194,11 @@ export class FilterService {
return !currentInputState.selected?.length;
}
console.warn(`Input type not supported: ${currentInputState?.type}`);
if (currentInputState?.type === InputType.DateRange) {
return !currentInputState.start && !currentInputState.stop;
}
console.warn(`Input type not supported`);
return true;
}
@@ -206,7 +261,9 @@ export class FilterService {
return;
}
const inputIndex = this.#commitdState().inputs.findIndex((i) => i.key === key);
const inputIndex = this.#commitdState().inputs.findIndex(
(i) => i.key === key,
);
if (inputIndex === -1) {
console.warn(`No committed input found with key: ${key}`);
@@ -240,6 +297,10 @@ export class FilterService {
return { ...input, selected: [] };
}
if (input.type === InputType.DateRange) {
return { ...input, start: undefined, stop: undefined };
}
return input;
});
@@ -330,6 +391,15 @@ export class FilterService {
result[input.key] = input.selected.join(';');
}
break;
case InputType.DateRange:
if (input.start && input.stop) {
result[input.key] = `"${input.start}"-"${input.stop}"`;
} else if (input.start) {
result[input.key] = `"${input.start}"-`;
} else if (input.stop) {
result[input.key] = `-"${input.stop}"`;
}
break;
}
}
@@ -344,7 +414,9 @@ export class FilterService {
isQueryParamsEqual(params: Record<string, string>): boolean {
const currentParams = this.queryParams();
return this.queryParamKeys().every((key) => params[key] === currentParams[key]);
return this.queryParamKeys().every(
(key) => params[key] === currentParams[key],
);
}
queryParamKeys = computed(() => {
@@ -371,6 +443,14 @@ export class FilterService {
acc[input.key] = input.value || '';
} else if (input.type === InputType.Checkbox) {
acc[input.key] = input.selected?.join(';') || '';
} else if (input.type === InputType.DateRange) {
if (input.start && input.stop) {
acc[input.key] = `"${input.start}"-"${input.stop}"`;
} else if (input.start) {
acc[input.key] = `"${input.start}"-`;
} else if (input.stop) {
acc[input.key] = `-"${input.stop}"`;
}
}
return acc;
}, {}),
@@ -379,6 +459,14 @@ export class FilterService {
acc[input.key] = input.value || '';
} else if (input.type === InputType.Checkbox) {
acc[input.key] = input.selected?.join(';') || '';
} else if (input.type === InputType.DateRange) {
if (input.start && input.stop) {
acc[input.key] = `"${input.start}"-"${input.stop}"`;
} else if (input.start) {
acc[input.key] = `"${input.start}"-`;
} else if (input.stop) {
acc[input.key] = `-"${input.stop}"`;
}
}
return acc;
}, {}),
@@ -395,13 +483,18 @@ export class FilterService {
});
});
parseQueryParams(params: Record<string, string>, options?: { commit: boolean }): void {
parseQueryParams(
params: Record<string, string>,
options?: { commit: boolean },
): void {
this.reset();
for (const key in params) {
if (key === 'orderBy') {
const [by, dir] = params[key].split(':');
const orderBy = this.orderBy().some((o) => o.by === by && o.dir === dir);
const orderBy = this.orderBy().some(
(o) => o.by === by && o.dir === dir,
);
if (orderBy) {
console.warn(`OrderBy already exists: ${by}:${dir}`);
@@ -419,6 +512,14 @@ export class FilterService {
case InputType.Checkbox:
this.setInputCheckboxValue(key, params[key].split(';'));
break;
case InputType.DateRange: {
const decoded = decodeURIComponent(params[key]);
const [startRaw, stopRaw] = decoded.split('-"');
const start = startRaw?.replace(/"/g, '') || undefined;
const stop = stopRaw?.replace(/"/g, '') || undefined;
this.setInputDateRangeValue(key, start, stop);
break;
}
default:
console.warn(`Input type not supported: ${inputType}`);
break;

View File

@@ -24,5 +24,7 @@ export function dateRangeFilterInputMapping(
type: InputType.DateRange,
start: input.options?.values?.[0]?.value,
stop: input.options?.values?.[1]?.value,
minStart: input.options?.values?.[0].minValue,
maxStart: input.options?.values?.[0].maxValue,
});
}

View File

@@ -22,12 +22,24 @@ export const DateRangeFilterInputSchema = BaseFilterInputSchema.extend({
.describe(
'ISO date string representing the beginning of the date range. Optional if only an end date is needed.',
),
minStart: z
.string()
.optional()
.describe(
'ISO date string representing the minimum start date of the range. Optional if only an end date is needed.',
),
stop: z
.string()
.optional()
.describe(
'ISO date string representing the end of the date range. Optional if only a start date is needed.',
),
maxStop: z
.string()
.optional()
.describe(
'ISO date string representing the maximum end date of the range. Optional if only a start date is needed.',
),
}).describe('DateRangeFilterInput');
export type DateRangeFilterInput = z.infer<typeof DateRangeFilterInputSchema>;

View File

@@ -1 +0,0 @@
<h1>📅📅📅</h1>

View File

@@ -1,3 +0,0 @@
.filter-datepicker-input {
@apply flex items-center justify-center bg-isa-white w-[18.375rem] h-[29.5rem] rounded-[1.25rem] shadow-[0px_0px_16px_0px_rgba(0,0,0,0.15)];
}

View File

@@ -1,37 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
ViewEncapsulation,
effect,
input,
untracked,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormControl } from '@angular/forms';
@Component({
selector: 'filter-datepicker-input',
templateUrl: './datepicker-input.component.html',
styleUrls: ['./datepicker-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
standalone: true,
host: {
'[class]': "['filter-datepicker-input']",
},
})
export class DatepickerInputComponent {
inputKey = input.required<string>();
datepicker = new FormControl({});
valueChanges = toSignal(this.datepicker.valueChanges);
constructor() {
effect(() => {
this.valueChanges();
untracked(() => {
console.log({ startTest: '2021-01-01', stopTest: '2021-12-31' });
});
});
}
}

View File

@@ -1 +0,0 @@
export * from './datepicker-input.component';

View File

@@ -0,0 +1,8 @@
@let inp = input();
@if (inp) {
<ui-range-datepicker
[formControl]="datepicker"
[min]="datepickerMin()"
[max]="datepickerMax()"
></ui-range-datepicker>
}

View File

@@ -0,0 +1,3 @@
.filter-datepicker-range-input {
@apply inline-block;
}

View File

@@ -0,0 +1,94 @@
import {
ChangeDetectionStrategy,
Component,
ViewEncapsulation,
computed,
effect,
inject,
input,
untracked,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { DateRangeValue, RangeDatepickerComponent } from '@isa/ui/datepicker';
import { DateRangeFilterInput, FilterService } from '../../core';
import { InputType } from '../../types';
@Component({
selector: 'filter-datepicker-range-input',
templateUrl: './datepicker-range-input.component.html',
styleUrls: ['./datepicker-range-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [RangeDatepickerComponent, ReactiveFormsModule],
host: { '[class]': "['filter-datepicker-range-input']" },
})
export class DatepickerRangeInputComponent {
readonly filterService = inject(FilterService);
inputKey = input.required<string>();
datepicker = new FormControl<DateRangeValue | undefined>(undefined);
valueChanges = toSignal(this.datepicker.valueChanges);
input = computed<DateRangeFilterInput>(() => {
const inputs = this.filterService.inputs();
const input = inputs.find(
(input) =>
input.key === this.inputKey() && input.type === InputType.DateRange,
) as DateRangeFilterInput;
if (!input) {
throw new Error(`Input not found for key: ${this.inputKey()}`);
}
return input;
});
datepickerMin = computed<Date | undefined>(() => {
const inp = this.input();
return inp.minStart ? new Date(inp.minStart) : undefined;
});
datepickerMax = computed<Date | undefined>(() => {
const inp = this.input();
return inp.maxStop ? new Date(inp.maxStop) : undefined;
});
constructor() {
effect(() => {
const input = this.input();
const startDate = input.start ? new Date(input.start) : undefined;
const stopDate = input.stop ? new Date(input.stop) : undefined;
this.datepicker.patchValue([startDate, stopDate]);
this.datepicker.updateValueAndValidity();
});
effect(() => {
this.valueChanges();
untracked(() => {
const startDate = this.datepicker?.value?.[0];
const stopDate = this.datepicker?.value?.[1];
if (!startDate && !stopDate) {
return;
}
const start = startDate?.toISOString();
const stop = stopDate?.toISOString();
const controlEqualsInput =
this.input().start === start && this.input().stop === stop;
if (!controlEqualsInput) {
this.filterService.setInputDateRangeValue(
this.inputKey(),
start,
stop,
);
}
});
});
}
}

View File

@@ -0,0 +1 @@
export * from './datepicker-range-input.component';

View File

@@ -1,4 +1,4 @@
export * from './search-bar-input';
export * from './checkbox-input';
export * from './datepicker-input';
export * from './datepicker-range-input';
export * from './input-renderer';

View File

@@ -1,9 +1,11 @@
@switch (filterInput().type) {
@case (InputType.Checkbox) {
<filter-checkbox-input [inputKey]="filterInput().key"> </filter-checkbox-input>
<filter-checkbox-input [inputKey]="filterInput().key">
</filter-checkbox-input>
}
@case (InputType.DateRange) {
<filter-datepicker-input [inputKey]="filterInput().key"> </filter-datepicker-input>
<filter-datepicker-range-input [inputKey]="filterInput().key">
</filter-datepicker-range-input>
}
@default {
<div class="text-isa-accent-red isa-text-body-1-bold">

View File

@@ -5,7 +5,7 @@ import {
ViewEncapsulation,
} from '@angular/core';
import { CheckboxInputComponent } from '../checkbox-input';
import { DatepickerInputComponent } from '../datepicker-input';
import { DatepickerRangeInputComponent } from '../datepicker-range-input';
import { FilterInput } from '../../core';
import { InputType } from '../../types';
@@ -16,7 +16,7 @@ import { InputType } from '../../types';
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [CheckboxInputComponent, DatepickerInputComponent],
imports: [CheckboxInputComponent, DatepickerRangeInputComponent],
host: {
'[class]': "['filter-input-renderer']",
},

View File

@@ -2,6 +2,6 @@
@apply inline-flex flex-col;
@apply bg-isa-white;
@apply rounded-[1.25rem];
@apply w-[14.3125rem] max-h-[33.5rem];
@apply min-w-[14.3125rem] max-w-[18.375rem] max-h-[33.5rem];
@apply shadow-overlay;
}