Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC : Multiple variables with suggesters #900

Merged
merged 3 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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,16 +46,19 @@
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
);

const handleChange = useCallback(
Expand All @@ -67,12 +73,12 @@
onSelect(search);
}
} else {
setOptions([]);
setOptions(defaultOptions ?? []);
onSelect(null);
setSearch('');
}
},
[searching, onSelect]

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

View workflow job for this annotation

GitHub Actions / test_lint

React Hook useCallback has a missing dependency: 'defaultOptions'. Either include it or remove the dependency array. If 'setOptions' needs the current value of 'defaultOptions', you can also switch to useReducer instead of useState and read 'defaultOptions' in the reducer
);

const defaultSearch = getSearch(search, value);
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 @@
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 @@
[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 @@
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,
};
Loading