Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/config events #626

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
19 changes: 19 additions & 0 deletions assets/@types/alert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface IApiAlert {
id: string;
title: string;
description?: string;
link: { url?: string; label?: string };
severity: "info" | "warning" | "alert";
details?: string;
date: string;
visibility: {
homepage: boolean;
contact: boolean;
map: boolean;
serviceLevel: boolean;
};
}

export interface IAlert extends Omit<IApiAlert, "date"> {
date: Date;
}
5 changes: 4 additions & 1 deletion assets/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FC } from "react";
import ErrorBoundary from "./components/Utils/ErrorBoundary";
import RouterRenderer from "./router/RouterRenderer";
import { RouteProvider } from "./router/router";
import AlertProvider from "./components/Provider/AlertProvider";

const queryClient = new QueryClient();

Expand All @@ -21,7 +22,9 @@ const App: FC = () => {

<RouteProvider>
<ErrorBoundary>
<RouterRenderer />
<AlertProvider>
<RouterRenderer />
</AlertProvider>
</ErrorBoundary>
</RouteProvider>
</PersistQueryClientProvider>
Expand Down
12 changes: 7 additions & 5 deletions assets/components/Input/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { fr } from "@codegouvfr/react-dsfr";
import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui";
import { cx } from "@codegouvfr/react-dsfr/tools/cx";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3";
import { DatePicker as MuiDatePicker } from "@mui/x-date-pickers/DatePicker";
import { DatePicker as MuiDatePicker, DatePickerProps as MuiDatePickerProps } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider/LocalizationProvider";
import { enGB as enGBLocale, fr as frLocale } from "date-fns/locale";
import { useId } from "react";
Expand All @@ -12,7 +12,7 @@ import { useLang } from "../../i18n/i18n";

const locales = { en: enGBLocale, fr: frLocale };

type DatePickerProps = {
interface DatePickerProps extends Omit<MuiDatePickerProps<Date, false>, "onChange"> {
id?: string;
label: string;
hintText?: string;
Expand All @@ -22,10 +22,10 @@ type DatePickerProps = {
minDate?: Date;
onChange?: (value: Date | undefined) => void;
className?: string;
};
}

const DatePicker = (props: DatePickerProps) => {
const { id, label, hintText, state, stateRelatedMessage, value, minDate, onChange, className } = props;
const { id, label, hintText, state, stateRelatedMessage, value, minDate, onChange, className, ...datePickerProps } = props;

const { lang } = useLang();

Expand All @@ -45,8 +45,10 @@ const DatePicker = (props: DatePickerProps) => {
</label>
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={locales[lang]}>
<MuiDatePicker
{...datePickerProps}
slotProps={{
field: { clearable: true, onClear: () => onChange?.(undefined) },
...datePickerProps.slotProps,
field: { ...datePickerProps.slotProps?.field, clearable: true, onClear: () => onChange?.(undefined) },
}}
sx={{ width: "100%" }}
timezone={"UTC"}
Expand Down
13 changes: 11 additions & 2 deletions assets/components/Input/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Markdown } from "tiptap-markdown";
import "../../sass/components/tiptap.scss";

type MarkdownEditorProps = {
className?: string;
label?: string;
hintText?: string;
value: string;
Expand All @@ -22,11 +23,19 @@ type MarkdownEditorProps = {
};

const MarkdownEditor: FC<MarkdownEditorProps> = (props) => {
const { label, hintText, value, state, stateRelatedMessage, placeholder = "", onChange } = props;
const { className, label, hintText, value, state, stateRelatedMessage, placeholder = "", onChange } = props;
const { isDark } = useIsDark();

const classNames = [fr.cx("fr-input-group")];
if (state === "error") {
classNames.push("fr-input-group--error");
}
if (className) {
classNames.push(className);
}

return (
<div className={fr.cx("fr-input-group", state === "error" && "fr-input-group--error")} data-color-mode={isDark ? "dark" : "light"}>
<div className={classNames.join(" ")} data-color-mode={isDark ? "dark" : "light"}>
{label && (
<label className={fr.cx("fr-label")}>
{label}
Expand Down
9 changes: 5 additions & 4 deletions assets/components/Layout/Main.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { fr } from "@codegouvfr/react-dsfr";
import { Breadcrumb, BreadcrumbProps } from "@codegouvfr/react-dsfr/Breadcrumb";
import { Notice } from "@codegouvfr/react-dsfr/Notice";
import { PropsWithChildren, ReactNode, memo, useContext, useMemo } from "react";
import { PropsWithChildren, memo, useContext, useMemo } from "react";

import getBreadcrumb from "../../modules/entrepot/breadcrumbs/Breadcrumb";
import { useRoute } from "../../router/router";
import SessionExpiredAlert from "../Utils/SessionExpiredAlert";
import useDocumentTitle from "../../hooks/useDocumentTitle";
import { datastoreContext } from "../../contexts/datastore";
import { IUseAlert } from "@/hooks/useAlert";

export interface MainProps {
customBreadcrumbProps?: BreadcrumbProps;
infoBannerMsg?: ReactNode;
noticeProps?: IUseAlert;
title?: string;
}

function Main(props: PropsWithChildren<MainProps>) {
const { children, customBreadcrumbProps, infoBannerMsg, title } = props;
const { children, customBreadcrumbProps, noticeProps, title } = props;
const route = useRoute();
useDocumentTitle(title);
const datastoreValue = useContext(datastoreContext);
Expand All @@ -32,7 +33,7 @@ function Main(props: PropsWithChildren<MainProps>) {
return (
<main id="main" role="main">
{/* doit être le premier élément atteignable après le lien d'évitement (Accessibilité) : https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/bandeau-d-information-importante */}
{infoBannerMsg && <Notice title={infoBannerMsg} isClosable={true} />}
{noticeProps && <Notice isClosable {...noticeProps} />}

<div className={fr.cx("fr-container", "fr-my-2w")}>
{breadcrumbProps && <Breadcrumb {...breadcrumbProps} />}
Expand Down
242 changes: 242 additions & 0 deletions assets/components/Modal/CreateAlert/CreateAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { FC, useEffect } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { symToStr } from "tsafe/symToStr";
import * as yup from "yup";
import { fr } from "@codegouvfr/react-dsfr";
import Input from "@codegouvfr/react-dsfr/Input";
import { Select } from "@codegouvfr/react-dsfr/SelectNext";
import ToggleSwitch from "@codegouvfr/react-dsfr/ToggleSwitch";
import isURL from "validator/lib/isURL";

import { useTranslation } from "../../../i18n";
import { IAlert } from "../../../@types/alert";

import PreviewAlert from "./PreviewAlert";
import { ModalProps } from "@codegouvfr/react-dsfr/Modal";
import { formatDateTimeLocal } from "../../../utils";
import MarkdownEditor from "../../Input/MarkdownEditor";

const date = new Date();
export const alertSchema = yup.object({
id: yup.string().required(),
title: yup.string().required("Titre requis"),
description: yup.string(),
link: yup.object({
label: yup.string(),
url: yup.string().test("check-url", "La chaîne doit être une url valide", (value) => value === "" || value?.startsWith("/") || isURL(value)),
}),
severity: yup.string().oneOf(["info", "warning", "alert"]).required("Sévérité requise"),
details: yup.string(),
date: yup.date().required("Date requise"),
visibility: yup.object({
homepage: yup.boolean().required(),
contact: yup.boolean().required(),
map: yup.boolean().required(),
serviceLevel: yup.boolean().required(),
}),
});

const severityOptions = [
{ value: "info", label: "info" },
{ value: "warning", label: "warning" },
{ value: "alert", label: "alert" },
];

interface CreateAlertProps {
alert: IAlert;
isEdit: boolean;
ModalComponent: (props: ModalProps) => JSX.Element;
onSubmit: (alert: IAlert) => void;
}

const CreateAlert: FC<CreateAlertProps> = (props) => {
const { alert, isEdit, ModalComponent, onSubmit } = props;
const { t } = useTranslation("alerts");

const methods = useForm({
mode: "onSubmit",
defaultValues: alert,
resolver: yupResolver(alertSchema),
});
const {
control,
formState: { errors },
handleSubmit,
register,
setValue,
} = methods;

useEffect(() => {
setValue("visibility", alert.visibility);
}, [alert.visibility, setValue]);

const addAlert = handleSubmit((values) => {
onSubmit(values);
});

return (
<ModalComponent
title={isEdit ? t("edit_alert") : t("create_alert")}
size="large"
buttons={[
{
doClosesModal: true,
children: t("modal.cancel"),
},
{
doClosesModal: false,
children: isEdit ? t("modal.edit") : t("modal.add"),
onClick: addAlert,
},
]}
>
<FormProvider {...methods}>
<form onSubmit={addAlert}>
<PreviewAlert />
<Input
label={t("alert.title")}
nativeInputProps={{
...register("title"),
}}
state={errors.title ? "error" : "default"}
stateRelatedMessage={errors?.title?.message?.toString()}
/>
<Input
label={t("alert.description")}
nativeInputProps={{
...register("description"),
}}
state={errors.description ? "error" : "default"}
stateRelatedMessage={errors?.description?.message?.toString()}
/>
<div className={fr.cx("fr-grid-row", "fr-grid-row--gutters")}>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<Input
label={t("alert.linkLabel")}
nativeInputProps={{
...register("link.label"),
}}
state={errors.link?.label ? "error" : "default"}
stateRelatedMessage={errors?.link?.label?.message?.toString()}
/>
</div>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<Input
label={t("alert.linkUrl")}
nativeInputProps={{
...register("link.url"),
}}
state={errors.link?.url ? "error" : "default"}
stateRelatedMessage={errors?.link?.url?.message?.toString()}
/>
</div>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<Select
label={t("alert.severity")}
nativeSelectProps={{
...register("severity"),
}}
options={severityOptions}
placeholder="Select an option"
state={errors.severity ? "error" : "default"}
stateRelatedMessage={errors?.severity?.message?.toString()}
/>
</div>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<Controller
control={control}
name="date"
render={({ field: { onChange, value } }) => (
<Input
label={t("alert.date")}
nativeInputProps={{
value: formatDateTimeLocal(value),
min: formatDateTimeLocal(date),
type: "datetime-local",
onChange,
}}
state={errors.date ? "error" : "default"}
stateRelatedMessage={errors.date?.message?.toString()}
/>
)}
/>
</div>
</div>
<Controller
control={control}
name="details"
render={({ field: { onChange, value } }) => (
<MarkdownEditor
className="fr-mt-3w"
label={t("alert.details")}
hintText={t("alert.details_hint")}
state={errors.details ? "error" : "default"}
stateRelatedMessage={errors?.details?.message?.toString()}
value={value ?? ""}
onChange={onChange}
/>
)}
/>
<div className={fr.cx("fr-grid-row", "fr-grid-row--gutters")}>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<div className={fr.cx("fr-input-group")}>
<Controller
control={control}
name="visibility.homepage"
render={({ field: { onChange, value } }) => (
<ToggleSwitch inputTitle={t("alert.homepage")} label={t("alert.homepage")} onChange={onChange} checked={value} />
)}
/>
</div>
</div>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<div className={fr.cx("fr-input-group")}>
<Controller
control={control}
name="visibility.contact"
render={({ field: { onChange, value } }) => (
<ToggleSwitch inputTitle={t("alert.contact")} label={t("alert.contact")} onChange={onChange} checked={value} />
)}
/>
</div>
</div>
</div>
<div className={fr.cx("fr-grid-row", "fr-grid-row--gutters")}>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<div className={fr.cx("fr-input-group")}>
<Controller
control={control}
name="visibility.map"
render={({ field: { onChange, value } }) => (
<ToggleSwitch inputTitle={t("alert.map")} label={t("alert.map")} onChange={onChange} checked={value} />
)}
/>
</div>
</div>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<div className={fr.cx("fr-input-group")}>
<Controller
control={control}
name="visibility.serviceLevel"
render={({ field: { onChange, value } }) => (
<ToggleSwitch
inputTitle={t("alert.serviceLevel")}
label={t("alert.serviceLevel")}
onChange={onChange}
checked={value}
/>
)}
/>
</div>
</div>
</div>
<input type="submit" hidden />
</form>
</FormProvider>
</ModalComponent>
);
};
CreateAlert.displayName = symToStr({ CreateAlert });

export default CreateAlert;
Loading