mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merge branch 'develop' into feature/5032-Filter-Menu-Refinement
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<h1>📅📅📅</h1>
|
||||
@@ -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)];
|
||||
}
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './datepicker-input.component';
|
||||
@@ -0,0 +1,8 @@
|
||||
@let inp = input();
|
||||
@if (inp) {
|
||||
<ui-range-datepicker
|
||||
[formControl]="datepicker"
|
||||
[min]="datepickerMin()"
|
||||
[max]="datepickerMax()"
|
||||
></ui-range-datepicker>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.filter-datepicker-range-input {
|
||||
@apply inline-block;
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './datepicker-range-input.component';
|
||||
@@ -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';
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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']",
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user