Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions src/components/declaration/DeclarationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type DeclarationFormProps = {
showLayoutComponent?: boolean;
isAiGenerated?: boolean;
mentionText?: string;
headerAction?: React.ReactNode;
};

export default function DeclarationForm({
Expand All @@ -33,6 +34,7 @@ export default function DeclarationForm({
showLayoutComponent = false,
isAiGenerated = false,
mentionText,
headerAction,
}: DeclarationFormProps) {
const { classes, cx } = useStyles();
const { classes: formClasses } = useAppStyles();
Expand All @@ -55,15 +57,17 @@ export default function DeclarationForm({
</>
)}
</p>
{isEditable && onToggleEdit && (
<Button
priority="secondary"
onClick={onToggleEdit}
{...(readOnly && { iconId: "fr-icon-edit-line" })}
>
{readOnly ? "Modifier" : "Annuler"}
</Button>
)}
<div className={classes.headerActionsWrapper}>
{isEditable && onToggleEdit && (
<Button
priority="secondary"
onClick={onToggleEdit}
{...(readOnly && { iconId: "fr-icon-edit-line" })}
>
{readOnly ? "Modifier" : "Annuler"}
</Button>
)}
</div>
</div>
{children}
</div>
Expand All @@ -90,6 +94,7 @@ export default function DeclarationForm({
<h1>
{breadcrumbLabel ?? ""} - {title}
</h1>
{headerAction}
</div>
{isAiGenerated && <VerifyGeneratedInfoHelpingMessage />}
<Layout>
Expand All @@ -109,6 +114,12 @@ const useStyles = tss.withName(DeclarationForm.name).create({
display: "flex",
flexDirection: "column",
},
headerActionsWrapper: {
display: "flex",
flexDirection: "row",
gap: fr.spacing("3v"),
alignItems: "center",
},
editButtonWrapper: {
display: "flex",
flexDirection: "row",
Expand Down
4 changes: 2 additions & 2 deletions src/components/declaration/Demarches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export default function Demarches({ declaration }: DemarchesProps) {
const actionsButtons = useMemo(() => {
const buttons: HelpingMessageProps["actionButtons"] = [
{
label: "Prévisualiser et publier",
children: "Prévisualiser et publier",
priority: "primary",
iconId: "fr-icon-upload-line",
onClick: () => router.push(`${declaration.id}/preview`),
Expand All @@ -187,7 +187,7 @@ export default function Demarches({ declaration }: DemarchesProps) {

if (isModified) {
buttons.unshift({
label: "Annuler les modifications",
children: "Annuler les modifications",
priority: "secondary",
iconId: "fr-icon-arrow-go-back-line",
onClick: () => revertToPublished({ id: declaration.id }),
Expand Down
110 changes: 59 additions & 51 deletions src/components/declaration/HelpingMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,80 @@
import { fr } from "@codegouvfr/react-dsfr";
import Button from "@codegouvfr/react-dsfr/Button";
import type {
FrIconClassName,
RiIconClassName,
} from "@codegouvfr/react-dsfr/fr/generatedFromCss/classNames";
import Button, { type ButtonProps } from "@codegouvfr/react-dsfr/Button";
import { tss } from "tss-react";

export interface HelpingMessageProps {
image: React.ReactNode;
message: string | React.ReactNode;
actionButtons?: {
label: string;
priority?: "primary" | "secondary";
iconId?: FrIconClassName | RiIconClassName;
onClick?: () => void;
}[];
actionButtons?: ButtonProps[];
params?: { flexDirection: "column" | "row" };
}

export default function HelpingMessage({
image,
message,
actionButtons,
params,
}: HelpingMessageProps) {
const { classes } = useStyles();
const { classes } = useStyles({
buttonsDirection: params?.flexDirection || "row",
});

return (
<div className={classes.helpingMessageContainer}>
{image}
<p className={classes.messageWrapper}>{message}</p>
{!!actionButtons?.length && (
<div className={classes.buttonsContainer}>
{actionButtons?.map((button) => (
<Button
key={button.label}
priority={button.priority || "primary"}
iconId={button.iconId as any}
onClick={button.onClick}
>
{button.label}
</Button>
))}
</div>
)}
<div>{image}</div>
<div className={classes.helpingMessageContent}>
<p className={classes.messageWrapper}>{message}</p>
{!!actionButtons?.length && (
<div className={classes.buttonsContainer}>
{actionButtons?.map((button) => (
<Button
key={button.children?.toString()}
priority={button.priority || "primary"}
iconId={button.iconId as any}
onClick={button.onClick}
>
{button.children}
</Button>
))}
</div>
)}
</div>
</div>
);
}

const useStyles = tss.withName(HelpingMessage.name).create({
helpingMessageContainer: {
display: "grid",
padding: fr.spacing("6v"),
gap: fr.spacing("6v"),
backgroundColor: fr.colors.decisions.background.contrast.blueFrance.default,
"@media (min-width: 830px)": {
gridTemplateColumns: "auto auto 1fr",
const useStyles = tss
.withName(HelpingMessage.name)
.withParams<{
buttonsDirection: "column" | "row";
}>()
.create(({ buttonsDirection }) => ({
helpingMessageContainer: {
display: "flex",
alignItems: "center",
padding: fr.spacing("6v"),
gap: fr.spacing("6v"),
backgroundColor:
fr.colors.decisions.background.contrast.blueFrance.default,
},
},
messageWrapper: {
display: "flex",
alignItems: "flex-start",
flexDirection: "column",
justifyContent: "center",
margin: 0,
},
buttonsContainer: {
display: "flex",
gap: fr.spacing("4v"),
alignItems: "center",
justifyContent: "flex-end",
},
});
helpingMessageContent: {
width: "100%",
display: "flex",
justifyContent: "space-between",
flexDirection: buttonsDirection,
gap: fr.spacing("4v"),
},
messageWrapper: {
display: "flex",
alignItems: "flex-start",
flexDirection: "column",
justifyContent: "center",
margin: 0,
},
buttonsContainer: {
display: "flex",
gap: fr.spacing("4v"),
alignItems: "center",
justifyContent: buttonsDirection === "column" ? "flex-start" : "flex-end",
},
}));
148 changes: 148 additions & 0 deletions src/components/modal/UpdateAuditFromAraModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { fr } from "@codegouvfr/react-dsfr";
import Alert from "@codegouvfr/react-dsfr/Alert";
import { createModal } from "@codegouvfr/react-dsfr/Modal";
import { useIsModalOpen } from "@codegouvfr/react-dsfr/Modal/useIsModalOpen";
import { useStore } from "@tanstack/react-form";
import { useEffect, useId, useState } from "react";
import { tss } from "tss-react";
import z from "zod";
import { api, type RouterOutputs } from "~/utils/api";
import { useAppForm } from "~/utils/form/context";

export type UpdateAuditFromAraModalActions = {
open?: () => void;
};

export type AraFetchedData = RouterOutputs["declaration"]["getInfoFromAra"]["data"];

const updateAuditFromAraFormSchema = z.object({
araUrl: z.string().min(1, { message: "L'URL de l'audit Ara est requise" }),
});

type UpdateAuditFromAraForm = z.infer<typeof updateAuditFromAraFormSchema>;

interface UpdateAuditFromAraModalProps {
onAraDataFetched: (data: AraFetchedData) => void;
actions: UpdateAuditFromAraModalActions;
}

export function UpdateAuditFromAraModal({
onAraDataFetched,
actions,
}: UpdateAuditFromAraModalProps) {
const { classes } = useStyles();
const id = useId();

const [modal] = useState(() =>
createModal({
id: `updateAuditFromAraModal-${id}`,
isOpenedByDefault: false,
}),
);

const {
mutateAsync: getInfoFromAra,
isPending,
isError,
} = api.declaration.getInfoFromAra.useMutation();

const form = useAppForm({
defaultValues: { araUrl: "" } as UpdateAuditFromAraForm,
validators: { onSubmit: updateAuditFromAraFormSchema },
onSubmit: async ({ value }) => {
const araId = value.araUrl.slice(value.araUrl.lastIndexOf("/") + 1);
const result = await getInfoFromAra({ id: araId });
if (result?.data) {
onAraDataFetched(result.data);
modal.close();
form.reset();
}
},
});

const canSubmit = useStore(
form.store,
(state) => state.isValid && !state.isPristine,
);

useEffect(() => {
actions.open = () => {
form.reset();
modal.open();
};
}, []);

useIsModalOpen(modal, {
onConceal: () => {
form.reset();
},
});

return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
onInvalid={() => form.validate("submit")}
>
<modal.Component
buttons={[
{ children: "Annuler", type: "button", disabled: isPending },
{
children: "Mettre à jour les informations",
type: "submit",
doClosesModal: false,
disabled: isPending || !canSubmit,
},
]}
title="Mettre à jour depuis Ara"
>
<section id="modal-header" className={classes.modalHeader}>
{(isPending || isError) && (
<Alert
small
severity={isError ? "error" : "info"}
description={
isError
? "Une erreur est survenue dans l’import. Nous vous invitons a réessayer."
: "Importation en cours, cela peut prendre quelques secondes"
}
/>
)}
<p className={classes.modalSubheading}>
<strong>L’intégralité des informations</strong> d’audit, de contact
et de plans d’action <strong>seront mises à jour</strong>.
</p>
</section>
<form.AppField name="araUrl">
{(field) => (
<field.TextField
label="Lien URL de la déclaration Ara (obligatoire)"
hintText="Format attendu : https://ara.numerique.gouv.fr/declaration/xxxxxxx"
nativeInputProps={{ type: "url" }}
required
/>
)}
</form.AppField>
</modal.Component>
</form>
);
}

const useStyles = tss.withName("UpdateAuditFromAraModal").create({
modalHeader: {
display: "flex",
flexDirection: "column",
gap: fr.spacing("6v"),
},
modalHeading: {
fontSize: "1.5rem",
lineHeight: "2rem",
marginBottom: 0,
},
modalSubheading: {
fontWeight: 400,
},
});
Loading