diff --git a/components/form-builder/app/edit/Edit.tsx b/components/form-builder/app/edit/Edit.tsx index c0e0b82753..f2bb52dc9d 100644 --- a/components/form-builder/app/edit/Edit.tsx +++ b/components/form-builder/app/edit/Edit.tsx @@ -12,6 +12,7 @@ import { useTemplateStore } from "../../store"; import { getQuestionNumber, sortByLayout } from "../../util"; import { Panel } from "../settings-modal/panel"; import { cleanInput } from "@formbuilder/util"; +import { SaveButton } from "../shared/SaveButton"; export const Edit = () => { const { t } = useTranslation("form-builder"); @@ -85,6 +86,9 @@ export const Edit = () => { return ( <>

{t("edit")}

+
+ +
{ const { t } = useTranslation("form-builder"); const { activePathname } = useActivePathname(); - return (
diff --git a/components/form-builder/app/navigation/LeftNavigation.tsx b/components/form-builder/app/navigation/LeftNavigation.tsx index 97dcbd2832..ed634d6204 100644 --- a/components/form-builder/app/navigation/LeftNavigation.tsx +++ b/components/form-builder/app/navigation/LeftNavigation.tsx @@ -2,7 +2,6 @@ import React from "react"; import { useTranslation } from "next-i18next"; import { DesignIcon, PreviewIcon, PublishIcon, GearIcon, MessageIcon } from "../../icons"; import { useTemplateContext } from "@components/form-builder/hooks"; -import { SaveButton } from "../shared/SaveButton"; import { useTemplateStore } from "../../store/useTemplateStore"; import { useSession } from "next-auth/react"; import { useActivePathname, cleanPath } from "../../hooks/useActivePathname"; @@ -67,11 +66,6 @@ export const LeftNavigation = () => { {t("responsesNavLabel")} - {!isPublished && activePathname === "/form-builder/edit" && ( -
  • - -
  • - )} ); diff --git a/components/form-builder/app/shared/DownloadFileButton.tsx b/components/form-builder/app/shared/DownloadFileButton.tsx index d96c215952..937b6e8f7b 100644 --- a/components/form-builder/app/shared/DownloadFileButton.tsx +++ b/components/form-builder/app/shared/DownloadFileButton.tsx @@ -53,12 +53,14 @@ export const DownloadFileButton = ({ showInfo = true, buttonText, autoShowDialog = false, + theme = "secondary", }: { className?: string; onClick?: any; // eslint-disable-line @typescript-eslint/no-explicit-any showInfo?: boolean; buttonText?: string; autoShowDialog?: boolean; + theme?: "primary" | "secondary"; }) => { const { t, i18n } = useTranslation("form-builder"); const { getSchema, form, name } = useTemplateStore((s) => ({ @@ -114,7 +116,7 @@ export const DownloadFileButton = ({
    + + ); + } + + return ( + <> + + + + {t("saved", { ns: "form-builder" })} + + ); +}; + +export const DateTime = ({ updatedAt }: { updatedAt: number }) => { + const { t, i18n } = useTranslation(["common", "form-builder"]); + const dateTime = + (updatedAt && formatDateTime(new Date(updatedAt).getTime(), `${i18n.language}-CA`)) || []; + + if (!dateTime || dateTime.length < 2) { + return null; + } + + const [date, time] = dateTime; + + return {` - ${t("lastSaved", { ns: "form-builder" })} ${time}, ${date}`}; +}; + +export const ErrorSavingForm = () => { + const { t, i18n } = useTranslation(["common", "form-builder"]); + const supportHref = `/${i18n.language}/form-builder/support`; + return ( + + + + + + {t("errorSavingForm.failedLink", { ns: "form-builder" })} + + + ); +}; export const SaveButton = () => { - const { id } = useTemplateStore((s) => ({ + const { isPublished, id } = useTemplateStore((s) => ({ + isPublished: s.isPublished, id: s.id, })); - const { error, saveForm } = useTemplateContext(); - + const { error, saveForm, templateIsDirty } = useTemplateContext(); + const { activePathname } = useActivePathname(); const { status } = useSession(); - const { t, i18n } = useTranslation(["common", "form-builder"]); - const { isReady, asPath } = useRouter(); - const [isStartPage, setIsStartPage] = useState(false); const { updatedAt, getTemplateById } = useTemplateStatus(); const handleSave = async () => { @@ -30,43 +90,33 @@ export const SaveButton = () => { } }; - useEffect(() => { - if (isReady) { - const activePathname = new URL(asPath, location.href).pathname; - if (activePathname === "/form-builder") { - setIsStartPage(true); - } else { - setIsStartPage(false); - } - } - }, [asPath, isReady]); + if (isPublished) { + return null; + } - const dateTime = - (updatedAt && formatDateTime(new Date(updatedAt).getTime(), `${i18n.language}-CA`)) || []; + const showSave = + activePathname === "/form-builder/edit" || activePathname === "/form-builder/edit/translate"; - return !isStartPage && status === "authenticated" ? ( + if (!showSave) { + return null; + } + + return status === "authenticated" ? (
    - - {error && ( -
    - {error} -
    + {error ? ( + + ) : ( + )} -
    - {dateTime.length == 2 && ( - <> -
    {t("lastSaved", { ns: "form-builder" })}
    -
    - {dateTime[0]} {t("at")} {dateTime[1]}{" "} -
    - - )} -
    + {updatedAt && }
    ) : null; }; diff --git a/components/form-builder/app/shared/Toast.tsx b/components/form-builder/app/shared/Toast.tsx index ae44a7d7ef..0d11d71ad3 100644 --- a/components/form-builder/app/shared/Toast.tsx +++ b/components/form-builder/app/shared/Toast.tsx @@ -51,11 +51,19 @@ type ToastContext = { export const ToastContainer = ({ autoClose = 3000, + width = "", + containerId = "", + limit, }: { autoClose?: number | false | undefined; + width?: string; + containerId?: string; + limit?: number; }) => { return ( { return `${ contextClass[context?.type || "default"]["background"] @@ -69,11 +77,13 @@ export const ToastContainer = ({ }) => { return `${contextClass[context?.type || "default"]["text"]} flex text-base`; }} + style={{ width: width }} position={originalToast.POSITION.TOP_CENTER} autoClose={autoClose} hideProgressBar={true} closeOnClick={true} transition={Bounce} + limit={limit} icon={(context?: ToastContext) => { return contextClass[context?.type || "default"]["icon"]; }} @@ -81,23 +91,27 @@ export const ToastContainer = ({ ); }; +const toastContent = (message: string | JSX.Element) => { + return React.isValidElement(message) ? message :

    {message}

    ; +}; + export const toast = { - success: (message: string) => { - originalToast.success(

    {message}

    ); + success: (message: string | JSX.Element, containerId = "default") => { + originalToast.success(toastContent(message), { containerId }); }, - error: (message: string) => { - originalToast.error(

    {message}

    ); + error: (message: string | JSX.Element, containerId = "default") => { + originalToast.error(toastContent(message), { containerId }); }, - info: (message: string) => { - originalToast.info(

    {message}

    ); + info: (message: string | JSX.Element, containerId = "default") => { + originalToast.info(toastContent(message), { containerId }); }, - warn: (message: string) => { - originalToast.warn(

    {message}

    ); + warn: (message: string | JSX.Element, containerId = "default") => { + originalToast.warn(toastContent(message), { containerId }); }, - warning: (message: string) => { - originalToast.warning(

    {message}

    ); + warning: (message: string | JSX.Element, containerId = "") => { + originalToast.warning(toastContent(message), { containerId }); }, - default: (message: string) => { - originalToast(

    {message}

    ); + default: (message: string | JSX.Element, containerId = "default") => { + originalToast(toastContent(message), { containerId }); }, }; diff --git a/components/form-builder/app/translate/Translate.tsx b/components/form-builder/app/translate/Translate.tsx index 9859cdbdd8..9b0d3aac6e 100644 --- a/components/form-builder/app/translate/Translate.tsx +++ b/components/form-builder/app/translate/Translate.tsx @@ -10,6 +10,7 @@ import { DownloadCSV } from "./DownloadCSV"; import { RichTextEditor } from "../edit/elements/lexical-editor/RichTextEditor"; import { LanguageLabel } from "./LanguageLabel"; import { FieldsetLegend, SectionTitle } from "."; +import { SaveButton } from "../shared/SaveButton"; import { FormElement } from "@lib/types"; import { alphabet, sortByLayout } from "../../util"; @@ -116,6 +117,10 @@ export const Translate = () => {

    {t("translateDescription")}


    +
    + +
    +
    diff --git a/components/form-builder/hooks/useTemplateContext.tsx b/components/form-builder/hooks/useTemplateContext.tsx index a1ed9e8bca..872db61377 100644 --- a/components/form-builder/hooks/useTemplateContext.tsx +++ b/components/form-builder/hooks/useTemplateContext.tsx @@ -4,22 +4,46 @@ import { useTemplateApi } from "../hooks"; import { useTranslation } from "next-i18next"; import { logMessage } from "@lib/logger"; import { useSession } from "next-auth/react"; +import { toast } from "../app/shared/Toast"; +import { StyledLink } from "@components/globals"; +import { DownloadFileButton } from "../app/shared/"; interface TemplateApiType { - error: string | null; + error: string | null | undefined; saveForm: () => Promise; + templateIsDirty: React.MutableRefObject; } const defaultTemplateApi: TemplateApiType = { error: null, saveForm: async () => false, + templateIsDirty: { current: false }, }; const TemplateApiContext = createContext(defaultTemplateApi); +const ErrorSaving = ({ supportHref, errorCode }: { supportHref: string; errorCode?: string }) => { + const { t } = useTranslation("form-builder"); + + return ( +
    +

    {t("errorSavingForm.title")}

    +

    + {t("errorSavingForm.description")}{" "} + {t("errorSavingForm.supportLink")}. +

    +

    + {errorCode && t("errorSavingForm.errorCode", { code: errorCode })} +

    + +
    + ); +}; + export function TemplateApiProvider({ children }: { children: React.ReactNode }) { - const { t } = useTranslation(["form-builder"]); - const [error, setError] = useState(null); + const { t, i18n } = useTranslation(["form-builder"]); + const [error, setError] = useState(); + const supportHref = `/${i18n.language}/form-builder/support`; const { id, getSchema, getName, hasHydrated, setId, getIsPublished } = useTemplateStore((s) => ({ id: s.id, getSchema: s.getSchema, @@ -67,12 +91,13 @@ export function TemplateApiProvider({ children }: { children: React.ReactNode }) } catch (err) { logMessage.error(err as Error); setError(t("errorSaving")); + toast.error(, "wide"); return false; } - }, [status, getIsPublished, getSchema, getName, id, save, setError, setId, t]); + }, [status, getIsPublished, getSchema, getName, id, save, setError, setId, t, supportHref]); return ( - + {children} ); diff --git a/components/form-builder/icons/SavedCheckIcon.tsx b/components/form-builder/icons/SavedCheckIcon.tsx new file mode 100644 index 0000000000..82e8b3d045 --- /dev/null +++ b/components/form-builder/icons/SavedCheckIcon.tsx @@ -0,0 +1,15 @@ +import React from "react"; +export const SavedCheckIcon = ({ className, title }: { className?: string; title?: string }) => ( + + {title && {title}} + + +); diff --git a/components/form-builder/icons/SavedFailIcon.tsx b/components/form-builder/icons/SavedFailIcon.tsx new file mode 100644 index 0000000000..061d7b6f5a --- /dev/null +++ b/components/form-builder/icons/SavedFailIcon.tsx @@ -0,0 +1,18 @@ +import React from "react"; +export const SavedFailIcon = ({ className, title }: { className?: string; title?: string }) => ( + + {title && {title}} + + +); diff --git a/components/form-builder/icons/index.ts b/components/form-builder/icons/index.ts index f5f06e2356..1abb875913 100644 --- a/components/form-builder/icons/index.ts +++ b/components/form-builder/icons/index.ts @@ -76,3 +76,5 @@ export { DeleteIcon } from "./DeleteIcon"; export { InboxIcon } from "./InboxIcon"; export { ForwardArrowIcon } from "./ForwardArrowIcon"; export { StartIcon } from "./StartIcon"; +export { SavedCheckIcon } from "./SavedCheckIcon"; +export { SavedFailIcon } from "./SavedFailIcon"; diff --git a/components/globals/layouts/TwoColumnLayout.tsx b/components/globals/layouts/TwoColumnLayout.tsx index ec262f673f..045c818829 100644 --- a/components/globals/layouts/TwoColumnLayout.tsx +++ b/components/globals/layouts/TwoColumnLayout.tsx @@ -25,12 +25,12 @@ export const TwoColumnLayout = ({
    - + + <>
    {leftColumnContent}
    -