mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-02 17:54:22 +10:00
refactor(web): answer confirmations and prompts with one dialog
This commit is contained in:
@@ -4,7 +4,7 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { PromptDialogProvider } from "@/hooks/use-prompt";
|
||||
import { ConfirmDialogProvider } from "@/hooks/use-confirm";
|
||||
import { RichTextEditor } from "./rich-text-editor";
|
||||
|
||||
const media = vi.hoisted(() => ({ mobile: false }));
|
||||
@@ -30,9 +30,9 @@ const renderEditor = () =>
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<PromptDialogProvider>
|
||||
<ConfirmDialogProvider>
|
||||
<RichTextEditor label="Description" value="<p>Shipped the redesign.</p>" onChange={() => {}} />
|
||||
</PromptDialogProvider>
|
||||
</ConfirmDialogProvider>
|
||||
</QueryClientProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { cn } from "@reactive-resume/utils/style";
|
||||
import { hasUnsupportedTableMarkup, richTextExtensions } from "@/components/input/rich-text-extensions";
|
||||
import { openAssistantFrom } from "@/features/assistant/open";
|
||||
import { useHasUsableAiProvider } from "@/features/settings/integrations/hooks/use-has-usable-ai-provider";
|
||||
import { usePrompt } from "@/hooks/use-prompt";
|
||||
import { usePrompt } from "@/hooks/use-confirm";
|
||||
import { ImprovePanel, lineAtCaret } from "./improve";
|
||||
|
||||
type ToolbarAction = {
|
||||
|
||||
@@ -27,7 +27,7 @@ import { toast } from "@reactive-resume/ui/components/toast";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { IconPicker } from "@/components/input/icon-picker";
|
||||
import { useCurrentBuilderResumeSelector, useResumeStore, useUpdateResumeData } from "@/features/resume/builder/draft";
|
||||
import { usePrompt } from "@/hooks/use-prompt";
|
||||
import { usePrompt } from "@/hooks/use-confirm";
|
||||
import { getSectionTitle } from "@/libs/resume/section";
|
||||
import { pendingProposals } from "../proposals/proposals";
|
||||
import { useEditorStore } from "../store";
|
||||
|
||||
@@ -9,7 +9,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { parseResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
||||
import { useResumeStore } from "@/features/resume/builder/draft";
|
||||
import { PromptDialogProvider } from "@/hooks/use-prompt";
|
||||
import { ConfirmDialogProvider } from "@/hooks/use-confirm";
|
||||
import { useEditorStore } from "../store";
|
||||
import { WritePanel } from "./write-panel";
|
||||
|
||||
@@ -66,9 +66,9 @@ function renderPanel(edit?: (data: ResumeData) => void) {
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<PromptDialogProvider>
|
||||
<ConfirmDialogProvider>
|
||||
<WritePanel />
|
||||
</PromptDialogProvider>
|
||||
</ConfirmDialogProvider>
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
@@ -19,8 +19,7 @@ import { toast } from "@reactive-resume/ui/components/toast";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { savePendingChanges, useCurrentResume, useResumeStore } from "@/features/resume/builder/draft";
|
||||
import { useEditorStore } from "@/features/resume/editor/store";
|
||||
import { useConfirm } from "@/hooks/use-confirm";
|
||||
import { usePrompt } from "@/hooks/use-prompt";
|
||||
import { useConfirm, usePrompt } from "@/hooks/use-confirm";
|
||||
import { getResumeErrorMessage } from "@/libs/error-message";
|
||||
import { orpc } from "@/libs/orpc/client";
|
||||
import { formatVersionMoment, formatVersionTime, getVersionDetail, getVersionTitle } from "./format";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { ORPCError } from "@orpc/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { PromptDialogProvider } from "@/hooks/use-prompt";
|
||||
import { ConfirmDialogProvider } from "@/hooks/use-confirm";
|
||||
import { LinkTab } from "./link-tab";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -43,7 +43,10 @@ vi.mock("@/libs/orpc/client", () => ({
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@/hooks/use-confirm", () => ({ useConfirm: () => vi.fn() }));
|
||||
vi.mock("@/hooks/use-confirm", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/hooks/use-confirm")>()),
|
||||
useConfirm: () => vi.fn(),
|
||||
}));
|
||||
vi.mock("@reactive-resume/ui/components/toast", () => ({ toast: { add: vi.fn(), close: vi.fn() } }));
|
||||
vi.mock("usehooks-ts", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("usehooks-ts")>()),
|
||||
@@ -65,9 +68,9 @@ function renderTab() {
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
|
||||
<PromptDialogProvider>
|
||||
<ConfirmDialogProvider>
|
||||
<LinkTab />
|
||||
</PromptDialogProvider>
|
||||
</ConfirmDialogProvider>
|
||||
</QueryClientProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button, buttonVariants } from "@reactive-resume/ui/components/button";
|
||||
import { Switch } from "@reactive-resume/ui/components/switch";
|
||||
import { toast } from "@reactive-resume/ui/components/toast";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { usePrompt } from "@/hooks/use-prompt";
|
||||
import { usePrompt } from "@/hooks/use-confirm";
|
||||
import { authClient } from "@/libs/auth/client";
|
||||
import { getReadableErrorMessage } from "@/libs/error-message";
|
||||
import { SettingsRow, SettingsSection } from "../section";
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { PromptDialogProvider, usePrompt } from "./use-prompt";
|
||||
import { ConfirmDialogProvider, usePrompt } from "./use-confirm";
|
||||
|
||||
type HookWrapperProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -13,7 +13,7 @@ beforeAll(() => {
|
||||
i18n.loadAndActivate({ locale: "en", messages: {} });
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: HookWrapperProps) => <PromptDialogProvider>{children}</PromptDialogProvider>;
|
||||
const wrapper = ({ children }: HookWrapperProps) => <ConfirmDialogProvider>{children}</ConfirmDialogProvider>;
|
||||
|
||||
const clickButton = (re: RegExp) => {
|
||||
const buttons = Array.from(document.body.querySelectorAll<HTMLButtonElement>("button"));
|
||||
@@ -21,8 +21,8 @@ const clickButton = (re: RegExp) => {
|
||||
};
|
||||
|
||||
describe("usePrompt", () => {
|
||||
it("throws when used outside PromptDialogProvider", () => {
|
||||
expect(() => renderHook(() => usePrompt())).toThrow(/usePrompt must be used within a <PromptDialogProvider \/>/);
|
||||
it("throws when used outside ConfirmDialogProvider", () => {
|
||||
expect(() => renderHook(() => usePrompt())).toThrow(/must be used within a <ConfirmDialogProvider \/>/);
|
||||
});
|
||||
|
||||
it("returns a function when wrapped in provider", () => {
|
||||
@@ -1,18 +1,23 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { ConfirmDialogProvider, useConfirm } from "./use-confirm";
|
||||
|
||||
type HookWrapperProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
i18n.loadAndActivate({ locale: "en", messages: {} });
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: HookWrapperProps) => <ConfirmDialogProvider>{children}</ConfirmDialogProvider>;
|
||||
|
||||
describe("useConfirm", () => {
|
||||
it("throws when used outside ConfirmDialogProvider", () => {
|
||||
expect(() => renderHook(() => useConfirm())).toThrow(/useConfirm must be used within a <ConfirmDialogProvider \/>/);
|
||||
expect(() => renderHook(() => useConfirm())).toThrow(/must be used within a <ConfirmDialogProvider \/>/);
|
||||
});
|
||||
|
||||
it("returns a confirm function when wrapped in provider", () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import * as React from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -9,72 +10,51 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@reactive-resume/ui/components/alert-dialog";
|
||||
import { Input } from "@reactive-resume/ui/components/input";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
interface ConfirmOptions {
|
||||
type AskOptions = {
|
||||
description?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
}
|
||||
/** Prompts only: the text the field starts with. */
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
interface ConfirmState extends ConfirmOptions {
|
||||
type AskState = AskOptions & {
|
||||
open: boolean;
|
||||
title: string;
|
||||
resolve: ((value: boolean) => void) | null;
|
||||
}
|
||||
|
||||
type ConfirmContextType = {
|
||||
confirm: (title: string, options?: ConfirmOptions) => Promise<boolean>;
|
||||
/** A prompt asks for text; a confirmation only for yes or no. */
|
||||
withInput: boolean;
|
||||
resolve: ((value: string | null) => void) | null;
|
||||
};
|
||||
|
||||
type ConfirmDialogProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
type Ask = (title: string, options: AskOptions | undefined, withInput: boolean) => Promise<string | null>;
|
||||
|
||||
const ConfirmContext = React.createContext<ConfirmContextType | null>(null);
|
||||
const AskContext = React.createContext<Ask | null>(null);
|
||||
|
||||
export function ConfirmDialogProvider({ children }: ConfirmDialogProviderProps) {
|
||||
const [state, setState] = React.useState<ConfirmState>({
|
||||
open: false,
|
||||
resolve: null,
|
||||
title: "",
|
||||
description: undefined,
|
||||
confirmText: undefined,
|
||||
cancelText: undefined,
|
||||
});
|
||||
/** One dialog answers both `useConfirm` and `usePrompt`: a question, and a text field when it's a prompt. */
|
||||
export function ConfirmDialogProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = React.useState<AskState>({ open: false, title: "", withInput: false, resolve: null });
|
||||
const [value, setValue] = React.useState("");
|
||||
|
||||
const confirm = React.useCallback((title: string, options?: ConfirmOptions): Promise<boolean> => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
setState({
|
||||
open: true,
|
||||
resolve,
|
||||
title,
|
||||
description: options?.description,
|
||||
confirmText: options?.confirmText,
|
||||
cancelText: options?.cancelText,
|
||||
});
|
||||
const ask: Ask = (title, options, withInput) =>
|
||||
new Promise((resolve) => {
|
||||
setValue(options?.defaultValue ?? "");
|
||||
setState({ ...options, open: true, title, withInput, resolve });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConfirm = React.useCallback(() => {
|
||||
if (state.resolve) state.resolve(true);
|
||||
|
||||
setState((prev) => ({ ...prev, open: false, resolve: null }));
|
||||
}, [state.resolve]);
|
||||
|
||||
const handleCancel = React.useCallback(() => {
|
||||
if (state.resolve) state.resolve(false);
|
||||
|
||||
setState((prev) => ({ ...prev, open: false, resolve: null }));
|
||||
}, [state.resolve]);
|
||||
|
||||
const contextValue = React.useMemo<ConfirmContextType>(() => ({ confirm }), [confirm]);
|
||||
// Cancelling answers null; confirming answers the text (empty for a confirmation). The state stays while closing.
|
||||
const answer = (result: string | null) => {
|
||||
state.resolve?.(result);
|
||||
setState((previous) => ({ ...previous, open: false, resolve: null }));
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmContext.Provider value={contextValue}>
|
||||
<AskContext.Provider value={ask}>
|
||||
{children}
|
||||
|
||||
<AlertDialog open={state.open} onOpenChange={(open) => !open && handleCancel()}>
|
||||
<AlertDialog open={state.open} onOpenChange={(open) => !open && answer(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{state.title}</AlertDialogTitle>
|
||||
@@ -83,22 +63,43 @@ export function ConfirmDialogProvider({ children }: ConfirmDialogProviderProps)
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
{state.withInput && (
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") answer(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancel}>{state.cancelText ?? "Cancel"}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirm}>{state.confirmText ?? "Confirm"}</AlertDialogAction>
|
||||
<AlertDialogCancel onClick={() => answer(null)}>{state.cancelText ?? t`Cancel`}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => answer(state.withInput ? value : "")}>
|
||||
{state.confirmText ?? t`Confirm`}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</ConfirmContext.Provider>
|
||||
</AskContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useAsk() {
|
||||
const ask = React.use(AskContext);
|
||||
if (!ask) throw new Error("useConfirm and usePrompt must be used within a <ConfirmDialogProvider />.");
|
||||
return ask;
|
||||
}
|
||||
|
||||
/** Resolves true when the user confirms. */
|
||||
export function useConfirm() {
|
||||
const context = React.use(ConfirmContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useConfirm must be used within a <ConfirmDialogProvider />.");
|
||||
}
|
||||
const ask = useAsk();
|
||||
return async (title: string, options?: Omit<AskOptions, "defaultValue">) =>
|
||||
(await ask(title, options, false)) !== null;
|
||||
}
|
||||
|
||||
return context.confirm;
|
||||
/** Resolves with the text entered, or null when the user cancels. */
|
||||
export function usePrompt() {
|
||||
const ask = useAsk();
|
||||
return (title: string, options?: AskOptions) => ask(title, options, true);
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import * as React from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@reactive-resume/ui/components/alert-dialog";
|
||||
import { Input } from "@reactive-resume/ui/components/input";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
type PromptOptions = {
|
||||
description?: string;
|
||||
defaultValue?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
inputProps?: Omit<React.ComponentProps<typeof Input>, "value" | "onChange" | "onKeyDown">;
|
||||
};
|
||||
|
||||
type PromptState = PromptOptions & {
|
||||
open: boolean;
|
||||
title: string;
|
||||
value: string;
|
||||
resolve: ((value: string | null) => void) | null;
|
||||
};
|
||||
|
||||
type PromptContextType = {
|
||||
prompt: (title: string, options?: PromptOptions) => Promise<string | null>;
|
||||
};
|
||||
|
||||
type PromptDialogProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const PromptContext = React.createContext<PromptContextType | null>(null);
|
||||
|
||||
export function PromptDialogProvider({ children }: PromptDialogProviderProps) {
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const [state, setState] = React.useState<PromptState>({
|
||||
open: false,
|
||||
resolve: null,
|
||||
title: "",
|
||||
value: "",
|
||||
description: undefined,
|
||||
defaultValue: undefined,
|
||||
confirmText: undefined,
|
||||
cancelText: undefined,
|
||||
inputProps: undefined,
|
||||
});
|
||||
|
||||
const cancelText = state.cancelText ?? t`Cancel`;
|
||||
const confirmText = state.confirmText ?? t`Confirm`;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!state.open) return;
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current.focus();
|
||||
}, 0);
|
||||
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [state.open]);
|
||||
|
||||
const prompt = React.useCallback((title: string, options?: PromptOptions): Promise<string | null> => {
|
||||
return new Promise<string | null>((resolve) => {
|
||||
setState({
|
||||
open: true,
|
||||
resolve,
|
||||
title,
|
||||
value: options?.defaultValue ?? "",
|
||||
description: options?.description,
|
||||
defaultValue: options?.defaultValue,
|
||||
confirmText: options?.confirmText,
|
||||
cancelText: options?.cancelText,
|
||||
inputProps: options?.inputProps,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConfirm = React.useCallback(() => {
|
||||
if (state.resolve) state.resolve(state.value);
|
||||
|
||||
setState((prev) => ({ ...prev, open: false, resolve: null }));
|
||||
}, [state.resolve, state.value]);
|
||||
|
||||
const handleCancel = React.useCallback(() => {
|
||||
if (state.resolve) state.resolve(null);
|
||||
|
||||
setState((prev) => ({ ...prev, open: false, resolve: null }));
|
||||
}, [state.resolve]);
|
||||
|
||||
const handleValueChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState((prev) => ({ ...prev, value: e.target.value }));
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") handleConfirm();
|
||||
},
|
||||
[handleConfirm],
|
||||
);
|
||||
|
||||
const contextValue = React.useMemo<PromptContextType>(() => ({ prompt }), [prompt]);
|
||||
|
||||
return (
|
||||
<PromptContext.Provider value={contextValue}>
|
||||
{children}
|
||||
|
||||
<AlertDialog open={state.open} onOpenChange={(open) => !open && handleCancel()}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{state.title}</AlertDialogTitle>
|
||||
<AlertDialogDescription className={cn(!state.description && "sr-only")}>
|
||||
{state.description}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={state.value}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleValueChange}
|
||||
{...state.inputProps}
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancel}>{cancelText}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirm}>{confirmText}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</PromptContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePrompt() {
|
||||
const context = React.use(PromptContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("usePrompt must be used within a <PromptDialogProvider />.");
|
||||
}
|
||||
|
||||
return context.prompt;
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import { DialogManager } from "@/dialogs/manager";
|
||||
import { CommandPalette } from "@/features/command-palette";
|
||||
import { ThemeProvider } from "@/features/theme/provider";
|
||||
import { ConfirmDialogProvider } from "@/hooks/use-confirm";
|
||||
import { PromptDialogProvider } from "@/hooks/use-prompt";
|
||||
import { loadRootContext } from "@/libs/root-context";
|
||||
|
||||
type RouterContext = {
|
||||
@@ -119,31 +118,29 @@ function RootComponent() {
|
||||
<DirectionProvider direction={dir}>
|
||||
<TooltipProvider>
|
||||
<ConfirmDialogProvider>
|
||||
<PromptDialogProvider>
|
||||
<Outlet />
|
||||
<Outlet />
|
||||
|
||||
{!isBuilder && <DonationToast />}
|
||||
<DialogManager />
|
||||
<CommandPalette />
|
||||
<Toaster />
|
||||
{!isBuilder && <DonationToast />}
|
||||
<DialogManager />
|
||||
<CommandPalette />
|
||||
<Toaster />
|
||||
|
||||
{import.meta.env.DEV && <BreakpointIndicator />}
|
||||
{import.meta.env.DEV && (
|
||||
<TanStackDevtools
|
||||
config={{ position: "bottom-left" }}
|
||||
plugins={[
|
||||
{
|
||||
name: "TanStack Query",
|
||||
render: <ReactQueryDevtoolsPanel />,
|
||||
},
|
||||
{
|
||||
name: "TanStack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</PromptDialogProvider>
|
||||
{import.meta.env.DEV && <BreakpointIndicator />}
|
||||
{import.meta.env.DEV && (
|
||||
<TanStackDevtools
|
||||
config={{ position: "bottom-left" }}
|
||||
plugins={[
|
||||
{
|
||||
name: "TanStack Query",
|
||||
render: <ReactQueryDevtoolsPanel />,
|
||||
},
|
||||
{
|
||||
name: "TanStack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</ConfirmDialogProvider>
|
||||
</TooltipProvider>
|
||||
</DirectionProvider>
|
||||
|
||||
@@ -25,7 +25,7 @@ import { createLetterFile, letterFileName } from "@/features/letters/export";
|
||||
import { useLetterEditorStore } from "@/features/letters/store";
|
||||
import { BackLink, DrawerControls } from "@/features/resume/editor/chrome";
|
||||
import { useEditorStore } from "@/features/resume/editor/store";
|
||||
import { usePrompt } from "@/hooks/use-prompt";
|
||||
import { usePrompt } from "@/hooks/use-confirm";
|
||||
import { getOrpcErrorMessage } from "@/libs/error-message";
|
||||
import { client, orpc } from "@/libs/orpc/client";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user