@mistertemp/libs-front-shared 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +18 -0
  3. package/index.ts +3 -0
  4. package/package.json +100 -0
  5. package/src/components/ControlledInputDate/ControlledInputDate.tsx +82 -0
  6. package/src/components/ControlledInputDate/__tests__/ControlledInputDate.unit.test.tsx +78 -0
  7. package/src/components/ControlledInputDate/index.ts +1 -0
  8. package/src/components/ControlledInputTime/ControlledInputTime.tsx +70 -0
  9. package/src/components/ControlledInputTime/__tests__/ControlledInputTime.unit.test.tsx +108 -0
  10. package/src/components/ControlledInputTime/index.ts +1 -0
  11. package/src/components/CopyToClipboard/CopyToClipboard.tsx +61 -0
  12. package/src/components/CopyToClipboard/__tests__/CopyToClipboard.unit.test.tsx +54 -0
  13. package/src/components/CopyToClipboard/index.ts +1 -0
  14. package/src/components/DateFilter/DateFilter.module.scss +4 -0
  15. package/src/components/DateFilter/DateFilter.tsx +117 -0
  16. package/src/components/DateFilter/DateFilter.utils.tsx +65 -0
  17. package/src/components/DateFilter/__tests__/DateFilter.unit.test.tsx +39 -0
  18. package/src/components/DateFilter/index.ts +1 -0
  19. package/src/components/EnvironmentFilter/EnvironmentFilter.tsx +74 -0
  20. package/src/components/EnvironmentFilter/__tests__/EnvironmentFilter.unit.test.tsx +99 -0
  21. package/src/components/EnvironmentFilter/index.ts +1 -0
  22. package/src/components/Filter/Filter.tsx +127 -0
  23. package/src/components/Filter/index.ts +1 -0
  24. package/src/components/ProfessionFilter/ProfessionFilter.module.scss +4 -0
  25. package/src/components/ProfessionFilter/ProfessionFilter.tsx +145 -0
  26. package/src/components/ProfessionFilter/__tests__/ProfessionFilter.unit.test.tsx +195 -0
  27. package/src/components/ProfessionFilter/index.ts +1 -0
  28. package/src/components/TextFilter/TextFilter.module.scss +3 -0
  29. package/src/components/TextFilter/TextFilter.tsx +112 -0
  30. package/src/components/TextFilter/__tests__/TextFilter.unit.test.tsx +75 -0
  31. package/src/components/TextFilter/index.ts +1 -0
  32. package/src/components/index.ts +8 -0
  33. package/src/hooks/index.ts +3 -0
  34. package/src/hooks/useBundledTranslation.ts +35 -0
  35. package/src/hooks/useDebouncedSearchInput.ts +36 -0
  36. package/src/hooks/usePrevious.ts +13 -0
  37. package/src/locales/fr/date-picker.json +11 -0
  38. package/src/locales/fr/scc-common-filters.json +22 -0
  39. package/src/locales/it/date-picker.json +11 -0
  40. package/src/locales/it/scc-common-filters.json +22 -0
  41. package/src/mocks/professions.ts +24 -0
  42. package/src/typings/filters.ts +7 -0
  43. package/src/typings/index.ts +1 -0
  44. package/tsconfig.json +25 -0
@@ -0,0 +1,117 @@
1
+ import {
2
+ DatePicker,
3
+ Dropdown,
4
+ DropdownProps,
5
+ DropdownSelectorTrigger,
6
+ DropdownSelectorTriggerProps,
7
+ } from '@mistertemp/design-system';
8
+ import {
9
+ DatePickerProps,
10
+ MixedDatePickerSelection,
11
+ } from '@mistertemp/design-system/src/components/design-system/DatePicker/DatePicker';
12
+ import { Locale } from 'date-fns';
13
+ import { fr } from 'date-fns/locale';
14
+ import { TFunction } from 'i18next';
15
+ import React, { useEffect, useRef, useState, VFC } from 'react';
16
+
17
+ import { useBundledTranslation } from '../../hooks';
18
+ import frDate from '../../locales/fr/date-picker.json';
19
+ import frFilters from '../../locales/fr/scc-common-filters.json';
20
+ import itDate from '../../locales/it/date-picker.json';
21
+ import itFilters from '../../locales/it/scc-common-filters.json';
22
+ import styles from './DateFilter.module.scss';
23
+ import { deduceMode, defaultDateFormat, getDisplayValue, sanitizeValue } from './DateFilter.utils';
24
+
25
+ export type DateFilterProps = Omit<
26
+ DropdownProps,
27
+ 'trigger' | 'onOpenChange' | 'dropdownValue' | 'onDropdownValueChanged' | 'withCTA' | 'ctaTitle'
28
+ > & {
29
+ name?: string;
30
+ format?: string;
31
+ locale?: Locale;
32
+ selectorProps?: Omit<DropdownSelectorTriggerProps, 'isSelected' | 'title'>;
33
+ dropdownValue?: MixedDatePickerSelection;
34
+ onDropdownValueChanged?: (value: MixedDatePickerSelection) => void;
35
+ modes?: DatePickerProps['availableModes'];
36
+ };
37
+
38
+ export const DateFilter: VFC<DateFilterProps> = ({
39
+ name = 'date',
40
+ 'data-testid': dataTestId = `filters+${name}`,
41
+ format = defaultDateFormat,
42
+ locale = fr,
43
+ dropdownValue,
44
+ onDropdownValueChanged,
45
+ modes,
46
+ selectorProps,
47
+ ...props
48
+ }) => {
49
+ const { t: tFilters } = useBundledTranslation('scc-common-filters', {
50
+ fr: frFilters,
51
+ it: itFilters,
52
+ });
53
+
54
+ const { t: tDate } = useBundledTranslation('date-picker', { fr: frDate, it: itDate });
55
+
56
+ const [open, setOpen] = useState(false);
57
+ const selectedValue = useRef(sanitizeValue(dropdownValue));
58
+
59
+ const mode = deduceMode(selectedValue.current, modes);
60
+
61
+ const handleOpenChange = (isOpen: boolean) => {
62
+ if (!isOpen) {
63
+ onDropdownValueChanged?.(selectedValue.current);
64
+ }
65
+
66
+ setOpen(isOpen);
67
+ };
68
+
69
+ useEffect(() => {
70
+ selectedValue.current = sanitizeValue(dropdownValue);
71
+ }, [dropdownValue]);
72
+
73
+ const isSelected = !!selectedValue.current;
74
+ const displayValue = getDisplayValue(selectedValue.current, format);
75
+ const placeholder = tFilters('scc-common-filters:filters.date.placeholder');
76
+ const triggerTitle = displayValue ? `${placeholder} : ${displayValue}` : placeholder;
77
+
78
+ return (
79
+ <Dropdown
80
+ isOpen={open}
81
+ onOpenChange={handleOpenChange}
82
+ dropdownClassName={styles.dropdown}
83
+ dropdownValue={{
84
+ [name]: selectedValue.current,
85
+ }}
86
+ onDropdownCTAClick={() => {}}
87
+ trigger={
88
+ <DropdownSelectorTrigger
89
+ data-testid={`${dataTestId}+trigger`}
90
+ title={triggerTitle}
91
+ isSelected={isSelected}
92
+ type="primary"
93
+ disabled={props.disabled}
94
+ {...selectorProps}
95
+ />
96
+ }>
97
+ <DatePicker
98
+ t={tDate as TFunction}
99
+ locale={locale}
100
+ data-testid={`${dataTestId}+picker`}
101
+ availableModes={modes}
102
+ showModeSwitch={!!modes?.length}
103
+ mode={mode}
104
+ selected={selectedValue.current}
105
+ onSubmit={(value) => {
106
+ onDropdownValueChanged?.(value);
107
+ setOpen(false);
108
+ }}
109
+ onSelect={(selected) => (selectedValue.current = selected)}
110
+ onCancel={() => {
111
+ selectedValue.current = sanitizeValue(dropdownValue);
112
+ setOpen(false);
113
+ }}
114
+ />
115
+ </Dropdown>
116
+ );
117
+ };
@@ -0,0 +1,65 @@
1
+ import {
2
+ DatePickerMode,
3
+ DatePickerProps,
4
+ MixedDatePickerSelection,
5
+ } from '@mistertemp/design-system/src/components/design-system/DatePicker/DatePicker';
6
+ import { format, isDate as isDateFn, min } from 'date-fns';
7
+ import { DateRange } from 'react-day-picker';
8
+
9
+ export const defaultDateFormat = 'dd/MM/yy';
10
+
11
+ const isMultiple = (selection: MixedDatePickerSelection): selection is Date[] =>
12
+ Array.isArray(selection);
13
+
14
+ const isRange = (selection: MixedDatePickerSelection): selection is DateRange =>
15
+ typeof selection === 'object' && 'from' in selection;
16
+
17
+ const isDate = (selection: MixedDatePickerSelection): selection is Date => isDateFn(selection);
18
+
19
+ export const deduceMode = (
20
+ selection: MixedDatePickerSelection,
21
+ availableModes: DatePickerProps['availableModes'],
22
+ ): DatePickerMode => {
23
+ switch (true) {
24
+ case isMultiple(selection):
25
+ return 'multiple';
26
+ case isRange(selection):
27
+ return 'range';
28
+ case isDate(selection):
29
+ return 'single';
30
+ default:
31
+ return availableModes?.length ? availableModes[0] : 'single';
32
+ }
33
+ };
34
+
35
+ export const sanitizeValue = (
36
+ value: MixedDatePickerSelection | object,
37
+ ): MixedDatePickerSelection => {
38
+ if (
39
+ !isDate(value as MixedDatePickerSelection) &&
40
+ typeof value === 'object' &&
41
+ !Object.keys(value).length
42
+ ) {
43
+ return undefined;
44
+ }
45
+
46
+ return value as MixedDatePickerSelection;
47
+ };
48
+
49
+ export const getDisplayValue = (
50
+ selection: MixedDatePickerSelection,
51
+ dateFormat: string,
52
+ ): string | undefined => {
53
+ switch (true) {
54
+ case isMultiple(selection) && !!selection.length:
55
+ return selection.length > 1
56
+ ? `${format(min(selection), dateFormat)} (+${selection.length - 1})`
57
+ : format(selection[0], dateFormat);
58
+ case isRange(selection) && !!selection.from:
59
+ return `${format(selection.from, dateFormat)} - ${selection.to ? format(selection.to, dateFormat) : ''}`;
60
+ case isDate(selection):
61
+ return format(selection, dateFormat);
62
+ default:
63
+ return undefined;
64
+ }
65
+ };
@@ -0,0 +1,39 @@
1
+ import { screen } from '@testing-library/react';
2
+ import { format, startOfToday, startOfTomorrow } from 'date-fns';
3
+
4
+ import { render } from '../../../../.jest/reactTestingLibrary.config';
5
+ import { DateFilter } from '../DateFilter';
6
+
7
+ describe('DateFilter', () => {
8
+ it('should display multiple values', () => {
9
+ const today = startOfToday();
10
+ const tomorrow = startOfTomorrow();
11
+
12
+ render(<DateFilter dropdownValue={[today, tomorrow]} />);
13
+
14
+ expect(screen.getByTestId('filters+date+trigger-button')).toHaveTextContent(
15
+ `${format(today, 'dd/MM/yy')} (+1)`,
16
+ );
17
+ });
18
+
19
+ it('should display range values', () => {
20
+ const today = startOfToday();
21
+ const tomorrow = startOfTomorrow();
22
+
23
+ render(<DateFilter dropdownValue={{ from: today, to: tomorrow }} />);
24
+
25
+ expect(screen.getByTestId('filters+date+trigger-button')).toHaveTextContent(
26
+ `${format(today, 'dd/MM/yy')} - ${format(tomorrow, 'dd/MM/yy')}`,
27
+ );
28
+ });
29
+
30
+ it('should display single values', () => {
31
+ const today = startOfToday();
32
+
33
+ render(<DateFilter dropdownValue={today} />);
34
+
35
+ expect(screen.getByTestId('filters+date+trigger-button')).toHaveTextContent(
36
+ format(today, 'dd/MM/yy'),
37
+ );
38
+ });
39
+ });
@@ -0,0 +1 @@
1
+ export * from './DateFilter';
@@ -0,0 +1,74 @@
1
+ import { ENVIRONMENTS_CODE, getAllFlattenedEnvironments } from '@mistertemp/libs-work-environments';
2
+ import Fuse, { IFuseOptions } from 'fuse.js';
3
+ import { merge } from 'lodash';
4
+ import React from 'react';
5
+ import { useMemo, useState } from 'react';
6
+
7
+ import { useBundledTranslation } from '../../hooks';
8
+ import fr from '../../locales/fr/scc-common-filters.json';
9
+ import it from '../../locales/it/scc-common-filters.json';
10
+ import { Filter, FilterDropdownOption, FilterProps } from '../Filter';
11
+
12
+ const DEFAULT_FUSE_OPTIONS: IFuseOptions<FilterDropdownOption<ENVIRONMENTS_CODE>> = {
13
+ isCaseSensitive: false,
14
+ shouldSort: true,
15
+ findAllMatches: true,
16
+ ignoreLocation: true,
17
+ keys: ['label'],
18
+ threshold: 0.2,
19
+ };
20
+
21
+ export type EnvironmentFilterProps = Omit<FilterProps<ENVIRONMENTS_CODE>, 'name' | 'options'> & {
22
+ name?: string;
23
+ fuseOptions?: IFuseOptions<FilterDropdownOption<ENVIRONMENTS_CODE>>;
24
+ };
25
+
26
+ const options = getAllFlattenedEnvironments().map((env) => ({
27
+ id: env.code,
28
+ label: env.label,
29
+ }));
30
+
31
+ export const EnvironmentFilter = ({
32
+ listProps,
33
+ name = 'environment',
34
+ mode = 'multiple',
35
+ fuseOptions,
36
+ ...props
37
+ }: EnvironmentFilterProps) => {
38
+ const { t } = useBundledTranslation('scc-common-filters', { fr, it });
39
+ const [inputValue, setInputValue] = useState<string | undefined>(undefined);
40
+
41
+ const fuse = useMemo(() => {
42
+ const fuse = new Fuse(options, { ...DEFAULT_FUSE_OPTIONS, ...fuseOptions });
43
+
44
+ return fuse;
45
+ }, [fuseOptions]);
46
+
47
+ const filteredOptions = useMemo(() => {
48
+ if (inputValue) {
49
+ return fuse.search(inputValue).map((r) => r.item);
50
+ }
51
+
52
+ return options;
53
+ }, [fuse, inputValue]);
54
+
55
+ return (
56
+ <Filter
57
+ name={name}
58
+ mode={mode}
59
+ options={filteredOptions}
60
+ placeholder={t('scc-common-filters:filters.environment.placeholder')}
61
+ listProps={merge<
62
+ FilterProps<ENVIRONMENTS_CODE>['listProps'],
63
+ FilterProps<ENVIRONMENTS_CODE>['listProps']
64
+ >(
65
+ {
66
+ search: { value: inputValue, onInputValueChanged: setInputValue },
67
+ withSelectedTags: true,
68
+ },
69
+ listProps,
70
+ )}
71
+ {...props}
72
+ />
73
+ );
74
+ };
@@ -0,0 +1,99 @@
1
+ import { ENVIRONMENTS_CODE, getEnvironmentByCode } from '@mistertemp/libs-work-environments';
2
+ import { fireEvent, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import React from 'react';
5
+
6
+ import { render } from '../../../../.jest/reactTestingLibrary.config';
7
+ import { EnvironmentFilter } from '../EnvironmentFilter';
8
+
9
+ describe('EnvironmentFilter', () => {
10
+ beforeEach(() => {
11
+ jest.clearAllMocks();
12
+ });
13
+
14
+ it('should display all statuses options', async () => {
15
+ render(<EnvironmentFilter />);
16
+
17
+ fireEvent.click(screen.getByTestId('filters+environment+trigger-button'));
18
+
19
+ Object.values(ENVIRONMENTS_CODE)
20
+ // We can't test everything because of the virtualizer
21
+ .slice(0, 6)
22
+ .forEach((status) => {
23
+ expect(
24
+ screen.getByTestId(`filters+environment_option_${status}`),
25
+ ).toBeInTheDocument();
26
+ });
27
+ });
28
+
29
+ it('should select one candidate status', async () => {
30
+ const mock = jest.fn();
31
+
32
+ render(<EnvironmentFilter onDropdownValueChanged={mock} />);
33
+
34
+ fireEvent.click(screen.getByTestId('filters+environment+trigger-button'));
35
+
36
+ fireEvent.click(
37
+ screen.getByTestId(
38
+ `filters+environment_${ENVIRONMENTS_CODE.CABINET_MEDICAL}_check_${ENVIRONMENTS_CODE.CABINET_MEDICAL}`,
39
+ ),
40
+ );
41
+
42
+ await userEvent.click(document.body);
43
+
44
+ await waitFor(() => {
45
+ expect(mock).toHaveBeenCalledWith([ENVIRONMENTS_CODE.CABINET_MEDICAL]);
46
+ });
47
+ });
48
+
49
+ it('should display default dropdown value', async () => {
50
+ render(<EnvironmentFilter dropdownValue={[ENVIRONMENTS_CODE.ADDICTOLOGIE]} />);
51
+
52
+ expect(screen.getByTestId('filters+environment+trigger-button-title')).toHaveTextContent(
53
+ getEnvironmentByCode(ENVIRONMENTS_CODE.ADDICTOLOGIE).label,
54
+ );
55
+ });
56
+
57
+ it('should update cta title', async () => {
58
+ const newCtaTitle = 'New cta title';
59
+
60
+ render(<EnvironmentFilter withCTA ctaTitle={newCtaTitle} />);
61
+
62
+ fireEvent.click(screen.getByTestId('filters+environment+trigger-button'));
63
+
64
+ expect(screen.getByTestId('filters+environment-bottom-action-button')).toHaveTextContent(
65
+ newCtaTitle,
66
+ );
67
+ });
68
+
69
+ it('should update placeholder title', async () => {
70
+ const newPlaceholder = 'New placeholder';
71
+
72
+ render(<EnvironmentFilter placeholder={newPlaceholder} />);
73
+
74
+ expect(screen.getByTestId('filters+environment+trigger-button-title')).toHaveTextContent(
75
+ `${newPlaceholder}`,
76
+ );
77
+ });
78
+
79
+ it('should click on cta button', async () => {
80
+ const ctaMock = jest.fn();
81
+ const valueChangeMock = jest.fn();
82
+
83
+ render(
84
+ <EnvironmentFilter
85
+ dropdownValue={[ENVIRONMENTS_CODE.ADDICTOLOGIE]}
86
+ onDropdownCTAClick={ctaMock}
87
+ onDropdownValueChanged={valueChangeMock}
88
+ />,
89
+ );
90
+
91
+ fireEvent.click(screen.getByTestId('filters+environment+trigger-button'));
92
+
93
+ fireEvent.click(screen.getByTestId('filters+environment-bottom-action-button'));
94
+
95
+ expect(ctaMock).toHaveBeenCalledTimes(1);
96
+ expect(valueChangeMock).toHaveBeenCalledTimes(1);
97
+ expect(valueChangeMock).toHaveBeenCalledWith([]);
98
+ });
99
+ });
@@ -0,0 +1 @@
1
+ export * from './EnvironmentFilter';
@@ -0,0 +1,127 @@
1
+ import {
2
+ Dropdown,
3
+ DropdownProps,
4
+ DropdownSelectorTrigger,
5
+ DropdownSelectorTriggerProps,
6
+ DropdownStaticListProvider,
7
+ DropdownStaticListProviderProps,
8
+ SelectionModes,
9
+ } from '@mistertemp/design-system';
10
+ import isEmpty from 'lodash/isEmpty';
11
+ import isEqual from 'lodash/isEqual';
12
+ import React, { useEffect } from 'react';
13
+ import { useCallback, useState } from 'react';
14
+
15
+ import { useBundledTranslation } from '../../hooks';
16
+ import fr from '../../locales/fr/scc-common-filters.json';
17
+ import it from '../../locales/it/scc-common-filters.json';
18
+
19
+ export type FilterDropdownOption<T extends string = string> = {
20
+ id: T;
21
+ label: string;
22
+ disabled?: boolean;
23
+ };
24
+
25
+ export type FilterProps<T extends string = string> = Omit<
26
+ DropdownProps,
27
+ 'trigger' | 'dropdownValue' | 'onDropdownValueChanged' | 'onOpenChange'
28
+ > & {
29
+ listProps?: Omit<
30
+ DropdownStaticListProviderProps,
31
+ 'name' | 'selectionMode' | 'items' | 'itemTemplate' | 'size'
32
+ >;
33
+ selectorProps?: Omit<DropdownSelectorTriggerProps, 'isSelected' | 'title'>;
34
+ name: string;
35
+ 'data-testid'?: string;
36
+ placeholder?: string;
37
+ options: Array<FilterDropdownOption<T>>;
38
+ mode?: Extract<SelectionModes, 'single' | 'multiple'>;
39
+ dropdownValue?: T[];
40
+ onDropdownValueChanged?: (value: T[]) => void;
41
+ ctaTitle?: string;
42
+ };
43
+
44
+ export function Filter<T extends string = string>({
45
+ name,
46
+ 'data-testid': dataTestId = `filters+${name}`,
47
+ placeholder,
48
+ mode = 'single',
49
+ options,
50
+ onDropdownValueChanged,
51
+ onDropdownCTAClick,
52
+ dropdownValue,
53
+ listProps,
54
+ selectorProps,
55
+ ...props
56
+ }: FilterProps<T>) {
57
+ const { t } = useBundledTranslation('scc-common-filters', { fr, it });
58
+
59
+ const [internalDropdownValue, setInternalDropdownValue] = useState<
60
+ FilterDropdownOption<T>[] | undefined
61
+ >(options.filter(({ id }) => dropdownValue?.includes(id)));
62
+
63
+ const handleOnOpenChange = useCallback(
64
+ (isOpen) => {
65
+ if (!isOpen && onDropdownValueChanged && internalDropdownValue) {
66
+ const targetValue = internalDropdownValue.map(({ id }) => id);
67
+
68
+ if (!isEqual(targetValue, dropdownValue)) {
69
+ onDropdownValueChanged(targetValue);
70
+ }
71
+ }
72
+ },
73
+ [internalDropdownValue, dropdownValue, onDropdownValueChanged],
74
+ );
75
+
76
+ const isSelected = !isEmpty(internalDropdownValue);
77
+ const displayedValues = internalDropdownValue?.map(({ label }) => label).join(', ') ?? '';
78
+ const triggerTitle = !isEmpty(displayedValues)
79
+ ? `${placeholder} : ${displayedValues}`
80
+ : placeholder;
81
+
82
+ useEffect(() => {
83
+ setInternalDropdownValue((current) =>
84
+ current?.filter(({ id }) => dropdownValue?.includes(id)),
85
+ );
86
+ }, [dropdownValue]);
87
+
88
+ return (
89
+ <Dropdown
90
+ data-testid={dataTestId}
91
+ trigger={
92
+ <DropdownSelectorTrigger
93
+ data-testid={`${dataTestId}+trigger`}
94
+ title={triggerTitle}
95
+ isSelected={isSelected}
96
+ type="primary"
97
+ disabled={props.disabled}
98
+ {...selectorProps}
99
+ />
100
+ }
101
+ withCTA
102
+ size={props?.size ?? 'small'}
103
+ ctaTitle={t('scc-common-filters:actions.removeFilter')}
104
+ dropdownValue={{
105
+ [name]: internalDropdownValue,
106
+ }}
107
+ onDropdownValueChanged={(value) => {
108
+ setInternalDropdownValue(value[name]);
109
+ }}
110
+ onDropdownCTAClick={() => {
111
+ setInternalDropdownValue([]);
112
+ onDropdownCTAClick?.();
113
+ }}
114
+ onOpenChange={handleOnOpenChange}
115
+ {...props}>
116
+ <DropdownStaticListProvider
117
+ data-testid={dataTestId}
118
+ selectionMode={mode}
119
+ items={options}
120
+ name={name}
121
+ size="small"
122
+ itemTemplate={mode === 'multiple' ? 'checkbox' : 'right-checked-list-item'}
123
+ {...listProps}
124
+ />
125
+ </Dropdown>
126
+ );
127
+ }
@@ -0,0 +1 @@
1
+ export * from './Filter';
@@ -0,0 +1,4 @@
1
+ .container {
2
+ overflow: auto;
3
+ max-height: 30rem;
4
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ Dropdown,
3
+ DropdownSelectorTrigger,
4
+ DropdownStaticListProvider,
5
+ } from '@mistertemp/design-system';
6
+ import { isEqual } from 'lodash';
7
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
+
9
+ import { useBundledTranslation, useDebouncedSearchInput } from '../../hooks';
10
+ import fr from '../../locales/fr/scc-common-filters.json';
11
+ import it from '../../locales/it/scc-common-filters.json';
12
+ import { SearchableFilterProps } from '../../typings';
13
+ import type { FilterDropdownOption, FilterProps } from '../Filter';
14
+ import styles from './ProfessionFilter.module.scss';
15
+
16
+ export type ProfessionFilterParentProps = Omit<FilterProps, 'name' | 'options'>;
17
+
18
+ export type ProfessionFilterDropdownOption = FilterDropdownOption;
19
+
20
+ type ProfessionFilterProps = Omit<ProfessionFilterParentProps, 'onDropdownValueChanged'> &
21
+ SearchableFilterProps & {
22
+ placeholder?: string;
23
+ options: FilterDropdownOption[];
24
+ onChangeSelectedOptions?: (options: FilterDropdownOption[]) => void;
25
+ };
26
+
27
+ export const ProfessionFilter = ({
28
+ dropdownValue,
29
+ onDropdownCTAClick,
30
+ isLoading,
31
+ onChangeSearchText,
32
+ options,
33
+ onChangeSelectedOptions,
34
+ placeholder,
35
+ searchConfig,
36
+ 'data-testid': dataTestId = 'filters+profession',
37
+ ...props
38
+ }: ProfessionFilterProps) => {
39
+ const { t } = useBundledTranslation('scc-common-filters', { fr, it });
40
+
41
+ const [searchInput, setSearchInput] = useState('');
42
+ const [sortedOptions, setSortedOptions] = useState<FilterDropdownOption[]>([]);
43
+ const searchValue = useDebouncedSearchInput(searchInput, searchConfig);
44
+
45
+ const selectedOptions = useRef<FilterDropdownOption[]>([]);
46
+ const triggerLabel = useRef<string>('');
47
+ const selectionHasChanged = useRef<boolean>(false);
48
+
49
+ const placeholderText = useMemo(
50
+ () => placeholder ?? t('scc-common-filters:filters.profession.placeholder'),
51
+ [placeholder, t],
52
+ );
53
+
54
+ const handleDropdownValueChanged = (values: { profession: FilterDropdownOption[] }) => {
55
+ if (!options) {
56
+ return;
57
+ }
58
+
59
+ if (!isEqual(selectedOptions.current, values.profession)) {
60
+ selectionHasChanged.current = true;
61
+ selectedOptions.current = values.profession;
62
+ }
63
+ };
64
+
65
+ const handleOpenChanged = useCallback(
66
+ (isOpen) => {
67
+ if (!isOpen && selectionHasChanged.current) {
68
+ selectionHasChanged.current = false;
69
+ onChangeSelectedOptions?.(selectedOptions.current);
70
+ }
71
+ },
72
+ //eslint-disable-next-line react-hooks/exhaustive-deps
73
+ [dropdownValue], // -> onDropdownValueChanged in deps list creates an infinite loop when used in a FilterGroup*/
74
+ );
75
+
76
+ const handleCTAClicked = () => {
77
+ onChangeSelectedOptions?.([]);
78
+ onDropdownCTAClick?.();
79
+ };
80
+
81
+ useEffect(() => {
82
+ triggerLabel.current = placeholderText;
83
+
84
+ if (!selectionHasChanged.current) {
85
+ selectedOptions.current = options.filter(({ id }) => dropdownValue?.includes(id));
86
+ }
87
+
88
+ if (selectedOptions.current.length) {
89
+ const selectedProfessionLabels = selectedOptions.current
90
+ .map(({ label }) => label)
91
+ .join(', ');
92
+
93
+ triggerLabel.current += `: ${selectedProfessionLabels}`;
94
+ }
95
+
96
+ const sortedOptionValues = selectedOptions.current.map(({ id }) => id);
97
+ const remainingOptions = options.filter(({ id }) => !sortedOptionValues.includes(id));
98
+
99
+ setSortedOptions([...selectedOptions.current, ...remainingOptions]);
100
+ }, [dropdownValue, options, placeholderText]);
101
+
102
+ useEffect(() => {
103
+ onChangeSearchText?.(searchValue);
104
+ }, [searchValue, onChangeSearchText]);
105
+
106
+ return (
107
+ <Dropdown<{ profession: FilterDropdownOption[] }>
108
+ trigger={
109
+ <DropdownSelectorTrigger
110
+ data-testid={`${dataTestId}+trigger`}
111
+ title={triggerLabel.current}
112
+ isSelected={!!dropdownValue?.length}
113
+ type="primary"
114
+ disabled={props.disabled}
115
+ />
116
+ }
117
+ data-testid={dataTestId}
118
+ withCTA
119
+ size="small"
120
+ ctaTitle={t('scc-common-filters:actions.removeFilter')}
121
+ dropdownValue={{ profession: selectedOptions.current }}
122
+ onDropdownCTAClick={handleCTAClicked}
123
+ onDropdownValueChanged={handleDropdownValueChanged}
124
+ onOpenChange={handleOpenChanged}
125
+ {...props}>
126
+ <div className={styles.container}>
127
+ <DropdownStaticListProvider
128
+ search={{
129
+ value: searchInput,
130
+ isLoading,
131
+ placeholder: t('scc-common-filters:filters.profession.search.placeholder'),
132
+ onInputValueChanged: (inputValue) => setSearchInput(inputValue ?? ''),
133
+ 'data-testid': `${dataTestId}+search`,
134
+ }}
135
+ data-testid={dataTestId}
136
+ selectionMode="multiple"
137
+ items={sortedOptions}
138
+ name="profession"
139
+ size="small"
140
+ itemTemplate="checkbox"
141
+ />
142
+ </div>
143
+ </Dropdown>
144
+ );
145
+ };