diff --git a/targets/frontend/__mocks__/@codegouvfr/react-dsfr/index.ts b/targets/frontend/__mocks__/@codegouvfr/react-dsfr/index.ts new file mode 100644 index 000000000..453296caa --- /dev/null +++ b/targets/frontend/__mocks__/@codegouvfr/react-dsfr/index.ts @@ -0,0 +1,25 @@ +const DEFAULT_VARIANTS = { + default: { + error: {}, + info: {}, + warning: {}, + success: {}, + grey: {}, + }, +}; +module.exports = { + fr: { + colors: { + decisions: { + text: { + ...DEFAULT_VARIANTS, + actionHigh: { blueCumulus: {} }, + label: { greenBourgeon: {} }, + }, + background: { + ...DEFAULT_VARIANTS, + }, + }, + }, + }, +}; diff --git a/targets/frontend/__mocks__/@codegouvfr/react-dsfr/useIsDark.ts b/targets/frontend/__mocks__/@codegouvfr/react-dsfr/useIsDark.ts new file mode 100644 index 000000000..47b287049 --- /dev/null +++ b/targets/frontend/__mocks__/@codegouvfr/react-dsfr/useIsDark.ts @@ -0,0 +1,3 @@ +module.exports = { + useIsDark: jest.fn().mockReturnValue(false), +}; diff --git a/targets/frontend/__mocks__/next/router.ts b/targets/frontend/__mocks__/next/router.ts new file mode 100644 index 000000000..cc310e84c --- /dev/null +++ b/targets/frontend/__mocks__/next/router.ts @@ -0,0 +1,17 @@ +const route = jest.fn(); +const router = { + asPath: "mock", + pathname: "mock", + push: jest.fn((path) => { + route(path); + return Promise.resolve(); + }), + query: { q: "" }, + route, +}; + +module.exports = { + ...jest.requireActual("next/router"), + + useRouter: () => router, +}; diff --git a/targets/frontend/package.json b/targets/frontend/package.json index 97ee33de4..1b75afc7c 100644 --- a/targets/frontend/package.json +++ b/targets/frontend/package.json @@ -81,7 +81,6 @@ "sentry-testkit": "^3.3.4", "serialize-error": "^9.1.1", "strip-markdown": "^4.2.0", - "styled-components": "^5.3.9", "swr": "^1.0.1", "unified": "^9.2.2", "unist-util-parents": "^1.0.3", diff --git a/targets/frontend/src/__tests__/contenus/contentList.test.tsx b/targets/frontend/src/__tests__/contenus/contentList.test.tsx index dcdf8a824..ec04b69fb 100644 --- a/targets/frontend/src/__tests__/contenus/contentList.test.tsx +++ b/targets/frontend/src/__tests__/contenus/contentList.test.tsx @@ -2,20 +2,6 @@ import { render, RenderResult } from "@testing-library/react"; import { act } from "react-dom/test-utils"; import { DocumentsPage } from "src/pages/contenus"; -jest.mock("next/router", () => ({ - ...jest.requireActual("next/router"), - useRouter: () => ({ - query: {}, - }), -})); - -jest.mock("@codegouvfr/react-dsfr/useIsDark", () => ({ - useIsDark: jest.fn().mockReturnValue(false), -})); -jest.mock("@codegouvfr/react-dsfr", () => ({ - fr: jest.fn(), -})); - describe("Given parameters", () => { describe("When rendering the component DocumentsPage", () => { let rendering: RenderResult; diff --git a/targets/frontend/src/components/contributions/answers/Answer.tsx b/targets/frontend/src/components/contributions/answers/Answer.tsx index de71ee336..f40103c68 100644 --- a/targets/frontend/src/components/contributions/answers/Answer.tsx +++ b/targets/frontend/src/components/contributions/answers/Answer.tsx @@ -3,8 +3,8 @@ import { Box, Button, FormControl, - Grid, Stack, + Typography, } from "@mui/material"; import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -12,14 +12,7 @@ import { useUser } from "src/hooks/useUser"; import { FormEditionField, FormRadioGroup, FormTextField } from "../../forms"; import { StatusContainer } from "../status"; -import { - Answer, - CdtnReference, - KaliReference, - LegiReference, - OtherReference, - Status, -} from "../type"; +import { Answer, Status } from "../type"; import { useContributionAnswerUpdateMutation } from "./answer.mutation"; import { useContributionAnswerQuery } from "./answer.query"; import { Comments } from "./Comments"; @@ -32,21 +25,12 @@ import { import { statusesMapping } from "../status/data"; import { getNextStatus, getPrimaryButtonLabel } from "../status/utils"; import { SnackBar } from "../../utils/SnackBar"; -import { BreadcrumbLink } from "src/components/utils"; +import { Breadcrumb, BreadcrumbLink } from "src/components/utils"; export type ContributionsAnswerProps = { id: string; }; -export type AnswerForm = { - otherAnswer?: string; - content?: string; - kaliReferences: KaliReference[]; - legiReferences: LegiReference[]; - otherReferences: OtherReference[]; - cdtnReferences: CdtnReference[]; -}; - const isNotEditable = (answer: Answer | undefined) => answer?.status.status !== "REDACTING" && answer?.status.status !== "TODO" && @@ -147,27 +131,32 @@ export const ContributionsAnswer = ({ ]; return ( <> - - -
    - - Contributions - - + + + + <> + + [{answer?.question?.order}] + {" "} {answer?.question?.content} - - {answer?.agreement?.id} -
-
+ + + {answer?.agreement?.id} + {answer?.status && ( - +
- +
)} -
-

{answer?.agreement?.name}

+
> => { - const [slug] = query?.split("/").reverse() ?? [""]; + const hrefWithoutQuery: string | undefined = query?.split("?")[0]; + const [slug] = hrefWithoutQuery?.split("/").reverse() ?? [""]; const title = `%${slug ?.split(/[\ \-\,]/gm) ?.map((text) => text.normalize().replace(/[\u0300-\u036f]/g, "")) diff --git a/targets/frontend/src/components/contributions/questionList/QuestionList.query.ts b/targets/frontend/src/components/contributions/questionList/QuestionList.query.ts index 94e8534d4..0349c320f 100644 --- a/targets/frontend/src/components/contributions/questionList/QuestionList.query.ts +++ b/targets/frontend/src/components/contributions/questionList/QuestionList.query.ts @@ -7,10 +7,12 @@ export const questionListQuery = `query questions_answers($search: String) { contribution_questions( where: { content: { _ilike: $search } - } + }, + order_by: {order: asc} ) { id, content, + order, answers { statuses(order_by: {created_at: desc}, limit: 1) { status @@ -22,7 +24,7 @@ export const questionListQuery = `query questions_answers($search: String) { } }`; export type QueryQuestionAnswer = Pick; -export type QueryQuestion = Pick & { +export type QueryQuestion = Pick & { answers: QueryQuestionAnswer[]; }; diff --git a/targets/frontend/src/components/contributions/questionList/QuestionList.tsx b/targets/frontend/src/components/contributions/questionList/QuestionList.tsx index d4909348a..d4a6c6793 100644 --- a/targets/frontend/src/components/contributions/questionList/QuestionList.tsx +++ b/targets/frontend/src/components/contributions/questionList/QuestionList.tsx @@ -10,18 +10,29 @@ import { TableHead, TableRow, TextField, + Tooltip, Typography, } from "@mui/material"; import { useState } from "react"; -import { useQuestionListQuery } from "./QuestionList.query"; -import { countAnswersWithStatus, QuestionRow } from "./QuestionRow"; +import { + QueryQuestionAnswer, + useQuestionListQuery, +} from "./QuestionList.query"; +import { QuestionRow } from "./QuestionRow"; import { fr } from "@codegouvfr/react-dsfr"; import { statusesMapping } from "../status/data"; +import { StatusStats } from "../status/StatusStats"; -export function getPercentage(count: number, total: number) { - return ((count / total) * 100).toFixed(2); -} +export const countAnswersWithStatus = ( + answers: QueryQuestionAnswer[] | undefined, + statusToCount: string +): number => { + const count = answers?.filter((answer) => { + return answer.status?.status === statusToCount; + }).length; + return count ?? 0; +}; export const QuestionList = (): JSX.Element => { const [search, setSearch] = useState(); @@ -33,7 +44,13 @@ export const QuestionList = (): JSX.Element => { const total = aggregatedRow.length; return ( - + { sx={{ fontWeight: "bold", color: fr.colors.decisions.text.default.grey.default, - marginBottom: "24px", }} > {total} @@ -58,18 +74,13 @@ export const QuestionList = (): JSX.Element => { - {Object.entries(statusesMapping).map(([status, { text, color }]) => { - const count = countAnswersWithStatus(aggregatedRow, status); - return ( - - - {text} - {count} - {getPercentage(count, total)}% - - - ); - })} + ({ + status, + count: countAnswersWithStatus(aggregatedRow, status), + }))} + total={total} + > @@ -77,13 +88,15 @@ export const QuestionList = (): JSX.Element => { Questions ({rows.length}) - {Object.entries(statusesMapping).map(([_, { text, color }]) => { - return ( - - {text} - - ); - })} + {Object.entries(statusesMapping).map( + ([_, { text, icon, color }]) => { + return ( + + {icon} + + ); + } + )} diff --git a/targets/frontend/src/components/contributions/questionList/QuestionRow.tsx b/targets/frontend/src/components/contributions/questionList/QuestionRow.tsx index 89ffdb58d..87a2454ef 100644 --- a/targets/frontend/src/components/contributions/questionList/QuestionRow.tsx +++ b/targets/frontend/src/components/contributions/questionList/QuestionRow.tsx @@ -3,13 +3,7 @@ import TableRow from "@mui/material/TableRow"; import { useRouter } from "next/router"; import { StatusRecap } from "../status"; -import { QueryQuestion, QueryQuestionAnswer } from "./QuestionList.query"; - -export const countAnswersWithStatus = ( - answers: QueryQuestionAnswer[] | undefined, - statusToCount: string -): number => - answers?.filter(({ status }) => status?.status === statusToCount).length ?? 0; +import { QueryQuestion } from "./QuestionList.query"; export const QuestionRow = (props: { row: QueryQuestion }) => { const { row } = props; @@ -26,9 +20,9 @@ export const QuestionRow = (props: { row: QueryQuestion }) => { hover > - {row.content} + {row.order} - {row.content} - + ); }; diff --git a/targets/frontend/src/components/contributions/questionList/__mocks__/QuestionList.query.ts b/targets/frontend/src/components/contributions/questionList/__mocks__/QuestionList.query.ts index 33e38911e..d089db9fd 100644 --- a/targets/frontend/src/components/contributions/questionList/__mocks__/QuestionList.query.ts +++ b/targets/frontend/src/components/contributions/questionList/__mocks__/QuestionList.query.ts @@ -29,6 +29,7 @@ export const mock: QuestionListQueryResult = { ], content: "question1", id: "questionId1", + order: 1, }, { answers: [ @@ -41,6 +42,7 @@ export const mock: QuestionListQueryResult = { ], content: "question2", id: "questionId2", + order: 2, }, ], }; diff --git a/targets/frontend/src/components/contributions/questionList/__tests__/QuestionList.test.tsx b/targets/frontend/src/components/contributions/questionList/__tests__/QuestionList.test.tsx index f3119e263..ca50c9b58 100644 --- a/targets/frontend/src/components/contributions/questionList/__tests__/QuestionList.test.tsx +++ b/targets/frontend/src/components/contributions/questionList/__tests__/QuestionList.test.tsx @@ -5,31 +5,6 @@ import { QuestionList } from ".."; jest.mock("../QuestionList.query"); -jest.mock("next/router", () => ({ - useRouter: () => { - return { push: jest.fn() }; - }, -})); -jest.mock("@codegouvfr/react-dsfr", () => ({ - fr: { - colors: { - decisions: { - text: { - default: { - error: {}, - info: {}, - warning: {}, - success: {}, - grey: {}, - }, - actionHigh: { blueCumulus: {} }, - label: { greenBourgeon: {} }, - }, - }, - }, - }, -})); - describe("QuestionList", () => { beforeEach(() => { render(); @@ -38,7 +13,7 @@ describe("QuestionList", () => { expect(screen.getByTestId("contributions-list-search")).toBeInTheDocument(); }); test("Verify question display", () => { - expect(screen.queryByText("question1")).toBeInTheDocument(); - expect(screen.queryByText("question2")).toBeInTheDocument(); + expect(screen.getByText(/question1/)).toBeInTheDocument(); + expect(screen.getByText(/question2/)).toBeInTheDocument(); }); }); diff --git a/targets/frontend/src/components/contributions/questions/EditQuestion.tsx b/targets/frontend/src/components/contributions/questions/EditQuestion.tsx index 829716848..87e80a849 100644 --- a/targets/frontend/src/components/contributions/questions/EditQuestion.tsx +++ b/targets/frontend/src/components/contributions/questions/EditQuestion.tsx @@ -1,14 +1,5 @@ import SentimentVeryDissatisfiedIcon from "@mui/icons-material/SentimentVeryDissatisfied"; -import { - Box, - Card, - CardContent, - Skeleton, - Stack, - Tab, - Tabs, - Typography, -} from "@mui/material"; +import { Box, Skeleton, Stack, Tab, Tabs, Typography } from "@mui/material"; import Link from "next/link"; import React from "react"; import { EditQuestionAnswerList } from "./EditQuestionAnswerList"; @@ -17,8 +8,9 @@ import { EditQuestionForm } from "./EditQuestionForm"; import { useQuestionQuery } from "./Question.query"; import { BreadcrumbLink } from "src/components/utils"; import { statusesMapping } from "../status/data"; -import { countAnswersWithStatus, getPercentage } from "../questionList"; +import { countAnswersWithStatus } from "../questionList"; import { Answer } from "../type"; +import { StatusStats } from "../status/StatusStats"; export type EditQuestionProps = { questionId: string; @@ -69,28 +61,28 @@ export const EditQuestion = ({ ); } - const Header = ({ answers }: { answers: Answer[] }) => ( - <> -
    - Contributions - {data?.question?.content} -
- - {Object.entries(statusesMapping).map(([status, { text, color }]) => { - const count = countAnswersWithStatus(answers, status); - return ( - - - {text} - {count} - {getPercentage(count, answers.length)}% - - - ); - })} - - - ); + const Header = ({ answers }: { answers: Answer[] }) => { + const statusCounts = Object.keys(statusesMapping).map((status) => { + const count = countAnswersWithStatus(answers, status); + return { status, count }; + }); + return ( + <> +
    + Contributions + + {`${data?.question?.order} - ${data?.question?.content}`} + +
+ + + + + ); + }; const handleTabChange = (event: React.SyntheticEvent, newValue: TabValue) => { setTabIndex(newValue); diff --git a/targets/frontend/src/components/contributions/questions/Question.query.ts b/targets/frontend/src/components/contributions/questions/Question.query.ts index dd6648d2d..f441f3e45 100644 --- a/targets/frontend/src/components/contributions/questions/Question.query.ts +++ b/targets/frontend/src/components/contributions/questions/Question.query.ts @@ -7,6 +7,7 @@ export const contributionQuestionQuery = ` query SelectQuestion($questionId: uuid) { contribution_questions(where: {id: {_eq: $questionId}}) { content + order id message { id diff --git a/targets/frontend/src/components/contributions/status/Status.tsx b/targets/frontend/src/components/contributions/status/Status.tsx index 622702582..8051f7901 100644 --- a/targets/frontend/src/components/contributions/status/Status.tsx +++ b/targets/frontend/src/components/contributions/status/Status.tsx @@ -17,11 +17,12 @@ export const StatusContainer = ({ style={{ color: statusesMapping[status.status].color, }} - alignItems="end" - justifyContent="center" + alignItems="center" + justifyContent="space-between" spacing={1} data-testid={dataTestid} > + {statusesMapping[status.status].icon} {statusesMapping[status.status].text}
diff --git a/targets/frontend/src/components/contributions/status/StatusRecap.tsx b/targets/frontend/src/components/contributions/status/StatusRecap.tsx index 2a6f0c185..c030e6137 100644 --- a/targets/frontend/src/components/contributions/status/StatusRecap.tsx +++ b/targets/frontend/src/components/contributions/status/StatusRecap.tsx @@ -4,10 +4,10 @@ import { countAnswersWithStatus, QueryQuestionAnswer } from "../questionList"; export const StatusRecap = ({ answers, - key, + uniqKey = "", }: { answers: QueryQuestionAnswer[] | undefined; - key?: string; + uniqKey?: string; }) => { return ( <> @@ -17,11 +17,11 @@ export const StatusRecap = ({ - {count > 0 && {count}} + {count ? count : "-"} ); })} diff --git a/targets/frontend/src/components/contributions/status/StatusStats.tsx b/targets/frontend/src/components/contributions/status/StatusStats.tsx new file mode 100644 index 000000000..4073c0e2b --- /dev/null +++ b/targets/frontend/src/components/contributions/status/StatusStats.tsx @@ -0,0 +1,45 @@ +import { Card, Stack, CardContent, Typography } from "@mui/material"; +import { statusesMapping } from "./data"; + +export function getPercentage(count: number, total: number) { + return ((count / total) * 100).toFixed(2); +} + +type StatusCount = { + status: string; + count: number; +}; + +export type StatusStatsResult = { + statusCounts: StatusCount[]; + total: number; +}; + +export const StatusStats = ({ statusCounts, total }: StatusStatsResult) => { + return ( + <> + {statusCounts.map(({ status, count }) => { + const { text, color, icon } = statusesMapping[status]; + return ( + + + + {icon} + {text} + + + {count} + ({getPercentage(count, total)}%) + + + + ); + })} + + ); +}; diff --git a/targets/frontend/src/components/contributions/status/data.tsx b/targets/frontend/src/components/contributions/status/data.tsx index b1bbb00c2..f967be521 100644 --- a/targets/frontend/src/components/contributions/status/data.tsx +++ b/targets/frontend/src/components/contributions/status/data.tsx @@ -1,31 +1,52 @@ +import CheckIcon from "@mui/icons-material/Check"; +import ClearIcon from "@mui/icons-material/Clear"; +import DescriptionIcon from "@mui/icons-material/Description"; +import EditNoteIcon from "@mui/icons-material/EditNote"; +import TaskAltIcon from "@mui/icons-material/TaskAlt"; +import VisibilityIcon from "@mui/icons-material/Visibility"; import { fr } from "@codegouvfr/react-dsfr"; -export const statusesMapping = { +export type StatusesMapping = { + [status: string]: { + color: string; + icon: JSX.Element; + text: string; + }; +}; + +export const statusesMapping: StatusesMapping = { TODO: { color: fr.colors.decisions.text.default.error.default, + icon: , text: "À traiter", }, REDACTING: { color: fr.colors.decisions.text.actionHigh.blueCumulus.default, + icon: , text: "En rédaction", }, REDACTED: { color: fr.colors.decisions.text.default.info.default, + icon: , text: "À validé", }, VALIDATING: { color: fr.colors.decisions.text.default.warning.default, - + icon: ( + + ), text: "En validation", }, VALIDATED: { color: fr.colors.decisions.text.label.greenBourgeon.default, + icon: , text: "Validé", }, PUBLISHED: { color: fr.colors.decisions.text.default.success.default, + icon: , text: "Publié", }, }; diff --git a/targets/frontend/src/components/contributions/type.ts b/targets/frontend/src/components/contributions/type.ts index 814641565..3da256dbf 100644 --- a/targets/frontend/src/components/contributions/type.ts +++ b/targets/frontend/src/components/contributions/type.ts @@ -31,6 +31,7 @@ export type Message = { export type Question = { id: string; content: string; + order: number; answers: Answer[]; message?: Message; }; diff --git a/targets/frontend/src/components/forms/EditionField/Editor.tsx b/targets/frontend/src/components/forms/EditionField/Editor.tsx index e6930ef59..3d6fe7164 100644 --- a/targets/frontend/src/components/forms/EditionField/Editor.tsx +++ b/targets/frontend/src/components/forms/EditionField/Editor.tsx @@ -7,6 +7,7 @@ import StarterKit from "@tiptap/starter-kit"; import React, { useEffect, useState } from "react"; import { FieldErrors } from "react-hook-form"; import { styled } from "@mui/system"; +import { fr } from "@codegouvfr/react-dsfr"; import { TitleBox } from "../TitleBox"; import { MenuSpecial } from "./MenuSpecial"; @@ -99,75 +100,72 @@ export const Editor = ({ content, onUpdate, error, disabled }: EditorProps) => { ); }; -const StyledEditorContent = styled(EditorContent)` - padding: 0 12px; - - .ProseMirror:focus { - outline: none; - } - - table { - th { - background-color: #f3f3f3; - min-width: 100px; - } - - td { - border: 1px solid black; - text-align: center; - } - } - - .ProseMirror { - > * + * { - margin-top: 0.75em; - } - - .is-empty::before { - content: attr(data-placeholder); - float: left; - color: #adb5bd; - pointer-events: none; - height: 0; - } - - .details { - display: flex; - margin: 1rem 0; - border: 0; - padding: 0.5rem; - - > button { - display: flex; - cursor: pointer; - background: transparent; - border: none; - padding: 0; - - &::before { - content: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGFyaWEtaGlkZGVuPSJ0cnVlIiBjbGFzcz0ic2MtaW1XWUFJIGpFTHpJTCI+PHBvbHlsaW5lIHBvaW50cz0iOSAxOCAxNSAxMiA5IDYiIHN0cm9rZT0iIzZmOGFjOSI+PC9wb2x5bGluZT48L3N2Zz4="); - display: flex; - justify-content: center; - align-items: center; - color: #6f8ac9; - font-weight: bold; - width: 2.5em; - height: 2em; - } - } - - &.is-open > button::before { - content: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGFyaWEtaGlkZGVuPSJ0cnVlIiBjbGFzcz0ic2MtaW1XWUFJIGpFTHpJTCI+PHBvbHlsaW5lIHBvaW50cz0iOSAxOCAxNSAxMiA5IDYiIHN0cm9rZT0iIzZmOGFjOSI+PC9wb2x5bGluZT48L3N2Zz4="); - transform: rotate(90deg); - } - - > div { - flex: 1 1 auto; - } - - :last-child { - margin-bottom: 0; - } - } - } -`; +const StyledEditorContent = styled(EditorContent)(() => { + return { + padding: "0 12px", + ".ProseMirror": { + ":focus": { + outline: "none", + }, + "> * + *": { + marginTop: "0.75em", + }, + ".is-empty::before": { + content: "attr(data-placeholder)", + float: "left", + color: "#adb5bd", + pointerEvents: "none", + height: "0", + }, + ".details": { + display: "flex", + margin: "1rem 0", + border: "0", + padding: "0.5rem", + "> button": { + display: "flex", + cursor: "pointer", + background: "transparent", + border: "none", + padding: "0", + "&::before": { + content: + 'url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGFyaWEtaGlkZGVuPSJ0cnVlIiBjbGFzcz0ic2MtaW1XWUFJIGpFTHpJTCI+PHBvbHlsaW5lIHBvaW50cz0iOSAxOCAxNSAxMiA5IDYiIHN0cm9rZT0iIzZmOGFjOSI+PC9wb2x5bGluZT48L3N2Zz4=")', + display: "flex", + justifyContent: "center", + alignItems: "center", + color: "#6f8ac9", + fontWeight: "bold", + width: "2.5em", + height: "2em", + }, + }, + "&.is-open > button::before": { + content: + 'url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGFyaWEtaGlkZGVuPSJ0cnVlIiBjbGFzcz0ic2MtaW1XWUFJIGpFTHpJTCI+PHBvbHlsaW5lIHBvaW50cz0iOSAxOCAxNSAxMiA5IDYiIHN0cm9rZT0iIzZmOGFjOSI+PC9wb2x5bGluZT48L3N2Zz4=")', + transform: "rotate(90deg)", + }, + "> div": { + flex: "1 1 auto", + }, + ":last-child": { + marginBottom: "0", + }, + }, + }, + table: { + tBody: { + borderColor: fr.colors.decisions.text.default.success.default, + }, + th: { + border: `1px solid ${fr.colors.decisions.text.default.grey.default}`, + backgroundColor: fr.colors.decisions.background.contrast.grey.default, + minWidth: "100px", + }, + td: { + border: `1px solid ${fr.colors.decisions.text.default.grey.default}`, + textAlign: "center", + }, + }, + }; +}); diff --git a/targets/frontend/src/components/forms/TitleBox.tsx b/targets/frontend/src/components/forms/TitleBox.tsx index 638f3a07d..543226793 100644 --- a/targets/frontend/src/components/forms/TitleBox.tsx +++ b/targets/frontend/src/components/forms/TitleBox.tsx @@ -1,5 +1,5 @@ import { Box, FormLabel } from "@mui/material"; -import styled, { css } from "styled-components"; +import { styled } from "@mui/system"; export const TitleBox = ({ title, @@ -26,33 +26,29 @@ export const TitleBox = ({ ); }; -const StyledBox = styled(Box)` - border: 1px solid #9e9e9e; - border-radius: 6px; - padding: 12px; - position: relative; - ${({ focus }: { focus: boolean }) => { - if (focus) { - return css` - border-color: #1565c0; - `; - } - }} -`; +const StyledBox = styled(Box, { + shouldForwardProp: (prop) => prop !== "focus", +})<{ focus?: boolean }>(({ theme, focus }) => { + return { + border: `1px solid ${theme.palette.text.primary}`, + borderRadius: "1px", + padding: "12px", + position: "relative", + borderColor: focus ? theme.palette.primary.main : undefined, + }; +}); -const StyledFormLabel = styled(FormLabel)` - position: absolute; - font-size: 12px; - background-color: white; - border-radius: 6px; - padding: 0 4px; - top: -8px; - left: 8px; - ${({ focus }: { focus: boolean }) => { - if (focus) { - return css` - color: #1565c0; - `; - } - }} -`; +const StyledFormLabel = styled(FormLabel, { + shouldForwardProp: (prop) => prop !== "focus", +})<{ focus?: boolean }>(({ theme, focus }) => { + return { + position: "absolute", + fontSize: "12px", + backgroundColor: theme.palette.background.default, + borderRadius: "12px", + padding: "0 4px", + top: "-8px", + left: "10px", + color: focus ? theme.palette.primary.main : theme.palette.text.default, + }; +}); diff --git a/targets/frontend/src/components/layout/Navigation.tsx b/targets/frontend/src/components/layout/Navigation.tsx index 7b71f210c..c9a861800 100644 --- a/targets/frontend/src/components/layout/Navigation.tsx +++ b/targets/frontend/src/components/layout/Navigation.tsx @@ -1,7 +1,6 @@ import { List } from "@mui/material"; import { useNavigationAggregation } from "./NavigationAggregation.query"; import { slugifyRepository } from "src/models"; -import { NavigationItem } from "./NavigationItem"; import { NavigationGroup } from "./NavigationGroup"; import { useState } from "react"; import { useRouter } from "next/router"; diff --git a/targets/frontend/src/components/layout/UserMenu.tsx b/targets/frontend/src/components/layout/UserMenu.tsx index 12e4575a4..b4defd85b 100644 --- a/targets/frontend/src/components/layout/UserMenu.tsx +++ b/targets/frontend/src/components/layout/UserMenu.tsx @@ -14,6 +14,7 @@ import { useUser } from "../../hooks/useUser"; import { useState } from "react"; import { theme } from "src/theme"; import { useIsDark } from "@codegouvfr/react-dsfr/useIsDark"; +import { fr } from "@codegouvfr/react-dsfr"; export function UserMenu() { const { user, logout } = useUser() as any; @@ -28,7 +29,7 @@ export function UserMenu() { const { isDark, setIsDark } = useIsDark(); return ( -
+ <> {user && ( @@ -74,6 +75,6 @@ export function UserMenu() { )} -
+ ); } diff --git a/targets/frontend/src/components/layout/auth.layout.tsx b/targets/frontend/src/components/layout/auth.layout.tsx index 9ee74c383..89192347b 100644 --- a/targets/frontend/src/components/layout/auth.layout.tsx +++ b/targets/frontend/src/components/layout/auth.layout.tsx @@ -1,10 +1,11 @@ import { Box, - Toolbar, - AppBar, - IconButton, Drawer, + IconButton, + styled, + Toolbar, Typography, + useTheme, } from "@mui/material"; import MenuIcon from "@mui/icons-material/Menu"; import Head from "next/head"; @@ -13,6 +14,10 @@ import { useState } from "react"; import { LogoAdmin } from "./LogoAdmin"; import { Navigation } from "./Navigation"; import { UserMenu } from "./UserMenu"; +import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import { fr } from "@codegouvfr/react-dsfr"; +import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar"; export type LayoutProps = { children: any; @@ -22,97 +27,125 @@ export type LayoutProps = { const drawerWidth = 340; const headerHeight = 70; +const DrawerHeader = styled("div")(({ theme }) => ({ + display: "flex", + alignItems: "center", + padding: theme.spacing(0, 1), + // necessary for content to be below app bar + ...theme.mixins.toolbar, + justifyContent: "flex-end", +})); + +const Main = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{ + open?: boolean; +}>(({ theme, open }) => ({ + flexGrow: 1, + padding: theme.spacing(3), + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + marginLeft: `-${drawerWidth}px`, + ...(open && { + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + marginLeft: 0, + }), +})); + +interface AppBarProps extends MuiAppBarProps { + open?: boolean; +} + +const AppBar = styled(MuiAppBar, { + shouldForwardProp: (prop) => prop !== "open", +})(({ theme, open }) => ({ + transition: theme.transitions.create(["margin", "width"], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + ...(open && { + width: `calc(100% - ${drawerWidth}px)`, + marginLeft: `${drawerWidth}px`, + transition: theme.transitions.create(["margin", "width"], { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + }), +})); + export function Layout({ children, title }: LayoutProps) { - const [mobileOpen, setMobileOpen] = useState(false); + const [menuOpen, setMenuOpen] = useState(true); const handleDrawerToggle = () => { - setMobileOpen(!mobileOpen); + setMenuOpen(!menuOpen); }; + const theme = useTheme(); return ( {title} | Admin cdtn - + + + {theme.direction === "ltr" ? ( + + ) : ( + + )} + + + + + + + + - - - - {title} + + + + + {title} + - - - - - - - - - - - - - - - +
{children} - +
); } diff --git a/targets/frontend/src/components/utils/BreadcrumbLink.tsx b/targets/frontend/src/components/utils/BreadcrumbLink.tsx index 417dea614..610ae769a 100644 --- a/targets/frontend/src/components/utils/BreadcrumbLink.tsx +++ b/targets/frontend/src/components/utils/BreadcrumbLink.tsx @@ -1,5 +1,31 @@ +import { Typography } from "@mui/material"; import Link from "next/link"; +import { styled } from "@mui/material/styles"; +import Breadcrumbs from "@mui/material/Breadcrumbs"; + +export const Breadcrumb = ({ + children, +}: { + children: JSX.Element | JSX.Element[]; +}): JSX.Element => ( + + {children} + +); + +const StyledBreadcrumbs = styled(Breadcrumbs)(() => { + return { + "ol > li::marker": { + content: '""', + }, + }; +}); + export const BreadcrumbLink = ({ href, children, @@ -10,16 +36,14 @@ export const BreadcrumbLink = ({ target?: string; }): JSX.Element => { return ( -
  • + <> {href ? ( - + {children} ) : ( - - {children} - + {children} )} -
  • + ); }; diff --git a/targets/frontend/src/components/utils/TitleBox.tsx b/targets/frontend/src/components/utils/TitleBox.tsx deleted file mode 100644 index 06c52f81a..000000000 --- a/targets/frontend/src/components/utils/TitleBox.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Box, FormLabel } from "@mui/material"; -import styled, { css } from "styled-components"; - -export const TitleBox = ({ - title, - focus = false, - children, -}: { - title: string; - focus?: boolean; - children?: JSX.Element | JSX.Element[] | null; -}): JSX.Element => { - return ( - <> - - {title} - {children} - - - ); -}; - -const StyledBox = styled(Box)` - margin-top: 24px; - border: 1px solid #9e9e9e; - border-radius: 6px; - padding: 12px; - position: relative; - ${({ focus }: { focus: boolean }) => { - if (focus) { - return css` - border-color: #1565c0; - `; - } - }} -`; - -const StyledFormLabel = styled(FormLabel)` - position: absolute; - font-size: 12px; - background-color: white; - border-radius: 6px; - padding: 0 4px; - top: -8px; - left: 8px; - ${({ focus }: { focus: boolean }) => { - if (focus) { - return css` - color: #1565c0; - `; - } - }} -`; diff --git a/targets/frontend/src/components/utils/index.ts b/targets/frontend/src/components/utils/index.ts index c081fe339..096d75dce 100644 --- a/targets/frontend/src/components/utils/index.ts +++ b/targets/frontend/src/components/utils/index.ts @@ -1,3 +1,2 @@ export * from "./Pagination"; -export * from "./TitleBox"; export * from "./BreadcrumbLink"; diff --git a/targets/hasura/metadata/databases/default/tables/contribution_questions.yaml b/targets/hasura/metadata/databases/default/tables/contribution_questions.yaml index 85f227409..e4cb80ac6 100644 --- a/targets/hasura/metadata/databases/default/tables/contribution_questions.yaml +++ b/targets/hasura/metadata/databases/default/tables/contribution_questions.yaml @@ -21,6 +21,7 @@ insert_permissions: - content - id - message_id + - order select_permissions: - role: super permission: @@ -28,6 +29,7 @@ select_permissions: - content - id - message_id + - order filter: {} allow_aggregations: true update_permissions: @@ -36,6 +38,7 @@ update_permissions: columns: - content - message_id + - order filter: {} check: null delete_permissions: diff --git a/targets/hasura/migrations/default/1695213519884_alter_table_contribution_questions_add_column_order/down.sql b/targets/hasura/migrations/default/1695213519884_alter_table_contribution_questions_add_column_order/down.sql new file mode 100644 index 000000000..4b0b2c08c --- /dev/null +++ b/targets/hasura/migrations/default/1695213519884_alter_table_contribution_questions_add_column_order/down.sql @@ -0,0 +1,2 @@ +alter table "contribution"."questions" drop constraint "questions_order_key"; +alter table "contribution"."questions" drop column "index"; diff --git a/targets/hasura/migrations/default/1695213519884_alter_table_contribution_questions_add_column_order/up.sql b/targets/hasura/migrations/default/1695213519884_alter_table_contribution_questions_add_column_order/up.sql new file mode 100644 index 000000000..5169af959 --- /dev/null +++ b/targets/hasura/migrations/default/1695213519884_alter_table_contribution_questions_add_column_order/up.sql @@ -0,0 +1,9 @@ +alter table "contribution"."questions" +add column "order" integer null; +alter table "contribution"."questions" +add constraint "questions_order_key" unique ("order"); +update contribution.questions +set "order" = q2."index" +from contrib.questions q2 +where q2.value = "content" + and q2."index" <> 55; diff --git a/yarn.lock b/yarn.lock index cfc911f5e..68667d04d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16974,7 +16974,7 @@ style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -styled-components@^5.3.0, styled-components@^5.3.9: +styled-components@^5.3.0: version "5.3.11" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.11.tgz#9fda7bf1108e39bf3f3e612fcc18170dedcd57a8" integrity sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==