Merged PR 1902: feat(shared-filter-inputs-checkbox-input): add bulk toggle functionality for...

feat(shared-filter-inputs-checkbox-input): add bulk toggle functionality for checkbox options

Replace individual option iteration with new toggleAllCheckboxOptions method
in FilterService. This improves performance and provides cleaner API for
selecting/deselecting all checkbox options at once. Updates component logic
to use the new bulk operation and fixes test expectations accordingly.

Ref: #5231
This commit is contained in:
Nino Righi
2025-07-31 16:42:37 +00:00
committed by Andreas Schickinger
parent 1e84223076
commit ad00899b6e
3 changed files with 114 additions and 63 deletions

View File

@@ -163,48 +163,19 @@ export class FilterService {
}
/**
* Sets the selection state of a specific checkbox option within a hierarchical structure.
* Sets the selection state of a specific checkbox option within a checkbox input.
*
* This method handles the selection/deselection of checkbox options with automatic
* parent-child relationship management and maximum options enforcement:
* - When selecting a parent option, all child options are implicitly selected
* - When deselecting a parent option, all child options are also deselected
* - Child options can be individually selected/deselected
* - If `maxOptions` is configured and selection would exceed the limit, oldest selections are removed (FIFO)
* This method allows you to select or deselect a specific checkbox option by its
* group, groupKey, and option keys. It handles the selection logic, including
* enforcing maximum options limits and managing parent-child relationships.
*
* @param checkboxOption - The checkbox option object containing the hierarchical path and metadata
* @param selected - Whether to select (true) or deselect (false) the option
* @param checkboxOption - The checkbox option to set the selection for
* @param selected - If true, selects the option; if false, deselects it
* @param options - Optional parameters
* @param options.commit - If true, commits the changes immediately
*
* @example
* ```typescript
* // Select a specific option
* filterService.setInputCheckboxOptionSelected(
* checkboxOption,
* true
* );
*
* // Deselect a parent option (also deselects all children)
* filterService.setInputCheckboxOptionSelected(
* parentCheckboxOption,
* false,
* { commit: true }
* );
*
* // When maxOptions is set to 3 and selecting would exceed the limit:
* // If currently selected: ['option1', 'option2', 'option3']
* // Selecting 'option4' results in: ['option2', 'option3', 'option4']
* // (oldest selection 'option1' is automatically removed)
* ```
*
* @remarks
* The maximum options enforcement only applies when selecting options. Deselection is not affected by the limit.
* The FIFO (First In, First Out) strategy ensures the most recently selected options are preserved when the limit is exceeded.
* @param options.commit - If true, commits the changes immediately after setting the selection
*/
setInputCheckboxOptionSelected(
checkboxOption: CheckboxFilterInputOption,
// [group, groupKey, ...optionKeys]: string[],
selected: boolean,
options?: { commit: boolean },
): void {
@@ -272,28 +243,88 @@ export class FilterService {
}
}
// getInputCheckboxOptionSelected([group, groupKey, ...optionKeys]: string[]):
// | boolean
// | undefined {
// const input = this.#state.inputs().find((input) => {
// return input.group === group && input.key === groupKey;
// });
/**
* Toggles the selection state of all checkbox options within a specific group and key.
*
* This method allows you to select or deselect all options in a checkbox input by
* providing the group and groupKey. It updates the selected state of the input
* accordingly, either selecting all options or clearing the selection.
*
* @param group - The group name of the checkbox input
* @param groupKey - The key of the checkbox input within the group
* @param checkboxOptions - The list of checkbox options to toggle
* @param selected - If true, selects all options; if false, clears all selections
* @param options - Optional parameters
* @param options.commit - If true, commits the changes immediately after toggling
*/
toggleAllCheckboxOptions({
group,
groupKey,
checkboxOptions,
selected,
options,
}: {
group: string;
groupKey: string;
checkboxOptions: CheckboxFilterInputOption[];
selected: boolean;
options?: { commit: boolean };
}): void {
// Get the group and groupKey from the first option
if (checkboxOptions.length === 0) {
this.#logger.warn('No checkbox options provided');
return;
}
// if (!input) {
// this.#logger.warn(`Input not found`, () => ({
// inputGroup: group,
// inputKey: groupKey,
// }));
// return undefined;
// }
const inputs = this.#state.inputs().map((input) => {
const target = input.group === group && input.key === groupKey;
// if (input.type !== InputType.Checkbox) {
// this.logUnsupportedInputType(input, 'getInputCheckboxValue');
// return undefined;
// }
if (!target) {
return input;
}
// return input.selected.includes(optionKeys.join('.'));
// }
if (input.type !== InputType.Checkbox) {
this.logUnsupportedInputType(
input,
'setAllInputCheckboxOptionsSelected',
);
return input;
}
if (selected) {
// Get all option keys recursively from all provided options
const getAllOptionKeys = (
options: CheckboxFilterInputOption[],
): string[] =>
options.flatMap((option) => {
const isParent =
Array.isArray(option.values) && option.values.length > 0;
if (isParent) {
// If the option has children, only include child keys
return getAllOptionKeys(option.values!);
} else {
// If no children, include the option itself
return [checkboxOptionKeysHelper(option).join('.')];
}
});
return {
...input,
selected: getAllOptionKeys(checkboxOptions),
};
} else {
// Unselect all options
return { ...input, selected: [] };
}
});
patchState(this.#state, { inputs });
if (options?.commit) {
this.commit();
}
}
/**
* Sets the date range values for an input with the specified key.

View File

@@ -38,6 +38,7 @@ describe('CheckboxInputComponent', () => {
const mockFilterService = {
inputs: mockInputsSignal,
setInputCheckboxOptionSelected: jest.fn(),
toggleAllCheckboxOptions: jest.fn(),
};
const createComponent = createComponentFactory({
@@ -64,7 +65,7 @@ describe('CheckboxInputComponent', () => {
label: 'label',
},
]);
spectator = createComponent({ props: { inputKey: 'key' } });
filterService = spectator.inject(FilterService);
jest.clearAllMocks();
@@ -81,12 +82,17 @@ describe('CheckboxInputComponent', () => {
});
it('should call setInputCheckboxOptionSelected for each option on toggleSelection', () => {
const group = 'group';
const key = 'key';
const options = [option];
spectator.detectChanges();
spectator.component.toggleSelection();
expect(filterService.setInputCheckboxOptionSelected).toHaveBeenCalledWith(
option,
true,
);
expect(filterService.toggleAllCheckboxOptions).toHaveBeenCalledWith({
group,
groupKey: key,
checkboxOptions: options,
selected: true,
});
});
it('should compute allChecked as true when all options are checked', () => {

View File

@@ -100,7 +100,16 @@ export class CheckboxInputComponent {
filterQuery = signal<string>('');
allChecked = computed(() => {
return this.input().options.every(
const input = this.input();
const allOptions = getAllCheckboxOptionsHelper(input);
// Only check leaf options (options without children)
// Parent options are automatically considered checked if all their children are checked
const leafOptions = allOptions.filter(
(option) => !option.values || option.values.length === 0,
);
return leafOptions.every(
(option) =>
checkboxSelectedHelper(this.filterService.inputs(), option) ===
'checked',
@@ -108,10 +117,15 @@ export class CheckboxInputComponent {
});
toggleSelection() {
const filterGroup = this.input().group;
const filterGroupKey = this.inputKey();
const options = this.input()?.options || [];
const allChecked = this.allChecked();
options.forEach((option) => {
this.filterService.setInputCheckboxOptionSelected(option, !allChecked);
this.filterService.toggleAllCheckboxOptions({
group: filterGroup,
groupKey: filterGroupKey,
checkboxOptions: options,
selected: !allChecked,
});
}
}