diff --git a/targets/frontend/src/components/contributions/answers/Answer.tsx b/targets/frontend/src/components/contributions/answers/Answer.tsx index ba14cab34..aaf1c478a 100644 --- a/targets/frontend/src/components/contributions/answers/Answer.tsx +++ b/targets/frontend/src/components/contributions/answers/Answer.tsx @@ -1,36 +1,71 @@ import { AlertColor, Box, + Button, + FormControl, Stack, - Tooltip, - TooltipProps, Typography, - styled, - tooltipClasses, } from "@mui/material"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; import { useUser } from "src/hooks/useUser"; +import { FormEditionField, FormRadioGroup, FormTextField } from "../../forms"; import { StatusContainer } from "../status"; import { Answer, Status } from "../type"; import { useContributionAnswerUpdateMutation } from "./answer.mutation"; import { useContributionAnswerQuery } from "./answer.query"; import { Comments } from "./Comments"; +import { + CdtnReferenceInput, + KaliReferenceInput, + LegiReferenceInput, + OtherReferenceInput, +} from "./references"; import { statusesMapping } from "../status/data"; +import { getNextStatus, getPrimaryButtonLabel } from "../status/utils"; import { SnackBar } from "../../utils/SnackBar"; import { Breadcrumb, BreadcrumbLink } from "src/components/utils"; -import { AnswerForm } from "./AnswerForm"; -import { fr } from "@codegouvfr/react-dsfr"; +import { FicheSpDocumentInput } from "./references/FicheSpDocumentInput"; export type ContributionsAnswerProps = { id: string; }; +const isNotEditable = (answer: Answer | undefined) => + answer?.status.status !== "REDACTING" && + answer?.status.status !== "TODO" && + answer?.status.status !== "VALIDATING"; + +const isCodeDuTravail = (answer: Answer): boolean => + answer.agreement.id === "0000"; + export const ContributionsAnswer = ({ id, }: ContributionsAnswerProps): JSX.Element => { const answer = useContributionAnswerQuery({ id }); const { user } = useUser() as any; + const [status, setStatus] = useState("TODO"); + useEffect(() => { + if (answer?.status) { + setStatus(answer.status.status); + } + }, [answer]); + const { control, getValues, trigger } = useForm({ + values: answer, + defaultValues: { + content: "", + contentType: "ANSWER", + status: { + status: "TODO", + }, + legiReferences: [], + kaliReferences: [], + otherReferences: [], + cdtnReferences: [], + contentFichesSpDocument: answer?.contentFichesSpDocument ? {} : undefined, + }, + }); const updateAnswer = useContributionAnswerUpdateMutation(); const [snack, setSnack] = useState<{ open: boolean; @@ -40,7 +75,19 @@ export const ContributionsAnswer = ({ open: false, }); - const onSubmit = async (newStatus: Status, data: Answer) => { + const onSubmit = async (newStatus: Status) => { + const isValid = await trigger(); + if (!isValid) { + return setSnack({ + open: true, + severity: "error", + message: "Formulaire invalide", + }); + } + + setStatus(newStatus); + const data = getValues(); + try { if (!answer || !answer.id) { throw new Error("Id non définit"); @@ -67,12 +114,38 @@ export const ContributionsAnswer = ({ setSnack({ open: true, severity: "error", message: e.message }); } }; + + const agreementResponseOptions = [ + { + label: "La convention collective ne prévoit rien", + value: "NOTHING", + }, + { + label: "La convention collective renvoie au Code du Travail", + value: "CDT", + }, + { + label: + "La convention collective intégralement moins favorable que le CDT", + value: "UNFAVOURABLE", + }, + { + label: "Nous n'avons pas la réponse", + value: "UNKNOWN", + }, + ]; + const genericResponseOptions = [ + { + label: "Utiliser la fiche service public", + value: "SP", + }, + ]; return ( <> <> - - - - {answer?.agreement?.name} - - - } - > - {answer?.agreement?.id} - - + {answer?.agreement?.id} {answer?.status && ( -
+
)} - + - {answer && ( - - )} +
{ + // This is a hack to prevent the form from being submitted by the tiptap editor. + // The details extension is not working properly and submit the form when click on the arrow. + // See https://github.com/ueberdosis/tiptap/issues/4384 + e.preventDefault(); + }} + > + + + + + + + + {answer && ( + + )} + {answer && isCodeDuTravail(answer) && ( + + )} + {answer && !isCodeDuTravail(answer) && ( + + )} + + + + + + + + + + + +
- {answer && answer.statuses && ( + {answer && ( )} -
- - + ); }; - -const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => ( - -))(({ theme }) => ({ - [`& .${tooltipClasses.tooltip}`]: { - backgroundColor: fr.colors.decisions.background.default.grey.hover, - color: fr.colors.decisions.text.default.grey.default, - maxWidth: 220, - fontSize: theme.typography.pxToRem(12), - border: `1px solid ${fr.colors.decisions.border.default.grey.default}`, - }, -})); diff --git a/targets/frontend/src/components/contributions/answers/AnswerForm.tsx b/targets/frontend/src/components/contributions/answers/AnswerForm.tsx deleted file mode 100644 index 08d078216..000000000 --- a/targets/frontend/src/components/contributions/answers/AnswerForm.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { Button, FormControl, Stack } from "@mui/material"; -import React, { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; - -import { FormEditionField, FormRadioGroup, FormTextField } from "../../forms"; -import { Answer, Status, answerFormSchema } from "../type"; -import { AnswerWithStatus } from "./answer.query"; -import { - CdtnReferenceInput, - KaliReferenceInput, - LegiReferenceInput, - OtherReferenceInput, -} from "./references"; -import { getNextStatus, getPrimaryButtonLabel } from "../status/utils"; -import { FicheSpDocumentInput } from "./references/FicheSpDocumentInput"; - -export type ContributionsAnswerProps = { - answer: AnswerWithStatus; - onSubmit: (status: Status, data: Answer) => void; -}; - -const isNotEditable = (answer: Answer | undefined) => - answer?.status?.status !== "REDACTING" && - answer?.status?.status !== "TODO" && - answer?.status?.status !== "VALIDATING"; - -const isCodeDuTravail = (answer: Answer): boolean => - answer?.agreement?.id === "0000"; - -export const AnswerForm = ({ - answer, - onSubmit, -}: ContributionsAnswerProps): JSX.Element => { - const [status, setStatus] = useState("TODO"); - useEffect(() => { - if (answer?.status) { - setStatus(answer.status.status); - } - }, [answer]); - const { control, getValues, trigger } = useForm({ - resolver: zodResolver(answerFormSchema), - shouldFocusError: true, - defaultValues: { - content: "", - contentType: "ANSWER", - status: { - status: "TODO", - }, - legiReferences: [], - kaliReferences: [], - otherReferences: [], - cdtnReferences: [], - contentFichesSpDocument: answer?.contentFichesSpDocument ? {} : undefined, - }, - }); - - const submit = async (newStatus: Status) => { - const isValid = await trigger(); - - if (isValid) { - setStatus(newStatus); - const data = getValues(); - onSubmit(newStatus, data); - } - }; - - const agreementResponseOptions = [ - { - label: "La convention collective ne prévoit rien", - value: "NOTHING", - }, - { - label: "La convention collective renvoie au Code du Travail", - value: "CDT", - }, - { - label: - "La convention collective intégralement moins favorable que le CDT", - value: "UNFAVOURABLE", - }, - { - label: "Nous n'avons pas la réponse", - value: "UNKNOWN", - }, - ]; - const genericResponseOptions = [ - { - label: "Utiliser la fiche service public", - value: "SP", - }, - ]; - return ( - <> -
{ - // This is a hack to prevent the form from being submitted by the tiptap editor. - // The details extension is not working properly and submit the form when click on the arrow. - // See https://github.com/ueberdosis/tiptap/issues/4384 - e.preventDefault(); - }} - > - - - - - - - - {answer && ( - - )} - {answer && isCodeDuTravail(answer) && ( - - )} - {answer?.agreement && !isCodeDuTravail(answer) && ( - - )} - - - - - - - - - -
- - ); -}; diff --git a/targets/frontend/src/components/contributions/answers/Comment.tsx b/targets/frontend/src/components/contributions/answers/Comment.tsx index f440c2c1a..609179c3a 100644 --- a/targets/frontend/src/components/contributions/answers/Comment.tsx +++ b/targets/frontend/src/components/contributions/answers/Comment.tsx @@ -39,7 +39,7 @@ export const Comment = ({ comment }: Props) => { }} > - {comment?.user?.name} + {comment.user.name} { overflow: "auto", }} > - {notifications.map((comment, index) => ( - + {notifications.map((comment) => ( + ))} diff --git a/targets/frontend/src/components/contributions/answers/__tests__/AnswerForm.test.tsx b/targets/frontend/src/components/contributions/answers/__tests__/AnswerForm.test.tsx deleted file mode 100644 index 05333003e..000000000 --- a/targets/frontend/src/components/contributions/answers/__tests__/AnswerForm.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import React from "react"; - -import { AnswerForm } from "../AnswerForm"; -import { AnswerWithStatus } from "../answer.query"; - -const answerBase: AnswerWithStatus = { - id: "369336d2-994f-48b1-b6ac-fec78cff240e", - questionId: "2c820037-62bd-4c0e-a1a8-ca80b97b5958", - agreementId: "0000", - content: "", - contentType: "ANSWER", - updatedAt: "2023-09-29T14:09:52.01401+00:00", - contentServicePublicCdtnId: null, - question: { - id: "2c820037-62bd-4c0e-a1a8-ca80b97b5958", - content: "Quelle est la durée maximale de la période d’essai ?", - order: 5, - }, - agreement: { - id: "0000", - name: "Convention collective nationale des transports routiers et activités auxiliaires du transport", - kaliId: "KALICONT000005635624", - }, - answerComments: [], - statuses: [], - kaliReferences: [], - legiReferences: [], - otherReferences: [], - cdtnReferences: [], - contentFichesSpDocument: null, - status: { - status: "TODO", - createdAt: new Date().toISOString(), - }, - updateDate: "29/09/2023", -}; - -describe("Given a component AnswerForm and a basic default generic answer", () => { - beforeEach(() => { - render( {}} />); - }); - test("Check options are displayed", () => { - expect(screen.queryByText("Afficher la réponse")).toBeInTheDocument(); - expect( - screen.queryByText("Utiliser la fiche service public") - ).toBeInTheDocument(); - expect( - screen.queryByText("La convention collective ne prévoit rien") - ).not.toBeInTheDocument(); - expect( - screen.queryByText("Nous n'avons pas la réponse") - ).not.toBeInTheDocument(); - }); -}); - -describe("Given a component AnswerForm and a basic default CC answer", () => { - beforeEach(() => { - const answer = { - ...answerBase, - agreementId: "0016", - agreement: { - ...answerBase.agreement, - id: "0016", - name: "0016", - kaliId: "0016", - }, - }; - render( {}} />); - }); - test("Check options are displayed", () => { - expect(screen.queryByText("Afficher la réponse")).toBeInTheDocument(); - expect( - screen.queryByText("Utiliser la fiche service public") - ).not.toBeInTheDocument(); - expect( - screen.queryByText("La convention collective ne prévoit rien") - ).toBeInTheDocument(); - expect( - screen.queryByText("Nous n'avons pas la réponse") - ).toBeInTheDocument(); - }); -}); diff --git a/targets/frontend/src/components/contributions/answers/answer.query.ts b/targets/frontend/src/components/contributions/answers/answer.query.ts index 9abaa4e6f..5d61f7236 100644 --- a/targets/frontend/src/components/contributions/answers/answer.query.ts +++ b/targets/frontend/src/components/contributions/answers/answer.query.ts @@ -83,10 +83,7 @@ type QueryProps = { id: string; }; -export type AnswerWithStatus = Answer & { - status: AnswerStatus; - updateDate?: string; -}; +type AnswerWithStatus = Answer & { status: AnswerStatus; updateDate: string }; type QueryResult = { contribution_answers: AnswerWithStatus[]; @@ -119,8 +116,6 @@ export const useContributionAnswerQuery = ({ return { ...answer, status: initStatus(answer), - updateDate: answer?.updatedAt - ? format(parseISO(answer?.updatedAt), "dd/MM/yyyy") - : undefined, + updateDate: format(parseISO(answer.updatedAt), "dd/MM/yyyy"), }; }; diff --git a/targets/frontend/src/components/contributions/answers/comments.mutation.ts b/targets/frontend/src/components/contributions/answers/comments.mutation.ts index 5121bd575..04d6bcd8a 100644 --- a/targets/frontend/src/components/contributions/answers/comments.mutation.ts +++ b/targets/frontend/src/components/contributions/answers/comments.mutation.ts @@ -1,4 +1,4 @@ -import { useMutation } from "urql"; +import { OperationResult, useMutation } from "urql"; import { Comments } from "../type"; diff --git a/targets/frontend/src/components/contributions/answers/references/CdtnReferenceInput.tsx b/targets/frontend/src/components/contributions/answers/references/CdtnReferenceInput.tsx index 437e1a650..a9149cbdb 100644 --- a/targets/frontend/src/components/contributions/answers/references/CdtnReferenceInput.tsx +++ b/targets/frontend/src/components/contributions/answers/references/CdtnReferenceInput.tsx @@ -1,4 +1,4 @@ -import { SourceRoute, getRouteBySource } from "@socialgouv/cdtn-sources"; +import { getRouteBySource } from "@socialgouv/cdtn-sources"; import { Control } from "react-hook-form"; import { CdtnReference } from "../../type"; import { useContributionSearchCdtnReferencesQuery } from "./cdtnReferencesSearch.query"; @@ -8,14 +8,12 @@ type Props = { name: string; control: Control; disabled?: boolean; - idcc?: string; }; export const CdtnReferenceInput = ({ name, control, disabled = false, - idcc, }: Props): React.ReactElement => ( isMultiple={true} @@ -25,19 +23,18 @@ export const CdtnReferenceInput = ({ disabled={disabled} control={control} fetcher={useContributionSearchCdtnReferencesQuery} - idcc={idcc} isEqual={(option, value) => value.document.cdtnId === option.document.cdtnId } getLabel={(item) => - `${getRouteBySource(item.document.source as SourceRoute)} > ${ - item.document.title - } (${item.document.slug})` + `${getRouteBySource(item.document.source)} > ${item.document.title} (${ + item.document.slug + })` } onClick={(item) => { const newWindow = window.open( `https://code.travail.gouv.fr/${getRouteBySource( - item.document.source as SourceRoute + item.document.source )}/${item.document.slug}`, "_blank", "noopener,noreferrer" diff --git a/targets/frontend/src/components/contributions/answers/references/FicheSpDocumentInput.tsx b/targets/frontend/src/components/contributions/answers/references/FicheSpDocumentInput.tsx index 1591c2403..d081e3693 100644 --- a/targets/frontend/src/components/contributions/answers/references/FicheSpDocumentInput.tsx +++ b/targets/frontend/src/components/contributions/answers/references/FicheSpDocumentInput.tsx @@ -1,4 +1,4 @@ -import { SourceRoute, getRouteBySource } from "@socialgouv/cdtn-sources"; +import { getRouteBySource } from "@socialgouv/cdtn-sources"; import { Control } from "react-hook-form"; import { Document } from "../../type"; import { ReferenceInput } from "./ReferenceInput"; @@ -26,9 +26,9 @@ export const FicheSpDocumentInput = ({ getLabel={(item) => `${item.title} (${item.slug})`} onClick={(item) => { const newWindow = window.open( - `https://code.travail.gouv.fr/${getRouteBySource( - item.source as SourceRoute - )}/${item.slug}`, + `https://code.travail.gouv.fr/${getRouteBySource(item.source)}/${ + item.slug + }`, "_blank", "noopener,noreferrer" ); diff --git a/targets/frontend/src/components/contributions/answers/references/KaliReferenceInput.tsx b/targets/frontend/src/components/contributions/answers/references/KaliReferenceInput.tsx index 194b8e2f5..e46aaacac 100644 --- a/targets/frontend/src/components/contributions/answers/references/KaliReferenceInput.tsx +++ b/targets/frontend/src/components/contributions/answers/references/KaliReferenceInput.tsx @@ -136,13 +136,13 @@ export const KaliReferenceInput = ({ ); })} - {!disabled && agreement.id && ( + {!disabled && ( { if (value) { - append({ kaliArticle: value, label: "" }); + append({ kaliArticle: value }); } }} /> diff --git a/targets/frontend/src/components/contributions/answers/references/ReferenceInput.tsx b/targets/frontend/src/components/contributions/answers/references/ReferenceInput.tsx index a99da315e..28b92c258 100644 --- a/targets/frontend/src/components/contributions/answers/references/ReferenceInput.tsx +++ b/targets/frontend/src/components/contributions/answers/references/ReferenceInput.tsx @@ -15,7 +15,7 @@ type Props = { label: string; name: string; control: Control; - fetcher: (query: string | undefined, idcc?: string) => Result; + fetcher: (query: string | undefined) => Result; isEqual: (value: Type, option: Type) => boolean; getLabel: (option: Type) => string; onClick: (value: Type) => void; @@ -29,7 +29,6 @@ type Props = { | "warning"; disabled: boolean; isMultiple?: true; - idcc?: string; }; export const ReferenceInput = ({ @@ -43,10 +42,9 @@ export const ReferenceInput = ({ color, disabled, isMultiple, - idcc, }: Props): ReactElement | null => { const [query, setQuery] = useState(); - const { data, fetching, error } = fetcher(query, idcc); + const { data, fetching, error } = fetcher(query); const [open, setOpen] = useState(false); const [options, setOptions] = useState([]); diff --git a/targets/frontend/src/components/contributions/answers/references/cdtnReferencesSearch.query.ts b/targets/frontend/src/components/contributions/answers/references/cdtnReferencesSearch.query.ts index e9a48e11a..14722b12d 100644 --- a/targets/frontend/src/components/contributions/answers/references/cdtnReferencesSearch.query.ts +++ b/targets/frontend/src/components/contributions/answers/references/cdtnReferencesSearch.query.ts @@ -21,14 +21,13 @@ export const getSlugFromUrl = (query: string | undefined): string => { }; export const SearchCdtnReferencesQuery = ` -query SearchCdtnReferences($sources: [String!], $slug: String, $title: String, $slugRegex: String) { +query SearchCdtnReferences($sources: [String!], $slug: String, $title: String) { documents(where: { _or: [{ title: {_ilike: $title} }, { slug: {_eq: $slug} }], - slug: { _regex: $slugRegex }, is_available: {_eq: true}, is_published: {_eq: true}, source: {_in: $sources} @@ -47,14 +46,10 @@ query SearchCdtnReferences($sources: [String!], $slug: String, $title: String, $ `; export const useContributionSearchCdtnReferencesQuery = ( - query: string | undefined, - idcc?: string + query: string | undefined ): Result> => { const slug = getSlugFromUrl(query); const title = getNormalizedTitle(slug); - const slugRegex = `^(${idcc ? `${idcc}|` : ""}[^0-9])${ - idcc ? `{${idcc.length}}` : "" - }[\-a-zA-Z0-9_]+$`; const [{ data, fetching, error }] = useQuery( { query: SearchCdtnReferencesQuery, @@ -71,7 +66,6 @@ export const useContributionSearchCdtnReferencesQuery = ( ], slug, title, - slugRegex, }, } ); diff --git a/targets/frontend/src/components/contributions/questionList/QuestionList.query.ts b/targets/frontend/src/components/contributions/questionList/QuestionList.query.ts index ca0d7d5ec..0349c320f 100644 --- a/targets/frontend/src/components/contributions/questionList/QuestionList.query.ts +++ b/targets/frontend/src/components/contributions/questionList/QuestionList.query.ts @@ -23,8 +23,10 @@ export const questionListQuery = `query questions_answers($search: String) { } } }`; -export type QueryQuestionAnswer = Answer; -export type QueryQuestion = Question; +export type QueryQuestionAnswer = Pick; +export type QueryQuestion = Pick & { + answers: QueryQuestionAnswer[]; +}; export type QueryResult = { contribution_questions: QueryQuestion[]; @@ -42,10 +44,10 @@ function formatAnswers(questions: QueryQuestion[] | undefined) { if (!questions) return []; return questions.map((question) => { - question.answers = question?.answers?.map((answer) => ({ - ...answer, - status: initStatus(answer as Answer), - })); + question.answers = question.answers.map((answer) => { + answer.status = initStatus(answer); + return answer; + }); return question; }); } diff --git a/targets/frontend/src/components/contributions/questionList/QuestionList.tsx b/targets/frontend/src/components/contributions/questionList/QuestionList.tsx index 2021c02c6..d4a6c6793 100644 --- a/targets/frontend/src/components/contributions/questionList/QuestionList.tsx +++ b/targets/frontend/src/components/contributions/questionList/QuestionList.tsx @@ -23,7 +23,6 @@ import { QuestionRow } from "./QuestionRow"; import { fr } from "@codegouvfr/react-dsfr"; import { statusesMapping } from "../status/data"; import { StatusStats } from "../status/StatusStats"; -import { Answer } from "../type"; export const countAnswersWithStatus = ( answers: QueryQuestionAnswer[] | undefined, @@ -41,9 +40,7 @@ export const QuestionList = (): JSX.Element => { search, }); - const aggregatedRow = rows.flatMap(({ answers }) => - answers?.length ? (answers as Answer[]) : [] - ); + const aggregatedRow = rows.map(({ answers }) => answers).flat(); const total = aggregatedRow.length; return ( diff --git a/targets/frontend/src/components/contributions/questionList/QuestionRow.tsx b/targets/frontend/src/components/contributions/questionList/QuestionRow.tsx index 9abc25d57..87a2454ef 100644 --- a/targets/frontend/src/components/contributions/questionList/QuestionRow.tsx +++ b/targets/frontend/src/components/contributions/questionList/QuestionRow.tsx @@ -4,7 +4,6 @@ import { useRouter } from "next/router"; import { StatusRecap } from "../status"; import { QueryQuestion } from "./QuestionList.query"; -import { Answer } from "../type"; export const QuestionRow = (props: { row: QueryQuestion }) => { const { row } = props; @@ -23,10 +22,7 @@ export const QuestionRow = (props: { row: QueryQuestion }) => { {row.order} - {row.content} - + ); }; diff --git a/targets/frontend/src/components/contributions/questions/EditQuestion.tsx b/targets/frontend/src/components/contributions/questions/EditQuestion.tsx index 7c15b4cf2..87e80a849 100644 --- a/targets/frontend/src/components/contributions/questions/EditQuestion.tsx +++ b/targets/frontend/src/components/contributions/questions/EditQuestion.tsx @@ -122,7 +122,7 @@ export const EditQuestion = ({ justifyContent="start" spacing={2} > -
+
diff --git a/targets/frontend/src/components/contributions/questions/EditQuestionAnswerList.tsx b/targets/frontend/src/components/contributions/questions/EditQuestionAnswerList.tsx index 110b55b7d..29e98eafd 100644 --- a/targets/frontend/src/components/contributions/questions/EditQuestionAnswerList.tsx +++ b/targets/frontend/src/components/contributions/questions/EditQuestionAnswerList.tsx @@ -44,8 +44,8 @@ export const EditQuestionAnswerList = ({ router.push(`/contributions/answers/${answer.id}`); }} > - {answer?.agreement?.id} - {answer?.agreement?.name} + {answer.agreement.id} + {answer.agreement.name} {answer.status && ( diff --git a/targets/frontend/src/components/contributions/questions/EditQuestionForm.tsx b/targets/frontend/src/components/contributions/questions/EditQuestionForm.tsx index 43256ed48..abdf987d9 100644 --- a/targets/frontend/src/components/contributions/questions/EditQuestionForm.tsx +++ b/targets/frontend/src/components/contributions/questions/EditQuestionForm.tsx @@ -1,12 +1,10 @@ import { AlertColor, Button, Card, Stack, Typography } from "@mui/material"; import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; import { FormSelect, FormTextField } from "src/components/forms"; import { useQuestionUpdateMutation } from "./Question.mutation"; -import { Message, Question, questionRelationSchema } from "../type"; +import { Message, Question } from "../type"; import { SnackBar } from "../../utils/SnackBar"; type EditQuestionProps = { @@ -14,22 +12,15 @@ type EditQuestionProps = { messages: Message[]; }; -const formDataSchema = questionRelationSchema.extend({ - message_id: z - .string({ - required_error: "Un message doit être sélectionné", - }) - .uuid("Un message doit être sélectionné"), -}); -export type FormData = z.infer; +type FormData = Omit & { + message_id: string; +}; export const EditQuestionForm = ({ question, messages, }: EditQuestionProps): JSX.Element => { const { control, watch, handleSubmit } = useForm({ - resolver: zodResolver(formDataSchema), - shouldFocusError: true, defaultValues: { content: question.content, id: question.id, @@ -86,6 +77,7 @@ export const EditQuestionForm = ({ name="content" control={control} label="Nom de la question" + rules={{ required: true }} multiline fullWidth /> @@ -97,6 +89,7 @@ export const EditQuestionForm = ({ }))} name="message_id" control={control} + rules={{ required: true }} label="Message associé à la question" fullWidth /> diff --git a/targets/frontend/src/components/contributions/questions/Question.query.ts b/targets/frontend/src/components/contributions/questions/Question.query.ts index 528599923..e12834b7b 100644 --- a/targets/frontend/src/components/contributions/questions/Question.query.ts +++ b/targets/frontend/src/components/contributions/questions/Question.query.ts @@ -1,7 +1,7 @@ import { useQuery } from "urql"; import { initStatus } from "../status/utils"; -import { Answer, Message, Question } from "../type"; +import { Message, Question } from "../type"; export const contributionQuestionQuery = ` query SelectQuestion($questionId: uuid) { @@ -75,11 +75,12 @@ export const useQuestionQuery = ({ ) { return "not_found"; } - const answers = - result.data.contribution_questions[0]?.answers?.map((answer) => ({ + const answers = result.data.contribution_questions[0].answers.map( + (answer) => ({ ...answer, - status: initStatus(answer as Answer), - })) ?? []; + status: initStatus(answer), + }) + ); const question = { ...result.data.contribution_questions[0], answers, diff --git a/targets/frontend/src/components/contributions/status/utils.ts b/targets/frontend/src/components/contributions/status/utils.ts index b64c28d9f..d18d219a5 100644 --- a/targets/frontend/src/components/contributions/status/utils.ts +++ b/targets/frontend/src/components/contributions/status/utils.ts @@ -1,12 +1,7 @@ -import { Answer, Status } from "../type"; +import { Status } from "../type"; -export const initStatus = (answer: Answer) => { - return ( - answer.statuses?.[0] || { - status: "TODO", - createdAt: new Date().toISOString(), - } - ); +export const initStatus = (answer: any) => { + return answer.statuses?.[0] || { status: "TODO" }; }; export const getNextStatus = (status: Status): Status => { diff --git a/targets/frontend/src/components/contributions/type.ts b/targets/frontend/src/components/contributions/type.ts index 7c45ff629..fc320a9b8 100644 --- a/targets/frontend/src/components/contributions/type.ts +++ b/targets/frontend/src/components/contributions/type.ts @@ -1,215 +1,120 @@ -import { z } from "zod"; - -export const userSchema = z.object({ - id: z.string().uuid().optional(), - name: z.string(), - email: z.string(), - created_at: z.string(), -}); -export type User = z.infer; - -export const agreementSchema = z.object({ - id: z.string(), - name: z.string(), - kaliId: z.string(), -}); -export type Agreement = z.infer; - -export const statusSchema = z.enum([ - "TODO", - "REDACTING", - "REDACTED", - "VALIDATING", - "VALIDATED", - "PUBLISHED", -]); -export type Status = z.infer; - -export const answerStatusSchema = z.object({ - id: z.string().uuid(), - createdAt: z.string(), - status: statusSchema, - userId: z.string(), - user: userSchema, -}); -export type AnswerStatus = z.infer; - -export const messageSchema = z.object({ - id: z.string().uuid(), - label: z.string(), - content: z.string(), -}); -export type Message = z.infer; +import { User } from "src/types"; +import { SourceRoute } from "@socialgouv/cdtn-sources"; + +export type Agreement = { + id: string; + name: string; + kaliId: string; +}; + +export type Status = + | "TODO" + | "REDACTING" + | "REDACTED" + | "VALIDATING" + | "VALIDATED" + | "PUBLISHED"; + +export type AnswerStatus = { + id: string; + createdAt: string; + status: Status; + userId: string; + user: User; +}; + +export type Message = { + id: string; + label: string; + content: string; +}; + +export type Question = { + id: string; + content: string; + order: number; + answers: Answer[]; + message?: Message; +}; + +export type Comments = { + id: string; + content: string; + answer: Answer; + answerId: string; + userId: string; + user: User; + createdAt: string; +}; export type CommentsAndStatuses = (AnswerStatus | Comments) & { createdAtDate: Date; }; -export const kaliArticleSchema = z.object({ - cid: z.string(), - id: z.string(), - path: z.string(), - label: z - .string({ required_error: "Un libellé doit être renseigner" }) - .min(1, "un label doit être renseigner"), - agreementId: z.string(), - createdAt: z.string(), -}); -export type KaliArticle = z.infer; - -export const legiArticleSchema = z.object({ - cid: z.string(), - id: z.string(), - label: z.string(), -}); -export type LegiArticle = z.infer; - -export const kaliReferenceSchema = z.object({ - kaliArticle: kaliArticleSchema.partial(), - label: z - .string({ required_error: "Un libellé doit être renseigner" }) - .min(1, "un label doit être renseigner"), -}); -export type KaliReference = z.infer; - -export const legiReferenceSchema = z.object({ - legiArticle: legiArticleSchema, -}); -export type LegiReference = z.infer; - -export const otherReferenceSchema = z.object({ - label: z - .string({ required_error: "un libellé doit être renseigner" }) - .min(1, "un nom doit être renseigné"), - url: z - .string({ required_error: "Une url doit être renseigné" }) - .url("le format du lien est invalide") - .optional() - .or(z.literal("")), -}); -export type OtherReference = z.infer; - -export const documentSchema = z.object({ - title: z.string(), - cdtnId: z.string(), - source: z.string(), - slug: z.string(), -}); -export type Document = z.infer; - -export const cdtnReferenceSchema = z.object({ - document: documentSchema, -}); -export type CdtnReference = z.infer; - -export const contentTypeSchema = z.enum([ - "ANSWER", - "NOTHING", - "CDT", - "UNFAVOURABLE", - "UNKNOWN", - "SP", -]); -export type ContentType = z.infer; - -const answerBaseSchema = z.object({ - id: z.string().uuid(), - agreementId: z.string(), - questionId: z.string().uuid(), - contentType: z.string({ - required_error: "Un type de réponse doit être sélectionner", - invalid_type_error: " type de réponse doit être sélectionner", - }), - contentServicePublicCdtnId: z.string().nullable().optional(), - content: z.string().nullable().optional(), - updatedAt: z.string(), -}); - -export const questionBaseSchema = z.object({ - id: z.string().uuid(), - content: z - .string({ - required_error: "une question doit être renseigner", - }) - .min(1, "une question doit être renseigner"), - order: z.number(), -}); - -export const commentsSchema = z.object({ - id: z.string().uuid(), - content: z.string(), - answer: answerBaseSchema, - answerId: z.string(), - user: userSchema, - userId: z.string(), - createdAt: z.string(), -}); -export type Comments = z.infer; - -const answerRelationSchema = answerBaseSchema.extend({ - agreement: agreementSchema, - statuses: z.array(answerStatusSchema), - status: answerStatusSchema, - kaliReferences: z.array(kaliReferenceSchema), - legiReferences: z.array(legiReferenceSchema), - otherReferences: z.array(otherReferenceSchema), - cdtnReferences: z.array(cdtnReferenceSchema), - contentFichesSpDocument: documentSchema.nullable().optional(), - question: questionBaseSchema, - answerComments: z.array(commentsSchema), -}); -export type Answer = z.infer; - -export const answerFormBaseSchema = answerRelationSchema.extend({ - id: z.string().uuid().optional(), - agreementId: z.string().optional(), - questionId: z.string().uuid().optional(), - updatedAt: z.string().optional(), - question: questionBaseSchema.deepPartial().optional(), - answerComments: z.array(commentsSchema.deepPartial()).optional(), - agreement: agreementSchema.deepPartial().optional(), - statuses: z.array(answerStatusSchema.deepPartial()).optional(), - status: answerStatusSchema.deepPartial().optional(), -}); - -export const questionRelationSchema = questionBaseSchema.extend({ - answers: z.array(answerBaseSchema.deepPartial()).optional(), - message: messageSchema.deepPartial().optional(), -}); -export type Question = z.infer; - -const answerWithAnswerSchema = answerFormBaseSchema.extend({ - contentType: z.literal("ANSWER"), - content: z - .string({ - required_error: "Une réponse doit être renseigner", - invalid_type_error: "Une réponse doit être renseigner", - }) - .min(1, "Une réponse doit être renseigner"), -}); -const answerWithNothingSchema = answerFormBaseSchema.extend({ - contentType: z.literal("NOTHING"), -}); -const answerWithCdtSchema = answerFormBaseSchema.extend({ - contentType: z.literal("CDT"), -}); -const answerWithUnfavourableSchema = answerFormBaseSchema.extend({ - contentType: z.literal("UNFAVOURABLE"), -}); -const answerWithUnknownSchema = answerFormBaseSchema.extend({ - contentType: z.literal("UNKNOWN"), -}); -const answerWithSPSchema = answerFormBaseSchema.extend({ - contentType: z.literal("SP"), - contentFichesSpDocument: documentSchema, -}); - -export const answerFormSchema = z.discriminatedUnion("contentType", [ - answerWithAnswerSchema, - answerWithNothingSchema, - answerWithCdtSchema, - answerWithUnfavourableSchema, - answerWithUnknownSchema, - answerWithSPSchema, -]); -export type AnswerForm = z.infer; +export type KaliArticle = { + cid: string; + id: string; + path: string; + label: string; + agreementId: string; + createdAt: string; +}; + +export type LegiArticle = { + cid: string; + id: string; + label: string; +}; + +export type KaliReference = { + kaliArticle: KaliArticle; + label?: string; +}; + +export type LegiReference = { + legiArticle: LegiArticle; +}; + +export type OtherReference = { + label: string; + url: string; +}; + +export type Document = { + title: string; + cdtnId: string; + source: SourceRoute; + slug: string; +}; + +export type CdtnReference = { + document: Document; +}; + +export type ContentType = + | "ANSWER" + | "NOTHING" + | "CDT" + | "UNFAVOURABLE" + | "UNKNOWN" + | "SP"; + +export type Answer = { + id: string; + agreementId: string; + questionId: string; + contentType?: ContentType; + contentServicePublicCdtnId?: string; + agreement: Agreement; + statuses: AnswerStatus[]; + status: AnswerStatus; + content?: string; + question: Omit; + answerComments: Comments[]; + updatedAt: string; + kaliReferences: KaliReference[]; + legiReferences: LegiReference[]; + otherReferences: OtherReference[]; + cdtnReferences: CdtnReference[]; + contentFichesSpDocument?: Document; +}; diff --git a/targets/frontend/src/components/forms/Autocomplete/index.tsx b/targets/frontend/src/components/forms/Autocomplete/index.tsx index 428a5cda0..9a4c94ba3 100644 --- a/targets/frontend/src/components/forms/Autocomplete/index.tsx +++ b/targets/frontend/src/components/forms/Autocomplete/index.tsx @@ -11,7 +11,6 @@ import { TextField, } from "@mui/material"; import { AutocompleteRenderGetTagProps } from "@mui/material/Autocomplete/Autocomplete"; -import { styled } from "@mui/system"; import React, { PropsWithChildren } from "react"; import { Controller } from "react-hook-form"; @@ -95,7 +94,6 @@ export const FormAutocomplete = ({ renderInput={(params) => ( ({ /> )} /> - {error && error.message === "Required" ? ( + {error && error.type === "required" ? ( Ce champ est requis ) : null} diff --git a/targets/frontend/src/components/forms/EditionField/Editor.tsx b/targets/frontend/src/components/forms/EditionField/Editor.tsx index 533c0adbd..3d6fe7164 100644 --- a/targets/frontend/src/components/forms/EditionField/Editor.tsx +++ b/targets/frontend/src/components/forms/EditionField/Editor.tsx @@ -19,20 +19,15 @@ import { DetailsContent } from "@tiptap-pro/extension-details-content"; import { Placeholder } from "@tiptap/extension-placeholder"; export type EditorProps = { - content?: string; + content?: string | null; onUpdate: (content: string) => void; + error?: FieldErrors; disabled?: boolean; - isError?: boolean; }; const emptyHtml = "

"; -export const Editor = ({ - content, - onUpdate, - disabled, - isError = false, -}: EditorProps) => { +export const Editor = ({ content, onUpdate, error, disabled }: EditorProps) => { const [focus, setFocus] = useState(false); const [isClient, setIsClient] = useState(false); const editor = useEditor({ @@ -88,12 +83,7 @@ export const Editor = ({ return ( <> {isClient && ( - + @@ -129,12 +119,9 @@ const StyledEditorContent = styled(EditorContent)(() => { }, ".details": { display: "flex", + margin: "1rem 0", border: "0", - padding: "1rem 0", - borderTop: `1px solid ${fr.colors.decisions.text.default.grey.default}`, - ":first-child": { - border: "none", - }, + padding: "0.5rem", "> button": { display: "flex", cursor: "pointer", @@ -172,7 +159,7 @@ const StyledEditorContent = styled(EditorContent)(() => { }, th: { border: `1px solid ${fr.colors.decisions.text.default.grey.default}`, - backgroundColor: fr.colors.decisions.background.default.grey.default, + backgroundColor: fr.colors.decisions.background.contrast.grey.default, minWidth: "100px", }, td: { diff --git a/targets/frontend/src/components/forms/EditionField/MenuSpecial.tsx b/targets/frontend/src/components/forms/EditionField/MenuSpecial.tsx index d5933881e..8737f5e04 100644 --- a/targets/frontend/src/components/forms/EditionField/MenuSpecial.tsx +++ b/targets/frontend/src/components/forms/EditionField/MenuSpecial.tsx @@ -60,7 +60,11 @@ export const MenuSpecial = ({ editor }: { editor: Editor | null }) => {