Skip to content

Commit d5a2b15

Browse files
review comment fixes
1 parent 66c6912 commit d5a2b15

6 files changed

Lines changed: 83 additions & 51 deletions

File tree

apps/backend/src/app/api/latest/internal/email-drafts/[id]/route.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { Prisma } from "@/generated/prisma/client";
2+
import { templateThemeIdToThemeMode, themeModeToTemplateThemeId } from "@/lib/email-drafts";
13
import { getPrismaClientForTenancy } from "@/prisma-client";
24
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
35
import { templateThemeIdSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
4-
import { templateThemeIdToThemeMode, themeModeToTemplateThemeId } from "@/lib/email-drafts";
6+
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
57

68
export const GET = createSmartRouteHandler({
79
metadata: { hidden: true },
@@ -25,7 +27,15 @@ export const GET = createSmartRouteHandler({
2527
}),
2628
async handler({ auth: { tenancy }, params }) {
2729
const prisma = await getPrismaClientForTenancy(tenancy);
28-
const d = await prisma.emailDraft.findFirstOrThrow({ where: { tenancyId: tenancy.id, id: params.id } });
30+
let d;
31+
try {
32+
d = await prisma.emailDraft.findFirstOrThrow({ where: { tenancyId: tenancy.id, id: params.id } });
33+
} catch (error) {
34+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
35+
throw new StatusError(StatusError.NotFound, "No draft found with given id");
36+
}
37+
throw error;
38+
}
2939
return {
3040
statusCode: 200,
3141
bodyType: "json",
@@ -94,9 +104,16 @@ export const DELETE = createSmartRouteHandler({
94104
}),
95105
async handler({ auth: { tenancy }, params }) {
96106
const prisma = await getPrismaClientForTenancy(tenancy);
97-
await prisma.emailDraft.delete({
98-
where: { tenancyId_id: { tenancyId: tenancy.id, id: params.id } },
99-
});
107+
try {
108+
await prisma.emailDraft.delete({
109+
where: { tenancyId_id: { tenancyId: tenancy.id, id: params.id } },
110+
});
111+
} catch (error) {
112+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
113+
throw new StatusError(StatusError.NotFound, "No draft found with given id");
114+
}
115+
throw error;
116+
}
100117
return {
101118
statusCode: 200,
102119
bodyType: "json",

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/[draftId]/page-client.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { EmailThemeSelector } from "@/components/email-theme-selector";
66
import { useRouterConfirm } from "@/components/router";
77
import { Alert, AlertDescription, AlertTitle, Badge, Button, Card, CardContent, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Skeleton, Typography } from "@/components/ui";
88
import { AssistantChat, CodeEditor, VibeCodeLayout, type ViewportMode, type WysiwygDebugInfo } from "@/components/vibe-coding";
9-
import { type WysiwygDebugInfo as EmailDebugInfo } from "@/components/email-preview";
109
import { ToolCallContent, createChatAdapter, createHistoryAdapter } from "@/components/vibe-coding/chat-adapters";
1110
import { EmailDraftUI } from "@/components/vibe-coding/draft-tool-components";
1211
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
@@ -51,11 +50,15 @@ export default function PageClient({ draftId }: { draftId: string }) {
5150
await stackAdminApp.updateEmailDraft(draftId, { tsxSource: currentCode, themeId: selectedThemeId });
5251
setSaveAlert({ variant: "success", title: "Draft saved" });
5352
} catch (error) {
54-
setSaveAlert({
55-
variant: "destructive",
56-
title: "Failed to save draft",
57-
description: getErrorMessage(error),
58-
});
53+
if (error instanceof KnownErrors.EmailRenderingError) {
54+
setSaveAlert({
55+
variant: "destructive",
56+
title: "Failed to save draft",
57+
description: error.message,
58+
});
59+
return;
60+
}
61+
throw error;
5962
}
6063
};
6164

@@ -81,7 +84,7 @@ export default function PageClient({ draftId }: { draftId: string }) {
8184
};
8285

8386
const previewActions = null;
84-
const isDirty = currentCode !== draft?.tsxSource || selectedThemeId !== draft.themeId;
87+
const isDirty = draft ? (currentCode !== draft.tsxSource || selectedThemeId !== draft.themeId) : false;
8588

8689
// Handle WYSIWYG edit commits - calls the AI endpoint to update source code
8790
const handleWysiwygEditCommit: OnWysiwygEditCommit = useCallback(async (data) => {

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates/[templateId]/page-client.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import { ToolCallContent } from "@/components/vibe-coding/chat-adapters";
1818
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
1919
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
20-
import { useCallback, useEffect, useState } from "react";
20+
import { useCallback, useEffect, useRef, useState } from "react";
2121
import { AppEnabledGuard } from "../../app-enabled-guard";
2222
import { PageLayout } from "../../page-layout";
2323
import { useAdminApp } from "../../use-admin-app";
@@ -61,19 +61,25 @@ export default function PageClient(props: { templateId: string }) {
6161
setIsLoading(true);
6262

6363
const fetchTemplate = async () => {
64-
const allTemplates = await stackAdminApp.listEmailTemplates();
64+
try {
65+
const allTemplates = await stackAdminApp.listEmailTemplates();
6566

66-
if (cancelled) return;
67+
if (cancelled) return;
6768

68-
const found = allTemplates.find((t) => t.id === props.templateId);
69+
const found = allTemplates.find((t) => t.id === props.templateId);
6970

70-
if (found) {
71-
setFetchedTemplate(found);
72-
setCurrentCode(found.tsxSource);
73-
setSelectedThemeId(found.themeId);
71+
if (found) {
72+
setFetchedTemplate(found);
73+
setCurrentCode(found.tsxSource);
74+
setSelectedThemeId(found.themeId);
75+
}
76+
} catch (error) {
77+
const fetchError = error instanceof Error ? error : new Error(String(error));
78+
setFetchError(fetchError);
79+
throw fetchError;
80+
} finally {
81+
setIsLoading(false);
7482
}
75-
76-
setIsLoading(false);
7783
};
7884

7985
runAsynchronously(fetchTemplate);
@@ -83,12 +89,16 @@ export default function PageClient(props: { templateId: string }) {
8389
};
8490
}, [templateFromHook, fetchedTemplate, stackAdminApp, props.templateId]);
8591

92+
const hasSyncedTemplateFromHook = useRef(false);
93+
8694
// When the template appears in the hook (e.g., after cache updates), sync state
8795
useEffect(() => {
88-
if (templateFromHook && !currentCode) {
96+
if (!templateFromHook || hasSyncedTemplateFromHook.current) return;
97+
if (!currentCode) {
8998
setCurrentCode(templateFromHook.tsxSource);
9099
setSelectedThemeId(templateFromHook.themeId);
91100
}
101+
hasSyncedTemplateFromHook.current = true;
92102
}, [templateFromHook, currentCode]);
93103

94104
useEffect(() => {

apps/dashboard/src/components/email-preview.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -572,9 +572,10 @@ const WYSIWYG_EDITOR_SCRIPT = `
572572
`;
573573

574574
/**
575-
* Debug info for WYSIWYG editing
575+
* Debug info for WYSIWYG editing.
576+
* Renamed to avoid colliding with VibeCodeLayout's debug type with the same name.
576577
*/
577-
export type WysiwygDebugInfo = {
578+
export type EmailPreviewDebugInfo = {
578579
renderedHtml?: string,
579580
editableRegions?: Record<string, unknown>,
580581
};
@@ -598,7 +599,7 @@ function EmailPreviewEditableContent({
598599
templateId?: string,
599600
templateTsxSource?: string,
600601
editableSource?: 'template' | 'theme' | 'both',
601-
onDebugInfoChange?: (info: WysiwygDebugInfo) => void,
602+
onDebugInfoChange?: (info: EmailPreviewDebugInfo) => void,
602603
onWysiwygEditCommit?: OnWysiwygEditCommit,
603604
}) {
604605
const stackAdminApp = useAdminApp();
@@ -756,7 +757,7 @@ type EmailPreviewProps =
756757
/** Callback when user commits a WYSIWYG edit. Should return updated source code. */
757758
onWysiwygEditCommit?: OnWysiwygEditCommit,
758759
/** Callback when debug info changes (edit mode only, dev mode only) */
759-
onDebugInfoChange?: (info: WysiwygDebugInfo) => void,
760+
onDebugInfoChange?: (info: EmailPreviewDebugInfo) => void,
760761
};
761762

762763
export default function EmailPreview({

apps/dashboard/src/components/ui/dropdown-menu.tsx

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icon
55
import { forwardRefIfNeeded } from "@stackframe/stack-shared/dist/utils/react";
66
import React from "react";
77

8+
import { cn } from "@/lib/utils";
89
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
910
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
10-
import { cn } from "@/lib/utils";
1111
import { Spinner } from "./spinner";
1212

1313
const DropdownMenuContext = React.createContext<{
@@ -120,6 +120,24 @@ const DropdownMenuItem = forwardRefIfNeeded<
120120
const { setOpen } = React.useContext(DropdownMenuContext) ?? throwErr("No DropdownMenuContext found");
121121
const [isLoading, setIsLoading] = React.useState(false);
122122

123+
// Share activation logic so keyboard onSelect matches mouse clicks.
124+
const handleItemAction = (event: { preventDefault: () => void, stopPropagation: () => void }) => {
125+
event.preventDefault();
126+
event.stopPropagation();
127+
const result = props.onClick?.(event as React.MouseEvent<HTMLDivElement, MouseEvent>);
128+
if (result && typeof (result as Promise<void>).then === "function") {
129+
setIsLoading(true);
130+
runAsynchronouslyWithAlert(
131+
Promise.resolve(result).finally(() => {
132+
setIsLoading(false);
133+
setOpen(false);
134+
})
135+
);
136+
} else {
137+
setOpen(false);
138+
}
139+
};
140+
123141
return <DropdownMenuPrimitive.Item
124142
ref={ref}
125143
className={cn(
@@ -129,25 +147,8 @@ const DropdownMenuItem = forwardRefIfNeeded<
129147
)}
130148
{...props}
131149
disabled={isLoading || props.disabled}
132-
onSelect={props.onClick ? (event) => {
133-
event.preventDefault();
134-
} : undefined}
135-
onClick={props.onClick ? (e) => {
136-
e.preventDefault();
137-
e.stopPropagation();
138-
const result = props.onClick?.(e);
139-
if (result && typeof (result as Promise<void>).then === "function") {
140-
setIsLoading(true);
141-
runAsynchronouslyWithAlert(
142-
Promise.resolve(result).finally(() => {
143-
setIsLoading(false);
144-
setOpen(false);
145-
})
146-
);
147-
} else {
148-
setOpen(false);
149-
}
150-
} : undefined}
150+
onSelect={props.onClick ? handleItemAction : undefined}
151+
onClick={props.onClick ? handleItemAction : undefined}
151152
>
152153
<div style={{ visibility: isLoading ? "visible" : "hidden", position: "absolute", inset: 0, display: "flex", justifyContent: "center", alignItems: "center" }}>
153154
<Spinner />

apps/e2e/tests/backend/endpoints/api/v1/internal/email-drafts.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -460,12 +460,12 @@ export function EmailTemplate({ user, project }: Props) {
460460
}
461461
`);
462462

463-
// Verify it no longer exists - Prisma throws NotFoundError which becomes 500
463+
// Verify it no longer exists - return 404 instead of a Prisma error
464464
const getRes2 = await niceBackendFetch(`/api/v1/internal/email-drafts/${draftId}`, {
465465
method: "GET",
466466
accessType: "admin",
467467
});
468-
expect(getRes2.status).toBe(500);
468+
expect(getRes2.status).toBe(404);
469469

470470
// Verify it's not in the list
471471
const listRes = await niceBackendFetch("/api/v1/internal/email-drafts", {
@@ -492,6 +492,6 @@ it("should return error when deleting non-existent draft", async ({ expect }) =>
492492
method: "DELETE",
493493
accessType: "admin",
494494
});
495-
// Prisma throws an error when the record doesn't exist
496-
expect(deleteRes.status).toBe(500);
495+
// Return a proper 404 so niceBackendFetch doesn't throw
496+
expect(deleteRes.status).toBe(404);
497497
});

0 commit comments

Comments
 (0)