♻️ refactor(filter): replace group-based filtering with target-based filtering

Replace the `group` property with `target` property in BaseFilterInputSchema to explicitly distinguish between 'filter' and 'input' query parameters. This improves code clarity and provides better semantic meaning.

**Changes:**
- Add `target` property to BaseFilterInputSchema with type 'filter' | 'input' and default 'input'
- Update filter.service.ts to use `target` instead of `group` for filtering inputs
- Update all filter input mappings (checkbox, date-range, number-range, text) to include `target` property
- Update all affected unit tests (9 test files) to include `target` in mock data

**Tests:** All 128 unit tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Lorenz Hilpert
2025-11-04 14:34:13 +01:00
parent bdb8aac8df
commit ce86014300
15 changed files with 169 additions and 115 deletions

View File

@@ -900,13 +900,15 @@ export class FilterService {
query = computed<Query>(() => {
const commited = this.#commitdState();
const filterGroup = commited.inputs.filter((i) => i.group === 'filter');
const inputGroup = commited.inputs.filter((i) => i.group === 'main');
const orderBy = commited.orderBy.filter((o) => o.dir);
return QuerySchema.parse({
filter: mapFilterInputToRecord(filterGroup),
input: mapFilterInputToRecord(inputGroup),
filter: mapFilterInputToRecord(
commited.inputs.filter((i) => i.target === 'filter'),
),
input: mapFilterInputToRecord(
commited.inputs.filter((i) => i.target === 'input'),
),
orderBy: orderBy
.filter((o) => o.selected)
.map((o) => {

View File

@@ -43,6 +43,7 @@ describe('checkboxFilterInputMapping', () => {
type: InputType.Checkbox,
defaultValue: undefined,
maxOptions: undefined,
target: undefined,
options: undefined,
selected: [],
description: undefined,
@@ -55,6 +56,7 @@ describe('checkboxFilterInputMapping', () => {
type: InputType.Checkbox,
defaultValue: undefined,
maxOptions: undefined,
target: undefined,
options: undefined,
selected: [],
description: undefined,
@@ -91,6 +93,7 @@ describe('checkboxFilterInputMapping', () => {
type: InputType.Checkbox,
defaultValue: 'defaultValue',
maxOptions: 3,
target: undefined,
options: [
{ label: 'Option 1', value: 'value1' },
{ label: 'Option 2', value: 'value2' },
@@ -106,6 +109,7 @@ describe('checkboxFilterInputMapping', () => {
type: InputType.Checkbox,
defaultValue: 'defaultValue',
maxOptions: 3,
target: undefined,
options: [
{ label: 'Option 1', value: 'value1' },
{ label: 'Option 2', value: 'value2' },
@@ -142,6 +146,7 @@ describe('checkboxFilterInputMapping', () => {
type: InputType.Checkbox,
defaultValue: undefined,
maxOptions: undefined,
target: undefined,
options: [
{ label: 'Option 1', value: 'value1' },
{ label: 'Option 2', value: 'value2' },
@@ -178,6 +183,7 @@ describe('checkboxFilterInputMapping', () => {
type: InputType.Checkbox,
defaultValue: undefined,
maxOptions: undefined,
target: undefined,
options: [],
selected: [],
description: undefined,
@@ -208,6 +214,7 @@ describe('checkboxFilterInputMapping', () => {
type: InputType.Checkbox,
defaultValue: undefined,
maxOptions: undefined,
target: undefined,
options: undefined,
selected: [],
description: undefined,

View File

@@ -44,6 +44,7 @@ export function checkboxFilterInputMapping(
type: InputType.Checkbox,
defaultValue: input.value,
maxOptions: input.options?.max,
target: input.target,
options: input.options?.values?.map((v) =>
checkboxOptionMapping(v, [group, input.key]),
),

View File

@@ -32,6 +32,7 @@ describe('dateRangeFilterInputMapping', () => {
key: 'testKey',
label: 'Test Label',
description: undefined,
target: undefined,
type: InputType.DateRange,
start: undefined,
stop: undefined,
@@ -42,6 +43,7 @@ describe('dateRangeFilterInputMapping', () => {
key: 'testKey',
label: 'Test Label',
description: undefined,
target: undefined,
type: InputType.DateRange,
start: undefined,
stop: undefined,
@@ -73,6 +75,7 @@ describe('dateRangeFilterInputMapping', () => {
key: 'testKey',
label: 'Test Label',
description: 'Test Description',
target: undefined,
type: InputType.DateRange,
start: '2023-01-01',
stop: '2023-12-31',
@@ -83,6 +86,7 @@ describe('dateRangeFilterInputMapping', () => {
key: 'testKey',
label: 'Test Label',
description: 'Test Description',
target: undefined,
type: InputType.DateRange,
start: '2023-01-01',
stop: '2023-12-31',
@@ -110,6 +114,7 @@ describe('dateRangeFilterInputMapping', () => {
key: 'testKey',
label: 'Test Label',
description: undefined,
target: undefined,
type: InputType.DateRange,
start: undefined,
stop: undefined,
@@ -120,6 +125,7 @@ describe('dateRangeFilterInputMapping', () => {
key: 'testKey',
label: 'Test Label',
description: undefined,
target: undefined,
type: InputType.DateRange,
start: undefined,
stop: undefined,
@@ -155,6 +161,7 @@ describe('dateRangeFilterInputMapping', () => {
key: 'testKey',
label: 'Test Label',
description: undefined,
target: undefined,
type: InputType.DateRange,
start: '2023-01-01',
stop: '2023-12-31',
@@ -167,6 +174,7 @@ describe('dateRangeFilterInputMapping', () => {
key: 'testKey',
label: 'Test Label',
description: undefined,
target: undefined,
type: InputType.DateRange,
start: '2023-01-01',
stop: '2023-12-31',

View File

@@ -1,30 +1,31 @@
import { Input, InputType } from '../../types';
import { DateRangeFilterInput, DateRangeFilterInputSchema } from '../schemas';
/**
* Maps an Input object to a DateRangeFilterInput object
*
* This function takes an input of type DateRange and maps it to a strongly-typed
* DateRangeFilterInput object, validating it against a schema. It extracts the start
* and stop dates from the input's option values.
*
* @param group - The group identifier that this input belongs to
* @param input - The source input object to map
* @returns A validated DateRangeFilterInput object
*/
export function dateRangeFilterInputMapping(
group: string,
input: Input,
): DateRangeFilterInput {
return DateRangeFilterInputSchema.parse({
group,
key: input.key,
label: input.label,
description: input.description,
type: InputType.DateRange,
start: input.options?.values?.[0]?.value,
stop: input.options?.values?.[1]?.value,
minStart: input.options?.values?.[0]?.minValue,
maxStop: input.options?.values?.[0]?.maxValue,
});
}
import { Input, InputType } from '../../types';
import { DateRangeFilterInput, DateRangeFilterInputSchema } from '../schemas';
/**
* Maps an Input object to a DateRangeFilterInput object
*
* This function takes an input of type DateRange and maps it to a strongly-typed
* DateRangeFilterInput object, validating it against a schema. It extracts the start
* and stop dates from the input's option values.
*
* @param group - The group identifier that this input belongs to
* @param input - The source input object to map
* @returns A validated DateRangeFilterInput object
*/
export function dateRangeFilterInputMapping(
group: string,
input: Input,
): DateRangeFilterInput {
return DateRangeFilterInputSchema.parse({
group,
key: input.key,
label: input.label,
description: input.description,
target: input.target,
type: InputType.DateRange,
start: input.options?.values?.[0]?.value,
stop: input.options?.values?.[1]?.value,
minStart: input.options?.values?.[0]?.minValue,
maxStop: input.options?.values?.[0]?.maxValue,
});
}

View File

@@ -61,13 +61,14 @@ describe('mapFilterInputToRecord', () => {
it('should skip empty values', () => {
const input: FilterInput[] = [
{ key: 'empty', type: InputType.Text, value: '' } as TextFilterInput,
{ key: 'empty', type: InputType.Text, value: '', target: 'input' } as TextFilterInput,
{
key: 'none',
type: InputType.Checkbox,
selected: [],
group: 'g',
options: [],
target: 'input',
} as CheckboxFilterInput,
];
expect(mapFilterInputToRecord(input)).toEqual({ empty: '', none: '' });
@@ -81,6 +82,7 @@ describe('mapFilterInputToRecord', () => {
type: InputType.Checkbox,
selected: ['electronics.phones'],
group: 'g',
target: 'input',
options: [
{
key: 'electronics',
@@ -108,6 +110,7 @@ describe('mapFilterInputToRecord', () => {
type: InputType.Checkbox,
selected: ['parent'],
group: 'g',
target: 'input',
options: [
{
key: 'parent',
@@ -132,8 +135,8 @@ describe('mapFilterInputToRecord', () => {
],
} as CheckboxFilterInput,
];
expect(mapFilterInputToRecord(input)).toEqual({
categories: 'child1-value;child2-value'
expect(mapFilterInputToRecord(input)).toEqual({
categories: 'child1-value;child2-value'
});
});
@@ -144,6 +147,7 @@ describe('mapFilterInputToRecord', () => {
type: InputType.Checkbox,
selected: ['parent1', 'parent2.child'],
group: 'g',
target: 'input',
options: [
{
key: 'parent1',
@@ -167,8 +171,8 @@ describe('mapFilterInputToRecord', () => {
],
} as CheckboxFilterInput,
];
expect(mapFilterInputToRecord(input)).toEqual({
filters: 'parent1-value;child-value'
expect(mapFilterInputToRecord(input)).toEqual({
filters: 'parent1-value;child-value'
});
});
@@ -179,6 +183,7 @@ describe('mapFilterInputToRecord', () => {
type: InputType.Checkbox,
selected: ['nonexistent.path', 'valid'],
group: 'g',
target: 'input',
options: [
{
key: 'valid',
@@ -199,6 +204,7 @@ describe('mapFilterInputToRecord', () => {
type: InputType.Checkbox,
selected: null as any,
group: 'g',
target: 'input',
options: [],
} as CheckboxFilterInput,
];
@@ -212,6 +218,7 @@ describe('mapFilterInputToRecord', () => {
type: InputType.Checkbox,
selected: ['parent'],
group: 'g',
target: 'input',
options: [
{
key: 'parent',
@@ -230,7 +237,7 @@ describe('mapFilterInputToRecord', () => {
],
} as CheckboxFilterInput,
];
expect(mapFilterInputToRecord(input)).toEqual({
expect(mapFilterInputToRecord(input)).toEqual({
mixed: 'parent-value' // Should use the parent's direct value, not collect children
});
});
@@ -244,10 +251,11 @@ describe('mapFilterInputToRecord', () => {
type: InputType.DateRange,
start: '2024-06-01T00:00:00.000Z',
stop: undefined,
target: 'input',
} as DateRangeFilterInput,
];
expect(mapFilterInputToRecord(input)).toEqual({
range: '"2024-06-01T00:00:00.000Z"-'
expect(mapFilterInputToRecord(input)).toEqual({
range: '"2024-06-01T00:00:00.000Z"-'
});
});
@@ -258,6 +266,7 @@ describe('mapFilterInputToRecord', () => {
type: InputType.DateRange,
start: undefined,
stop: '2024-06-05T00:00:00.000Z',
target: 'input',
} as DateRangeFilterInput,
];
const result = mapFilterInputToRecord(input);
@@ -271,6 +280,7 @@ describe('mapFilterInputToRecord', () => {
type: InputType.DateRange,
start: undefined,
stop: undefined,
target: 'input',
} as DateRangeFilterInput,
];
expect(mapFilterInputToRecord(input)).toEqual({});
@@ -279,12 +289,13 @@ describe('mapFilterInputToRecord', () => {
it('should handle multiple filter types together', () => {
const input: FilterInput[] = [
{ key: 'name', type: InputType.Text, value: 'test' } as TextFilterInput,
{ key: 'name', type: InputType.Text, value: 'test', target: 'input' } as TextFilterInput,
{
key: 'tags',
type: InputType.Checkbox,
selected: ['tag1'],
group: 'g',
target: 'input',
options: [
{
key: 'tag1',
@@ -299,6 +310,7 @@ describe('mapFilterInputToRecord', () => {
type: InputType.DateRange,
start: '2024-01-01T00:00:00.000Z',
stop: '2024-01-31T23:59:59.999Z',
target: 'input',
} as DateRangeFilterInput,
];
const result = mapFilterInputToRecord(input);

View File

@@ -14,6 +14,7 @@ export function numberRangeFilterInputMapping(
label: input.label,
description: input.description,
type: InputType.NumberRange,
target: input.target,
min: input.options?.values?.[0]?.value
? Number(input.options?.values?.[0]?.value)
: undefined,

View File

@@ -34,6 +34,7 @@ describe('textFilterInputMapping', () => {
defaultValue: undefined,
value: undefined,
placeholder: undefined,
target: undefined,
});
expect(result).toEqual({
@@ -45,6 +46,7 @@ describe('textFilterInputMapping', () => {
defaultValue: undefined,
value: undefined,
placeholder: undefined,
target: undefined,
});
});
@@ -73,6 +75,7 @@ describe('textFilterInputMapping', () => {
defaultValue: 'defaultValue',
value: 'defaultValue',
placeholder: 'Enter text...',
target: undefined,
});
expect(result).toEqual({
@@ -84,6 +87,7 @@ describe('textFilterInputMapping', () => {
defaultValue: 'defaultValue',
value: 'defaultValue',
placeholder: 'Enter text...',
target: undefined,
});
});
});

View File

@@ -1,28 +1,29 @@
import { Input, InputType } from '../../types';
import { TextFilterInput, TextFilterInputSchema } from '../schemas';
/**
* Maps an Input object to a TextFilterInput object
*
* This function takes an input of type Text and maps it to a strongly-typed
* TextFilterInput object, validating it against a schema.
*
* @param group - The group identifier that this input belongs to
* @param input - The source input object to map
* @returns A validated TextFilterInput object
*/
export function textFilterInputMapping(
group: string,
input: Input,
): TextFilterInput {
return TextFilterInputSchema.parse({
group,
key: input.key,
label: input.label,
description: input.description,
type: InputType.Text,
defaultValue: input.value,
value: input.value,
placeholder: input.placeholder,
});
}
import { Input, InputType } from '../../types';
import { TextFilterInput, TextFilterInputSchema } from '../schemas';
/**
* Maps an Input object to a TextFilterInput object
*
* This function takes an input of type Text and maps it to a strongly-typed
* TextFilterInput object, validating it against a schema.
*
* @param group - The group identifier that this input belongs to
* @param input - The source input object to map
* @returns A validated TextFilterInput object
*/
export function textFilterInputMapping(
group: string,
input: Input,
): TextFilterInput {
return TextFilterInputSchema.parse({
group,
key: input.key,
label: input.label,
description: input.description,
type: InputType.Text,
defaultValue: input.value,
value: input.value,
placeholder: input.placeholder,
target: input.target,
});
}

View File

@@ -1,44 +1,50 @@
import { z } from 'zod';
import { InputType } from '../../types';
/**
* Base schema for all filter input types.
* Contains common properties that all filter inputs must have.
*
* @property group - Group identifier that this input belongs to
* @property key - Unique identifier for the input within its group
* @property label - Optional display name for the input
* @property description - Optional detailed explanation of the input
* @property type - The type of input control (Text, Checkbox, DateRange)
*/
export const BaseFilterInputSchema = z
.object({
group: z
.string()
.describe(
'Identifier for the group this filter input belongs to. Used for organizing related filters.',
),
key: z
.string()
.describe(
'Unique identifier for this input within its group. Used as a key in requests and state management.',
),
label: z
.string()
.optional()
.describe('Human-readable display name shown to users in the UI.'),
description: z
.string()
.optional()
.describe(
'Detailed explanation of what this filter does, displayed as helper text in the UI.',
),
type: z
.nativeEnum(InputType)
.describe(
'Determines the type of input control and its behavior (Text, Checkbox, DateRange, etc.).',
),
})
.describe('BaseFilterInput');
export type BaseFilterInput = z.infer<typeof BaseFilterInputSchema>;
import { z } from 'zod';
import { InputType } from '../../types';
/**
* Base schema for all filter input types.
* Contains common properties that all filter inputs must have.
*
* @property group - Group identifier that this input belongs to
* @property key - Unique identifier for the input within its group
* @property label - Optional display name for the input
* @property description - Optional detailed explanation of the input
* @property type - The type of input control (Text, Checkbox, DateRange)
*/
export const BaseFilterInputSchema = z
.object({
group: z
.string()
.describe(
'Identifier for the group this filter input belongs to. Used for organizing related filters.',
),
key: z
.string()
.describe(
'Unique identifier for this input within its group. Used as a key in requests and state management.',
),
label: z
.string()
.optional()
.describe('Human-readable display name shown to users in the UI.'),
description: z
.string()
.optional()
.describe(
'Detailed explanation of what this filter does, displayed as helper text in the UI.',
),
type: z
.nativeEnum(InputType)
.describe(
'Determines the type of input control and its behavior (Text, Checkbox, DateRange, etc.).',
),
target: z
.union([z.literal('filter'), z.literal('input')])
.default('input') // TODO: Fix settings before removing default (qs settings dont have target yet, but needs to be there)
.describe(
'Specifies whether this input is part of the main query parameters or filter parameters.',
),
})
.describe('BaseFilterInput');
export type BaseFilterInput = z.infer<typeof BaseFilterInputSchema>;

View File

@@ -10,6 +10,7 @@ describe('checkboxSelectedHelper', () => {
options: [],
selected: [],
label: 'label',
target: 'input',
};
it('should return "none" if input not found', () => {
@@ -34,6 +35,7 @@ describe('checkboxSelectedHelper', () => {
key: 'key',
type: InputType.Text,
value: 'test',
target: 'input',
};
expect(checkboxSelectedHelper([input], option)).toBe('none');
});
@@ -273,6 +275,7 @@ describe('checkboxSelectedHelperBoolean', () => {
options: [],
selected: [],
label: 'label',
target: 'input',
};
it('should return true for "checked" state', () => {

View File

@@ -74,6 +74,7 @@ describe('getAllCheckboxOptionsHelper', () => {
key: 'test-checkbox',
label: 'Test Checkbox',
type: InputType.Checkbox,
target: 'input',
options,
selected: [],
});
@@ -212,6 +213,7 @@ describe('getAllCheckboxOptionsHelper', () => {
key: 'test-checkbox',
label: 'Test Checkbox',
type: InputType.Checkbox,
target: 'input',
options: undefined as unknown as CheckboxFilterInputOption[], // Simulating potential undefined scenario
selected: [],
};

View File

@@ -47,6 +47,7 @@ describe('CheckboxInputControlComponent', () => {
options: [option, parentOption],
selected: [],
label: 'label',
target: 'input',
},
]);
@@ -105,6 +106,7 @@ describe('CheckboxInputControlComponent', () => {
options: [option],
selected: ['opt'],
label: 'label',
target: 'input',
},
]);
spectator.detectChanges();
@@ -124,6 +126,7 @@ describe('CheckboxInputControlComponent', () => {
options: [parentOption],
selected: ['parent.child1'],
label: 'label',
target: 'input',
},
]);
spectator.detectChanges();

View File

@@ -128,6 +128,7 @@ describe('CheckboxInputComponent', () => {
options: manyOptions,
selected: [],
label: 'label',
target: 'input',
},
]);
spectator.detectChanges();
@@ -174,6 +175,7 @@ describe('CheckboxInputComponent', () => {
options: multipleOptions,
selected: [],
label: 'label',
target: 'input',
},
]);

View File

@@ -49,6 +49,7 @@ describe('FilterInputMenuComponent', () => {
group: 'test-group',
type: InputType.Text,
label: 'Test Label',
target: 'input',
};
spectator.setInput('filterInput', filterInput);