From 110fbfb91f7b503d1dcbf579915ff3bdf0269e57 Mon Sep 17 00:00:00 2001 From: Roman Pominov Date: Sat, 15 Oct 2022 16:51:09 +0400 Subject: [PATCH] use tooltips and toasts & minor fixes / cleanup --- .../sidebar-left/panels/pages/pages.tsx | 14 ++- .../sidebar-left/panels/pages/settings.tsx | 110 ++++++++++++------ 2 files changed, 81 insertions(+), 43 deletions(-) diff --git a/apps/designer/app/designer/features/sidebar-left/panels/pages/pages.tsx b/apps/designer/app/designer/features/sidebar-left/panels/pages/pages.tsx index 8801c574f3..00164bd516 100644 --- a/apps/designer/app/designer/features/sidebar-left/panels/pages/pages.tsx +++ b/apps/designer/app/designer/features/sidebar-left/panels/pages/pages.tsx @@ -1,4 +1,3 @@ -import { useFetcher } from "@remix-run/react"; import { TreeNodeLabel, TreeNode, @@ -19,6 +18,7 @@ import { } from "~/designer/shared/nano-states"; import { SettingsPanel } from "./settings-panel"; import { NewPageSettings } from "./settings"; +import { Fetcher } from "@remix-run/react"; type TabContentProps = { onSetActiveTab: (tabName: TabName) => void; @@ -138,7 +138,7 @@ export const TabContent = (props: TabContentProps) => { }; const [newPageOpen, setNewPageOpen] = useState(false); - const newPageFetcher = useFetcher(); + const [newPageState, setNewPageState] = useState("idle"); if (currentPageId === undefined || project === undefined) { return null; @@ -148,15 +148,19 @@ export const TabContent = (props: TabContentProps) => { <> props.onSetActiveTab("none")} - onNewPage={() => setNewPageOpen((value) => !value)} + onNewPage={() => setNewPageOpen(true)} onSelect={handleSelect} selectedPageId={currentPageId} /> setNewPageOpen(false)} + onClose={() => { + if (newPageState === "idle") { + setNewPageOpen(false); + } + }} + onFetcherStateChange={setNewPageState} onSuccess={(page) => { setNewPageOpen(false); handleSelect(page.id); diff --git a/apps/designer/app/designer/features/sidebar-left/panels/pages/settings.tsx b/apps/designer/app/designer/features/sidebar-left/panels/pages/settings.tsx index d543df9a56..76b3356bb9 100644 --- a/apps/designer/app/designer/features/sidebar-left/panels/pages/settings.tsx +++ b/apps/designer/app/designer/features/sidebar-left/panels/pages/settings.tsx @@ -7,12 +7,14 @@ import { useId, styled, Flex, + toast, + Tooltip, } from "@webstudio-is/design-system"; -import { type FetcherWithComponents } from "@remix-run/react"; +import { useFetcher, type FetcherWithComponents } from "@remix-run/react"; import { ChevronDoubleLeftIcon } from "@webstudio-is/icons"; import type { ZodError } from "zod"; import { BaseHeader } from "../../lib/header"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { type Page } from "@webstudio-is/project"; const Group = styled(Flex, { @@ -25,48 +27,73 @@ export type PutPageData = | { ok: true; page: Page } | { errors: string | ZodError["formErrors"] }; -const getErrors = ( - fetcher: FetcherWithComponents -): ZodError["formErrors"] | undefined => { - if (fetcher.data === undefined || !("errors" in fetcher.data)) { - return undefined; - } - if (typeof fetcher.data.errors === "string") { - return { formErrors: [fetcher.data.errors], fieldErrors: {} }; - } - return fetcher.data.errors; +const ErrorsTooltip = ({ + errors, + children, +}: { + errors: string[] | undefined; + children: React.ComponentProps["children"]; +}) => { + const content = errors?.map((error, i) =>
{error}
); + return ( + // We intentionally always pass non empty content to avoid optimization inside Tooltip + // where it renders {children} directly if content is empty. + // If this optimization accur, the will remount which will cause focus loss + // and current value loss. + + {children} + + ); }; export const NewPageSettings = ({ onClose, onSuccess, - fetcher, + onFetcherStateChange, projectId, }: { onClose?: () => void; onSuccess?: (page: Page) => void; - fetcher: FetcherWithComponents; + onFetcherStateChange?: ( + state: FetcherWithComponents["state"] + ) => void; projectId: string; }) => { const nameFieldId = useId(); const pathFieldId = useId(); - const errors = getErrors(fetcher); + const fetcher = useFetcher(); + + const [fieldErrors, setFieldErrors] = useState< + ZodError["formErrors"]["fieldErrors"] + >({}); const isSubmitting = fetcher.state !== "idle"; const prevFetcher = useRef(fetcher); useEffect(() => { - if ( - prevFetcher.current.state !== "idle" && - fetcher.state === "idle" && - fetcher.data !== undefined && - "ok" in fetcher.data - ) { - onSuccess?.(fetcher.data.page); + if (prevFetcher.current.state !== fetcher.state) { + onFetcherStateChange?.(fetcher.state); + if (fetcher.state === "idle" && fetcher.data !== undefined) { + if ("ok" in fetcher.data) { + onSuccess?.(fetcher.data.page); + } else { + const errors = + typeof fetcher.data.errors === "string" + ? { formErrors: [fetcher.data.errors], fieldErrors: {} } + : fetcher.data.errors; + + if (errors.formErrors.length > 0) { + for (const message of errors.formErrors) { + toast.error(message); + } + } + setFieldErrors(errors.fieldErrors); + } + } } prevFetcher.current = fetcher; - }, [fetcher, onSuccess]); + }, [fetcher, onSuccess, onFetcherStateChange]); return ( <> @@ -84,30 +111,37 @@ export const NewPageSettings = ({ /> - {/* @todo: banner? toast? */} - {errors !== undefined && - errors.formErrors.length > 0 && - JSON.stringify(errors.formErrors)} - - - {/* @todo: tooltip */} - {errors !== undefined && - errors.fieldErrors.name !== undefined && - JSON.stringify(errors.fieldErrors.name)} + + { + setFieldErrors(({ name, ...rest }) => rest); + }} + /> + - - {/* @todo: tooltip */} - {errors !== undefined && - errors.fieldErrors.path !== undefined && - JSON.stringify(errors.fieldErrors.path)} + + { + setFieldErrors(({ path, ...rest }) => rest); + }} + /> +