From 60b590e04eb8a518c5ca2d8d04285318a12a4b57 Mon Sep 17 00:00:00 2001 From: alten-dturus Date: Tue, 23 Dec 2025 10:01:54 +0100 Subject: [PATCH 01/20] Update ActiveFiltersChips with hasSubmitButton prop --- src/features/filters/components/ActiveFiltersChips.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/features/filters/components/ActiveFiltersChips.tsx b/src/features/filters/components/ActiveFiltersChips.tsx index 5d0bd25..9d5a8b3 100644 --- a/src/features/filters/components/ActiveFiltersChips.tsx +++ b/src/features/filters/components/ActiveFiltersChips.tsx @@ -1,12 +1,13 @@ import React from 'react' import { Button, Chip, Divider, Stack } from '@mui/material' -import type { ActiveFilters, FiltersHandler } from '../filters.types' +import type { ActiveFilters, FilterHandler } from '../filters.types' import { getLocalizedValue } from '../../../utils/common.utils' type ActiveFilterChips = { activeFilters: ActiveFilters - onRemoveActiveFilter: FiltersHandler + onRemoveActiveFilter: FilterHandler onResetActiveFilters: VoidFunction + hasSubmitButton?: boolean rightContent?: React.ReactNode } @@ -14,6 +15,7 @@ export const ActiveFilterChips: React.FC = ({ activeFilters, onRemoveActiveFilter, onResetActiveFilters, + hasSubmitButton, rightContent, }) => { if (activeFilters.length <= 0 && !rightContent) return null @@ -36,12 +38,12 @@ export const ActiveFilterChips: React.FC = ({ {activeFilters.map(({ value, label, type, filterKey }) => ( ))} - {activeFilters.length > 1 && ( + {!hasSubmitButton && activeFilters.length > 1 && ( + + + )} ) } From 3715ef15ef72bedd5240f1206a2ffd4474193ef8 Mon Sep 17 00:00:00 2001 From: alten-dturus Date: Thu, 8 Jan 2026 18:12:31 +0100 Subject: [PATCH 07/20] Update DatePickerFilterField, FreetextFilterField and NumericFilterField adding hasSubmitButton prop --- .../components/DatepickerFilterField.tsx | 22 ++++++++++++------- .../components/FreetextFilterField.tsx | 5 +++-- .../filters/components/NumericFilterField.tsx | 5 +++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/features/filters/components/DatepickerFilterField.tsx b/src/features/filters/components/DatepickerFilterField.tsx index 3f45ad2..6428564 100644 --- a/src/features/filters/components/DatepickerFilterField.tsx +++ b/src/features/filters/components/DatepickerFilterField.tsx @@ -15,6 +15,7 @@ export const DatepickerFilterField: React.FC = ({ value, onChangeActiveFilter, onFieldsValuesChange, + hasSubmitButton, }) => { const field = _field as DatepickerFilterFieldOptions const filterKey = field.name @@ -54,18 +55,23 @@ export const DatepickerFilterField: React.FC = ({ inputAdornment: (inputAdornmentProps) => ( <> - - - + {!hasSubmitButton && ( + + + + )} ), }} slotProps={{ - textField: { size: 'small', onKeyDown: handleDatepickerKeyDown }, + textField: { + size: 'small', + onKeyDown: hasSubmitButton ? undefined : handleDatepickerKeyDown, + }, }} /> diff --git a/src/features/filters/components/FreetextFilterField.tsx b/src/features/filters/components/FreetextFilterField.tsx index dfb57ec..5a8789a 100644 --- a/src/features/filters/components/FreetextFilterField.tsx +++ b/src/features/filters/components/FreetextFilterField.tsx @@ -9,6 +9,7 @@ export const FreetextFilterField: React.FC = ({ value, onChangeActiveFilter, onFieldsValuesChange, + hasSubmitButton, }) => { const searchIconAriaLabel = getLocalizedValue({ it: 'Filtra', en: 'Filter' }) const filterKey = field.name @@ -40,9 +41,9 @@ export const FreetextFilterField: React.FC = ({ name={field.name} value={value} onChange={handleFieldValueChange} - onKeyDown={handleKeyDown} + onKeyDown={hasSubmitButton ? undefined : handleKeyDown} InputProps={{ - endAdornment: ( + endAdornment: hasSubmitButton ? undefined : ( diff --git a/src/features/filters/components/NumericFilterField.tsx b/src/features/filters/components/NumericFilterField.tsx index 1b7dc14..a54a186 100644 --- a/src/features/filters/components/NumericFilterField.tsx +++ b/src/features/filters/components/NumericFilterField.tsx @@ -9,6 +9,7 @@ export const NumericFilterField: React.FC = ({ value, onChangeActiveFilter, onFieldsValuesChange, + hasSubmitButton, }) => { const field = _field as NumericFilterFieldOptions const searchIconAriaLabel = getLocalizedValue({ it: 'Filtra', en: 'Filter' }) @@ -61,7 +62,7 @@ export const NumericFilterField: React.FC = ({ name={field.name} value={value} onChange={handleTextFieldChange} - onKeyDown={handleKeyDown} + onKeyDown={hasSubmitButton ? undefined : handleKeyDown} onBlur={handleBlur} InputProps={{ inputProps: { @@ -69,7 +70,7 @@ export const NumericFilterField: React.FC = ({ max: field.max, style: { paddingRight: 20 }, }, - endAdornment: ( + endAdornment: hasSubmitButton ? undefined : ( From d74934448ccc403ca256a894a8997a326ea3fc72 Mon Sep 17 00:00:00 2001 From: alten-dturus Date: Thu, 8 Jan 2026 18:13:39 +0100 Subject: [PATCH 08/20] Update AutocompleteMultipleFilterField and AutocompleteSingleFilterField adding hasSubmitButton prop --- .../components/AutocompleteMultipleFilterField.tsx | 13 ++++++++----- .../components/AutocompleteSingleFilterField.tsx | 8 ++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/features/filters/components/AutocompleteMultipleFilterField.tsx b/src/features/filters/components/AutocompleteMultipleFilterField.tsx index 2eb79ad..a847706 100644 --- a/src/features/filters/components/AutocompleteMultipleFilterField.tsx +++ b/src/features/filters/components/AutocompleteMultipleFilterField.tsx @@ -13,6 +13,7 @@ export const AutocompleteMultipleFilterField: React.FC = value, onChangeActiveFilter, onFieldsValuesChange, + hasSubmitButton, }) => { const field = _field as AutocompleteFilterFieldOptions const filterKey = field.name @@ -21,11 +22,13 @@ export const AutocompleteMultipleFilterField: React.FC = const handleAutocompleteMultipleChange = (data: FilterFieldsValues['string']) => { onFieldsValuesChange(filterKey, data) - clearTimeout(debounceRef.current) - debounceRef.current = setTimeout( - () => onChangeActiveFilter('autocomplete-multiple', filterKey, data), - 300 - ) + if (!hasSubmitButton) { + clearTimeout(debounceRef.current) + debounceRef.current = setTimeout( + () => onChangeActiveFilter('autocomplete-multiple', filterKey, data), + 300 + ) + } } return ( diff --git a/src/features/filters/components/AutocompleteSingleFilterField.tsx b/src/features/filters/components/AutocompleteSingleFilterField.tsx index ade42d4..2f7ee92 100644 --- a/src/features/filters/components/AutocompleteSingleFilterField.tsx +++ b/src/features/filters/components/AutocompleteSingleFilterField.tsx @@ -9,20 +9,24 @@ import { AutocompleteBaseFilterField } from './AutocompleteBaseFilterField' export const AutocompleteSingleFilterField: React.FC = ({ field: _field, + value, onChangeActiveFilter, + onFieldsValuesChange, + hasSubmitButton, }) => { const field = _field as AutocompleteFilterFieldOptions const filterKey = field.name const handleAutocompleteSingleChange = (data: FilterFieldsValues['string']) => { - onChangeActiveFilter('autocomplete-single', filterKey, data) + onFieldsValuesChange(filterKey, data) + if (!hasSubmitButton) onChangeActiveFilter('autocomplete-single', filterKey, data) } return ( label={field.label} blurOnSelect - value={null as unknown as FilterOption} + value={value as FilterOption} options={field.options} onInputChange={field?.onTextInputChange} onChange={(_, data) => { From 7573fdf137d9fe0ddd077cfdc8d9bcdac2cf7ced Mon Sep 17 00:00:00 2001 From: alten-dturus Date: Thu, 8 Jan 2026 18:15:07 +0100 Subject: [PATCH 09/20] Add new width prop to fields and the disable button check to FilterFields --- .../filters/components/FiltersFields.tsx | 21 +++++++++++++++++-- src/features/filters/filters.types.ts | 4 ++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/features/filters/components/FiltersFields.tsx b/src/features/filters/components/FiltersFields.tsx index 0730c42..8f82051 100644 --- a/src/features/filters/components/FiltersFields.tsx +++ b/src/features/filters/components/FiltersFields.tsx @@ -13,6 +13,11 @@ import { NumericFilterField } from './NumericFilterField' import { FreetextFilterField } from './FreetextFilterField' import { getLocalizedValue } from '@/utils/common.utils' +/** + * The default width of the filter field in a 12-column grid system. + */ +const filterFieldDefaultWidth = 3 + type FiltersFieldsProps = { fields: FilterFields onChangeActiveFilter: FilterHandler @@ -41,6 +46,11 @@ export const FiltersFields: React.FC = ({ it: 'Filtra', en: 'Filter', }) + + const buttonDisabled = Object.entries(fieldsValues).every( + ([_, value]) => value === null || value === '' || (Array.isArray(value) && value.length === 0) + ) + return ( {fields.map((field) => { @@ -52,7 +62,7 @@ export const FiltersFields: React.FC = ({ hasSubmitButton, } return ( - + {field.type === 'freetext' && } {field.type === 'numeric' && } {field.type === 'autocomplete-multiple' && ( @@ -67,7 +77,13 @@ export const FiltersFields: React.FC = ({ })} {hasSubmitButton && ( - diff --git a/src/features/filters/filters.types.ts b/src/features/filters/filters.types.ts index 8b087a1..1f34f84 100644 --- a/src/features/filters/filters.types.ts +++ b/src/features/filters/filters.types.ts @@ -28,6 +28,10 @@ type FilterFieldCommon = { * The label of the filter field. */ label: string + /** + * The width of the filter field in a 12-column grid system. + */ + width?: number } export type FreetextFilterFieldOptions = FilterFieldCommon & { From 937676cf7a03fa6502d5862307ff0074ab762e22 Mon Sep 17 00:00:00 2001 From: alten-dturus Date: Mon, 12 Jan 2026 10:02:12 +0100 Subject: [PATCH 10/20] Add state update on handleRemoveActiveFilter action --- src/features/filters/components/Filters.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/features/filters/components/Filters.tsx b/src/features/filters/components/Filters.tsx index e347beb..2554255 100644 --- a/src/features/filters/components/Filters.tsx +++ b/src/features/filters/components/Filters.tsx @@ -46,6 +46,9 @@ export const Filters: React.FC = ({ filterKey, fieldValue.filter(({ value: v }) => v !== value) ) + } else { + const defaultValues = getFiltersFieldsDefaultValue(fields, hasSubmitButton) + handleFieldsValuesChange(filterKey, defaultValues[filterKey]) } onRemoveActiveFilter(type, filterKey, value) } From f8d084e1e4a2ead85d22131da187b4c893b91db4 Mon Sep 17 00:00:00 2001 From: alten-dturus Date: Mon, 12 Jan 2026 10:02:56 +0100 Subject: [PATCH 11/20] fix test --- src/features/filters/__tests__/Filters.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/features/filters/__tests__/Filters.test.tsx b/src/features/filters/__tests__/Filters.test.tsx index 4c9157d..fd9763e 100644 --- a/src/features/filters/__tests__/Filters.test.tsx +++ b/src/features/filters/__tests__/Filters.test.tsx @@ -58,6 +58,7 @@ describe('Filters component', () => { { { { { { { { { Date: Mon, 12 Jan 2026 15:41:29 +0100 Subject: [PATCH 12/20] Update AutocompleteBaseFilterField adding hasSubmitButton prop and label management --- .../AutocompleteBaseFilterField.tsx | 41 ++++++++++++++++++- .../AutocompleteMultipleFilterField.tsx | 1 + 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/features/filters/components/AutocompleteBaseFilterField.tsx b/src/features/filters/components/AutocompleteBaseFilterField.tsx index dfb6ce2..a90ee71 100644 --- a/src/features/filters/components/AutocompleteBaseFilterField.tsx +++ b/src/features/filters/components/AutocompleteBaseFilterField.tsx @@ -14,7 +14,7 @@ type AutocompleteBaseFilterFieldProps = Omit< | 'size' | 'renderInput' | 'onInputChange' -> & { label: string; onInputChange?: (value: string) => void } +> & { label: string; onInputChange?: (value: string) => void; hasSubmitButton?: boolean } export const AutocompleteBaseFilterField = ( props: AutocompleteBaseFilterFieldProps @@ -24,6 +24,36 @@ export const AutocompleteBaseFilterField = ( en: 'No results', }) + function getSelectedElementsLabelText(): { label: string; ariaLabel: string } { + let label = props.label + let ariaLabel = props.label + + if (props.hasSubmitButton && Array.isArray(props.value) && props.value.length >= 1) { + const selectedElements = props.value + + label = getLocalizedValue({ + it: `${props.label} (${selectedElements.length})`, + en: `${props.label} (${selectedElements.length})`, + }) + + if (props.value.length === 1) { + ariaLabel = getLocalizedValue({ + it: `${props.label} (${selectedElements.length} elemento selezionato)`, + en: `${props.label} (${selectedElements.length} element selected)`, + }) + } + + if (props.value.length > 1) { + ariaLabel = getLocalizedValue({ + it: `${props.label} (${selectedElements.length} elementi selezionati)`, + en: `${props.label} (${selectedElements.length} elements selected)`, + }) + } + } + + return { label, ariaLabel } + } + return ( {...props} @@ -46,7 +76,14 @@ export const AutocompleteBaseFilterField = ( props.onChange?.(event, data, reason) }} renderInput={(params) => { - return + return ( + + ) }} /> ) diff --git a/src/features/filters/components/AutocompleteMultipleFilterField.tsx b/src/features/filters/components/AutocompleteMultipleFilterField.tsx index a847706..1d48599 100644 --- a/src/features/filters/components/AutocompleteMultipleFilterField.tsx +++ b/src/features/filters/components/AutocompleteMultipleFilterField.tsx @@ -53,6 +53,7 @@ export const AutocompleteMultipleFilterField: React.FC = ) }} + hasSubmitButton={hasSubmitButton} /> ) } From 756dc623ef447c36668781c94b2415f4f49e8d62 Mon Sep 17 00:00:00 2001 From: alten-dturus Date: Mon, 12 Jan 2026 17:36:02 +0100 Subject: [PATCH 13/20] Update AutocompleteBaseFilterField aria-label --- .../filters/components/AutocompleteBaseFilterField.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/features/filters/components/AutocompleteBaseFilterField.tsx b/src/features/filters/components/AutocompleteBaseFilterField.tsx index a90ee71..8bd7049 100644 --- a/src/features/filters/components/AutocompleteBaseFilterField.tsx +++ b/src/features/filters/components/AutocompleteBaseFilterField.tsx @@ -81,7 +81,9 @@ export const AutocompleteBaseFilterField = ( variant="outlined" {...params} label={getSelectedElementsLabelText().label} - aria-label={getSelectedElementsLabelText().ariaLabel} + aria-label={ + props.hasSubmitButton ? getSelectedElementsLabelText().ariaLabel : undefined + } /> ) }} From 9b37b58477b86ff9756c83c059c4accd846c4e7c Mon Sep 17 00:00:00 2001 From: borgesis95 Date: Tue, 24 Mar 2026 12:10:27 +0100 Subject: [PATCH 14/20] refactor: added breakpoint, refactor method --- .../__snapshots__/Table.test.tsx.snap | 52 ++-- .../__snapshots__/Filters.test.tsx.snap | 274 +++++++++--------- .../filters/components/ActiveFiltersChips.tsx | 51 ++-- .../AutocompleteBaseFilterField.tsx | 40 ++- src/features/filters/components/Filters.tsx | 6 +- .../filters/components/FiltersFields.tsx | 4 +- src/features/filters/filters.utils.ts | 1 - src/features/filters/hooks/useFilters.ts | 144 ++++----- .../filters/stories/FiltersExample.tsx | 16 +- .../pagination/__tests__/Pagination.test.tsx | 6 +- 10 files changed, 287 insertions(+), 307 deletions(-) diff --git a/src/components/__tests__/__snapshots__/Table.test.tsx.snap b/src/components/__tests__/__snapshots__/Table.test.tsx.snap index 439ef2f..819d9d2 100644 --- a/src/components/__tests__/__snapshots__/Table.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Table.test.tsx.snap @@ -80,7 +80,7 @@ exports[`Checks that Table snapshots didn't change > renders Table empty state 1 class="MuiTableCell-root MuiTableCell-body MuiTableCell-sizeMedium css-1rpd3i8-MuiTableCell-root" >
renders Table empty state 1 class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignRight MuiTableCell-sizeMedium css-2u9kxv-MuiTableCell-root" >
Single Filter Field @@ -89,7 +89,7 @@ exports[`Filters component > matches the snapshot with more than one active filt
matches the snapshot with more than one active filt
Numeric Field @@ -175,11 +175,11 @@ exports[`Filters component > matches the snapshot with more than one active filt
matches the snapshot with more than one active filt value="" />
Multiple Filter Field @@ -276,11 +276,11 @@ exports[`Filters component > matches the snapshot with more than one active filt
matches the snapshot with more than one active filt value="" />
Autocomplete Single Field @@ -387,7 +387,7 @@ exports[`Filters component > matches the snapshot with more than one active filt className="MuiStack-root css-1xvxq01-MuiStack-root" >
matches the snapshot with more than one active filt
matches the snapshot with more than one active filt
matches the snapshot with more than one active filt />
-
- -
+ Annulla filtri +
@@ -523,13 +519,13 @@ exports[`Filters component > matches the snapshot with more than one active filt exports[`Filters component > matches the snapshot with more than one active filters and right content 1`] = `
matches the snapshot with more than one active filt
Single Filter Field @@ -610,7 +606,7 @@ exports[`Filters component > matches the snapshot with more than one active filt
matches the snapshot with more than one active filt
Numeric Field @@ -696,11 +692,11 @@ exports[`Filters component > matches the snapshot with more than one active filt
matches the snapshot with more than one active filt value="" />
Multiple Filter Field @@ -797,11 +793,11 @@ exports[`Filters component > matches the snapshot with more than one active filt
matches the snapshot with more than one active filt value="" />
Autocomplete Single Field @@ -908,7 +904,7 @@ exports[`Filters component > matches the snapshot with more than one active filt className="MuiStack-root css-1xvxq01-MuiStack-root" >
matches the snapshot with more than one active filt
matches the snapshot with more than one active filt
matches the snapshot with more than one active filt />
-
- -
+ Annulla filtri +
Right Content @@ -1047,13 +1039,13 @@ exports[`Filters component > matches the snapshot with more than one active filt exports[`Filters component > matches the snapshot with one active filter 1`] = `
matches the snapshot with one active filter 1`] = `
Single Filter Field @@ -1134,7 +1126,7 @@ exports[`Filters component > matches the snapshot with one active filter 1`] = `
matches the snapshot with one active filter 1`] = `
Numeric Field @@ -1220,11 +1212,11 @@ exports[`Filters component > matches the snapshot with one active filter 1`] = `
matches the snapshot with one active filter 1`] = ` value="" />
Multiple Filter Field @@ -1324,11 +1316,11 @@ exports[`Filters component > matches the snapshot with one active filter 1`] = `
matches the snapshot with one active filter 1`] = ` value="" />
Autocomplete Single Field @@ -1438,7 +1430,7 @@ exports[`Filters component > matches the snapshot with one active filter 1`] = ` className="MuiStack-root css-1xvxq01-MuiStack-root" >
matches the snapshot with one active filter 1`] = ` exports[`Filters component > matches the snapshot with one active filter and a right content 1`] = `
matches the snapshot with one active filter and a r
Single Filter Field @@ -1566,7 +1558,7 @@ exports[`Filters component > matches the snapshot with one active filter and a r
matches the snapshot with one active filter and a r
Numeric Field @@ -1652,11 +1644,11 @@ exports[`Filters component > matches the snapshot with one active filter and a r
matches the snapshot with one active filter and a r value="" />
Multiple Filter Field @@ -1756,11 +1748,11 @@ exports[`Filters component > matches the snapshot with one active filter and a r
matches the snapshot with one active filter and a r value="" />
Autocomplete Single Field @@ -1870,7 +1862,7 @@ exports[`Filters component > matches the snapshot with one active filter and a r className="MuiStack-root css-1xvxq01-MuiStack-root" >
matches the snapshot with one active filter and a r exports[`Filters component > matches the snapshot without active filters 1`] = `
matches the snapshot without active filters 1`] = `
Single Filter Field @@ -2001,7 +1993,7 @@ exports[`Filters component > matches the snapshot without active filters 1`] = `
matches the snapshot without active filters 1`] = `
Numeric Field @@ -2087,11 +2079,11 @@ exports[`Filters component > matches the snapshot without active filters 1`] = `
matches the snapshot without active filters 1`] = ` value="" />
Multiple Filter Field @@ -2188,11 +2180,11 @@ exports[`Filters component > matches the snapshot without active filters 1`] = `
matches the snapshot without active filters 1`] = ` value="" />
Autocomplete Single Field diff --git a/src/features/filters/components/ActiveFiltersChips.tsx b/src/features/filters/components/ActiveFiltersChips.tsx index 9d5a8b3..bc347a3 100644 --- a/src/features/filters/components/ActiveFiltersChips.tsx +++ b/src/features/filters/components/ActiveFiltersChips.tsx @@ -3,7 +3,7 @@ import { Button, Chip, Divider, Stack } from '@mui/material' import type { ActiveFilters, FilterHandler } from '../filters.types' import { getLocalizedValue } from '../../../utils/common.utils' -type ActiveFilterChips = { +type ActiveFilterChipsProps = { activeFilters: ActiveFilters onRemoveActiveFilter: FilterHandler onResetActiveFilters: VoidFunction @@ -11,19 +11,29 @@ type ActiveFilterChips = { rightContent?: React.ReactNode } -export const ActiveFilterChips: React.FC = ({ +const chipFocusStyles = { + '&.Mui-focusVisible': { + boxShadow: '0px 0px 8px 2px rgba(25, 118, 210, 0.3)', + transform: 'scale(1.05)', + transition: 'transform 0.2s ease-in-out', + }, +} + +const cancelFiltersLabel = getLocalizedValue({ + it: 'Annulla filtri', + en: 'Cancel filters', +}) + +export const ActiveFilterChips: React.FC = ({ activeFilters, onRemoveActiveFilter, onResetActiveFilters, hasSubmitButton, rightContent, }) => { - if (activeFilters.length <= 0 && !rightContent) return null + if (activeFilters.length === 0 && !rightContent) return null - const cancelFiltersLabel = getLocalizedValue({ - it: 'Annulla filtri', - en: 'Cancel filters', - }) + const showResetButton = !hasSubmitButton && activeFilters.length > 1 return ( <> @@ -38,23 +48,22 @@ export const ActiveFilterChips: React.FC = ({ {activeFilters.map(({ value, label, type, filterKey }) => ( onRemoveActiveFilter(type, filterKey, value)} /> ))} - {!hasSubmitButton && activeFilters.length > 1 && ( - - - + {showResetButton && ( + )} {rightContent} diff --git a/src/features/filters/components/AutocompleteBaseFilterField.tsx b/src/features/filters/components/AutocompleteBaseFilterField.tsx index 8bd7049..d1a56e3 100644 --- a/src/features/filters/components/AutocompleteBaseFilterField.tsx +++ b/src/features/filters/components/AutocompleteBaseFilterField.tsx @@ -25,31 +25,27 @@ export const AutocompleteBaseFilterField = ( }) function getSelectedElementsLabelText(): { label: string; ariaLabel: string } { - let label = props.label - let ariaLabel = props.label - - if (props.hasSubmitButton && Array.isArray(props.value) && props.value.length >= 1) { - const selectedElements = props.value + // If the filter has no submit button, or if it has a submit button but the value is empty, the label is just the field label + // otherwise it shows the number of selected elements + if (!props.hasSubmitButton || !Array.isArray(props.value) || props.value.length === 0) { + return { label: props.label, ariaLabel: props.label } + } - label = getLocalizedValue({ - it: `${props.label} (${selectedElements.length})`, - en: `${props.label} (${selectedElements.length})`, - }) + const elementNumber = props.value.length - if (props.value.length === 1) { - ariaLabel = getLocalizedValue({ - it: `${props.label} (${selectedElements.length} elemento selezionato)`, - en: `${props.label} (${selectedElements.length} element selected)`, - }) - } + const label = getLocalizedValue({ + it: `${props.label} (${elementNumber})`, + en: `${props.label} (${elementNumber})`, + }) - if (props.value.length > 1) { - ariaLabel = getLocalizedValue({ - it: `${props.label} (${selectedElements.length} elementi selezionati)`, - en: `${props.label} (${selectedElements.length} elements selected)`, - }) - } - } + const ariaLabel = getLocalizedValue({ + it: `${props.label} (${elementNumber} ${ + elementNumber === 1 ? 'elemento selezionato' : 'elementi selezionati' + })`, + en: `${props.label} (${elementNumber} ${ + elementNumber === 1 ? 'element' : 'elements' + } selected)`, + }) return { label, ariaLabel } } diff --git a/src/features/filters/components/Filters.tsx b/src/features/filters/components/Filters.tsx index 2554255..5bf4b55 100644 --- a/src/features/filters/components/Filters.tsx +++ b/src/features/filters/components/Filters.tsx @@ -58,19 +58,19 @@ export const Filters: React.FC = ({ onResetActiveFilters() } - const onSubmit = () => { + const handleSubmit = () => { onSetActiveFilters(fields, fieldsValues) } return ( - + = ({ hasSubmitButton, } return ( - + {field.type === 'freetext' && } {field.type === 'numeric' && } {field.type === 'autocomplete-multiple' && ( @@ -76,7 +76,7 @@ export const FiltersFields: React.FC = ({ ) })} {hasSubmitButton && ( - +

matches the snapshot with more than one active filt

matches the snapshot with one active filter 1`] = `

matches the snapshot with one active filter and a r

= ({ return ( <> - + = ({ ) })} {hasSubmitButton && ( - +
Single Filter Field @@ -161,10 +161,10 @@ exports[`Filters component > matches the snapshot with more than one active filt
Numeric Field @@ -179,7 +179,7 @@ exports[`Filters component > matches the snapshot with more than one active filt >
matches the snapshot with more than one active filt value="" />
Multiple Filter Field @@ -280,7 +280,7 @@ exports[`Filters component > matches the snapshot with more than one active filt >
matches the snapshot with more than one active filt value="" />
Autocomplete Single Field @@ -387,7 +387,7 @@ exports[`Filters component > matches the snapshot with more than one active filt className="MuiStack-root css-1xvxq01-MuiStack-root" >
matches the snapshot with more than one active filt
matches the snapshot with more than one active filt
matches the snapshot with more than one active filt
Single Filter Field @@ -678,10 +678,10 @@ exports[`Filters component > matches the snapshot with more than one active filt
Numeric Field @@ -696,7 +696,7 @@ exports[`Filters component > matches the snapshot with more than one active filt >
matches the snapshot with more than one active filt value="" />
Multiple Filter Field @@ -797,7 +797,7 @@ exports[`Filters component > matches the snapshot with more than one active filt >
matches the snapshot with more than one active filt value="" />
Autocomplete Single Field @@ -904,7 +904,7 @@ exports[`Filters component > matches the snapshot with more than one active filt className="MuiStack-root css-1xvxq01-MuiStack-root" >
matches the snapshot with more than one active filt
matches the snapshot with more than one active filt
matches the snapshot with more than one active filt
Single Filter Field @@ -1198,10 +1198,10 @@ exports[`Filters component > matches the snapshot with one active filter 1`] = `
Numeric Field @@ -1216,7 +1216,7 @@ exports[`Filters component > matches the snapshot with one active filter 1`] = ` >
matches the snapshot with one active filter 1`] = ` value="" />
Multiple Filter Field @@ -1320,7 +1320,7 @@ exports[`Filters component > matches the snapshot with one active filter 1`] = ` >
matches the snapshot with one active filter 1`] = ` value="" />
Autocomplete Single Field @@ -1430,7 +1430,7 @@ exports[`Filters component > matches the snapshot with one active filter 1`] = ` className="MuiStack-root css-1xvxq01-MuiStack-root" >
matches the snapshot with one active filter and a r
Single Filter Field @@ -1630,10 +1630,10 @@ exports[`Filters component > matches the snapshot with one active filter and a r
Numeric Field @@ -1648,7 +1648,7 @@ exports[`Filters component > matches the snapshot with one active filter and a r >
matches the snapshot with one active filter and a r value="" />
Multiple Filter Field @@ -1752,7 +1752,7 @@ exports[`Filters component > matches the snapshot with one active filter and a r >
matches the snapshot with one active filter and a r value="" />
Autocomplete Single Field @@ -1862,7 +1862,7 @@ exports[`Filters component > matches the snapshot with one active filter and a r className="MuiStack-root css-1xvxq01-MuiStack-root" >
matches the snapshot without active filters 1`] = `
Single Filter Field @@ -2065,10 +2065,10 @@ exports[`Filters component > matches the snapshot without active filters 1`] = `
Numeric Field @@ -2083,7 +2083,7 @@ exports[`Filters component > matches the snapshot without active filters 1`] = ` >
matches the snapshot without active filters 1`] = ` value="" />
Multiple Filter Field @@ -2184,7 +2184,7 @@ exports[`Filters component > matches the snapshot without active filters 1`] = ` >
matches the snapshot without active filters 1`] = ` value="" />
Autocomplete Single Field diff --git a/src/features/pagination/__tests__/Pagination.test.tsx b/src/features/pagination/__tests__/Pagination.test.tsx index e58b77a..54a736e 100644 --- a/src/features/pagination/__tests__/Pagination.test.tsx +++ b/src/features/pagination/__tests__/Pagination.test.tsx @@ -49,17 +49,16 @@ describe('Pagination component', () => { expect(screen).toBeDefined() }) - it('Should be available [10,24,36] as rows per page as default options', async () => { + it.only('Should be available [10,24,36] as rows per page as default options', async () => { const screen = render() - const selectElement = screen.getByTestId('rows-per-page-select') - const selectButton = within(selectElement).getByRole('combobox') - - screen.debug(selectButton) - expect(selectElement).toBeDefined() + const selectElement = screen.getByTestId('rows-per-page-select') + const selectButton = within(selectElement).getByRole('button') await userEvent.click(selectButton) await waitFor(() => { + // screen.debug() + const getOptions = screen.getAllByRole('option') expect(getOptions).toHaveLength(3) From 43bff3360211f887786c18af3fd347b64595c417 Mon Sep 17 00:00:00 2001 From: borgesis95 Date: Thu, 26 Mar 2026 11:32:10 +0100 Subject: [PATCH 17/20] test: removed only --- src/features/pagination/__tests__/Pagination.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/pagination/__tests__/Pagination.test.tsx b/src/features/pagination/__tests__/Pagination.test.tsx index 54a736e..52dd97a 100644 --- a/src/features/pagination/__tests__/Pagination.test.tsx +++ b/src/features/pagination/__tests__/Pagination.test.tsx @@ -49,7 +49,7 @@ describe('Pagination component', () => { expect(screen).toBeDefined() }) - it.only('Should be available [10,24,36] as rows per page as default options', async () => { + it('Should be available [10,24,36] as rows per page as default options', async () => { const screen = render() const selectElement = screen.getByTestId('rows-per-page-select') From b6d248c9794a7fa1e0682e7e2a98f22481e41541 Mon Sep 17 00:00:00 2001 From: borgesis95 Date: Thu, 26 Mar 2026 17:40:30 +0100 Subject: [PATCH 18/20] refactor: copilot error fixing --- .../__snapshots__/Filters.test.tsx.snap | 16 +++++----- .../filters/__tests__/filter.utils.test.ts | 1 + .../filters/components/ActiveFiltersChips.tsx | 6 ++-- src/features/filters/components/Filters.tsx | 4 +-- src/features/filters/filters.utils.ts | 29 +++++++++---------- src/features/filters/hooks/useFilters.ts | 8 +---- .../pagination/__tests__/Pagination.test.tsx | 2 -- 7 files changed, 28 insertions(+), 38 deletions(-) diff --git a/src/features/filters/__tests__/__snapshots__/Filters.test.tsx.snap b/src/features/filters/__tests__/__snapshots__/Filters.test.tsx.snap index 4f07f2f..9e67153 100644 --- a/src/features/filters/__tests__/__snapshots__/Filters.test.tsx.snap +++ b/src/features/filters/__tests__/__snapshots__/Filters.test.tsx.snap @@ -387,7 +387,7 @@ exports[`Filters component > matches the snapshot with more than one active filt className="MuiStack-root css-1xvxq01-MuiStack-root" >
matches the snapshot with more than one active filt
matches the snapshot with more than one active filt
matches the snapshot with more than one active filt className="MuiStack-root css-1xvxq01-MuiStack-root" >
matches the snapshot with more than one active filt
matches the snapshot with more than one active filt
matches the snapshot with one active filter 1`] = ` className="MuiStack-root css-1xvxq01-MuiStack-root" >
matches the snapshot with one active filter and a r className="MuiStack-root css-1xvxq01-MuiStack-root" >
{ const result = getFiltersFieldsDefaultValue(fieldMocks) expect(result).toEqual({ + 'autocomplete-single-field': null, 'datepicker-field': null, 'single-field': '', 'multiple-field': [], diff --git a/src/features/filters/components/ActiveFiltersChips.tsx b/src/features/filters/components/ActiveFiltersChips.tsx index 0a71cbb..8cf2e2c 100644 --- a/src/features/filters/components/ActiveFiltersChips.tsx +++ b/src/features/filters/components/ActiveFiltersChips.tsx @@ -2,6 +2,7 @@ import React from 'react' import { Button, Chip, Divider, Stack } from '@mui/material' import type { ActiveFilters, FilterHandler } from '../filters.types' import { getLocalizedValue } from '../../../utils/common.utils' +import { blue } from '@mui/material/colors' type ActiveFilterChipsProps = { activeFilters: ActiveFilters @@ -13,9 +14,8 @@ type ActiveFilterChipsProps = { const chipFocusStyles = { '&.Mui-focusVisible': { - boxShadow: '0px 0px 8px 2px rgba(25, 118, 210, 0.3)', - transform: 'scale(1.05)', - transition: 'transform 0.2s ease-in-out', + outline: `2px solid ${blue[400]}`, + outlineOffset: '1px', }, } diff --git a/src/features/filters/components/Filters.tsx b/src/features/filters/components/Filters.tsx index 5bf4b55..5ad4cf4 100644 --- a/src/features/filters/components/Filters.tsx +++ b/src/features/filters/components/Filters.tsx @@ -47,14 +47,14 @@ export const Filters: React.FC = ({ fieldValue.filter(({ value: v }) => v !== value) ) } else { - const defaultValues = getFiltersFieldsDefaultValue(fields, hasSubmitButton) + const defaultValues = getFiltersFieldsDefaultValue(fields) handleFieldsValuesChange(filterKey, defaultValues[filterKey]) } onRemoveActiveFilter(type, filterKey, value) } const handleResetActiveFilters = () => { - setFieldsValues(getFiltersFieldsDefaultValue(fields, hasSubmitButton)) + setFieldsValues(getFiltersFieldsDefaultValue(fields)) onResetActiveFilters() } diff --git a/src/features/filters/filters.utils.ts b/src/features/filters/filters.utils.ts index 6946ae8..e18b5dc 100644 --- a/src/features/filters/filters.utils.ts +++ b/src/features/filters/filters.utils.ts @@ -8,23 +8,20 @@ import type { } from './filters.types' /** Map passed fields options to the field state default value */ -export function getFiltersFieldsDefaultValue( - fields: FilterFields, - hasSubmitButton?: boolean -): FilterFieldsValues { - return fields.reduce((prev, field) => { - if (field.type === 'autocomplete-multiple') { - return { ...prev, [field.name]: [] } - } - if (field.type === 'autocomplete-single') { - if (hasSubmitButton) return { ...prev, [field.name]: null } - // autocomplete single has no need to be in the fields state when no submit button - return prev - } - if (field.type === 'datepicker') { - return { ...prev, [field.name]: null } +export function getFiltersFieldsDefaultValue(fields: FilterFields): FilterFieldsValues { + return fields.reduce((acc, field) => { + switch (field.type) { + case 'autocomplete-multiple': + acc[field.name] = [] + break + case 'autocomplete-single': + case 'datepicker': + acc[field.name] = null + break + default: + acc[field.name] = '' } - return { ...prev, [field.name]: '' } + return acc }, {}) } diff --git a/src/features/filters/hooks/useFilters.ts b/src/features/filters/hooks/useFilters.ts index 3d0aab9..e24eeb1 100644 --- a/src/features/filters/hooks/useFilters.ts +++ b/src/features/filters/hooks/useFilters.ts @@ -131,8 +131,6 @@ export function useFilters( const onSetActiveFilters = React.useCallback( (fields, fieldsValues) => { setSearchParams((searchParams) => { - let hasChanges = false - fields.forEach((field) => { const { type, name: filterKey } = field const value = fieldsValues[filterKey] @@ -141,14 +139,10 @@ export function useFilters( searchParams.delete(filterKey) } else { setFilterParam(searchParams, type, filterKey, value) - hasChanges = true } }) - if (hasChanges) { - searchParams.delete('offset') - } - + searchParams.delete('offset') return searchParams }) }, diff --git a/src/features/pagination/__tests__/Pagination.test.tsx b/src/features/pagination/__tests__/Pagination.test.tsx index 52dd97a..2fe3fac 100644 --- a/src/features/pagination/__tests__/Pagination.test.tsx +++ b/src/features/pagination/__tests__/Pagination.test.tsx @@ -57,8 +57,6 @@ describe('Pagination component', () => { await userEvent.click(selectButton) await waitFor(() => { - // screen.debug() - const getOptions = screen.getAllByRole('option') expect(getOptions).toHaveLength(3) From e937e8d78b5347c09384cb6516e9459e114e46e4 Mon Sep 17 00:00:00 2001 From: Alepazz Date: Tue, 31 Mar 2026 12:08:23 +0200 Subject: [PATCH 19/20] test: add hasSubmitButton flow test coverage (PIN-7358) --- .../filters/__tests__/Filters.test.tsx | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/src/features/filters/__tests__/Filters.test.tsx b/src/features/filters/__tests__/Filters.test.tsx index fd9763e..9792cac 100644 --- a/src/features/filters/__tests__/Filters.test.tsx +++ b/src/features/filters/__tests__/Filters.test.tsx @@ -243,3 +243,151 @@ describe('Filters component', () => { expect(onResetActiveFilters).toBeCalled() }) }) + +describe('Filters component with hasSubmitButton', () => { + it('should render "Filtra" and "Annulla filtri" buttons', () => { + const screen = renderWithRouter( + + ) + + expect(screen.getByRole('button', { name: 'Filtra' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Annulla filtri' })).toBeInTheDocument() + }) + + it('should not render submit buttons when hasSubmitButton is not set', () => { + const screen = renderWithRouter( + + ) + + expect(screen.queryByRole('button', { name: 'Filtra' })).not.toBeInTheDocument() + }) + + it('should not apply filters immediately when typing in a freetext field', async () => { + const user = userEvent.setup() + const onChangeActiveFilterFn = vi.fn() + + const screen = renderWithRouter( + + ) + + const singleFilterField = screen.getByLabelText('Single Filter Field') as HTMLInputElement + await user.type(singleFilterField, 'test-value') + expect(singleFilterField.value).toBe('test-value') + expect(onChangeActiveFilterFn).not.toHaveBeenCalled() + }) + + it('should not apply filters on Enter key in a freetext field', async () => { + const user = userEvent.setup() + const onChangeActiveFilterFn = vi.fn() + + const screen = renderWithRouter( + + ) + + const singleFilterField = screen.getByLabelText('Single Filter Field') as HTMLInputElement + await user.type(singleFilterField, 'test-value{enter}') + expect(onChangeActiveFilterFn).not.toHaveBeenCalled() + }) + + it('should not render search icon in freetext fields', () => { + const screen = renderWithRouter( + + ) + + expect(screen.queryByLabelText('Filtra')).not.toBeInTheDocument() + }) + + it('should call onSetActiveFilters with all field values when clicking "Filtra"', async () => { + const user = userEvent.setup() + const onSetActiveFiltersFn = vi.fn() + + const screen = renderWithRouter( + + ) + + const singleFilterField = screen.getByLabelText('Single Filter Field') as HTMLInputElement + await user.type(singleFilterField, 'test-value') + + const submitButton = screen.getByRole('button', { name: 'Filtra' }) + await user.click(submitButton) + + expect(onSetActiveFiltersFn).toHaveBeenCalledWith( + fieldMocks, + expect.objectContaining({ 'single-field': 'test-value' }) + ) + }) + + it('should reset fields and call onResetActiveFilters when clicking "Annulla filtri"', async () => { + const user = userEvent.setup() + const onResetActiveFiltersFn = vi.fn() + + const screen = renderWithRouter( + + ) + + const singleFilterField = screen.getByLabelText('Single Filter Field') as HTMLInputElement + await user.type(singleFilterField, 'something') + expect(singleFilterField.value).toBe('something') + + const cancelButton = screen.getByRole('button', { name: 'Annulla filtri' }) + await user.click(cancelButton) + + expect(onResetActiveFiltersFn).toHaveBeenCalled() + expect(singleFilterField.value).toBe('') + }) +}) From 8211698c97a33bf8d8237cba2f556a83a518d561 Mon Sep 17 00:00:00 2001 From: Alepazz Date: Wed, 1 Apr 2026 14:31:07 +0200 Subject: [PATCH 20/20] test: add hasSubmitButton integration tests (PIN-9625) --- .../__tests__/FiltersIntegration.test.tsx | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/features/filters/__tests__/FiltersIntegration.test.tsx diff --git a/src/features/filters/__tests__/FiltersIntegration.test.tsx b/src/features/filters/__tests__/FiltersIntegration.test.tsx new file mode 100644 index 0000000..a81793f --- /dev/null +++ b/src/features/filters/__tests__/FiltersIntegration.test.tsx @@ -0,0 +1,155 @@ +import React from 'react' +import { describe, expect, it } from 'vitest' +import userEvent from '@testing-library/user-event' +import { render, screen, waitFor } from '@testing-library/react' +import { MemoryRouter, Route, Routes, useSearchParams } from 'react-router-dom' +import { Filters } from '../components/Filters' +import { useFilters } from '../hooks/useFilters' +import type { FilterFields, FiltersParams } from '../filters.types' + +const fieldMocks: FilterFields = [ + { name: 'q', type: 'freetext', label: 'Name' }, + { + name: 'status', + type: 'autocomplete-multiple', + options: [ + { label: 'Active', value: 'active' }, + { label: 'Suspended', value: 'suspended' }, + ], + label: 'Status', + }, +] + +/** + * Integration wrapper that wires useFilters + Filters together, + * matching the real usage pattern in the frontend. + * Includes a hidden element exposing current URL search params for assertions. + */ +const FiltersWrapper: React.FC<{ hasSubmitButton?: boolean }> = ({ hasSubmitButton }) => { + const { filtersParams: _filtersParams, ...filtersHandlers } = + useFilters(fieldMocks) + const [searchParams] = useSearchParams() + return ( + <> + +
{searchParams.toString()}
+ + ) +} + +function renderFiltersWrapper(hasSubmitButton = true, initialSearch?: string) { + const initialEntries = initialSearch ? [`/?${initialSearch}`] : ['/'] + return render( + + + } /> + + + ) +} + +function getSearchParams(): string { + return screen.getByTestId('search-params').textContent ?? '' +} + +describe('Filters integration with hasSubmitButton', () => { + it('should have "Filtra" and "Annulla filtri" buttons disabled when no filter value is entered', () => { + renderFiltersWrapper() + + expect(screen.getByRole('button', { name: 'Filtra' })).toBeDisabled() + expect(screen.getByRole('button', { name: 'Annulla filtri' })).toBeDisabled() + }) + + it('should enable buttons when a filter value is entered', async () => { + const user = userEvent.setup() + renderFiltersWrapper() + + await user.type(screen.getByLabelText('Name'), 'test') + + expect(screen.getByRole('button', { name: 'Filtra' })).toBeEnabled() + expect(screen.getByRole('button', { name: 'Annulla filtri' })).toBeEnabled() + }) + + it('should update URL params and show chip after clicking "Filtra"', async () => { + const user = userEvent.setup() + renderFiltersWrapper() + + await user.type(screen.getByLabelText('Name'), 'test-value') + await user.click(screen.getByRole('button', { name: 'Filtra' })) + + await waitFor(() => { + expect(getSearchParams()).toContain('q=test-value') + }) + + expect(screen.getByText('test-value')).toBeInTheDocument() + }) + + it('should remove chip and URL param when a chip is deleted', async () => { + const user = userEvent.setup() + renderFiltersWrapper(true, 'q=existing-filter') + + expect(screen.getByText('existing-filter')).toBeInTheDocument() + expect(getSearchParams()).toContain('q=existing-filter') + + await user.click(screen.getByTestId('CancelIcon')) + + await waitFor(() => { + expect(getSearchParams()).not.toContain('q=') + }) + + expect(screen.queryByText('existing-filter')).not.toBeInTheDocument() + }) + + it('should clear URL params when clicking "Annulla filtri" after submitting a filter', async () => { + const user = userEvent.setup() + renderFiltersWrapper() + + // Type and submit + await user.type(screen.getByLabelText('Name'), 'some-filter') + await user.click(screen.getByRole('button', { name: 'Filtra' })) + + await waitFor(() => { + expect(getSearchParams()).toContain('q=some-filter') + }) + expect(screen.getByText('some-filter')).toBeInTheDocument() + + // Type something new to re-enable the buttons, then cancel + await user.type(screen.getByLabelText('Name'), 'x') + await user.click(screen.getByRole('button', { name: 'Annulla filtri' })) + + await waitFor(() => { + expect(getSearchParams()).not.toContain('q=') + }) + + expect((screen.getByLabelText('Name') as HTMLInputElement).value).toBe('') + }) + + it('should delete offset from URL when submitting filters', async () => { + const user = userEvent.setup() + renderFiltersWrapper(true, 'offset=20') + + expect(getSearchParams()).toContain('offset=20') + + await user.type(screen.getByLabelText('Name'), 'test') + await user.click(screen.getByRole('button', { name: 'Filtra' })) + + await waitFor(() => { + expect(getSearchParams()).not.toContain('offset=') + }) + expect(getSearchParams()).toContain('q=test') + }) + + it('should delete offset from URL when removing a filter chip', async () => { + const user = userEvent.setup() + renderFiltersWrapper(true, 'q=test&offset=20') + + expect(getSearchParams()).toContain('offset=20') + + await user.click(screen.getByTestId('CancelIcon')) + + await waitFor(() => { + expect(getSearchParams()).not.toContain('offset=') + }) + expect(getSearchParams()).not.toContain('q=') + }) +})