mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 04:55:06 +09:00
use tooltips and toasts & minor fixes / cleanup
This commit is contained in:
1 parent
5abf9d558c
commit
110fbfb91f
2 files changed
+81
-43
No files matched your search
@@ -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<Fetcher["state"]>("idle");
|
||||
|
||||
if (currentPageId === undefined || project === undefined) {
|
||||
return null;
|
||||
@@ -148,15 +148,19 @@ export const TabContent = (props: TabContentProps) => {
|
||||
<>
|
||||
<Pages
|
||||
onClose={() => props.onSetActiveTab("none")}
|
||||
onNewPage={() => setNewPageOpen((value) => !value)}
|
||||
onNewPage={() => setNewPageOpen(true)}
|
||||
onSelect={handleSelect}
|
||||
selectedPageId={currentPageId}
|
||||
/>
|
||||
<SettingsPanel isOpen={newPageOpen}>
|
||||
<NewPageSettings
|
||||
fetcher={newPageFetcher}
|
||||
projectId={project.id}
|
||||
onClose={() => setNewPageOpen(false)}
|
||||
onClose={() => {
|
||||
if (newPageState === "idle") {
|
||||
setNewPageOpen(false);
|
||||
}
|
||||
}}
|
||||
onFetcherStateChange={setNewPageState}
|
||||
onSuccess={(page) => {
|
||||
setNewPageOpen(false);
|
||||
handleSelect(page.id);
|
||||
|
||||
@@ -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<PutPageData>
|
||||
): 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<typeof Tooltip>["children"];
|
||||
}) => {
|
||||
const content = errors?.map((error, i) => <div key={i}>{error}</div>);
|
||||
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 <TextField> will remount which will cause focus loss
|
||||
// and current value loss.
|
||||
<Tooltip content={content || " "} open={errors !== undefined}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewPageSettings = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
fetcher,
|
||||
onFetcherStateChange,
|
||||
projectId,
|
||||
}: {
|
||||
onClose?: () => void;
|
||||
onSuccess?: (page: Page) => void;
|
||||
fetcher: FetcherWithComponents<PutPageData>;
|
||||
onFetcherStateChange?: (
|
||||
state: FetcherWithComponents<unknown>["state"]
|
||||
) => void;
|
||||
projectId: string;
|
||||
}) => {
|
||||
const nameFieldId = useId();
|
||||
const pathFieldId = useId();
|
||||
|
||||
const errors = getErrors(fetcher);
|
||||
const fetcher = useFetcher<PutPageData>();
|
||||
|
||||
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 = ({
|
||||
/>
|
||||
<Box css={{ overflow: "auto", padding: "$2 $3" }}>
|
||||
<fetcher.Form method="put" action={`/rest/pages/${projectId}`}>
|
||||
{/* @todo: banner? toast? */}
|
||||
{errors !== undefined &&
|
||||
errors.formErrors.length > 0 &&
|
||||
JSON.stringify(errors.formErrors)}
|
||||
|
||||
<Group>
|
||||
<Label htmlFor={nameFieldId} size={2}>
|
||||
Page Name
|
||||
</Label>
|
||||
<TextField id={nameFieldId} name="name" disabled={isSubmitting} />
|
||||
{/* @todo: tooltip */}
|
||||
{errors !== undefined &&
|
||||
errors.fieldErrors.name !== undefined &&
|
||||
JSON.stringify(errors.fieldErrors.name)}
|
||||
<ErrorsTooltip errors={fieldErrors.name}>
|
||||
<TextField
|
||||
state={fieldErrors.name && "invalid"}
|
||||
id={nameFieldId}
|
||||
name="name"
|
||||
disabled={isSubmitting}
|
||||
onChange={() => {
|
||||
setFieldErrors(({ name, ...rest }) => rest);
|
||||
}}
|
||||
/>
|
||||
</ErrorsTooltip>
|
||||
</Group>
|
||||
<Group>
|
||||
<Label htmlFor={pathFieldId} size={2}>
|
||||
Path
|
||||
</Label>
|
||||
<TextField id={pathFieldId} name="path" disabled={isSubmitting} />
|
||||
{/* @todo: tooltip */}
|
||||
{errors !== undefined &&
|
||||
errors.fieldErrors.path !== undefined &&
|
||||
JSON.stringify(errors.fieldErrors.path)}
|
||||
<ErrorsTooltip errors={fieldErrors.path}>
|
||||
<TextField
|
||||
state={fieldErrors.path && "invalid"}
|
||||
id={pathFieldId}
|
||||
name="path"
|
||||
disabled={isSubmitting}
|
||||
onChange={() => {
|
||||
setFieldErrors(({ path, ...rest }) => rest);
|
||||
}}
|
||||
/>
|
||||
</ErrorsTooltip>
|
||||
</Group>
|
||||
<Group css={{ alignItems: "end" }}>
|
||||
<Button type="submit" variant="green" disabled={isSubmitting}>
|
||||
|
||||
Reference in new issue
Block a user