Skip to content

Wired up welcome emails customization modal to persist design settings#27070

Merged
cmraible merged 15 commits intomainfrom
chris-ny-1165-wiring-wire-up-the-customization-modal-to-the-api-for
Apr 2, 2026
Merged

Wired up welcome emails customization modal to persist design settings#27070
cmraible merged 15 commits intomainfrom
chris-ny-1165-wiring-wire-up-the-customization-modal-to-the-api-for

Conversation

@cmraible
Copy link
Copy Markdown
Collaborator

@cmraible cmraible commented Apr 2, 2026

closes https://linear.app/ghost/issue/NY-1165/wiring-wire-up-the-customization-modal-to-the-api-for-persistence

Summary

  • Wired the welcome email customization modal to the automated_emails/design/ API so settings persist
  • Added proper save UX with dirty tracking, loading states, error toasts, and a retry flow
  • Replaced window.confirm with an AlertDialog for unsaved changes
  • Normalized semantic color values (light, transparent, accent) to hex for the color picker

I'd recommend reviewing this one commit at a time.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new automated email design API module with types and read/edit hooks for /automated_emails/design/. Normalizes color inputs in the color picker and propagates accentColor through related fields. Converts EmailDesignModal to a controlled component with dirty-confirm alert and afterClose callback. Splits email design types into persisted vs preview fields and updates defaults. Integrates automated-email design backend APIs into the welcome-email customize modal with form-driven state, adds accessibility attributes for header image, introduces unit and e2e tests/locators, and marks the automated-design response type as unmapped in the Ember state bridge.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main objective: wiring the welcome email customization modal to persist design settings via the API.
Description check ✅ Passed The PR description is directly related to the changeset, explaining the key modifications including API integration, save UX improvements, dialog changes, and color normalization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chris-ny-1165-wiring-wire-up-the-customization-modal-to-the-api-for

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cmraible cmraible changed the title Chris ny 1165 wiring wire up the customization modal to the api for Wired up welcome emails customization modal to persist design settings Apr 2, 2026
@cmraible cmraible force-pushed the chris-ny-1165-wiring-wire-up-the-customization-modal-to-the-api-for branch 2 times, most recently from 4f06eda to 0477591 Compare April 2, 2026 05:19
@cmraible cmraible marked this pull request as ready for review April 2, 2026 05:29
@cmraible cmraible requested a review from 9larsons as a code owner April 2, 2026 05:29
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx (1)

61-70: ⚠️ Potential issue | 🟠 Major

Prevent Cmd/Ctrl+S from bypassing disabled/loading save state.

The button is disabled when isLoading || saveDisabled, but the keyboard handler still calls onSave directly, allowing duplicate submits during in-flight saves.

Proposed fix
 useEffect(() => {
     const handleKeyDown = (e: KeyboardEvent) => {
-        if ((e.metaKey || e.ctrlKey) && e.key === 's') {
-            e.preventDefault();
-            onSaveRef.current();
-        }
+        if (!(e.metaKey || e.ctrlKey) || e.key !== 's') {
+            return;
+        }
+        if (!open || isLoading || okProps?.disabled) {
+            return;
+        }
+        e.preventDefault();
+        onSaveRef.current();
     };
     window.addEventListener('keydown', handleKeyDown);
     return () => window.removeEventListener('keydown', handleKeyDown);
-}, []);
+}, [open, isLoading, okProps]);

Also applies to: 113-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx`
around lines 61 - 70, The keyboard handler (handleKeyDown used in the useEffect)
currently calls onSaveRef.current() unconditionally, allowing Cmd/Ctrl+S to
trigger saves even when isLoading or saveDisabled are true; update handleKeyDown
to preventDefault then check the current save state and only call
onSaveRef.current() when !isLoading && !saveDisabled (also guard that
onSaveRef.current is defined). Apply the same conditional check to the other
keyboard shortcut handler in this component (the similar call around lines
113-114) so keyboard-triggered saves respect the disabled/loading state.
🧹 Nitpick comments (4)
apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx (1)

84-88: Consider replacing nested ternary with a small map/helper.

This is readable now, but a mapping object would be easier to extend as button color variants grow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx`
around lines 84 - 88, The nested ternary assigning saveButtonClassName from
saveColor is fragile and hard to extend; replace it with a small mapping or
helper function (e.g., a colorToClassMap object or
getSaveButtonClass(saveColor)) that returns the class string for 'green', 'red',
or a default undefined, and use that to set saveButtonClassName; update any
references to saveColor/saveButtonClassName in the email-design-modal component
accordingly so adding new variants only requires adding map entries or cases in
the helper.
apps/admin-x-framework/src/api/automated-email-design.ts (1)

32-32: Narrow the edit payload type to writable fields only.

Current typing allows read-only/system-managed fields (slug, timestamps) to be passed to PUT, which weakens compile-time safety.

Proposed refactor
-export type EditAutomatedEmailDesign = Omit<Partial<AutomatedEmailDesign>, 'id'>;
+export type EditAutomatedEmailDesign = Partial<
+    Omit<AutomatedEmailDesign, 'id' | 'slug' | 'created_at' | 'updated_at'>
+>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-x-framework/src/api/automated-email-design.ts` at line 32,
EditAutomatedEmailDesign currently allows system-managed/read-only fields;
change its definition to only include writable fields by omitting read-only
keys. Replace the current type alias (EditAutomatedEmailDesign =
Omit<Partial<AutomatedEmailDesign>, 'id'>) with a type that omits id, slug and
timestamp fields (for example: Partial<Omit<AutomatedEmailDesign, 'id' | 'slug'
| 'createdAt' | 'updatedAt'>>), or explicitly Pick the writable properties from
AutomatedEmailDesign; update the alias and any usages to use this narrowed type.
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx (2)

359-361: Replace void operator with proper promise handling.

SonarCloud flags the void operator usage. Using .catch() or restructuring makes intent clearer.

♻️ Proposed fix
     const handleClose = useCallback(() => {
-        void modal.hide();
+        modal.hide().catch(() => {
+            // Modal hide errors are non-critical
+        });
     }, [modal]);

Alternatively, if modal.hide() never actually rejects and void is used purely to silence the floating promise lint, you can use a comment to clarify intent:

     const handleClose = useCallback(() => {
+        // Fire-and-forget: modal animation handles its own cleanup
-        void modal.hide();
+        modal.hide();
     }, [modal]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`
around lines 359 - 361, The handleClose callback currently uses the void
operator to ignore the promise from modal.hide(); replace that with explicit
promise handling: either make handleClose async and await modal.hide() inside a
try/catch (or call modal.hide().catch(err => {/* log or ignore */})), or if
modal.hide() is truly non-rejecting add an explanatory eslint-disable comment;
update the handleClose function (referenced by name) to use one of these
patterns so the floating promise is handled instead of using void.

179-201: Consider extracting nested ternary for readability.

The nested ternary handling loading → error → content states is functional but harder to scan. Extracting to a helper function or using early returns would improve clarity.

♻️ Suggested refactor
+const SidebarContent: React.FC<{
+    isLoading: boolean;
+    errorMessage?: string;
+    generalSettings: GeneralSettings;
+    onGeneralChange: (updates: Partial<GeneralSettings>) => void;
+    siteTitle: string | undefined;
+    emailDomain: string;
+}> = ({isLoading, errorMessage, generalSettings, onGeneralChange, siteTitle, emailDomain}) => {
+    if (isLoading) {
+        return (
+            <div className="flex flex-1 items-center justify-center">
+                <LoadingIndicator size="md" />
+            </div>
+        );
+    }
+    if (errorMessage) {
+        return (
+            <div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-gray-700 dark:text-gray-300">
+                {errorMessage}
+            </div>
+        );
+    }
+    return (
+        <>
+            <TabsContent className='min-h-0 flex-1 overflow-y-auto px-5 pb-5' value="general">
+                <GeneralTab
+                    emailDomain={emailDomain}
+                    generalSettings={generalSettings}
+                    siteTitle={siteTitle}
+                    onGeneralChange={onGeneralChange}
+                />
+            </TabsContent>
+            <TabsContent className='min-h-0 flex-1 overflow-y-auto px-5 pb-5' value="design">
+                <DesignTab />
+            </TabsContent>
+        </>
+    );
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`
around lines 179 - 201, The JSX contains a nested ternary rendering isLoading →
errorMessage → content which reduces readability; refactor by extracting the
conditional rendering into a small helper (e.g. renderContent) or use early
return branches inside the WelcomeEmailCustomizeModal component: check isLoading
first and return the LoadingIndicator wrapper, then check errorMessage and
return the error wrapper, otherwise return the TabsContent block with GeneralTab
and DesignTab; reference the existing symbols isLoading, errorMessage,
TabsContent, GeneralTab and DesignTab when moving the blocks so behavior remains
identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/admin-x-settings/src/components/settings/email-design/color-picker-field.tsx`:
- Around line 17-28: normalizeColorValue currently maps all non-hex inputs to
'#ffffff', which loses semantic values like 'accent'; update normalizeColorValue
(in color-picker-field.tsx) to handle the 'accent' token explicitly (e.g., add a
case for 'accent' that returns 'accent' instead of '#ffffff') so accent-backed
fields from DEFAULT_EMAIL_DESIGN are preserved and not converted to white on
edit.

In `@e2e/tests/admin/settings/member-welcome-emails.test.ts`:
- Around line 173-230: Rename the three test titles to the lowercase "what is
tested - expected outcome" format by updating the string literals passed to the
test(...) calls: change 'saving design settings persists to the API' -> 'save
design settings - persists to api', 'saving general settings persists to the
API' -> 'save general settings - persists to api', and 'saved design settings
load when modal is reopened' -> 'saved design settings - loads when modal is
reopened'; keep the test bodies and identifiers (MemberWelcomeEmailsSection,
customizeModalFooterTextarea, saveCustomizeModal, etc.) unchanged so only the
human-readable test names are modified to match the guideline.

---

Outside diff comments:
In
`@apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx`:
- Around line 61-70: The keyboard handler (handleKeyDown used in the useEffect)
currently calls onSaveRef.current() unconditionally, allowing Cmd/Ctrl+S to
trigger saves even when isLoading or saveDisabled are true; update handleKeyDown
to preventDefault then check the current save state and only call
onSaveRef.current() when !isLoading && !saveDisabled (also guard that
onSaveRef.current is defined). Apply the same conditional check to the other
keyboard shortcut handler in this component (the similar call around lines
113-114) so keyboard-triggered saves respect the disabled/loading state.

---

Nitpick comments:
In `@apps/admin-x-framework/src/api/automated-email-design.ts`:
- Line 32: EditAutomatedEmailDesign currently allows system-managed/read-only
fields; change its definition to only include writable fields by omitting
read-only keys. Replace the current type alias (EditAutomatedEmailDesign =
Omit<Partial<AutomatedEmailDesign>, 'id'>) with a type that omits id, slug and
timestamp fields (for example: Partial<Omit<AutomatedEmailDesign, 'id' | 'slug'
| 'createdAt' | 'updatedAt'>>), or explicitly Pick the writable properties from
AutomatedEmailDesign; update the alias and any usages to use this narrowed type.

In
`@apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx`:
- Around line 84-88: The nested ternary assigning saveButtonClassName from
saveColor is fragile and hard to extend; replace it with a small mapping or
helper function (e.g., a colorToClassMap object or
getSaveButtonClass(saveColor)) that returns the class string for 'green', 'red',
or a default undefined, and use that to set saveButtonClassName; update any
references to saveColor/saveButtonClassName in the email-design-modal component
accordingly so adding new variants only requires adding map entries or cases in
the helper.

In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`:
- Around line 359-361: The handleClose callback currently uses the void operator
to ignore the promise from modal.hide(); replace that with explicit promise
handling: either make handleClose async and await modal.hide() inside a
try/catch (or call modal.hide().catch(err => {/* log or ignore */})), or if
modal.hide() is truly non-rejecting add an explanatory eslint-disable comment;
update the handleClose function (referenced by name) to use one of these
patterns so the floating promise is handled instead of using void.
- Around line 179-201: The JSX contains a nested ternary rendering isLoading →
errorMessage → content which reduces readability; refactor by extracting the
conditional rendering into a small helper (e.g. renderContent) or use early
return branches inside the WelcomeEmailCustomizeModal component: check isLoading
first and return the LoadingIndicator wrapper, then check errorMessage and
return the error wrapper, otherwise return the TabsContent block with GeneralTab
and DesignTab; reference the existing symbols isLoading, errorMessage,
TabsContent, GeneralTab and DesignTab when moving the blocks so behavior remains
identical.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b3512f00-97eb-4323-b00e-d5ebc6fd55a5

📥 Commits

Reviewing files that changed from the base of the PR and between c51c2de and 0477591.

📒 Files selected for processing (10)
  • apps/admin-x-framework/src/api/automated-email-design.ts
  • apps/admin-x-settings/src/components/settings/email-design/color-picker-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx
  • apps/admin-x-settings/src/components/settings/email-design/header-image-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/types.ts
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx
  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
  • e2e/tests/admin/settings/member-welcome-emails.test.ts
  • ghost/admin/app/services/state-bridge.js
  • ghost/admin/tests/unit/services/state-bridge-test.js

cmraible added 5 commits April 2, 2026 15:41
Loaded persisted design settings into the welcome email modal, saved changes back to the design endpoint, and added E2E coverage for the customization flow.
Matched the singleton design endpoint contract so the frontend no longer sends a fake id when saving customization settings.
Prevented successful automated email design saves from surfacing as frontend errors when React-only data types invalidate through the admin state bridge.
Mapped backend values such as light and transparent to valid hex colors so the design color picker can render saved settings without crashing.
Kept the customization modal open after save, matched the shared dirty-confirmation flow, and used the same toast and retry patterns as the newsletter editor.
@cmraible cmraible force-pushed the chris-ny-1165-wiring-wire-up-the-customization-modal-to-the-api-for branch from 0477591 to 27c0a86 Compare April 2, 2026 16:42
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/admin-x-settings/src/components/settings/email-design/color-picker-field.tsx (1)

18-24: ⚠️ Potential issue | 🟠 Major

accent still normalizes to white, which loses persisted semantic intent.

On Line 22, accent falls into the default branch and becomes #ffffff. That can cause accent-backed fields to render/save as white after user interaction.

Suggested fix
 const normalizeColorValue = (value: string): string => {
     if (VALID_HEX.test(value)) {
         return value;
     }

     switch (value) {
     case 'light':
     case 'transparent':
         return '#ffffff';
+    case 'accent':
+        // Map to the resolved accent hex used by this settings flow.
+        // (Use the same resolver/source of truth already used elsewhere.)
+        return resolvedAccentHex;
     default:
         return '#ffffff';
     }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/email-design/color-picker-field.tsx`
around lines 18 - 24, The switch on the incoming value currently falls through
to the default and maps 'accent' to white; add an explicit case for 'accent' in
the switch (the same switch handling value) so it returns the intended accent
color hex (or the theme ACCENT color constant) instead of '#ffffff', ensuring
accent-backed fields preserve their semantic color rather than normalizing to
white.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/admin-x-settings/src/components/settings/email-design/color-picker-field.tsx`:
- Around line 18-24: The switch on the incoming value currently falls through to
the default and maps 'accent' to white; add an explicit case for 'accent' in the
switch (the same switch handling value) so it returns the intended accent color
hex (or the theme ACCENT color constant) instead of '#ffffff', ensuring
accent-backed fields preserve their semantic color rather than normalizing to
white.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 66f17e50-0d82-4f74-a4c6-ceab2e1aa2f2

📥 Commits

Reviewing files that changed from the base of the PR and between 0477591 and 27c0a86.

📒 Files selected for processing (10)
  • apps/admin-x-framework/src/api/automated-email-design.ts
  • apps/admin-x-settings/src/components/settings/email-design/color-picker-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx
  • apps/admin-x-settings/src/components/settings/email-design/header-image-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/types.ts
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx
  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
  • e2e/tests/admin/settings/member-welcome-emails.test.ts
  • ghost/admin/app/services/state-bridge.js
  • ghost/admin/tests/unit/services/state-bridge-test.js
✅ Files skipped from review due to trivial changes (5)
  • ghost/admin/app/services/state-bridge.js
  • apps/admin-x-settings/src/components/settings/email-design/header-image-field.tsx
  • ghost/admin/tests/unit/services/state-bridge-test.js
  • apps/admin-x-framework/src/api/automated-email-design.ts
  • apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx

cmraible added 7 commits April 2, 2026 17:19
ref #27070
Accent-backed button and link fields now resolve to the site accent color in the picker instead of rendering as white.
ref #27070
The new test titles now follow the lowercase what-is-tested to expected-outcome naming convention used elsewhere in the suite.
ref #27070
The customize modal now exposes section title color and the preview headings render with the configured value.
ref #27070
The welcome email UI now matches the existing regular link-style contract instead of presenting regular links as italic.
ref #27070
The API mapping and save payload now spread persisted design fields so future additions are preserved while preview-only fields stay local.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
e2e/tests/admin/settings/member-welcome-emails.test.ts (1)

20-44: Consider importing AutomatedEmailDesign type from the framework package.

The AutomatedEmailDesignResponse interface duplicates the shape defined in apps/admin-x-framework/src/api/automated-email-design.ts. Importing from the shared location would reduce maintenance burden if fields change.

However, keeping it local in e2e tests is also reasonable for isolation and to avoid coupling test code to implementation details.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/admin/settings/member-welcome-emails.test.ts` around lines 20 - 44,
Replace the duplicated AutomatedEmailDesign type with the shared type from the
framework: remove the local AutomatedEmailDesign interface declaration and
import AutomatedEmailDesign from the framework package used by the repo, then
update AutomatedEmailDesignResponse to reference the imported
AutomatedEmailDesign (i.e., automated_email_design: AutomatedEmailDesign[]);
keep the local AutomatedEmailDesignResponse interface if still needed for typing
the API response.
apps/admin-x-settings/test/unit/email-design/color-fields.test.tsx (1)

21-25: Consider using a more specific selector or test ID for the swatch element.

trigger.querySelector('span') relies on internal DOM structure. If ColorPickerField changes its markup, these tests will break. Adding a data-testid to the swatch span would make the tests more resilient.

Also applies to: 39-43

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-x-settings/test/unit/email-design/color-fields.test.tsx` around
lines 21 - 25, The test relies on fragile DOM traversal
(trigger.querySelector('span')) to find the swatch; update the ColorPickerField
implementation to add a stable data-testid attribute on the swatch element
(e.g., data-testid="button-color-swatch") and change the tests in
color-fields.test.tsx (the assertions around trigger and swatch) to select the
swatch via screen.getByTestId('button-color-swatch') instead of querySelector;
do the same for the other case referenced (lines 39-43) so tests use the new
data-testid and remain resilient to markup changes.
apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx (1)

68-75: Consider consolidating color-to-className mapping.

The conditional chain for saveButtonClassName handles three cases but only two have custom classes. If more colors are added later, this could grow unwieldy.

♻️ Optional: Extract to a mapping object
-    const saveButtonClassName = saveColor === 'green'
-        ? 'bg-green text-white hover:bg-green/90'
-        : saveColor === 'red'
-            ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
-            : undefined;
+    const colorClassMap: Record<string, string | undefined> = {
+        green: 'bg-green text-white hover:bg-green/90',
+        red: 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
+    };
+    const saveButtonClassName = colorClassMap[saveColor];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx`
around lines 68 - 75, The saveButtonClassName logic uses a conditional chain on
saveColor (derived from okProps.color) which will become hard to maintain as
colors grow; replace the ternary chain by extracting a color→className mapping
(e.g., a plain object or helper function) and look up saveColor to produce
saveButtonClassName (falling back to undefined), updating the code that computes
saveButtonClassName to use that map instead of the nested conditional.
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx (3)

254-266: Same type-safety concern in payload builder.

The Object.fromEntries pattern here has the same issue - TypeScript cannot verify the resulting object matches EditAutomatedEmailDesign. Consider explicit field mapping or a helper type that enforces the shape.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`
around lines 254 - 266, The payload builder buildAutomatedEmailDesignPayload
uses Object.fromEntries which loses TypeScript guarantees for
EditAutomatedEmailDesign; replace the dynamic fromEntries approach with an
explicitly typed mapping: iterate state.designSettings and reduce into a typed
accumulator of EditAutomatedEmailDesign (or construct an explicit object that
picks each allowed design field), filtering with PREVIEW_ONLY_FIELDS and
NON_DESIGN_FIELDS but ensuring the result is declared as
EditAutomatedEmailDesign so the compiler can verify shape; ensure header_image,
show_header_title, show_badge and footer_content are assigned as before from
state.generalSettings and return the typed object.

239-252: Type assertion loses compile-time safety.

The Object.fromEntries(...) as PersistedEmailDesignSettings cast bypasses TypeScript's structural checking. If a field is missing from apiData or NON_DESIGN_FIELDS is misconfigured, the returned object may not actually satisfy PersistedEmailDesignSettings.

Consider adding runtime validation or using a more explicit mapping that TypeScript can verify:

♻️ Suggested explicit mapping for type safety
 export function mapApiToDesignSettings(
     apiData: PersistedEmailDesignSettings
 ): EmailDesignSettings {
-    const persistedDesign = Object.fromEntries(
-        Object.entries(apiData).filter(([key]) => !NON_DESIGN_FIELDS.has(key))
-    ) as PersistedEmailDesignSettings;
-
     return {
-        ...persistedDesign,
+        background_color: apiData.background_color,
+        title_font_category: apiData.title_font_category,
+        title_font_weight: apiData.title_font_weight,
+        body_font_category: apiData.body_font_category,
+        header_background_color: apiData.header_background_color,
+        section_title_color: apiData.section_title_color,
+        button_color: apiData.button_color,
+        button_style: apiData.button_style,
+        button_corners: apiData.button_corners,
+        link_color: apiData.link_color,
+        link_style: apiData.link_style,
+        image_corners: apiData.image_corners,
+        divider_color: apiData.divider_color,
         // Local-only fields not stored in the backend
         post_title_color: DEFAULT_EMAIL_DESIGN.post_title_color,
         title_alignment: DEFAULT_EMAIL_DESIGN.title_alignment
     };
 }

This trades some verbosity for compile-time guarantees that all required fields are present.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`
around lines 239 - 252, The current mapApiToDesignSettings uses
Object.fromEntries casted to PersistedEmailDesignSettings which loses
compile-time safety; replace the unsafe cast by explicitly mapping required
design fields from apiData (or validating them at runtime) so TypeScript can
verify shape — in mapApiToDesignSettings iterate the known design property names
(those expected in PersistedEmailDesignSettings), pick them from apiData (or
apply defaults from DEFAULT_EMAIL_DESIGN), and build a properly typed object
before returning; reference NON_DESIGN_FIELDS to exclude non-design keys and
ensure the returned EmailDesignSettings includes post_title_color and
title_alignment set from DEFAULT_EMAIL_DESIGN if missing, or throw/return an
error if required fields are absent.

298-305: Consider distinguishing between fetch errors and missing design data.

The check if (!design) could be true due to:

  1. Still loading (covered by isLoading)
  2. Fetch error (covered by isError)
  3. Unexpected empty response

The error message "Unable to load email design settings" is appropriate for case 2, but case 3 might warrant a different message. Currently this is fine since the API should always return design data, but consider logging case 3 separately for debugging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`
around lines 298 - 305, In onSave, distinguish why design is falsy: check
isLoading and isError first and handle those separately (use existing isLoading
to avoid premature error, and when isError show the current 'Unable to load
email design settings' toast with SAVE_ERROR_TOAST_ID and call
setHasSaveError(true)); if neither isLoading nor isError but design is still
missing, treat it as an unexpected empty response—use a different toast message
and log/debug details (e.g. include state/context) before throwing a distinct
Error to aid debugging; update references to onSave, design, isLoading, isError,
SAVE_ERROR_TOAST_ID, and setHasSaveError accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`:
- Around line 359-363: The Retry button is disabled during fetch errors because
the component uses the combined isLoading || isError to drive the modal/button
loading state; create a distinct save-loading flag (e.g., isSaving) or change
the loading prop to only reflect the save operation (not initial fetch) so
retries remain clickable. Update the code that sets isLoading on the
Modal/button to use the save-specific flag (replace isLoading || isError with
isSaving or isLoading && !isError) and ensure the save handler (onOk/save
function referenced where modalOkProps and hasSaveError are used) toggles this
save-loading flag during the save request so modalOkProps (color/label) and
clickability behave correctly.
- Line 373: The modal's isLoading prop in welcome-email-customize-modal.tsx is
currently set to isLoading || isError which inadvertently disables the modal's
Save/Retry button (email-design-modal.tsx disables the button when isLoading is
true). Change the modal invocation to pass only the true loading state
(isLoading) and move explicit control of the button state into the
modalOkProps/okProps (e.g., set modalOkProps.okProps.disabled to isLoading ||
saveDisabled) so that fetch errors (isError) do not automatically disable the
Retry button but you still can disable the button during real loading or when
saveDisabled is true.

---

Nitpick comments:
In
`@apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx`:
- Around line 68-75: The saveButtonClassName logic uses a conditional chain on
saveColor (derived from okProps.color) which will become hard to maintain as
colors grow; replace the ternary chain by extracting a color→className mapping
(e.g., a plain object or helper function) and look up saveColor to produce
saveButtonClassName (falling back to undefined), updating the code that computes
saveButtonClassName to use that map instead of the nested conditional.

In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`:
- Around line 254-266: The payload builder buildAutomatedEmailDesignPayload uses
Object.fromEntries which loses TypeScript guarantees for
EditAutomatedEmailDesign; replace the dynamic fromEntries approach with an
explicitly typed mapping: iterate state.designSettings and reduce into a typed
accumulator of EditAutomatedEmailDesign (or construct an explicit object that
picks each allowed design field), filtering with PREVIEW_ONLY_FIELDS and
NON_DESIGN_FIELDS but ensuring the result is declared as
EditAutomatedEmailDesign so the compiler can verify shape; ensure header_image,
show_header_title, show_badge and footer_content are assigned as before from
state.generalSettings and return the typed object.
- Around line 239-252: The current mapApiToDesignSettings uses
Object.fromEntries casted to PersistedEmailDesignSettings which loses
compile-time safety; replace the unsafe cast by explicitly mapping required
design fields from apiData (or validating them at runtime) so TypeScript can
verify shape — in mapApiToDesignSettings iterate the known design property names
(those expected in PersistedEmailDesignSettings), pick them from apiData (or
apply defaults from DEFAULT_EMAIL_DESIGN), and build a properly typed object
before returning; reference NON_DESIGN_FIELDS to exclude non-design keys and
ensure the returned EmailDesignSettings includes post_title_color and
title_alignment set from DEFAULT_EMAIL_DESIGN if missing, or throw/return an
error if required fields are absent.
- Around line 298-305: In onSave, distinguish why design is falsy: check
isLoading and isError first and handle those separately (use existing isLoading
to avoid premature error, and when isError show the current 'Unable to load
email design settings' toast with SAVE_ERROR_TOAST_ID and call
setHasSaveError(true)); if neither isLoading nor isError but design is still
missing, treat it as an unexpected empty response—use a different toast message
and log/debug details (e.g. include state/context) before throwing a distinct
Error to aid debugging; update references to onSave, design, isLoading, isError,
SAVE_ERROR_TOAST_ID, and setHasSaveError accordingly.

In `@apps/admin-x-settings/test/unit/email-design/color-fields.test.tsx`:
- Around line 21-25: The test relies on fragile DOM traversal
(trigger.querySelector('span')) to find the swatch; update the ColorPickerField
implementation to add a stable data-testid attribute on the swatch element
(e.g., data-testid="button-color-swatch") and change the tests in
color-fields.test.tsx (the assertions around trigger and swatch) to select the
swatch via screen.getByTestId('button-color-swatch') instead of querySelector;
do the same for the other case referenced (lines 39-43) so tests use the new
data-testid and remain resilient to markup changes.

In `@e2e/tests/admin/settings/member-welcome-emails.test.ts`:
- Around line 20-44: Replace the duplicated AutomatedEmailDesign type with the
shared type from the framework: remove the local AutomatedEmailDesign interface
declaration and import AutomatedEmailDesign from the framework package used by
the repo, then update AutomatedEmailDesignResponse to reference the imported
AutomatedEmailDesign (i.e., automated_email_design: AutomatedEmailDesign[]);
keep the local AutomatedEmailDesignResponse interface if still needed for typing
the API response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 572d8839-6acc-4e9e-9d3b-2214503ddb60

📥 Commits

Reviewing files that changed from the base of the PR and between 27c0a86 and f69b64a.

📒 Files selected for processing (12)
  • apps/admin-x-settings/src/components/settings/email-design/color-picker-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/button-color-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/link-color-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/link-style-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx
  • apps/admin-x-settings/src/components/settings/email-design/welcome-email-preview-content.tsx
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx
  • apps/admin-x-settings/test/unit/email-design/color-fields.test.tsx
  • apps/admin-x-settings/test/unit/email-design/design-payload.test.ts
  • apps/admin-x-settings/test/unit/email-design/link-style.test.tsx
  • apps/admin-x-settings/test/unit/email-design/section-title-color.test.tsx
  • e2e/tests/admin/settings/member-welcome-emails.test.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/button-color-field.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/admin-x-settings/src/components/settings/email-design/color-picker-field.tsx

Comment on lines +38 to +39
post_title_color: null,
title_alignment: 'center'
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These aren't used for welcome emails at all - tracking this as a cleanup issue here: https://linear.app/ghost/issue/NY-1204/clean-up-post-title-alignment-and-title-color-attributes

Comment on lines +57 to +60
const PREVIEW_ONLY_FIELDS = new Set([
'post_title_color',
'title_alignment'
]);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx (1)

91-98: Consider extracting the green button variant logic.

The inline conditional for green color styling works but is slightly fragile if the design system changes. If this pattern is used elsewhere, consider creating a helper or extending the Button component's variants.

// Alternative: Extract to a variable for clarity
const buttonVariant = okProps?.color === 'red' ? 'destructive' : 'default';
const greenOverride = okProps?.color === 'green' 
    ? 'bg-green text-white hover:bg-green/90' 
    : undefined;

This is minor and can be deferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx`
around lines 91 - 98, Extract the inline green styling and variant logic for the
Save Button in email-design-modal.tsx by creating a small helper (or local
constants) that computes the Button props from okProps: determine the variant
value (use okProps?.color === 'red' ? 'destructive' : 'default') and compute a
greenOverride className when okProps?.color === 'green'; then pass those
computed values into the Button component (replace the inline className and
variant conditionals on Button with the new variables) so the logic is clearer
and reusable for Button/okProps handling in the component.
🤖 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/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx`:
- Around line 91-98: Extract the inline green styling and variant logic for the
Save Button in email-design-modal.tsx by creating a small helper (or local
constants) that computes the Button props from okProps: determine the variant
value (use okProps?.color === 'red' ? 'destructive' : 'default') and compute a
greenOverride className when okProps?.color === 'green'; then pass those
computed values into the Button component (replace the inline className and
variant conditionals on Button with the new variables) so the logic is clearer
and reusable for Button/okProps handling in the component.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 844e9cf0-ca25-4e03-bccd-5f5735469939

📥 Commits

Reviewing files that changed from the base of the PR and between 54dffa1 and 56cad92.

📒 Files selected for processing (1)
  • apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx

@cmraible cmraible requested a review from troyciesco April 2, 2026 19:35
@cmraible cmraible added the ok to merge for me You can merge this on my behalf if you want. label Apr 2, 2026
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 2, 2026

@cmraible cmraible merged commit 1f5d4cc into main Apr 2, 2026
37 checks passed
@cmraible cmraible deleted the chris-ny-1165-wiring-wire-up-the-customization-modal-to-the-api-for branch April 2, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ok to merge for me You can merge this on my behalf if you want.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants