[Refactor][Feat][Fix] Rework Email Section With New Sent Page, Better Drafts Page, and Settings Page#1221
[Refactor][Feat][Fix] Rework Email Section With New Sent Page, Better Drafts Page, and Settings Page#1221
Conversation
They take you to a page where you can edit them. Currently, we can only edit the scheduled at, whether its paused, and whether to cancel the email. This is because the EmailOutBoxUpdateOptions only supports these for now. We may expand this in a future commit. Note that sent emails cant be edited.
Note: in the stats bar, we mark green- sent,opened,clicked,delivery-delayed,skipped. Mark red-bounced Mark yellow-marked as spam Mark Orange-errors (server-error, render-error). Mark gray- the rest. These correlations are between the colors in the stats bar and the statuses we pull from the outbox api.
Clicking the button sets a boost expiredat time. When this time is set and still valid, the capacity rate is multiplied by 4. When the button is clicked, trigger a loading spinner until the route finishes processing. When the timer runs out, we reset the button back to its original state. We dont need to wrap the onclick with runAsyncWithAlert because the component does that already.
This is mainly about text size and badge colors.
Mostly UI Improvements
Can step backwards through stepper. Archive used drafts since drafts are meant to be fire and forget. draft stage now persists on refresh. Settings stage split into recipients and scheduling. Clicking on archived draft takes you to its sent page.
What is comes down to is that the search bar and the DataPaginatedTable are both children of the suspense with skeleton fallback, and upon the user typing in a new character, global filters updates which causes rerender but also causes useUser to be called again, inside which you will find a use() which is what triggers the suspense to use the fallback UI. the fix is non trivial. Removing the suspense makes it so that the page reloads on every change. The suspense cant just cover the DataPaginationTable with some sort of decoupled search bar because the user stuff has to be in the suspense boundary. We cant just completely decouple search bar from DataPaginationTable because DataPaginationTable -> DataTableBase-> DataTableView-> DataTableToolbar which renders the bar, and datatabletoolbar is what gives the searchbar component access to the table to set filters from. Dependency inversion here. We mimic the users table flow, and get rid of the redundant components.
When creating a draft from a template that uses variables (e.g. passwordResetLink), the variable values are now stored on the draft record in the database. Previously I tried putting them in an in-memory Map + sessionStorage on the client, which caused render errors when navigating between drafts or reloading the page, or closing the tab and trying to open up the project again. - Add templateVariables JSON column to EmailDraft table - Update draft CRUD routes to read/write template_variables - Send-email route now reads variables from the draft record directly - Remove all client-side variable storage (in-memory Map, sessionStorage) - Simplify DraftFlowContext to only hold UI state (recipients, schedule)
useEmailDrafts gets the email drafts, but uses a cache. Because of this, when you navigate to the drafts page without a reload (caused by next.js optimizations), it uses stale cache. This becomes a problem because if you send a draft and then navigate back, the cached version of the draft has sentAt marked as null, so you can reuse the draft. Drafts are meant to be fire-and-forget. We add functionality to refresh drafts cache to protect against stale cache issues. We refresh on sending an email.
… deal with scrollbar appearing in iframe
An outage will generally take more than a few seconds to resolve. 2 second base backoff means more often than not we're just waiting for the next queue step iteration. waiting longer might be a good idea
Themes can now only be navigated to from the settings
stepper now makes it evident which ones you can click by hover and visual hints.
A gradient at the bottom of the central theme, with the text overlaid in bold.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/[draftId]/page-client.tsx (2)
230-230: Type-widening cast onsetStagemay accept invalid stage values.The cast
setStage as (stepId: string) => voidbypasses theDraftStagetype constraint. IfDraftProgressBarpasses a step ID that isn't a validDraftStage,setStageInternalwould set invalid state without compile-time detection.Consider adding runtime validation or updating the types to ensure type safety:
🛡️ Suggested defensive wrapper
- onStepClick={setStage as (stepId: string) => void} + onStepClick={(stepId) => { + if (isValidStage(stepId)) { + setStage(stepId); + } + }}Also applies to: 305, 312, 373, 529
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-drafts/[draftId]/page-client.tsx at line 230, The prop cast on DraftProgressBar (onStepClick={setStage as (stepId: string) => void}) weakens the DraftStage type and can allow invalid stages; replace the cast by providing a small wrapper function that validates the incoming stepId against the DraftStage union (or a known list of allowed stages) before calling setStageInternal/setStage, or narrow the DraftProgressBar prop type to accept DraftStage; implement the wrapper where DraftProgressBar is used (referencing setStage, setStageInternal, DraftProgressBar, DraftStage, and onStepClick) so only validated DraftStage values are passed to state setters and invalid values are rejected or logged.
498-506: Type cast bypasses discriminated union type checking.Line 506 uses
as Parameters<typeof stackAdminApp.sendEmail>[0]to work around TypeScript's limitation with discriminated unions in ternaries. While this is a known pattern, it could mask type errors if thesendEmailAPI signature changes.Per coding guidelines: "Do NOT use
as/any/type casts or anything else to bypass the type system unless you specifically asked the user about it."Consider restructuring to avoid the cast:
♻️ Suggested refactor to avoid type cast
- await stackAdminApp.sendEmail( - (scope === "users" - ? { draftId, userIds: selectedUserIds, scheduledAt } - : { draftId, allUsers: true, scheduledAt } - ) as Parameters<typeof stackAdminApp.sendEmail>[0] - ); + if (scope === "users") { + await stackAdminApp.sendEmail({ draftId, userIds: selectedUserIds, scheduledAt }); + } else { + await stackAdminApp.sendEmail({ draftId, allUsers: true, scheduledAt }); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-drafts/[draftId]/page-client.tsx around lines 498 - 506, The code bypasses TypeScript's discriminated-union checks by casting the ternary result to Parameters<typeof stackAdminApp.sendEmail>[0]; instead, construct the payload in a type-safe way: create a local variable (e.g., payload) typed as the actual sendEmail parameter type or as a discriminated union, set scheduledAt from scheduleMode/scheduledDate/scheduledTime, then assign either { draftId, userIds: selectedUserIds, scheduledAt } when scope === "users" or { draftId, allUsers: true, scheduledAt } when scope !== "users", and pass that payload to stackAdminApp.sendEmail without using as/any; reference stackAdminApp.sendEmail, scheduledAt, scheduleMode, selectedUserIds, draftId, and scope to locate and update the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-drafts/[draftId]/page-client.tsx:
- Line 230: The prop cast on DraftProgressBar (onStepClick={setStage as (stepId:
string) => void}) weakens the DraftStage type and can allow invalid stages;
replace the cast by providing a small wrapper function that validates the
incoming stepId against the DraftStage union (or a known list of allowed stages)
before calling setStageInternal/setStage, or narrow the DraftProgressBar prop
type to accept DraftStage; implement the wrapper where DraftProgressBar is used
(referencing setStage, setStageInternal, DraftProgressBar, DraftStage, and
onStepClick) so only validated DraftStage values are passed to state setters and
invalid values are rejected or logged.
- Around line 498-506: The code bypasses TypeScript's discriminated-union checks
by casting the ternary result to Parameters<typeof stackAdminApp.sendEmail>[0];
instead, construct the payload in a type-safe way: create a local variable
(e.g., payload) typed as the actual sendEmail parameter type or as a
discriminated union, set scheduledAt from
scheduleMode/scheduledDate/scheduledTime, then assign either { draftId, userIds:
selectedUserIds, scheduledAt } when scope === "users" or { draftId, allUsers:
true, scheduledAt } when scope !== "users", and pass that payload to
stackAdminApp.sendEmail without using as/any; reference stackAdminApp.sendEmail,
scheduledAt, scheduleMode, selectedUserIds, draftId, and scope to locate and
update the logic.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/[draftId]/page-client.tsx
When you send an email, the state of the draft/template that created it is changed. A cache refresh here is good to ensure the changed state is picked up.
3ab0470 to
684d44c
Compare
Remove the template-variable extraction flow and switch draft creation to a single AI rewrite step that produces standalone draft TSX. - add internal rewrite endpoint for template TSX with runtime render validation - remove extraction route and extraction API surface from backend/SDK interfaces - update dashboard create-from-template flow to skip variable modal and use rewrite - add rewrite e2e coverage (regular schema, renamed schema, no-schema pass-through) - align AI invocation with internal AI routes, including mock-mode behavior
8b6e09c to
dace887
Compare
Matches how we do it for other endpoints, avoids codegen issues.
Context
We didn't have an easy place for a user to see their domain statistics and track their sent emails, either overall or by draft. Additionally, there was scope creep with the sidebar, where we were supporting more pages. Our emails landing page was also rather confusing, especially toggling/ working with different email server types. So, we decide to add a "sent" page, to track email logs and email statistics, as well as let users temporarily override their sending limits if need be. Additionally, a user may want to see a particular email in more detail: what stage is it in? How did it proceed through time? How can I pause the sending of this email or change the scheduled time or edit the code? We allow for that to happen.
Summary of Changes
New Pages
Bug Fixes
TeamMemberSearchTable: This was broken. Every time you tried to enter or remove a character, it would trigger skeleton loading that overlapped the search bar too, preventing you from adding/removing more. This was caused because theuseUserhook eventually ended up calling ausehook, which throws a promise that triggers a suspense. This, coupled with the fact that the implementation ofTeamMemberSearchTableinvolved a prop-drilling/ dependency inversion approach to passing down its toolbar to a base table component, meant the suspense would cover the toolbar too and couldn't be scoped to just the table. A refactor has gotten rid of the need for those base components while fixing tables inpayments/customers,teams/team_id, andpayments/transactionson top of the existing use in email drafts recipients stage. We also dedupped some code.useEmailDraftsuses an asyncCache to cache the fetched drafts. It is used on the drafts landing page to render the drafts. When a draft is sent, itssentAtis marked versus when it is still active, it is marked as null. The cache was stale and so navigating to the landing page after firing off a draft would errorneously represent that draft as still active and indeed, even allow you to edit it and fire it again. This violated the principle of drafts being fire and forget. This has been dealt with by adding functionality to refresh the draft cache upon firing off a draft.Other Changes
email-queue-stepto 20 seconds. The previous base was two seconds, and this effectively just made it wait until the next iteration of theemail-queue-stepcron job or at most an iteration that wasn't too far away. When an outage with our provider happens, it may take a while for it to be resolved, so a longer backoff is justifiedUI Demos
Sent Page Demo:
Sent.Page.Demo.mov
Drafts Page Demo
Drafts.Page.Demo.mov
Settings Page Demo
Settings.Page.Demo.mov
Email Viewer Page Demo
Email.Viewer.Page.Demo.mov
Summary by CodeRabbit