@mistertemp/libs-front-shared
v3.1.3
Published
Shared React components and hooks for MisterTemp fronts built on `@mistertemp/design-system` (React 18): dropdown filters (generic, account, work environment, profession, text, date), `react-hook-form` date and time inputs, a copy-to-clipboard button, and
Readme
@mistertemp/libs-front-shared
Shared React components and hooks for MisterTemp fronts built on @mistertemp/design-system (React 18): dropdown filters (generic, account, work environment, profession, text, date), react-hook-form date and time inputs, a copy-to-clipboard button, and a few hooks.
Installation
npm install @mistertemp/libs-front-sharedPeer dependencies:
| Package | Version |
| --- | --- |
| @mistertemp/design-system | >=14.0.0 |
| @mistertemp/libs-work-environments | ^1.0.0 |
| react / react-dom | ^18.3.1 |
| react-hook-form | ^7.81.0 |
| i18next | ^25.4.2 |
| i18next-browser-languagedetector | >=8.2.1 |
| i18next-http-backend | >=3.0.2 |
| react-i18next | ^15.7.4 |
The components also import lodash, fuse.js and date-fns at runtime. They are not declared as dependencies or peer dependencies, so the consuming app must install them.
The package ships its TypeScript sources ("main": "index.ts"), SCSS modules and JSON locale files, not a build. The consuming app's bundler must handle .tsx, .module.scss (with sass) and JSON imports. Some files import private paths of @mistertemp/design-system (@mistertemp/design-system/src/...).
Prerequisites
i18nextmust be initialized withreact-i18next(initReactI18next) before the components render. The lib's labels are bundled and added to the current language at runtime, under thescc-common-filtersanddate-pickernamespaces (French, Italian, and Spanish for the filters).CopyToClipboardshows its toast through the design system'sToastContext: wrap the app in the design system's toast provider to see it.
Exports
Filters
All filters are design-system Dropdowns with a DropdownSelectorTrigger. The trigger title is "<placeholder> : <selected values>" when something is selected, and the dropdown has a "remove filter" CTA.
| Component | Value type | Description |
| --- | --- | --- |
| Filter<T> | T[] (option IDs) | Generic single or multiple selection list |
| AccountFilter | string[] | Filter with a search input driven by the caller (server-side search) |
| EnvironmentFilter | ENVIRONMENTS_CODE[] | Multiple selection of work environments, with a local fuzzy search |
| ProfessionFilter | string[] (IDs in), FilterDropdownOption[] (out) | Multiple selection of professions, with a debounced search driven by the caller |
| TextFilter | string | Free text input |
| DateFilter | MixedDatePickerSelection | Date picker: single date, multiple dates or range |
Form inputs
| Component | Description |
| --- | --- |
| ControlledInputDate | react-hook-form date input with a configurable string format and min/max rules |
| ControlledInputTime | react-hook-form time input with a configurable separator |
Other
| Export | Kind | Description |
| --- | --- | --- |
| CopyToClipboard | Component | Button that copies a value and optionally shows a toast |
| useDebouncedSearchInput | Hook | Debounced search value with a minimum length |
| usePrevious | Hook | Previous value of a variable |
| useBundledTranslation | Hook | Adds bundled translations to the i18next instance |
| SearchableFilterProps, DebouncedSearchConfig | Types | Search configuration |
| FilterProps, FilterDropdownOption, AccountFilterProps, EnvironmentFilterProps, ProfessionFilterParentProps, ProfessionFilterDropdownOption, TextFilterProps, DateFilterProps, ControlledInputDateProps, ControlledInputTimeProps, CopyToClipboardProps, UseBundedTranslationResponse | Types | Props and return types |
Filters
Filter
import { Filter, FilterDropdownOption } from '@mistertemp/libs-front-shared';
type Status = 'open' | 'closed';
const statusOptions: FilterDropdownOption<Status>[] = [
{ id: 'open', label: 'Open' },
{ id: 'closed', label: 'Closed' },
];
export const StatusFilter = ({ value, onChange }: { value: Status[]; onChange: (v: Status[]) => void }) => (
<Filter<Status>
name="status"
placeholder="Status"
mode="multiple"
options={statusOptions}
dropdownValue={value}
onDropdownValueChanged={onChange}
/>
);| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| name | string | — | Filter name, also the key of the dropdown value |
| options | FilterDropdownOption<T>[] | — | { id: T; label: string; disabled?: boolean } |
| mode | 'single' \| 'multiple' | 'single' | Radio-like list or checkboxes |
| dropdownValue | T[] | — | Selected option IDs |
| onDropdownValueChanged | (value: T[]) => void | — | Called when the dropdown closes, only if the selection changed |
| onDropdownCTAClick | () => void | — | Called by the "remove filter" CTA, after the internal selection is cleared |
| placeholder | string | — | Trigger title |
| listProps | DropdownStaticListProviderProps (without name, selectionMode, items, itemTemplate, size) | — | Passed to the list, for example search or withSelectedTags |
| selectorProps | DropdownSelectorTriggerProps (without isSelected, title) | — | Passed to the trigger |
| data-testid | string | `filters+${name}` | The trigger gets +trigger |
| ...props | DropdownProps (without trigger, dropdownValue, onDropdownValueChanged, onOpenChange) | — | Passed to the Dropdown. size defaults to 'small' |
The initial selection is read from dropdownValue on mount. Afterwards, dropdownValue changes only remove unselected options from the internal selection.
AccountFilter
Same props as Filter, except:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| name | string | 'account' | |
| options | FilterDropdownOption[] | — | Search results to display |
| onInputValueChanged | (value: string \| undefined) => void | — | Called on every keystroke in the search input |
| isLoading | boolean | false | Loading state of the search |
The search input and selected tags are configured through listProps: passing your own listProps replaces them.
<AccountFilter
placeholder="Customer"
mode="multiple"
options={accounts.map(({ id, name }) => ({ id, label: name }))}
isLoading={isFetching}
onInputValueChanged={(text) => setSearch(text ?? '')}
dropdownValue={selectedAccountIds}
onDropdownValueChanged={setSelectedAccountIds}
/>EnvironmentFilter
Options come from getAllFlattenedEnvironments() of @mistertemp/libs-work-environments (id is the ENVIRONMENTS_CODE). Typing in the search input filters them with fuse.js.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| name | string | 'environment' | |
| mode | 'single' \| 'multiple' | 'multiple' | |
| fuseOptions | IFuseOptions<FilterDropdownOption<ENVIRONMENTS_CODE>> | — | Merged over the defaults: case insensitive, sorted, ignoreLocation, key label, threshold: 0.2 |
| listProps | see Filter | — | Deep-merged over the built-in search and selected tags config |
| other Filter props | | | Except name and options. The placeholder defaults to the translated "Environment" |
import { EnvironmentFilter } from '@mistertemp/libs-front-shared';
import { ENVIRONMENTS_CODE } from '@mistertemp/libs-work-environments';
const [environments, setEnvironments] = useState<ENVIRONMENTS_CODE[]>([]);
<EnvironmentFilter dropdownValue={environments} onDropdownValueChanged={setEnvironments} />;ProfessionFilter
The caller owns the search: the filter reports the debounced search text, and the caller passes back the matching options. Selected options are listed first.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| options | FilterDropdownOption[] | — | Professions to display |
| dropdownValue | string[] | — | Selected profession IDs |
| onChangeSelectedOptions | (options: FilterDropdownOption[]) => void | — | Called when the dropdown closes with a changed selection, and with [] on "remove filter" |
| searchConfig | DebouncedSearchConfig | — | Required. See useDebouncedSearchInput |
| onChangeSearchText | (text: string) => void | — | Called with the debounced search value |
| isLoading | boolean | — | Loading state of the search |
| placeholder | string | translated "Qualification" | Trigger title |
| onDropdownCTAClick | () => void | — | Called after onChangeSelectedOptions([]) |
| data-testid | string | 'filters+profession' | The search input gets +search |
| ...props | DropdownProps | — | Passed to the Dropdown |
import { ProfessionFilter } from '@mistertemp/libs-front-shared';
<ProfessionFilter
options={professions}
isLoading={isFetching}
dropdownValue={selectedIds}
searchConfig={{ debounceDelay: 300, inputThreshold: 2, forceEmptyValue: true }}
onChangeSearchText={setProfessionSearch}
onChangeSelectedOptions={(options) => setSelectedIds(options.map(({ id }) => id))}
/>;TextFilter
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| name | string | — | |
| placeholder | string | — | Required. Trigger title |
| dropdownValue | string | — | Current value |
| onDropdownValueChanged | (value: string \| undefined) => void | — | Called on submit (Enter or the submit icon) and when the dropdown closes, if the value changed |
| size | 'small' \| 'medium' | 'small' | Dropdown, trigger and input size |
| onOpenChange | (isOpen: boolean) => void | — | Called before the value is committed on close |
| selectorProps | DropdownSelectorTriggerProps (without isSelected, title, data-testid, disabled, size) | — | |
| inputProps | InputTextProps (without data-testid, value, size, type, onSubmit, onChange, inputType) | — | |
| data-testid | string | `filters+${name}` | The trigger gets +trigger, the input +input |
<TextFilter name="reference" placeholder="Reference" dropdownValue={reference} onDropdownValueChanged={setReference} />DateFilter
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| name | string | 'date' | |
| dropdownValue | MixedDatePickerSelection (Date, Date[] or DateRange) | — | An empty object counts as no value |
| onDropdownValueChanged | (value: MixedDatePickerSelection) => void | — | Called when the picker is submitted and when the dropdown closes |
| modes | DatePickerProps['availableModes'] | — | Available modes. With at least one, the mode switch is shown |
| format | string (date-fns) | 'dd/MM/yy' | Display format in the trigger. Multiple dates show the earliest date and (+n) |
| locale | Locale (date-fns) | fr | Picker locale |
| selectorProps | DropdownSelectorTriggerProps (without isSelected, title) | — | |
| disabled | boolean | — | Disables the trigger |
| data-testid | string | `filters+${name}` | The trigger gets +trigger, the picker +picker |
The mode is deduced from the value (array, range or date), otherwise the first of modes, otherwise single. Cancel restores dropdownValue. Other DropdownProps are accepted by the type but not passed to the Dropdown.
import { DateFilter } from '@mistertemp/libs-front-shared';
<DateFilter
modes={['single', 'range']}
dropdownValue={period}
onDropdownValueChanged={setPeriod}
/>;Form inputs
Both inputs take the react-hook-form Controller props (name, control, rules, defaultValue…) plus inputProps for the design-system Input (without inputType and value). inputProps.onChange is called after the form value is updated, with a synthetic { target: { name, type, value } } event. hasError is set when the field has an error.
ControlledInputDate
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| format | string (date-fns) | 'dd/MM/yyyy' | Format of the form value |
| minErrorMessage | string | 'min exceeded' | Message of the min rule |
| maxErrorMessage | string | 'max exceeded' | Message of the max rule |
| inputProps.min / inputProps.max | | — | Added to the controller rules as min / max |
The form value is a string in format, or '' when the input is cleared. On a min or max error, the input hint shows the error message, unless inputProps.hint is set: inputProps are spread last and win.
import { ControlledInputDate, ControlledInputTime } from '@mistertemp/libs-front-shared';
import { useForm } from 'react-hook-form';
type MissionForm = { startDate: string; startTime: string };
export const MissionDates = () => {
const { control } = useForm<MissionForm>({ defaultValues: { startDate: '', startTime: '' } });
return (
<>
<ControlledInputDate<MissionForm, 'startDate'>
name="startDate"
control={control}
format="yyyy-MM-dd"
rules={{ required: true }}
minErrorMessage="The date is in the past"
inputProps={{ label: 'Start date', min: '2026-01-01' }}
/>
<ControlledInputTime<MissionForm, 'startTime'>
name="startTime"
control={control}
separator=":"
inputProps={{ label: 'Start time' }}
/>
</>
);
};ControlledInputTime
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| separator | string | ':' | Separator in the form value |
| inputProps.timeSeparator | string | design-system DEFAULT_TIME_SEPARATOR | Separator displayed in the input |
CopyToClipboard
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| value | string \| null | — | Text to copy. Nothing happens when it is empty |
| toast | string \| Omit<ToastProps, 'id'> | — | A string shows a success toast with that title. Requires ToastContext |
| onCopy | (event: MouseEvent, value: string) => void | — | Called after the copy |
| icon | SvgIcon | DuplicateSvg | Button icon |
| className | string | — | |
| data-testid | string | 'copyToClipboard' | |
<CopyToClipboard value={candidate.email} toast="Email copied" />It uses navigator.clipboard.writeText, which requires a secure context (HTTPS or localhost).
Hooks
useDebouncedSearchInput(inputValue, config?)
Returns a debounced search string that only changes when the input is long enough.
| Option (DebouncedSearchConfig) | Type | Default | Description |
| --- | --- | --- | --- |
| debounceDelay | number | 100 | Debounce delay in ms |
| inputThreshold | number | 2 | The value is returned only when its length is strictly greater than this |
| forceEmptyValue | boolean | — | Returns '' when the input drops to the threshold or below |
const [text, setText] = useState('');
const search = useDebouncedSearchInput(text, { debounceDelay: 300, forceEmptyValue: true });
useEffect(() => {
fetchProfessions(search);
}, [search]);usePrevious(value)
Returns the previous distinct value (compared with !==), or undefined until the value changes once.
useBundledTranslation(namespace, translations)
Adds translations[lang] to the current i18next instance under namespace (lang is the first two letters of i18n.language) if the bundle is missing, and returns { t, isReady }. Until ready, t returns the key. It relies on react-i18next's useTranslation, so i18next must already be initialized.
import { useBundledTranslation } from '@mistertemp/libs-front-shared';
import fr from './locales/fr/my-feature.json';
import it from './locales/it/my-feature.json';
const { t, isReady } = useBundledTranslation('my-feature', { fr, it });Versioning
Versions are managed by release-please from Conventional Commits. Do not bump package.json by hand.
