From 35b3db512bd97cd5eabd08a356f0d7b6a2081bd5 Mon Sep 17 00:00:00 2001 From: Chloe Renaud Date: Tue, 18 Feb 2025 13:38:58 +0100 Subject: [PATCH] feat: codes lists page --- next/package.json | 4 +- next/src/api/codesLists.test.ts | 27 ++ next/src/api/codesLists.ts | 26 ++ next/src/api/questionnaires.ts | 22 ++ next/src/api/utils/codesLists.test.ts | 33 ++- next/src/api/utils/codesLists.ts | 44 ++- next/src/components/codesLists/CodesLists.tsx | 110 -------- .../codesLists/create/CreateCodesList.tsx | 33 +++ .../create/CreateCodesListCSVImport.tsx | 10 + .../codesLists/create/CreateCodesListForm.tsx | 67 +++++ .../codesLists/edit/EditCodesList.tsx | 40 +++ .../codesLists/edit/EditCodesListForm.tsx | 71 +++++ .../codesLists/form/CodesListForm.tsx | 254 ++++++++++++++++++ .../overview/CodesListOverviewItem.tsx | 120 +++++++++ .../overview/CodesListsOverview.tsx | 61 +++++ .../createCodesList/CreateCodesList.tsx | 227 ---------------- .../CreateQuestionnaire.test.tsx | 2 +- .../CreateQuestionnaire.tsx | 4 +- .../components/layout/QuestionnaireLayout.tsx | 8 +- next/src/components/ui/Accordion.tsx | 10 +- next/src/components/ui/Breadcrumb.tsx | 2 +- next/src/components/ui/Button.tsx | 15 +- next/src/components/ui/ButtonIcon.tsx | 23 +- next/src/components/ui/ButtonLink.tsx | 8 +- next/src/components/ui/Dialog.test.tsx | 21 ++ next/src/components/ui/Dialog.tsx | 63 +++++ next/src/components/ui/Input.tsx | 7 +- next/src/components/ui/Menu.tsx | 26 +- next/src/components/ui/icons/AddIcon.tsx | 18 ++ next/src/components/ui/icons/ArrowUpIcon.tsx | 17 ++ next/src/i18n/locales/en.json | 53 ++-- next/src/i18n/locales/fr.json | 53 ++-- next/src/routeTree.gen.ts | 31 ++- .../_layout-q/codes-list/$codesListId.tsx | 35 +++ .../_layout-q/codes-lists/index.tsx | 7 +- .../_layout-q/codes-lists/new.tsx | 10 +- next/src/routes/index.tsx | 4 +- next/src/vite-env.d.ts | 2 +- next/yarn.lock | 40 ++- 39 files changed, 1159 insertions(+), 449 deletions(-) create mode 100644 next/src/api/codesLists.test.ts create mode 100644 next/src/api/codesLists.ts delete mode 100644 next/src/components/codesLists/CodesLists.tsx create mode 100644 next/src/components/codesLists/create/CreateCodesList.tsx create mode 100644 next/src/components/codesLists/create/CreateCodesListCSVImport.tsx create mode 100644 next/src/components/codesLists/create/CreateCodesListForm.tsx create mode 100644 next/src/components/codesLists/edit/EditCodesList.tsx create mode 100644 next/src/components/codesLists/edit/EditCodesListForm.tsx create mode 100644 next/src/components/codesLists/form/CodesListForm.tsx create mode 100644 next/src/components/codesLists/overview/CodesListOverviewItem.tsx create mode 100644 next/src/components/codesLists/overview/CodesListsOverview.tsx delete mode 100644 next/src/components/createCodesList/CreateCodesList.tsx create mode 100644 next/src/components/ui/Dialog.test.tsx create mode 100644 next/src/components/ui/Dialog.tsx create mode 100644 next/src/components/ui/icons/AddIcon.tsx create mode 100644 next/src/components/ui/icons/ArrowUpIcon.tsx create mode 100644 next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-list/$codesListId.tsx diff --git a/next/package.json b/next/package.json index 1ab789207..c03ae7943 100644 --- a/next/package.json +++ b/next/package.json @@ -14,8 +14,9 @@ "preview": "vite preview" }, "dependencies": { - "@base-ui-components/react": "^1.0.0-alpha.5", + "@base-ui-components/react": "^1.0.0-alpha.6", "@fontsource-variable/open-sans": "^5.1.1", + "@hookform/resolvers": "^4.1.0", "@tanstack/react-form": "^0.41.3", "@tanstack/react-query": "^5.64.1", "@tanstack/react-router": "^1.97.1", @@ -27,6 +28,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-error-boundary": "^5.0.0", + "react-hook-form": "^7.54.2", "react-hot-toast": "^2.5.1", "react-i18next": "^15.4.0", "vite-envs": "^4.4.10", diff --git a/next/src/api/codesLists.test.ts b/next/src/api/codesLists.test.ts new file mode 100644 index 000000000..b1c95666c --- /dev/null +++ b/next/src/api/codesLists.test.ts @@ -0,0 +1,27 @@ +import nock from 'nock'; + +import { deleteCodesList, putCodesList } from './codesLists'; + +vi.mock('@/contexts/oidc'); + +it('Delete codes list', async () => { + nock('https://mock-api') + .delete('/persistence/questionnaire/my-q-id/codes-list/my-code-list-id') + .reply(204); + + const res = await deleteCodesList('my-q-id', 'my-code-list-id'); + expect(res.status).toEqual(204); +}); + +it('Create codes list', async () => { + nock('https://mock-api') + .put('/persistence/questionnaire/my-q-id/codes-list/my-code-list-id') + .reply(201); + + const res = await putCodesList('my-q-id', 'my-code-list-id', { + codes: [], + id: 'id', + label: 'label', + }); + expect(res.status).toEqual(201); +}); diff --git a/next/src/api/codesLists.ts b/next/src/api/codesLists.ts new file mode 100644 index 000000000..8c74ef3a5 --- /dev/null +++ b/next/src/api/codesLists.ts @@ -0,0 +1,26 @@ +import type { CodesList } from '@/models/codesLists'; + +import { instance } from './instance'; + +/** Create or update a codes list. */ +export async function putCodesList( + questionnaireId: string, + codeListId: string, + codeList: CodesList, +): Promise { + return instance.put( + `/persistence/questionnaire/${questionnaireId}/codes-list/${codeListId}`, + codeList, + { headers: { 'Content-Type': 'application/json' } }, + ); +} + +/** Delete a codes list. */ +export async function deleteCodesList( + questionnaireId: string, + codeListId: string, +): Promise { + return instance.delete( + `/persistence/questionnaire/${questionnaireId}/codes-list/${codeListId}`, + ); +} diff --git a/next/src/api/questionnaires.ts b/next/src/api/questionnaires.ts index b3a6abf64..51a63be61 100644 --- a/next/src/api/questionnaires.ts +++ b/next/src/api/questionnaires.ts @@ -101,6 +101,28 @@ export async function addQuestionnaireCodesList( return putQuestionnaire(questionnaireId, questionnaire); } +/** Update the questionnaire of the provided id with a new codes list. */ +export async function updateCodesList( + questionnaireId: string, + newCodesList: CodesList, +): Promise { + const newCodesLists = []; + const questionnaire = await getPoguesQuestionnaire(questionnaireId); + const codesLists = questionnaire.CodeLists?.CodeList || []; + let i = 0; + for (const codesList of codesLists) { + if (codesList.id === newCodesList.id) { + newCodesLists.push( + ...codesLists.splice(i, 1, ...computePoguesCodesLists([newCodesList])), + ); + } + i++; + } + questionnaire.CodeLists = { CodeList: newCodesLists }; + + return putQuestionnaire(questionnaireId, questionnaire); +} + /** Retrieve a questionnaire by id with the pogues model. */ async function getPoguesQuestionnaire( id: string, diff --git a/next/src/api/utils/codesLists.test.ts b/next/src/api/utils/codesLists.test.ts index ca6c9991e..683e4b69f 100644 --- a/next/src/api/utils/codesLists.test.ts +++ b/next/src/api/utils/codesLists.test.ts @@ -8,8 +8,23 @@ const codesLists: CodesList[] = [ id: 'id-1', label: 'label-1', codes: [ - { label: 'code-1', value: 'code-value-1' }, - { label: 'code-2', value: 'code-value-2' }, + { + label: 'code-1', + value: 'value-1', + codes: [ + { + label: 'sub-code-1', + value: 'sub-value-1', + codes: [ + { + label: 'sub-sub-code-1', + value: 'sub-sub-value-1', + }, + ], + }, + ], + }, + { label: 'code-2', value: 'value-2' }, ], }, ]; @@ -20,8 +35,18 @@ const poguesCodesLists: PoguesCodesList[] = [ Name: 'label-1', Label: 'label-1', Code: [ - { Label: 'code-1', Value: 'code-value-1' }, - { Label: 'code-2', Value: 'code-value-2' }, + { Label: 'code-1', Value: 'value-1' }, + { + Label: 'sub-code-1', + Value: 'sub-value-1', + Parent: 'value-1', + }, + { + Label: 'sub-sub-code-1', + Value: 'sub-sub-value-1', + Parent: 'sub-value-1', + }, + { Label: 'code-2', Value: 'value-2' }, ], }, ]; diff --git a/next/src/api/utils/codesLists.ts b/next/src/api/utils/codesLists.ts index 56f67f3ef..ffe3f731a 100644 --- a/next/src/api/utils/codesLists.ts +++ b/next/src/api/utils/codesLists.ts @@ -11,29 +11,52 @@ export function computeCodesLists( ): CodesList[] { const res: CodesList[] = []; for (const codesList of codesLists) { - const datum = { - id: codesList.id, - label: codesList.Label, - codes: computeCodes(codesList.Code), - }; - res.push(datum); + if (!codesList.Urn) { + const datum = { + id: codesList.id, + label: codesList.Label, + codes: computeRootCodes(codesList.Code), + }; + res.push(datum); + } } return res; } -function computeCodes(codes: PoguesCode[] = []): Code[] { +// compute codes at the root of the list: they should not have a parent +function computeRootCodes(codes: PoguesCode[] = []): Code[] { const res: Code[] = []; for (const code of codes) { + if (code.Parent) continue; const datum = { label: code.Label, value: code.Value, - // TODO subcodes + codes: getSubCodes(codes, code.Value), }; res.push(datum); } return res; } +// compute a subcode: they have a parent and we should get their children too +function getSubCodes( + codes: PoguesCode[], + parentValue: string, +): Code[] | undefined { + const subCodes = []; + for (const code of codes) { + if (code.Parent === parentValue) { + const datum = { + label: code.Label, + value: code.Value, + codes: getSubCodes(codes, code.Value), + }; + subCodes.push(datum); + } + } + return subCodes.length > 0 ? subCodes : undefined; +} + /** Compute codes lists that can be sent to the API from our app data. */ export function computePoguesCodesLists( codesLists: CodesList[] = [], @@ -51,15 +74,16 @@ export function computePoguesCodesLists( return res; } -function computePoguesCodes(codes: Code[] = []): PoguesCode[] { +function computePoguesCodes(codes: Code[] = [], parent?: Code): PoguesCode[] { const res: PoguesCode[] = []; for (const code of codes) { const datum = { Label: code.label, Value: code.value, - // TODO subcodes + Parent: parent ? parent.value : undefined, }; res.push(datum); + res.push(...computePoguesCodes(code.codes, code)); } return res; } diff --git a/next/src/components/codesLists/CodesLists.tsx b/next/src/components/codesLists/CodesLists.tsx deleted file mode 100644 index 4e24e0b11..000000000 --- a/next/src/components/codesLists/CodesLists.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import Accordion, { AccordionItem } from '@/components/ui/Accordion'; -import ButtonIcon from '@/components/ui/ButtonIcon'; -import ButtonLink, { ButtonType } from '@/components/ui/ButtonLink'; -import ContentHeader from '@/components/ui/ContentHeader'; -import ContentMain from '@/components/ui/ContentMain'; -import Menu from '@/components/ui/Menu'; -import EditIcon from '@/components/ui/icons/EditIcon'; -import { type CodesList } from '@/models/codesLists'; - -interface CodesListsProps { - codesLists?: CodesList[]; - questionnaireId?: string; -} - -export default function CodesLists({ - codesLists = [], - questionnaireId = '', -}: Readonly) { - const { t } = useTranslation(); - return ( -
- - {t('codesLists.create')} - - } - /> - -
- - {codesLists.map((codesList) => ( - - - - ))} - -
-
-
- ); -} - -interface CodesListProps { - codesList: CodesList; - questionnaireId: string; -} - -function CodesListWrapper({ - codesList, - questionnaireId, -}: Readonly) { - const [isDirtyState, setIsDirtyState] = useState(false); - - const { t } = useTranslation(); - - return ( - <> -
-
-
Valeur
-
Label
-
- {codesList.codes.map((code) => ( - -
- {code.value} - setIsDirtyState(true)} - /> -
-
- {code.label} - setIsDirtyState(true)} - /> -
-
- -
-
- ))} -
-
- {isDirtyState ? ( - - {t('common.validate')} - - ) : null} - - ); -} diff --git a/next/src/components/codesLists/create/CreateCodesList.tsx b/next/src/components/codesLists/create/CreateCodesList.tsx new file mode 100644 index 000000000..cb95608f7 --- /dev/null +++ b/next/src/components/codesLists/create/CreateCodesList.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from 'react-i18next'; + +import ContentHeader from '@/components/ui/ContentHeader'; +import ContentMain from '@/components/ui/ContentMain'; + +import CreateCodesListCSVImport from './CreateCodesListCSVImport'; +import CreateCodesListForm from './CreateCodesListForm'; + +interface CreateCodesListProps { + questionnaireId: string; +} + +/** + * Allow to create a new codes list (either by filling the form or by importing + * a CSV). + */ +export default function CreateCodesList({ + questionnaireId, +}: Readonly) { + const { t } = useTranslation(); + + return ( +
+ + + +
+ +
+
+
+ ); +} diff --git a/next/src/components/codesLists/create/CreateCodesListCSVImport.tsx b/next/src/components/codesLists/create/CreateCodesListCSVImport.tsx new file mode 100644 index 000000000..dcc7fabe5 --- /dev/null +++ b/next/src/components/codesLists/create/CreateCodesListCSVImport.tsx @@ -0,0 +1,10 @@ +import { useTranslation } from 'react-i18next'; + +import Button from '@/components/ui/Button'; + +/** Allow to import a CSV which will create a codes list. */ +export default function CreateCodesListCSVImport() { + const { t } = useTranslation(); + + return ; +} diff --git a/next/src/components/codesLists/create/CreateCodesListForm.tsx b/next/src/components/codesLists/create/CreateCodesListForm.tsx new file mode 100644 index 000000000..9c8d88e49 --- /dev/null +++ b/next/src/components/codesLists/create/CreateCodesListForm.tsx @@ -0,0 +1,67 @@ +import { useMutation } from '@tanstack/react-query'; +import { useNavigate, useRouteContext } from '@tanstack/react-router'; +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; + +import { putCodesList } from '@/api/codesLists'; +import { CodesList } from '@/models/codesLists'; +import { uid } from '@/utils/utils'; + +import CodesListForm, { FormValues } from '../form/CodesListForm'; + +interface CreateCodesListFormProps { + questionnaireId: string; +} + +/** Create a new code list. */ +export default function CreateCodesListForm({ + questionnaireId, +}: Readonly) { + const { t } = useTranslation(); + const { queryClient } = useRouteContext({ from: '__root__' }); + const navigate = useNavigate(); + + const mutation = useMutation({ + mutationFn: ({ + codesList, + questionnaireId, + }: { + codesList: CodesList; + questionnaireId: string; + }) => { + return putCodesList(questionnaireId, codesList.id, codesList); + }, + onSuccess: (questionnaireId) => + queryClient.invalidateQueries({ + queryKey: ['questionnaire', { questionnaireId }], + }), + }); + + const submitForm = async ({ label, codes }: FormValues) => { + const id = uid(); + const codesList = { id, label, codes }; + const promise = mutation.mutateAsync( + { questionnaireId, codesList }, + { + onSuccess: () => + void navigate({ + to: '/questionnaire/$questionnaireId/codes-lists', + params: { questionnaireId }, + }), + }, + ); + toast.promise(promise, { + loading: t('common.loading'), + success: t('codesList.create.success'), + error: (err: Error) => err.toString(), + }); + }; + + return ( + + ); +} diff --git a/next/src/components/codesLists/edit/EditCodesList.tsx b/next/src/components/codesLists/edit/EditCodesList.tsx new file mode 100644 index 000000000..6ca0e6aac --- /dev/null +++ b/next/src/components/codesLists/edit/EditCodesList.tsx @@ -0,0 +1,40 @@ +import { useTranslation } from 'react-i18next'; + +import ContentHeader from '@/components/ui/ContentHeader'; +import ContentMain from '@/components/ui/ContentMain'; +import { CodesList } from '@/models/codesLists'; + +import EditCodesListForm from './EditCodesListForm'; + +interface EditCodesListProps { + codesList?: CodesList; + questionnaireId: string; +} + +/** Allow to edit an existing code list. */ +export default function EditCodesList({ + codesList, + questionnaireId, +}: Readonly) { + const { t } = useTranslation(); + + return ( + <> + + + {!codesList ? ( +
Not found
+ ) : ( +
+ +
+ )} +
+ + ); +} diff --git a/next/src/components/codesLists/edit/EditCodesListForm.tsx b/next/src/components/codesLists/edit/EditCodesListForm.tsx new file mode 100644 index 000000000..41ac48b90 --- /dev/null +++ b/next/src/components/codesLists/edit/EditCodesListForm.tsx @@ -0,0 +1,71 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; +import { t } from 'i18next'; +import toast from 'react-hot-toast'; + +import { putCodesList } from '@/api/codesLists'; +import { CodesList } from '@/models/codesLists'; + +import CodesListForm, { FormValues } from '../form/CodesListForm'; + +interface EditCodesListFormProps { + /** Initial codes list value. */ + codesList: CodesList; + /** Related questionnaire id. */ + questionnaireId: string; +} + +/** Form to edit an existing code list. */ +export default function EditCodesListForm({ + codesList, + questionnaireId, +}: Readonly) { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + + const mutation = useMutation({ + mutationFn: ({ + codesList, + questionnaireId, + }: { + codesList: CodesList; + questionnaireId: string; + }) => { + return putCodesList(questionnaireId, codesList.id, codesList); + }, + onSuccess: (questionnaireId) => + queryClient.invalidateQueries({ + queryKey: ['questionnaire', { questionnaireId }], + }), + }); + + const onSubmit = async ({ label, codes }: FormValues) => { + const updatedCodesList = { id: codesList.id, label, codes }; + const promise = mutation.mutateAsync( + { questionnaireId, codesList: updatedCodesList }, + { + onSuccess: () => + void navigate({ + to: '/questionnaire/$questionnaireId/codes-lists', + params: { questionnaireId }, + }), + }, + ); + toast.promise(promise, { + loading: t('common.loading'), + success: t('codesList.edit.success', { + label, + }), + error: (err: Error) => err.toString(), + }); + }; + + return ( + + ); +} diff --git a/next/src/components/codesLists/form/CodesListForm.tsx b/next/src/components/codesLists/form/CodesListForm.tsx new file mode 100644 index 000000000..1eb67de0d --- /dev/null +++ b/next/src/components/codesLists/form/CodesListForm.tsx @@ -0,0 +1,254 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { t } from 'i18next'; +import { + type Control, + Controller, + type SubmitHandler, + type UseFieldArrayMove, + type UseFieldArrayRemove, + useFieldArray, + useForm, +} from 'react-hook-form'; +import { z } from 'zod'; + +import Button, { ButtonStyle } from '@/components/ui/Button'; +import ButtonIcon, { ButtonIconStyle } from '@/components/ui/ButtonIcon'; +import ButtonLink from '@/components/ui/ButtonLink'; +import Input from '@/components/ui/Input'; +import Label from '@/components/ui/Label'; +import AddIcon from '@/components/ui/icons/AddIcon'; +import ArrowDownIcon from '@/components/ui/icons/ArrowDownIcon'; +import ArrowUpIcon from '@/components/ui/icons/ArrowUpIcon'; +import DeleteIcon from '@/components/ui/icons/DeleteIcon'; +import { type CodesList } from '@/models/codesLists'; + +interface CodesListFormProps { + /** In an update case, initial codes list value. */ + codesList?: CodesList; + /** Related questionnaire id. */ + questionnaireId: string; + /** Function that will be called with form data when the user submit the form. */ + onSubmit: SubmitHandler>; + /** Label to display on the submit button */ + submitLabel: string; +} + +const codeSchema = z.object({ + value: z.string().min(1, 'Your code must have a value'), + label: z.string().min(1, 'Your code must have a label'), +}); + +const codesSchema = codeSchema.extend({ + codes: z.lazy(() => codeSchema.array()).optional(), +}); + +const schema = z.object({ + label: z.string().min(1, 'You must provide a label'), + codes: codesSchema.array().min(1, 'You must provide at least one code'), +}); + +export type FormValues = z.infer; + +/** + * Create or edit a codes list. + * + * A code list has a label and codes (defined by a label and value). + * + * A code can have subcodes. + * + * {@link CodesList} + */ +export default function CodesListForm({ + codesList = undefined, + questionnaireId, + onSubmit, + submitLabel, +}: Readonly) { + const { + control, + handleSubmit, + formState: { isDirty, isValid }, + } = useForm>({ + defaultValues: codesList, + resolver: zodResolver(schema), + }); + + return ( +
+ ( + + )} + /> +
+ + + +
+
+ + {t('common.cancel')} + + +
+ + ); +} + +interface CodesFieldsProps { + control: Control; +} + +function CodesFields({ control }: Readonly) { + const name = 'codes'; + const { fields, append, remove, move } = useFieldArray({ + control, + name, + }); + + return ( + <> + {fields.map((v, index) => ( + + ))} + + + ); +} + +interface CodesFieldProps { + control: Control; + index: number; + remove: UseFieldArrayRemove; + move: UseFieldArrayMove; + isFirst?: boolean; + isLast?: boolean; + parentName: string; + subCodeIteration?: number; +} + +function CodesField({ + control, + index, + remove, + move, + isFirst = false, + isLast = false, + parentName, + subCodeIteration = 0, +}: Readonly) { + const namePrefix = `${parentName}.${index}`; + const { + fields, + append: appendSubCode, + remove: removeSubCode, + move: moveSubCode, + } = useFieldArray({ + control, + name: `${namePrefix}.codes` as 'codes', + }); + + return ( + <> +
+
+ move(index, index - 1)} + /> + move(index, index + 1)} + disabled={isLast} + /> +
+ ( + + )} + /> +
+ ( + + )} + /> + appendSubCode({ label: '', value: '' })} + /> + remove(index)} + buttonStyle={ButtonIconStyle.Delete} + /> + {fields.map((v, index) => ( + + ))} + + ); +} diff --git a/next/src/components/codesLists/overview/CodesListOverviewItem.tsx b/next/src/components/codesLists/overview/CodesListOverviewItem.tsx new file mode 100644 index 000000000..074aade7e --- /dev/null +++ b/next/src/components/codesLists/overview/CodesListOverviewItem.tsx @@ -0,0 +1,120 @@ +import React from 'react'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; + +import { deleteCodesList } from '@/api/codesLists'; +import ButtonLink from '@/components/ui/ButtonLink'; +import Dialog from '@/components/ui/Dialog'; +import type { Code, CodesList } from '@/models/codesLists'; + +interface CodesListProps { + codesList: CodesList; + questionnaireId: string; +} + +/** Display a codes list and allow to edit it. */ +export default function CodesListOverviewItem({ + codesList, + questionnaireId, +}: Readonly) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: ({ + questionnaireId, + codesListId, + }: { + questionnaireId: string; + codesListId: string; + }) => { + return deleteCodesList(questionnaireId, codesListId); + }, + onSuccess: (questionnaireId) => + queryClient.invalidateQueries({ + queryKey: ['questionnaire', { questionnaireId }], + }), + }); + + function onDelete() { + const promise = mutation.mutateAsync({ + questionnaireId, + codesListId: codesList.id, + }); + toast.promise(promise, { + loading: t('common.loading'), + success: t('codesList.overview.deleteSuccess', { + label: codesList.label, + }), + error: (err: Error) => err.toString(), + }); + } + + return ( +
+
+

{codesList.label}

+ + + + + + + + + {codesList.codes.map((code) => ( + + + + ))} + +
{t('codesList.common.value')}{t('codesList.common.label')}
+
+
+ + {t('common.edit')} + + +
+
+ ); +} + +interface CodeLineProps { + code: Code; + subCodeIteration?: number; +} + +function CodeLine({ code, subCodeIteration = 0 }: Readonly) { + return ( + <> + + +
+ {code.value} +
+ + {code.label} + + {code.codes?.map((code) => ( + + ))} + + ); +} diff --git a/next/src/components/codesLists/overview/CodesListsOverview.tsx b/next/src/components/codesLists/overview/CodesListsOverview.tsx new file mode 100644 index 000000000..4a4585e54 --- /dev/null +++ b/next/src/components/codesLists/overview/CodesListsOverview.tsx @@ -0,0 +1,61 @@ +import { useTranslation } from 'react-i18next'; + +import ButtonLink, { ButtonStyle } from '@/components/ui/ButtonLink'; +import ContentHeader from '@/components/ui/ContentHeader'; +import ContentMain from '@/components/ui/ContentMain'; +import { type CodesList } from '@/models/codesLists'; + +import CodesListOverviewItem from './CodesListOverviewItem'; + +interface CodesListsProps { + codesLists?: CodesList[]; + questionnaireId: string; +} + +/** + * Display the codes lists of the selected questionnaire and allow to edit them + * or create a new one. + */ +export default function CodesListsOverview({ + codesLists = [], + questionnaireId, +}: Readonly) { + const { t } = useTranslation(); + + return ( +
+ + {t('codesList.overview.create')} + + } + /> + + {codesLists.length > 0 ? ( + <> + {codesLists.map((codesList) => ( + + ))} + + ) : ( + + {t('codesList.overview.create')} + + )} + +
+ ); +} diff --git a/next/src/components/createCodesList/CreateCodesList.tsx b/next/src/components/createCodesList/CreateCodesList.tsx deleted file mode 100644 index fe77b0cdc..000000000 --- a/next/src/components/createCodesList/CreateCodesList.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { useForm } from '@tanstack/react-form'; -import { useMutation } from '@tanstack/react-query'; - -import { - useNavigate, - useParams, - useRouteContext, -} from '@tanstack/react-router'; - import i18next from 'i18next'; -import toast from 'react-hot-toast'; -import { useTranslation } from 'react-i18next'; -import { z } from 'zod'; - -import { addQuestionnaireCodesList } from '@/api/questionnaires'; -import Button, { ButtonType } from '@/components/ui/Button'; -import ButtonLink from '@/components/ui/ButtonLink'; -import ContentHeader from '@/components/ui/ContentHeader'; -import ContentMain from '@/components/ui/ContentMain'; -import Input from '@/components/ui/Input'; -import Label from '@/components/ui/Label'; -import { Code, CodesList } from '@/models/codesLists'; -import { uid } from '@/utils/utils'; - -interface FormValues { - label: string; - codes: Code[]; -} - -const questionnaireSchema = z.object({ - label: z.string().min(1, 'You must provide a label'), - codes: z - .array( - z.object({ - value: z.string().min(1, 'Your code must have a value'), - label: z.string().min(1, 'Your code must have a label'), - }), - ) - .min(1, 'You must provide at least one code'), -}); - -/** - * Create a new codes list. - * - * A code list has a label and codes (defined by a label and value). - * - * A code can have subcodes. - * - * {@link CodesList} - */ -export default function CreateQuestionnaire() { - const { t } = useTranslation(); - const { queryClient } = useRouteContext({ from: '__root__' }); - const { questionnaireId } = useParams({ strict: false }); - const navigate = useNavigate(); - - const mutation = useMutation({ - mutationFn: ({ - questionnaireId, - codesList, - }: { - questionnaireId: string; - codesList: CodesList; - }) => { - return addQuestionnaireCodesList(questionnaireId, codesList); - }, - onSuccess: (questionnaireId) => - queryClient.invalidateQueries({ - queryKey: ['questionnaire', { questionnaireId }], - }), - }); - - const { Field, Subscribe, handleSubmit } = useForm({ - defaultValues: { - label: '', - codes: [], - }, - validators: { onMount: questionnaireSchema, onChange: questionnaireSchema }, - onSubmit: async ({ value }) => await submitForm(value), - }); - - const submitForm = async ({ label, codes }: FormValues) => { - const id = uid(); - const codesList = { id, label, codes }; - - const promise = mutation.mutateAsync( - { questionnaireId: questionnaireId!, codesList }, - { - onSuccess: () => - void navigate({ - to: '/questionnaire/$questionnaireId/codes-lists', - params: { questionnaireId: questionnaireId! }, - }), - }, - ); - toast.promise(promise, { - loading: i18next.t('common.loading'), - success: i18next.t('createCodeList.created'), - error: (err: Error) => err.toString(), - }); - }; - - return ( -
- - - -
-
- - {(field) => ( - field.handleChange(v as string)} - autoFocus - value={field.state.value} - error={ - field.state.meta.isTouched - ? field.state.meta.errors.join(', ') - : '' - } - required - /> - )} - -
- - - {(field) => ( - <> -
- - -
- {field.state.value.map((_, i) => { - return ( - <> - - {(subField) => { - return ( - - subField.handleChange(v as string) - } - value={subField.state.value} - error={ - subField.state.meta.isTouched - ? subField.state.meta.errors.join(', ') - : '' - } - required - /> - ); - }} - - - {(subField) => { - return ( - - subField.handleChange(v as string) - } - value={subField.state.value} - error={ - subField.state.meta.isTouched - ? subField.state.meta.errors.join(', ') - : '' - } - required - /> - ); - }} - - - - ); - })} -
- - {field.state.meta.isTouched ? ( -
- {field.state.meta.errors.join(', ')} -
- ) : null} - - )} -
-
-
-
- - {t('common.cancel')} - - [state.canSubmit, state.isSubmitting]} - > - {([canSubmit, isSubmitting]) => ( - - )} - -
-
-
-
- ); -} diff --git a/next/src/components/createQuestionnaire/CreateQuestionnaire.test.tsx b/next/src/components/createQuestionnaire/CreateQuestionnaire.test.tsx index 4ec53d32c..f762965e2 100644 --- a/next/src/components/createQuestionnaire/CreateQuestionnaire.test.tsx +++ b/next/src/components/createQuestionnaire/CreateQuestionnaire.test.tsx @@ -6,7 +6,7 @@ import { renderWithRouter } from '@/utils/tests'; import CreateQuestionnaire from './CreateQuestionnaire'; describe('CreateQuestionnaire', () => { - it('is disabled on mount', () => { + it('is disabled on mount', async () => { const { getByText } = renderWithRouter(); expect(getByText('Valider')).toBeInTheDocument(); expect(getByText('Valider')).toBeDisabled(); diff --git a/next/src/components/createQuestionnaire/CreateQuestionnaire.tsx b/next/src/components/createQuestionnaire/CreateQuestionnaire.tsx index 5e6093233..948e69200 100644 --- a/next/src/components/createQuestionnaire/CreateQuestionnaire.tsx +++ b/next/src/components/createQuestionnaire/CreateQuestionnaire.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import { postQuestionnaire } from '@/api/questionnaires'; -import Button, { ButtonType } from '@/components/ui/Button'; +import Button, { ButtonStyle } from '@/components/ui/Button'; import ButtonLink from '@/components/ui/ButtonLink'; import Checkbox from '@/components/ui/Checkbox'; import ContentHeader from '@/components/ui/ContentHeader'; @@ -242,7 +242,7 @@ export default function CreateQuestionnaire() { > {([canSubmit, isSubmitting]) => ( ); } diff --git a/next/src/components/ui/ButtonLink.tsx b/next/src/components/ui/ButtonLink.tsx index b4d7891cd..007ec6438 100644 --- a/next/src/components/ui/ButtonLink.tsx +++ b/next/src/components/ui/ButtonLink.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { LinkComponent, createLink } from '@tanstack/react-router'; -export enum ButtonType { +export enum ButtonStyle { Primary, Secondary, } @@ -10,18 +10,18 @@ export enum ButtonType { interface BasicLinkProps extends React.AnchorHTMLAttributes { // Add any additional props you want to pass to the anchor element disabled: boolean; - buttonType?: ButtonType; + buttonStyle?: ButtonStyle; } const AnchorButtonComponent = React.forwardRef< HTMLAnchorElement, BasicLinkProps ->(({ buttonType = ButtonType.Secondary, children, ...props }, ref) => ( +>(({ buttonStyle = ButtonStyle.Secondary, children, ...props }, ref) => ( { + it('can be opened and closed', async () => { + const user = userEvent.setup(); + const { getByText } = render( + , + ); + expect(getByText('label')).toBeInTheDocument(); + await user.click(screen.getByText('label')); + expect(getByText('title')).toBeInTheDocument(); + expect(getByText('body')).toBeInTheDocument(); + expect(getByText('close')).toBeInTheDocument(); + await user.click(screen.getByText('close')); + expect(getByText('title')).not.toBeInTheDocument(); + expect(getByText('body')).not.toBeInTheDocument(); + }); +}); diff --git a/next/src/components/ui/Dialog.tsx b/next/src/components/ui/Dialog.tsx new file mode 100644 index 000000000..92d535d2d --- /dev/null +++ b/next/src/components/ui/Dialog.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; + +import { Dialog as UIDialog } from '@base-ui-components/react/dialog'; +import { useTranslation } from 'react-i18next'; + +import Button, { ButtonStyle } from './Button'; + +interface DialogProps { + /** Body message in the dialog. */ + body: React.ReactNode; + /** Label of the button that opens the dialog. */ + label: string; + /** + * Function to execute if the user click on "validate". + * + * The validate button is only present if this function is provided. + */ + onValidate?: () => void; + /** Title of the dialog. */ + title: React.ReactNode; +} + +/** Display a button that opens a confirmation dialog. */ +export default function Dialog({ + body, + label, + onValidate, + title, +}: Readonly) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + return ( + + {label}} /> + + + + + {title} + + + {body} + +
+ {t('common.cancel')}} /> + {onValidate ? ( + + ) : null} +
+
+
+
+ ); +} diff --git a/next/src/components/ui/Input.tsx b/next/src/components/ui/Input.tsx index b3769ad07..2121f0fcc 100644 --- a/next/src/components/ui/Input.tsx +++ b/next/src/components/ui/Input.tsx @@ -2,6 +2,7 @@ import { Field } from '@base-ui-components/react/field'; interface InputProps { autoFocus?: boolean; + className?: string; description?: string; disabled?: boolean; error?: string; @@ -9,11 +10,13 @@ interface InputProps { onChange?: (v: string | number | readonly string[] | undefined) => void; placeholder?: string; required?: boolean; + style?: object; value: string | number | readonly string[] | undefined; } export default function Input({ autoFocus, + className = '', description, disabled, error, @@ -21,13 +24,15 @@ export default function Input({ onChange = () => {}, placeholder, required, + style = {}, value, }: Readonly) { return ( {label ? ( diff --git a/next/src/components/ui/Menu.tsx b/next/src/components/ui/Menu.tsx index 126877ad5..84cdfb72c 100644 --- a/next/src/components/ui/Menu.tsx +++ b/next/src/components/ui/Menu.tsx @@ -3,21 +3,31 @@ import * as React from 'react'; import { Menu as UIMenu } from '@base-ui-components/react/menu'; import i18next from 'i18next'; -import ButtonIcon from './ButtonIcon'; import DeleteIcon from './icons/DeleteIcon'; import MenuIcon from './icons/MenuIcon'; -export default function Menu() { +interface MenuProps { + /** If there is no title we display an icon. */ + title?: string; +} + +export default function Menu({ title }: Readonly) { return ( - - - - {false ? ( + {title ? ( - Song + {title} + + ) : ( + +
+ +
- ) : null} + )} diff --git a/next/src/components/ui/icons/AddIcon.tsx b/next/src/components/ui/icons/AddIcon.tsx new file mode 100644 index 000000000..30ae61fa0 --- /dev/null +++ b/next/src/components/ui/icons/AddIcon.tsx @@ -0,0 +1,18 @@ +/** Plus icon which should be used when one wants to add something. */ +export default function AddIcon({ + height = '24px', + width = '24px', + ...props +}: Readonly>) { + return ( + + + + ); +} diff --git a/next/src/components/ui/icons/ArrowUpIcon.tsx b/next/src/components/ui/icons/ArrowUpIcon.tsx new file mode 100644 index 000000000..8a82e5abe --- /dev/null +++ b/next/src/components/ui/icons/ArrowUpIcon.tsx @@ -0,0 +1,17 @@ +export default function ArrowUpIcon({ + height = '24px', + width = '24px', + ...props +}: Readonly>) { + return ( + + + + ); +} diff --git a/next/src/i18n/locales/en.json b/next/src/i18n/locales/en.json index 4ea7de21e..51abc36c9 100644 --- a/next/src/i18n/locales/en.json +++ b/next/src/i18n/locales/en.json @@ -15,6 +15,7 @@ "complete": "Complete", "validate": "Validate", "add": "Add", + "create": "Create", "delete": "Delete", "help": "Help", "backToHome": "Back to home", @@ -58,15 +59,38 @@ "metadata": "Metadata" } }, - "codesLists": { - "title": "Code Lists", - "create": "Create a code list", - "code": "Code", - "lastUpdate": "Last modified", - "usedIn": "Used in", - "addList": "Add a list", - "value": "Value", - "label": "Label" + "codesList": { + "common": { + "code": "Code", + "code_other": "Codes", + "label": "Label", + "value": "Value" + }, + "overview": { + "create": "Create a code list", + "deleteDialogTitle": "Delete the codes list: {{label}}", + "deleteDialogConfirm": "Are you sure you want to delete this codes list?", + "deleteSuccess": "Codes list {{label}} deleted", + "title": "Codes Lists" + }, + "create": { + "importCSV": "Import a codes list from a CSV file", + "success": "The code list has been created", + "title": "New codes list" + }, + "edit": { + "success": "The codes list {{label}} has been edited", + "title": "Edit the codes list: {{label}}" + }, + "form": { + "addCode": "+ Add a code", + "addSubCode": "Add a subcode", + "mustProvideLabel": "Your code must have a label", + "mustProvideValue": "Your code must have a value", + "mustProvideCodes": "You must provide at least one code", + "moveUp": "Move up", + "moveDown": "Move down" + } }, "createQuestionnaire": { "mode": "Collection mode", @@ -79,16 +103,5 @@ "mustProvideTitle": "You must provide a title", "mustProvideTarget": "You must select at least one target mode", "loading": "Loading..." - }, - "createCodeList": { - "title": "New code list", - "label": "Label", - "value": "Value", - "add": "+ Add a code", - "importCsv": "Import a code list from a CSV file", - "created": "The code list has been created", - "mustProvideLabel": "Your code must have a label", - "mustProvideValue": "Your code must have a value", - "mustProvideCodes": "You must provide at least one code" } } diff --git a/next/src/i18n/locales/fr.json b/next/src/i18n/locales/fr.json index e6fe867a3..ad3348790 100644 --- a/next/src/i18n/locales/fr.json +++ b/next/src/i18n/locales/fr.json @@ -12,6 +12,7 @@ "next": "Suivant", "close": "Fermer", "cancel": "Annuler", + "create": "Créer", "complete": "Completer", "validate": "Valider", "add": "Ajouter", @@ -58,15 +59,38 @@ "metadata": "Métadonnées" } }, - "codesLists": { - "title": "Listes de codes", - "create": "Créer une liste de codes", - "code": "Code", - "lastUpdate": "Dernière modification", - "usedIn": "Utilisée dans", - "addList": "Ajouter une liste", - "value": "Valeur", - "label": "Label" + "codesList": { + "common": { + "code": "Code", + "code_other": "Codes", + "label": "Label", + "value": "Valeur" + }, + "overview": { + "create": "Créer une liste de codes", + "deleteDialogTitle": "Supprimer la liste de codes : {{label}}", + "deleteDialogConfirm": "Êtes-vous sûr de vouloir supprimer cette liste de codes ?", + "deleteSuccess": "Liste de codes {{label}} supprimée", + "title": "Listes de codes" + }, + "create": { + "importCSV": "Importer une liste de codes depuis un fichier CSV", + "success": "La liste de codes a été créée", + "title": "Nouvelle liste de codes" + }, + "edit": { + "success": "La liste de codes {{label}} a été modifiée", + "title": "Editer la liste de codes : {{label}}" + }, + "form": { + "addCode": "+ Ajouter un code", + "addSubCode": "Ajouter un sous-code", + "mustProvideLabel": "Votre code doit avoir un label", + "mustProvideValue": "Votre code doit avoir une valeur", + "mustProvideCodes": "Vous devez renseigner au moins un code", + "moveUp": "Monter d'un niveau", + "moveDown": "Descendre d'un niveau" + } }, "createQuestionnaire": { "mode": "Mode de collecte", @@ -79,16 +103,5 @@ "mustProvideTitle": "Vous devez renseigner un titre", "mustProvideTarget": "Vous devez sélectionner au moins un mode de collecte", "loading": "Chargement..." - }, - "createCodeList": { - "title": "Nouvelle liste de codes", - "label": "Label", - "value": "Valeur", - "add": "+ Ajouter un code", - "importCsv": "Importer une liste de codes depuis un fichier CSV", - "created": "La liste de codes a été créée", - "mustProvideLabel": "Votre code doit avoir un label", - "mustProvideValue": "Votre code doit avoir une valeur", - "mustProvideCodes": "Vous devez renseigner au moins un code" } } diff --git a/next/src/routeTree.gen.ts b/next/src/routeTree.gen.ts index cbef37428..c32387964 100644 --- a/next/src/routeTree.gen.ts +++ b/next/src/routeTree.gen.ts @@ -8,6 +8,7 @@ import { Route as rootRoute } from './routes/__root'; import { Route as LayoutImport } from './routes/_layout'; import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q'; +import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQCodesListCodesListIdImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-list/$codesListId'; import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsIndexImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/index'; import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsNewImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/new'; import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsRouteImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/route'; @@ -116,6 +117,13 @@ const LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsNewRoute = LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsRouteRoute, } as any); +const LayoutQuestionnaireQuestionnaireIdLayoutQCodesListCodesListIdRoute = + LayoutQuestionnaireQuestionnaireIdLayoutQCodesListCodesListIdImport.update({ + id: '/codes-list/$codesListId', + path: '/codes-list/$codesListId', + getParentRoute: () => LayoutQuestionnaireQuestionnaireIdLayoutQRoute, + } as any); + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -204,6 +212,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQIndexImport; parentRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQImport; }; + '/_layout/questionnaire/$questionnaireId/_layout-q/codes-list/$codesListId': { + id: '/_layout/questionnaire/$questionnaireId/_layout-q/codes-list/$codesListId'; + path: '/codes-list/$codesListId'; + fullPath: '/questionnaire/$questionnaireId/codes-list/$codesListId'; + preLoaderRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListCodesListIdImport; + parentRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQImport; + }; '/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists/new': { id: '/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists/new'; path: '/new'; @@ -263,6 +278,7 @@ interface LayoutQuestionnaireQuestionnaireIdLayoutQRouteChildren { LayoutQuestionnaireQuestionnaireIdLayoutQMergeRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQMergeRoute; LayoutQuestionnaireQuestionnaireIdLayoutQTcmCompositionRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQTcmCompositionRoute; LayoutQuestionnaireQuestionnaireIdLayoutQIndexRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQIndexRoute; + LayoutQuestionnaireQuestionnaireIdLayoutQCodesListCodesListIdRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListCodesListIdRoute; } const LayoutQuestionnaireQuestionnaireIdLayoutQRouteChildren: LayoutQuestionnaireQuestionnaireIdLayoutQRouteChildren = @@ -277,6 +293,8 @@ const LayoutQuestionnaireQuestionnaireIdLayoutQRouteChildren: LayoutQuestionnair LayoutQuestionnaireQuestionnaireIdLayoutQTcmCompositionRoute, LayoutQuestionnaireQuestionnaireIdLayoutQIndexRoute: LayoutQuestionnaireQuestionnaireIdLayoutQIndexRoute, + LayoutQuestionnaireQuestionnaireIdLayoutQCodesListCodesListIdRoute: + LayoutQuestionnaireQuestionnaireIdLayoutQCodesListCodesListIdRoute, }; const LayoutQuestionnaireQuestionnaireIdLayoutQRouteWithChildren = @@ -325,6 +343,7 @@ export interface FileRoutesByFullPath { '/questionnaire/$questionnaireId/merge': typeof LayoutQuestionnaireQuestionnaireIdLayoutQMergeRoute; '/questionnaire/$questionnaireId/tcm-composition': typeof LayoutQuestionnaireQuestionnaireIdLayoutQTcmCompositionRoute; '/questionnaire/$questionnaireId/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQIndexRoute; + '/questionnaire/$questionnaireId/codes-list/$codesListId': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListCodesListIdRoute; '/questionnaire/$questionnaireId/codes-lists/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsNewRoute; '/questionnaire/$questionnaireId/codes-lists/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsIndexRoute; } @@ -338,6 +357,7 @@ export interface FileRoutesByTo { '/questionnaire/$questionnaireId/composition': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCompositionRoute; '/questionnaire/$questionnaireId/merge': typeof LayoutQuestionnaireQuestionnaireIdLayoutQMergeRoute; '/questionnaire/$questionnaireId/tcm-composition': typeof LayoutQuestionnaireQuestionnaireIdLayoutQTcmCompositionRoute; + '/questionnaire/$questionnaireId/codes-list/$codesListId': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListCodesListIdRoute; '/questionnaire/$questionnaireId/codes-lists/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsNewRoute; '/questionnaire/$questionnaireId/codes-lists': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsIndexRoute; } @@ -356,6 +376,7 @@ export interface FileRoutesById { '/_layout/questionnaire/$questionnaireId/_layout-q/merge': typeof LayoutQuestionnaireQuestionnaireIdLayoutQMergeRoute; '/_layout/questionnaire/$questionnaireId/_layout-q/tcm-composition': typeof LayoutQuestionnaireQuestionnaireIdLayoutQTcmCompositionRoute; '/_layout/questionnaire/$questionnaireId/_layout-q/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQIndexRoute; + '/_layout/questionnaire/$questionnaireId/_layout-q/codes-list/$codesListId': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListCodesListIdRoute; '/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsNewRoute; '/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsIndexRoute; } @@ -374,6 +395,7 @@ export interface FileRouteTypes { | '/questionnaire/$questionnaireId/merge' | '/questionnaire/$questionnaireId/tcm-composition' | '/questionnaire/$questionnaireId/' + | '/questionnaire/$questionnaireId/codes-list/$codesListId' | '/questionnaire/$questionnaireId/codes-lists/new' | '/questionnaire/$questionnaireId/codes-lists/'; fileRoutesByTo: FileRoutesByTo; @@ -386,6 +408,7 @@ export interface FileRouteTypes { | '/questionnaire/$questionnaireId/composition' | '/questionnaire/$questionnaireId/merge' | '/questionnaire/$questionnaireId/tcm-composition' + | '/questionnaire/$questionnaireId/codes-list/$codesListId' | '/questionnaire/$questionnaireId/codes-lists/new' | '/questionnaire/$questionnaireId/codes-lists'; id: @@ -402,6 +425,7 @@ export interface FileRouteTypes { | '/_layout/questionnaire/$questionnaireId/_layout-q/merge' | '/_layout/questionnaire/$questionnaireId/_layout-q/tcm-composition' | '/_layout/questionnaire/$questionnaireId/_layout-q/' + | '/_layout/questionnaire/$questionnaireId/_layout-q/codes-list/$codesListId' | '/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists/new' | '/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists/'; fileRoutesById: FileRoutesById; @@ -472,7 +496,8 @@ export const routeTree = rootRoute "/_layout/questionnaire/$questionnaireId/_layout-q/composition", "/_layout/questionnaire/$questionnaireId/_layout-q/merge", "/_layout/questionnaire/$questionnaireId/_layout-q/tcm-composition", - "/_layout/questionnaire/$questionnaireId/_layout-q/" + "/_layout/questionnaire/$questionnaireId/_layout-q/", + "/_layout/questionnaire/$questionnaireId/_layout-q/codes-list/$codesListId" ] }, "/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists": { @@ -499,6 +524,10 @@ export const routeTree = rootRoute "filePath": "_layout/questionnaire.$questionnaireId/_layout-q/index.tsx", "parent": "/_layout/questionnaire/$questionnaireId/_layout-q" }, + "/_layout/questionnaire/$questionnaireId/_layout-q/codes-list/$codesListId": { + "filePath": "_layout/questionnaire.$questionnaireId/_layout-q/codes-list/$codesListId.tsx", + "parent": "/_layout/questionnaire/$questionnaireId/_layout-q" + }, "/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists/new": { "filePath": "_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/new.tsx", "parent": "/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists" diff --git a/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-list/$codesListId.tsx b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-list/$codesListId.tsx new file mode 100644 index 000000000..a991a27e2 --- /dev/null +++ b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-list/$codesListId.tsx @@ -0,0 +1,35 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; +import { createFileRoute } from '@tanstack/react-router'; + +import { questionnaireQueryOptions } from '@/api/questionnaires'; +import EditCodesList from '@/components/codesLists/edit/EditCodesList'; + +export const Route = createFileRoute( + '/_layout/questionnaire/$questionnaireId/_layout-q/codes-list/$codesListId', +)({ + component: RouteComponent, + loader: async ({ + context: { queryClient }, + params: { codesListId, questionnaireId }, + }) => { + queryClient.ensureQueryData(questionnaireQueryOptions(questionnaireId)); + return { crumb: `Liste de codes ${codesListId}` }; + }, +}); + +function RouteComponent() { + const { questionnaireId, codesListId } = Route.useParams(); + const { + data: { codesLists }, + } = useSuspenseQuery(questionnaireQueryOptions(questionnaireId)); + let codesList; + if (codesLists) { + for (const element of codesLists) { + if (element.id === codesListId) codesList = element; + } + } + + return ( + + ); +} diff --git a/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/index.tsx b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/index.tsx index 0b8eac757..f863a017b 100644 --- a/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/index.tsx +++ b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/index.tsx @@ -2,7 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; import { questionnaireQueryOptions } from '@/api/questionnaires'; -import CodesLists from '@/components/codesLists/CodesLists'; +import CodesListsOverview from '@/components/codesLists/overview/CodesListsOverview'; export const Route = createFileRoute( '/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists/', @@ -19,6 +19,9 @@ function RouteComponent() { } = useSuspenseQuery(questionnaireQueryOptions(questionnaireId)); return ( - + ); } diff --git a/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/new.tsx b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/new.tsx index 511c2bded..d56ab73a4 100644 --- a/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/new.tsx +++ b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/codes-lists/new.tsx @@ -1,10 +1,16 @@ import { createFileRoute } from '@tanstack/react-router'; -import CreateCodesList from '@/components/createCodesList/CreateCodesList'; +import CreateCodesList from '@/components/codesLists/create/CreateCodesList'; export const Route = createFileRoute( '/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists/new', )({ - component: CreateCodesList, + component: RouteComponent, loader: () => ({ crumb: 'Nouveau' }), }); + +function RouteComponent() { + const questionnaireId = Route.useParams().questionnaireId; + + return ; +} diff --git a/next/src/routes/index.tsx b/next/src/routes/index.tsx index 60f3b6886..ac2e6ee3d 100644 --- a/next/src/routes/index.tsx +++ b/next/src/routes/index.tsx @@ -2,7 +2,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; import { Trans, useTranslation } from 'react-i18next'; import Button from '@/components/ui/Button'; -import ButtonLink, { ButtonType } from '@/components/ui/ButtonLink'; +import ButtonLink, { ButtonStyle } from '@/components/ui/ButtonLink'; import poguesLogo from '/pogues-logo.png'; @@ -26,7 +26,7 @@ function App() {
{t('home.label')}
- + {t('common.start')} diff --git a/next/src/vite-env.d.ts b/next/src/vite-env.d.ts index 80434533a..7acf0b576 100644 --- a/next/src/vite-env.d.ts +++ b/next/src/vite-env.d.ts @@ -13,7 +13,7 @@ export type ImportMetaEnv = { MODE: string; DEV: boolean; PROD: boolean; - APP_VERSION: any; + APP_VERSION: string; // @user-defined-start /* * You can use this section to explicitly extend the type definition of `import.meta.env` diff --git a/next/yarn.lock b/next/yarn.lock index 398fdaec1..5a3e1b23c 100644 --- a/next/yarn.lock +++ b/next/yarn.lock @@ -181,7 +181,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.9" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.26.0": +"@babel/runtime@^7.12.5": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== @@ -195,6 +195,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.26.7": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.9.tgz#aa4c6facc65b9cb3f87d75125ffd47781b475433" + integrity sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" @@ -246,15 +253,15 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@base-ui-components/react@^1.0.0-alpha.5": - version "1.0.0-alpha.5" - resolved "https://registry.yarnpkg.com/@base-ui-components/react/-/react-1.0.0-alpha.5.tgz#dd69ec6e6acfd64cac0e016ae5370a7b0668354c" - integrity sha512-/hkuXkfiuMZpX1rp1alx2QAah7dUnojgUxtmbEjxYaeS7xiifw9N645ek8e3Dd3QPhVYmDZYfqj/aYUS0IwIqA== +"@base-ui-components/react@^1.0.0-alpha.6": + version "1.0.0-alpha.6" + resolved "https://registry.yarnpkg.com/@base-ui-components/react/-/react-1.0.0-alpha.6.tgz#d577bd77f963b00517349a17fb63a6162d438a3f" + integrity sha512-0Jkp8twk3z3TJNOTUyzrK1dPOF7w1/LH+TYksuZc8TyJzuJx8V2O15BgpMJczvFdSR2ncdN1WQxsTe2ayYJ6cw== dependencies: - "@babel/runtime" "^7.26.0" + "@babel/runtime" "^7.26.7" "@floating-ui/react" "^0.27.3" "@floating-ui/utils" "^0.2.9" - "@react-aria/overlays" "^3.24.0" + "@react-aria/overlays" "^3.25.0" prop-types "^15.8.1" use-sync-external-store "^1.4.0" @@ -678,6 +685,13 @@ dependencies: tslib "2" +"@hookform/resolvers@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-4.1.0.tgz#17ba2b68d2883be6a06af1c7507be8e7c2f684b9" + integrity sha512-fX/uHKb+OOCpACLc6enuTQsf0ZpRrKbeBBPETg5PCPLCIYV6osP2Bw6ezuclM61lH+wBF9eXcuC0+BFh9XOEnQ== + dependencies: + caniuse-lite "^1.0.30001698" + "@humanfs/core@^0.19.1": version "0.19.1" resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" @@ -1037,7 +1051,7 @@ "@react-types/shared" "^3.27.0" "@swc/helpers" "^0.5.0" -"@react-aria/overlays@^3.24.0": +"@react-aria/overlays@^3.25.0": version "3.25.0" resolved "https://registry.yarnpkg.com/@react-aria/overlays/-/overlays-3.25.0.tgz#794c4f2f08ea6ccdd18b3030776b3929a4950cdb" integrity sha512-UEqJJ4duowrD1JvwXpPZreBuK79pbyNjNxFUVpFSskpGEJe3oCWwsSDKz7P1O7xbx5OYp+rDiY8fk/sE5rkaKw== @@ -2021,6 +2035,11 @@ caniuse-lite@^1.0.30001688: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8" integrity sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w== +caniuse-lite@^1.0.30001698: + version "1.0.30001700" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz#26cd429cf09b4fd4e745daf4916039c794d720f6" + integrity sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ== + chai@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.2.tgz#3afbc340b994ae3610ca519a6c70ace77ad4378d" @@ -3783,6 +3802,11 @@ react-error-boundary@^5.0.0: dependencies: "@babel/runtime" "^7.12.5" +react-hook-form@^7.54.2: + version "7.54.2" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.54.2.tgz#8c26ed54c71628dff57ccd3c074b1dd377cfb211" + integrity sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg== + react-hot-toast@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.5.1.tgz#fcb182d96353c803ee5af82e96c806d5eaa4dcfa"