Skip to content

Commit

Permalink
feat: add fillers behaviour
Browse files Browse the repository at this point in the history
Fix #949
  • Loading branch information
Grafikart committed Aug 5, 2024
1 parent a632f9f commit 09e8060
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 13 deletions.
25 changes: 25 additions & 0 deletions e2e/fillers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect, test } from '@playwright/test';
import {
expectPageToHaveText,
gotoNextPage,
gotoPreviousPage,
goToStory,
} from './utils';

test.describe('Fillers', () => {
test(`can fill data`, async ({ page }) => {
await goToStory(page, 'behaviour-fillers--default#t100');

// First filling
await page.getByLabel('Code postal').fill('34000');
await gotoNextPage(page);
await expectPageToHaveText(page, 'Chargement');
await expect(page.getByLabel('Ville')).toHaveValue('Montpellier');

// Second filling
await gotoPreviousPage(page);
await page.getByLabel('Code postal').fill('31000');
await gotoNextPage(page);
await expect(page.getByLabel('Ville')).toHaveValue('Toulouse');
});
});
33 changes: 33 additions & 0 deletions lunatic-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@
},
"maxPage": {
"type": "string"
},
"fillers": {
"type": "array",
"items": {
"$ref": "#/$defs/FillerDefinition"
}
}
},
"required": ["components", "variables"],
Expand Down Expand Up @@ -1461,6 +1467,33 @@
}
},
"required": ["name", "fields", "queryParser", "version"]
},
"FillerDefinition": {
"type": "object",
"properties": {
"endpoint": {
"type": "object",
"properties": {
"url": {
"type": "string"
}
},
"required": ["url"]
},
"responses": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": ["name"]
}
}
},
"required": ["responses", "endpoint"]
}
}
}
11 changes: 11 additions & 0 deletions src/components/FillerLoader/FillerLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { slottableComponent } from '../shared/HOC/slottableComponent';

/**
* Displays a loader while fetching data to fill the form
*/
export const FillerLoader = slottableComponent(
'FillerLoader',
function FillerLoader() {
return <p>Chargement des données...</p>;
}
);
2 changes: 2 additions & 0 deletions src/components/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { PairwiseLinks } from './PairwiseLinks/PairwiseLinks';
import { CheckboxOne } from './CheckboxOne/CheckboxOne';
import { Suggester } from './Suggester/Suggester';
import { Summary } from './Summary/Summary';
import { FillerLoader } from './FillerLoader/FillerLoader';

// List of all the "componentType"
export const library = {
Expand Down Expand Up @@ -51,6 +52,7 @@ export const library = {
Suggester: Suggester,
Summary: Summary,
Accordion: Accordion,
FillerLoader: FillerLoader,
} satisfies {
[Property in LunaticComponentType]: ComponentType<
LunaticComponentProps<Property>
Expand Down
2 changes: 2 additions & 0 deletions src/components/shared/HOC/slottableComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type { SummaryResponses, SummaryTitle } from '../../Summary/Summary';
import type { LunaticComponentProps } from '../../type';
import type { MarkdownLink } from '../MDLabel/MarkdownLink';
import type { Accordion } from '../../Accordion/Accordion';
import type { FillerLoader } from '../../FillerLoader/FillerLoader';

/**
* Contains the type of every customizable component
Expand Down Expand Up @@ -109,6 +110,7 @@ export type LunaticSlotComponents = {
>;
MarkdownLink: typeof MarkdownLink;
Accordion: typeof Accordion;
FillerLoader: typeof FillerLoader;
};

const empty = {} as Partial<LunaticSlotComponents> | undefined;
Expand Down
3 changes: 3 additions & 0 deletions src/components/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ export type ComponentPropsByType = {
iterations?: VtlExpression;
}>;
};
FillerLoader: {
componentType?: 'FillerLoader';
};
};

export type LunaticComponentType = keyof ComponentPropsByType;
Expand Down
31 changes: 31 additions & 0 deletions src/stories/behaviour/fillers/fillers.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import source from './source.json';
import Orchestrator from '../../utils/orchestrator.jsx';

export default {
title: 'Behaviour/Fillers',
component: Orchestrator,
};

export const Default = {
args: {
source: source,
data: {},
mockFiller: (data) => {
return new Promise((resolve) => {
setTimeout(
() => {
if (data.CODE === '34000') {
return resolve({ CITY: 'Montpellier' });
} else if (data.CODE === '31000') {
return resolve({ CITY: 'Toulouse' });
}
return resolve({});
},
window.location.hash.startsWith('#t')
? parseInt(window.location.hash.replace('#t', ''), 10)
: 1500
);
});
},
},
};
77 changes: 77 additions & 0 deletions src/stories/behaviour/fillers/source.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"$schema": "../../../../lunatic-schema.json",
"maxPage": "2",
"fillers": [
{
"endpoint": {
"url": "https://inseefr.github.io/Lunatic/"
},
"responses": [
{
"name": "CODE"
}
]
}
],
"components": [
{
"id": "a",
"page": "1",
"componentType": "Input",
"label": {
"type": "TXT",
"value": "Code postal"
},
"response": {
"name": "CODE"
},
"declarations": [
{
"declarationType": "COMMENT",
"position": "DETACHABLE",
"id": "d_a",
"label": {
"value": "Ce code permettra de remplir la ville (34000 ou 31000)",
"type": "TXT"
}
}
]
},
{
"id": "a",
"page": "2",
"componentType": "Input",
"label": {
"type": "TXT",
"value": "Ville"
},
"response": {
"name": "CITY"
}
}
],
"variables": [
{
"variableType": "COLLECTED",
"values": {
"COLLECTED": null,
"EDITED": null,
"INPUTTED": null,
"FORCED": null,
"PREVIOUS": null
},
"name": "CODE"
},
{
"variableType": "COLLECTED",
"values": {
"COLLECTED": null,
"EDITED": null,
"INPUTTED": null,
"FORCED": null,
"PREVIOUS": null
},
"name": "CITY"
}
]
}
4 changes: 4 additions & 0 deletions src/stories/utils/orchestrator.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ function OrchestratorForStories({
refusedButton,
readOnly = false,
disabled = false,
mockFiller = null,
...rest
}) {
const { maxPage } = source;
Expand Down Expand Up @@ -154,6 +155,9 @@ function OrchestratorForStories({
withOverview: showOverview,
dontKnowButton,
refusedButton,
mocks: {
filler: mockFiller,
},
});

const components = getComponents();
Expand Down
123 changes: 123 additions & 0 deletions src/use-lunatic/hooks/useFillers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { FillerDefinition } from '../../type.source';
import type { LunaticVariablesStore } from '../commons/variables/lunatic-variables-store';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { LunaticChangesHandler } from '../type';

type Args = {
variables: LunaticVariablesStore;
fillers: FillerDefinition[];
handleChanges: LunaticChangesHandler;
fetchMock:
| null
| ((data: Record<string, unknown>) => Promise<Record<string, unknown>>);
};

/**
* Behaviour that fills variables from a server when some variables are collected
* The filling happens when the user move forward in the form
*/
export function useFillers({
variables,
fillers,
handleChanges,
fetchMock,
}: Args) {
const watchedVariables = useMemo(
() => buildWatchedVariableMap(fillers),
[fillers]
);
const activeFillers = useRef(new Set<FillerDefinition>()); // List fillers that should be triggerred in the next navigation
const [isFilling, setFilling] = useState<boolean>(false);

// Listen for change on variables to detect if a filler need to be triggered on next page change
useEffect(() => {
const listener = (
e: CustomEvent<{
name: string;
}>
) => {
if (watchedVariables.has(e.detail.name)) {
activeFillers.current.add(watchedVariables.get(e.detail.name)!);
}
};
variables.on('change', listener);
return () => {
variables.off('change', listener);
};
}, [variables, watchedVariables]);

// Trigger fillers
const triggerFillers = useCallback(async () => {
if (activeFillers.current.size === 0) {
return;
}
setFilling(true);
Promise.all(
Array.from(activeFillers.current).map((filler) => {
const values = Object.fromEntries(
filler.responses.map((r) => [r.name, variables.get(r.name)])
);
return fetchFillerData(filler, values, fetchMock).then((data) => {
handleChanges(
Object.entries(data).map((d) => ({
name: d[0],
value: d[1],
}))
);
});
})
)
.catch((e) => {
console.error(e);
alert(e);
})
.finally(() => {
setFilling(false);
activeFillers.current.clear();
});
}, [activeFillers, variables, handleChanges]);

return {
triggerFillers,
isFilling,
};
}

/**
* Creates a map of FillerDefinition indexed by variable name (improves performance)
*/
function buildWatchedVariableMap(
definitions: FillerDefinition[]
): Map<string, FillerDefinition> {
const map = new Map<string, FillerDefinition>();
for (const definition of definitions) {
for (const response of definition.responses) {
map.set(response.name, definition);
}
}
return map;
}

/**
* Fetch new data from the server (use mock if it exists)
*/
function fetchFillerData(
filler: FillerDefinition,
data: Record<string, unknown>,
mock:
| null
| ((data: Record<string, unknown>) => Promise<Record<string, unknown>>)
): Promise<Record<string, unknown>> {
if (mock) {
return mock(data);
}

return fetch(filler.endpoint.url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}).then((res) => res.json());
}
5 changes: 5 additions & 0 deletions src/use-lunatic/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ export type LunaticOptions = {
// Enable change tracking to keep a track of what variable changed (allow using getChangedData())
trackChanges?: boolean;
logger?: LunaticLogger;
mocks?: {
filler:
| null
| ((data: Record<string, unknown>) => Promise<Record<string, unknown>>);
};
};

// Type representing the return type of "useLunatic()"
Expand Down
Loading

0 comments on commit 09e8060

Please sign in to comment.