Skip to content

Commit

Permalink
POC : Multiple variables with suggesters (#900)
Browse files Browse the repository at this point in the history
  • Loading branch information
Grafikart authored Mar 8, 2024
1 parent 07767a0 commit 812b798
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 9 deletions.
18 changes: 12 additions & 6 deletions src/components/Suggester/CustomSuggester.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ type Props = {
className?: string;
classNamePrefix?: string;
placeholder?: string;
onSelect: (s: string | null) => void;
onSelect: (
option: string | null | { id?: string; [key: string]: ReactNode }
) => void;
value: string | null;
labelRenderer: LunaticComponentProps<'Suggester'>['labelRenderer'];
optionRenderer: LunaticComponentProps<'Suggester'>['optionRenderer'];
disabled?: boolean;
readOnly?: boolean;
id?: string;
searching: (
searching?: (
s: string | null
) => Promise<{ results: ComboboxOptionType[]; search: string }>;
label?: ReactNode;
description?: ReactNode;
errors?: LunaticError[];
defaultOptions?: ComboboxOptionType[];
};

export const CustomSuggester = slottableComponent<Props>(
Expand All @@ -43,14 +46,17 @@ export const CustomSuggester = slottableComponent<Props>(
label,
description,
errors,
defaultOptions,
}) => {
const [search, setSearch] = useState('');
const [options, setOptions] = useState<Array<ComboboxOptionType>>([]);
const [options, setOptions] = useState<Array<ComboboxOptionType>>(
defaultOptions ?? []
);
const lastSearch = useRef('');

const handleSelect = useCallback(
function (id: string | null) {
onSelect(id ? id : null);
(id: string | null) => {
onSelect(id ? options.find((o) => o.id === id)! : null);
},
[onSelect]

Check warning on line 61 in src/components/Suggester/CustomSuggester.tsx

View workflow job for this annotation

GitHub Actions / test_lint

React Hook useCallback has a missing dependency: 'options'. Either include it or remove the dependency array
);
Expand All @@ -67,7 +73,7 @@ export const CustomSuggester = slottableComponent<Props>(
onSelect(search);
}
} else {
setOptions([]);
setOptions(defaultOptions ?? []);
onSelect(null);
setSearch('');
}
Expand Down
51 changes: 48 additions & 3 deletions src/components/Suggester/Suggester.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { type ReactNode, useEffect, useMemo } from 'react';
import type { LunaticComponentProps } from '../type';
import { CustomSuggester } from './CustomSuggester';
import { createSearching } from './helpers';
Expand All @@ -23,10 +23,29 @@ export function Suggester({
readOnly,
workersBasePath,
response,
optionResponses = [],
executeExpression,
iteration,
}: LunaticComponentProps<'Suggester'>) {
const { state, fetchInfos, infos } = useSuggesterInfo(storeName, idbVersion);
const onChange = (v: string | null) => {
handleChange(response, v);
const onChange = (
v: string | null | { id?: string; [key: string]: ReactNode }
) => {
if (v && typeof v === 'object' && optionResponses) {
if (v.id) {
handleChange(response, v.id);
}
for (const optionResponse of optionResponses) {
if (optionResponse.attribute in v) {
handleChange(
{ name: optionResponse.name },
v[optionResponse.attribute] as string | null
);
}
}
} else {
handleChange(response, v as string | null);
}
};

// Fetch suggester info when the suggester is mounted
Expand All @@ -44,6 +63,31 @@ export function Suggester({
[infos, storeName, idbVersion, workersBasePath]
);

// Default options should not change between render
// so we can break the rule of hooks here
const defaultOptions = useMemo(() => {
if (!value) {
return [];
}
const labelResponse = optionResponses?.find((o) => o.attribute === 'label');
if (!labelResponse) {
return [];
}
const label = executeExpression<ReactNode>(labelResponse.name, {
iteration,
});
if (!label) {
return [];
}
return [
{
id: value,
label: label,
value: value,
},
];
}, []);

Check warning on line 89 in src/components/Suggester/Suggester.tsx

View workflow job for this annotation

GitHub Actions / test_lint

React Hook useMemo has missing dependencies: 'executeExpression', 'iteration', 'optionResponses', and 'value'. Either include them or remove the dependency array. If 'executeExpression' changes too often, find the parent component that defines it and wrap that definition in useCallback

return (
<SuggesterStatus
storeName={storeName}
Expand All @@ -68,6 +112,7 @@ export function Suggester({
className={className}
optionRenderer={optionRenderer}
labelRenderer={labelRenderer}
defaultOptions={defaultOptions}
onSelect={onChange}
searching={searching}
disabled={disabled}
Expand Down
3 changes: 3 additions & 0 deletions src/components/shared/Combobox/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Props = ComboboxSelectionProps &
errors?: LunaticError[];
onChange?: (s: string | null) => void;
onSelect: (s: string | null) => void;
onBlur?: () => void;
options: ComboboxOptionType[];
readOnly?: boolean;
};
Expand All @@ -56,6 +57,7 @@ function LunaticComboBox({
label,
description,
errors,
onBlur,
}: Props) {
const [expanded, setExpanded] = useState(false);
const [focused, setFocused] = useState(false);
Expand All @@ -76,6 +78,7 @@ function LunaticComboBox({
if (disabled || readOnly) {
return;
}
onBlur?.();
setExpanded(false);
setFocused(false);
};
Expand Down
3 changes: 3 additions & 0 deletions src/components/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ type ComponentPropsByType = {
idbVersion?: string;
focused: boolean;
response: { name: string };
optionResponses?: { name: string; attribute: string }[];
executeExpression: LunaticState['executeExpression'];
iteration: LunaticState['pager']['iteration'];
};
Summary: LunaticBaseProps<string | null> & {
executeExpression: LunaticState['executeExpression'];
Expand Down
12 changes: 12 additions & 0 deletions src/stories/suggester/fakeReferentiel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"id": "brosse",
"label": "Brosse à cheveux",
"price": 20
},
{
"id": "balle",
"label": "Balle rebondissante",
"price": 10
}
]
145 changes: 145 additions & 0 deletions src/stories/suggester/source-option-responses.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
{
"suggesters": [
{
"name": "products",
"fields": [
{
"name": "label",
"rules": ["[\\w]+"],
"language": "French",
"min": 3,
"stemmer": false
}
],
"queryParser": {
"type": "tokenized",
"params": {
"language": "French",
"pattern": "[\\w.]+",
"min": 3,
"stemmer": false
}
},
"version": "1"
}
],
"components": [
{
"componentType": "Suggester",
"response": {
"name": "PRODUCT"
},
"optionResponses": [
{
"name": "PRODUCT_NAME",
"attribute": "label"
},
{
"name": "PRODUCT_PRICE",
"attribute": "price"
}
],
"storeName": "products",
"conditionFilter": {
"type": "VTL",
"value": "true"
},
"id": "lt4ezymk",
"page": "1",
"label": {
"type": "VTL|MD",
"value": "\"➡ 1. \" || \"Quel est votre produit préféré ?\""
},
"mandatory": false,
"maxLength": 249
},
{
"componentType": "Input",
"response": {
"name": "NOM"
},
"conditionFilter": {
"type": "VTL",
"value": "true"
},
"id": "prenom",
"page": "2",
"label": {
"type": "VTL|MD",
"value": "\"➡ 2. Vous aimez \" || PRODUCT_NAME || \" à \" || cast(PRODUCT_PRICE, string) || \"€ mais quel est votre prénom ?\""
},
"mandatory": false,
"maxLength": 249
}
],
"pagination": "question",
"resizing": {},
"label": {
"type": "VTL|MD",
"value": "Suggester"
},
"lunaticModelVersion": "2.5.0",
"modele": "SUGGESTER",
"enoCoreVersion": "2.7.1",
"generatingDate": "27-02-2024 13:43:43",
"missing": false,
"id": "lt4f6mib",
"maxPage": "2",
"variables": [
{
"variableType": "COLLECTED",
"values": {
"COLLECTED": null,
"EDITED": null,
"INPUTED": null,
"FORCED": null,
"PREVIOUS": null
},
"name": "PRODUCT"
},
{
"variableType": "COLLECTED",
"values": {
"COLLECTED": null,
"EDITED": null,
"INPUTED": null,
"FORCED": null,
"PREVIOUS": null
},
"name": "PRODUCT_PRICE"
},
{
"variableType": "COLLECTED",
"values": {
"COLLECTED": null,
"EDITED": null,
"INPUTED": null,
"FORCED": null,
"PREVIOUS": null
},
"name": "PRODUCT_NAME"
},
{
"variableType": "COLLECTED",
"values": {
"COLLECTED": null,
"EDITED": null,
"INPUTED": null,
"FORCED": null,
"PREVIOUS": null
},
"name": "PRODUCT_PRICE"
},
{
"variableType": "COLLECTED",
"values": {
"COLLECTED": null,
"EDITED": null,
"INPUTED": null,
"FORCED": null,
"PREVIOUS": null
},
"name": "PRENOM"
}
]
}
18 changes: 18 additions & 0 deletions src/stories/suggester/suggester.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import defaultArgTypes from '../utils/default-arg-types';
import Orchestrator from '../utils/orchestrator';
import { getReferentiel } from '../utils/referentiel';
import source from './source';
import sourceOptionResponses from './source-option-responses';

const stories = {
title: 'Components/Suggester',
Expand All @@ -15,10 +16,27 @@ export default stories;
const Template = (args) => <Orchestrator {...args} />;
export const Default = Template.bind({});

const getFakeReferentiel = async (name) => {
try {
return (await import(`./fakeReferentiel.json`)).default;
} catch (error) {
console.log('error', error);
throw new Error(`Unknown référentiel ${name}`);
}
};

Default.args = {
id: 'suggester',
source,
autoSuggesterLoading: true,
getReferentiel,
pagination: true,
};
export const OptionResponses = Template.bind({});
OptionResponses.args = {
id: 'suggester-with-option',
source: sourceOptionResponses,
getReferentiel: getFakeReferentiel,
autoSuggesterLoading: true,
pagination: true,
};

0 comments on commit 812b798

Please sign in to comment.