From 4b27331995d41cb3c9a6202f8ce0854734495777 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 6 Mar 2024 12:39:34 +0100 Subject: [PATCH 1/3] poc: suggester with multiple variables --- src/components/Suggester/CustomSuggester.tsx | 18 ++- src/components/Suggester/Suggester.tsx | 51 ++++++- src/components/shared/Combobox/Combobox.tsx | 3 + src/components/type.ts | 3 + src/stories/suggester/fakeReferentiel.json | 12 ++ .../suggester/source-option-responses.json | 142 ++++++++++++++++++ src/stories/suggester/suggester.stories.jsx | 18 +++ 7 files changed, 238 insertions(+), 9 deletions(-) create mode 100644 src/stories/suggester/fakeReferentiel.json create mode 100644 src/stories/suggester/source-option-responses.json diff --git a/src/components/Suggester/CustomSuggester.tsx b/src/components/Suggester/CustomSuggester.tsx index 0e9162ec8..0aa7e2eb4 100644 --- a/src/components/Suggester/CustomSuggester.tsx +++ b/src/components/Suggester/CustomSuggester.tsx @@ -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( @@ -43,14 +46,17 @@ export const CustomSuggester = slottableComponent( label, description, errors, + defaultOptions, }) => { const [search, setSearch] = useState(''); - const [options, setOptions] = useState>([]); + const [options, setOptions] = useState>( + 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] ); @@ -67,7 +73,7 @@ export const CustomSuggester = slottableComponent( onSelect(search); } } else { - setOptions([]); + setOptions(defaultOptions ?? []); onSelect(null); setSearch(''); } diff --git a/src/components/Suggester/Suggester.tsx b/src/components/Suggester/Suggester.tsx index b535ccda2..4eb0f8c85 100644 --- a/src/components/Suggester/Suggester.tsx +++ b/src/components/Suggester/Suggester.tsx @@ -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'; @@ -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 @@ -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(labelResponse.name, { + iteration, + }); + if (!label) { + return []; + } + return [ + { + id: value, + label: label, + value: value, + }, + ]; + }, []); + return ( void; onSelect: (s: string | null) => void; + onBlur?: () => void; options: ComboboxOptionType[]; readOnly?: boolean; }; @@ -56,6 +57,7 @@ function LunaticComboBox({ label, description, errors, + onBlur, }: Props) { const [expanded, setExpanded] = useState(false); const [focused, setFocused] = useState(false); @@ -76,6 +78,7 @@ function LunaticComboBox({ if (disabled || readOnly) { return; } + onBlur?.(); setExpanded(false); setFocused(false); }; diff --git a/src/components/type.ts b/src/components/type.ts index 5ae22755c..73e04ad26 100644 --- a/src/components/type.ts +++ b/src/components/type.ts @@ -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 & { executeExpression: LunaticState['executeExpression']; diff --git a/src/stories/suggester/fakeReferentiel.json b/src/stories/suggester/fakeReferentiel.json new file mode 100644 index 000000000..abc2ea01f --- /dev/null +++ b/src/stories/suggester/fakeReferentiel.json @@ -0,0 +1,12 @@ +[ + { + "id": "brosse", + "label": "Brosse à cheveux", + "price": 20 + }, + { + "id": "balle", + "label": "Balle rebondissante", + "price": 10 + } +] diff --git a/src/stories/suggester/source-option-responses.json b/src/stories/suggester/source-option-responses.json new file mode 100644 index 000000000..e27c1d9cc --- /dev/null +++ b/src/stories/suggester/source-option-responses.json @@ -0,0 +1,142 @@ +{ + "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 || \" 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" + } + ] +} diff --git a/src/stories/suggester/suggester.stories.jsx b/src/stories/suggester/suggester.stories.jsx index dfc8a88fd..4500aa231 100644 --- a/src/stories/suggester/suggester.stories.jsx +++ b/src/stories/suggester/suggester.stories.jsx @@ -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', @@ -15,6 +16,15 @@ export default stories; const Template = (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, @@ -22,3 +32,11 @@ Default.args = { getReferentiel, pagination: true, }; +export const OptionResponses = Template.bind({}); +OptionResponses.args = { + id: 'suggester-with-option', + source: sourceOptionResponses, + getReferentiel: getFakeReferentiel, + autoSuggesterLoading: true, + pagination: true, +}; From 3316c4e67ecfdfdc1994fd6091a16c5d420f40fb Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 6 Mar 2024 13:32:35 +0100 Subject: [PATCH 2/3] poc: suggester with multiple variables --- src/stories/suggester/source-option-responses.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stories/suggester/source-option-responses.json b/src/stories/suggester/source-option-responses.json index e27c1d9cc..f66b722b1 100644 --- a/src/stories/suggester/source-option-responses.json +++ b/src/stories/suggester/source-option-responses.json @@ -63,7 +63,7 @@ "page": "2", "label": { "type": "VTL|MD", - "value": "\"➡ 2. Vous aimez \" || PRODUCT_NAME || \" mais quel est votre prénom ?\"" + "value": "\"➡ 2. Vous aimez \" || PRODUCT_NAME || \" à \" || cast(PRODUCT_PRICE, string) || \"€ mais quel est votre prénom ?\"" }, "mandatory": false, "maxLength": 249 From a5cb73e1d51e5365308802c5cb1b7ed7c7c2c17b Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 6 Mar 2024 14:08:54 +0100 Subject: [PATCH 3/3] fix: prettier --- src/stories/suggester/fakeReferentiel.json | 20 +++++++++---------- .../suggester/source-option-responses.json | 17 +++++++++------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/stories/suggester/fakeReferentiel.json b/src/stories/suggester/fakeReferentiel.json index abc2ea01f..f1fa25eb9 100644 --- a/src/stories/suggester/fakeReferentiel.json +++ b/src/stories/suggester/fakeReferentiel.json @@ -1,12 +1,12 @@ [ - { - "id": "brosse", - "label": "Brosse à cheveux", - "price": 20 - }, - { - "id": "balle", - "label": "Balle rebondissante", - "price": 10 - } + { + "id": "brosse", + "label": "Brosse à cheveux", + "price": 20 + }, + { + "id": "balle", + "label": "Balle rebondissante", + "price": 10 + } ] diff --git a/src/stories/suggester/source-option-responses.json b/src/stories/suggester/source-option-responses.json index f66b722b1..58d62a748 100644 --- a/src/stories/suggester/source-option-responses.json +++ b/src/stories/suggester/source-option-responses.json @@ -29,13 +29,16 @@ "response": { "name": "PRODUCT" }, - "optionResponses": [{ - "name": "PRODUCT_NAME", - "attribute": "label" - },{ - "name": "PRODUCT_PRICE", - "attribute": "price" - }], + "optionResponses": [ + { + "name": "PRODUCT_NAME", + "attribute": "label" + }, + { + "name": "PRODUCT_PRICE", + "attribute": "price" + } + ], "storeName": "products", "conditionFilter": { "type": "VTL",