Skip to content

Updated welcome email modal to utilize useForm#25660

Merged
troyciesco merged 1 commit intomainfrom
NY-856_welcome-email-modal-use-form
Dec 9, 2025
Merged

Updated welcome email modal to utilize useForm#25660
troyciesco merged 1 commit intomainfrom
NY-856_welcome-email-modal-use-form

Conversation

@troyciesco
Copy link
Copy Markdown
Contributor

ref https://linear.app/ghost/issue/NY-856

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Dec 8, 2025

Walkthrough

Refactors member email UI and tests. In member-email-editor the onChange now stringifies incoming data and only calls onChange when the string differs; useCallback deps updated. welcome-email-modal was migrated to a form-driven flow (useForm/useHandleError), added lexical empty-content validation, moved subject into form state, uses global settings to display sender and conditional Reply‑to, adds Ctrl/Cmd+S save shortcut, and changes dirty/save handling. Public props for WelcomeEmailModal now require emailType and automatedEmail. E2E helpers updated (removed sender/reply locators, added modalSavedButton, changed subject input selector) and one sender-edit test was removed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx — verify JSON stringify comparison, dependency correctness, and no new infinite-loop or performance regressions.
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx — review form integration, validation (isEmptyLexical and lexical errors), subject binding, global-data usage for sender display, Ctrl/Cmd+S handling, save-state/dirtiness semantics, and test-email dropdown scaffolding.
  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts — confirm locator removals/addition, updated subject input selector, and revised save/wait flow.
  • e2e/tests/admin/settings/member-welcome-emails.test.ts — confirm removal of the sender-edit test and assess test coverage impact.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main change: migrating the welcome email modal to use the useForm hook for form handling.
Description check ✅ Passed The description is directly related to the changeset, providing specific context about the form migration, related issues resolved, and new features implemented.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch NY-856_welcome-email-modal-use-form

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ade31c42337bcd736a6754af58e495740030ac33 and d2ce740.

📒 Files selected for processing (4)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx (1 hunks)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (3 hunks)
  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts (3 hunks)
  • e2e/tests/admin/settings/member-welcome-emails.test.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • e2e/tests/admin/settings/member-welcome-emails.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx
  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-11T07:31:40.433Z
Learnt from: aileen
Repo: TryGhost/Ghost PR: 24879
File: apps/admin-x-settings/src/hooks/useLimiter.tsx:125-125
Timestamp: 2025-09-11T07:31:40.433Z
Learning: In apps/admin-x-settings/src/hooks/useLimiter.tsx, the useMemo dependency array intentionally includes the entire `config` object rather than just `config.hostSettings?.limits` to enforce re-renders when anything in the config changes, particularly for subscription/plan change scenarios.

Applied to files:

  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
📚 Learning: 2025-10-30T17:13:26.190Z
Learnt from: sam-lord
Repo: TryGhost/Ghost PR: 25303
File: ghost/core/core/server/services/email-service/BatchSendingService.js:19-19
Timestamp: 2025-10-30T17:13:26.190Z
Learning: In ghost/core/core/server/services/email-service/BatchSendingService.js and similar files in the Ghost codebase, prefer using `{...options}` spread syntax without explicit guards like `...(options || {})` when spreading potentially undefined objects, as the maintainer prefers cleaner syntax over defensive patterns when the behavior is safe.

Applied to files:

  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
🧬 Code graph analysis (1)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (4)
apps/admin-x-framework/src/api/automated-emails.ts (2)
  • AutomatedEmail (4-16)
  • useEditAutomatedEmail (41-50)
apps/admin-x-framework/src/api/current-user.ts (1)
  • useCurrentUser (14-31)
apps/admin-x-framework/src/api/settings.ts (1)
  • getSettingValues (85-87)
ghost/core/core/server/services/koenig/node-renderers/header-v1-renderer.js (1)
  • div (55-55)
🔇 Additional comments (5)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (5)

20-43: Lexical “empty content” helper and validation wiring look solid

The isEmptyLexical helper, its use in onValidate, and the corresponding border + Hint rendering give clear feedback when content is missing and match the desired UX (no silent validation failures anymore). No changes requested here.

Also applies to: 73-75, 213-223


56-64: useForm integration and save handler are consistent and type-safe

The switch to useForm (initial state, onSave, onSaveError, onValidate) looks consistent with other modals, and making automatedEmail required removes the unsafe cast in the save payload. This should keep the mutation path clean and predictable.

Also applies to: 66-71


105-121: Cmd/Ctrl+S shortcut correctly avoids stale handleSave references

Storing handleSave in a ref and attaching the keydown listener once with an empty dependency array is a good pattern here: you avoid re‑binding on every render while still invoking the latest save handler. Cleanup on unmount is also handled correctly.


123-125: Read-only From/Reply-to presentation matches the new spec

Deriving senderEmail/replyToEmail from automatedEmail with a defaultEmailAddress fallback and conditionally rendering the Reply‑to row when it differs from the From address fits the requirements and keeps the UI simple and non-editable. Looks good.

Also applies to: 183-195


131-132: Dirty-state and Save button behavior align with useForm’s contract

Using dirty={saveState === 'unsaved'} for the modal, driving the button via okProps (color/label/disabled), and calling handleSave({fakeWhenUnchanged: true}) from both the button and Cmd/Ctrl+S gives a consistent “Saved” affordance without closing the modal. Subject binding to formState with errors.subject also looks correct.

Also applies to: 175-179, 201-207


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.

@troyciesco troyciesco force-pushed the NY-856_welcome-email-modal-use-form branch from 74f6a90 to a1e2e5a Compare December 8, 2025 19:40
@troyciesco troyciesco marked this pull request as ready for review December 8, 2025 20:23
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 8, 2025

React E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20041200217 -n playwright-report-react -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 8, 2025

Ember E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20041200217 -n playwright-report-ember -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

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: 1

🧹 Nitpick comments (3)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (3)

59-82: useForm integration is solid; consider using more of okProps for clearer Save UX

The useForm wiring (initialState, onSave, onSaveError, onValidate) looks correct and the fakeWhenUnchanged: true call on click nicely supports the “Saved” state even without edits.

Two small refinements could improve consistency with the rest of the form system:

  • The Modal’s dirty={saveState === 'unsaved'} is straightforward, but you might want to confirm whether other saveState values (like a validation-error state, if any) should still mark the form as dirty.
  • The Save button currently ignores any okProps.disabled, relying only on saveState === 'saving'. If useForm sets okProps.disabled when invalid or mid-save, consider:
<Button
    color={okProps.color}
    disabled={okProps.disabled ?? saveState === 'saving'}
    label={okProps.label || 'Save'}
    onClick={async () => await handleSave({fakeWhenUnchanged: true})}
/>

This would keep the visual/interaction behavior fully aligned with what useForm intends to expose.

Also applies to: 131-131, 175-179


108-121: Global Cmd/Ctrl+S handler is correct; consider matching click behavior

The keydown effect is wired cleanly: it guards on handleSave, adds/removes the listener correctly, and the dependency on handleSave keeps it from re‑adding on every render. This is a good fit for the shared useForm save flow.

One detail to double‑check: the click handler calls handleSave({fakeWhenUnchanged: true}), while the keyboard shortcut calls handleSave() with no options. If fakeWhenUnchanged affects the “Saved” UX (e.g., showing a saved state even when nothing changed), you may want the shortcut to mirror the button:

handleSave({fakeWhenUnchanged: true});

so Cmd/Ctrl+S and clicking Save feel identical from the user’s perspective.


53-58: Sender/Reply-to display and form wiring align well with the new behavior

The way you derive and display the sender details looks good:

  • senderEmail / replyToEmail falling back to defaultEmailAddress, and hiding the Reply‑to row when those match, gives the intended “no duplicate reply‑to” behavior.
  • Using automatedEmail?.sender_name || siteTitle for the display name keeps things in sync with site defaults while preserving any existing override.
  • Subject and lexical fields are correctly wired to formState and updateForm, and pair nicely with the updated MemberEmailEditor that only emits changes when the underlying JSON string actually changes.

Minor robustness thought: if defaultEmailAddress is ever absent or still loading, you might want a simple fallback (e.g., not rendering the angle‑bracketed email until it’s available) to avoid showing <undefined>, but that’s more of an edge case than a blocker.

Also applies to: 123-124, 184-196, 197-207, 218-219

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a8a9c0 and 551d91b76bd0cd9b49f937c869c959817d8caf8a.

📒 Files selected for processing (2)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx (1 hunks)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: sam-lord
Repo: TryGhost/Ghost PR: 25303
File: ghost/core/core/server/services/email-service/BatchSendingService.js:19-19
Timestamp: 2025-10-30T17:13:26.190Z
Learning: In ghost/core/core/server/services/email-service/BatchSendingService.js and similar files in the Ghost codebase, prefer using `{...options}` spread syntax without explicit guards like `...(options || {})` when spreading potentially undefined objects, as the maintainer prefers cleaner syntax over defensive patterns when the behavior is safe.
Learnt from: danielraffel
Repo: TryGhost/Ghost PR: 25366
File: apps/admin/src/layout/app-sidebar/NavHeader.tsx:13-23
Timestamp: 2025-11-06T05:35:41.162Z
Learning: In apps/admin/src/layout/app-sidebar/NavHeader.tsx, the React component dispatches a synthetic KeyboardEvent to trigger the Ember keymaster.js search modal shortcut. This approach is known to have cross-browser reliability issues but was deferred for architectural refactoring in a separate PR. The recommended fix is to expose a global function or custom DOM event from the Ember app instead of relying on synthetic keyboard events with keymaster.js.
📚 Learning: 2025-09-11T07:31:40.433Z
Learnt from: aileen
Repo: TryGhost/Ghost PR: 24879
File: apps/admin-x-settings/src/hooks/useLimiter.tsx:125-125
Timestamp: 2025-09-11T07:31:40.433Z
Learning: In apps/admin-x-settings/src/hooks/useLimiter.tsx, the useMemo dependency array intentionally includes the entire `config` object rather than just `config.hostSettings?.limits` to enforce re-renders when anything in the config changes, particularly for subscription/plan change scenarios.

Applied to files:

  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Build & Push
🔇 Additional comments (1)
apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx (1)

23-30: handleChange comparison to value cleanly avoids spurious dirty state

Stringifying once and only calling onChange when stringified !== value is a good way to ignore Koenig’s initial onChange while still emitting real content changes. Dependency on value keeps the comparison up to date, so this looks solid and aligns with the new useForm-based flow.

@troyciesco troyciesco requested a review from cmraible December 8, 2025 20:38
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: 1

♻️ Duplicate comments (1)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (1)

211-222: Lexical validation error is still not displayed to the user.

The errors.lexical value is set during validation (line 77) but is never rendered in the UI. When a user tries to save with empty content, the save is blocked silently with no visual feedback explaining why.

Consider adding an error message below the editor:

                     <MemberEmailEditor
                         key={automatedEmail?.id || 'new'}
                         nodes='DEFAULT_NODES'
                         placeholder='Write your welcome email content...'
                         singleParagraph={false}
                         value={formState.lexical}
                         onChange={lexical => updateForm(state => ({...state, lexical}))}
                     />
+                    {errors.lexical && (
+                        <p className='mt-2 text-sm text-red'>{errors.lexical}</p>
+                    )}
                 </div>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 551d91b76bd0cd9b49f937c869c959817d8caf8a and f85948dca06e4525005798a2912a6b0efaf6ba65.

📒 Files selected for processing (1)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: danielraffel
Repo: TryGhost/Ghost PR: 25366
File: apps/admin/src/layout/app-sidebar/NavHeader.tsx:13-23
Timestamp: 2025-11-06T05:35:41.162Z
Learning: In apps/admin/src/layout/app-sidebar/NavHeader.tsx, the React component dispatches a synthetic KeyboardEvent to trigger the Ember keymaster.js search modal shortcut. This approach is known to have cross-browser reliability issues but was deferred for architectural refactoring in a separate PR. The recommended fix is to expose a global function or custom DOM event from the Ember app instead of relying on synthetic keyboard events with keymaster.js.
📚 Learning: 2025-09-11T07:31:40.433Z
Learnt from: aileen
Repo: TryGhost/Ghost PR: 24879
File: apps/admin-x-settings/src/hooks/useLimiter.tsx:125-125
Timestamp: 2025-09-11T07:31:40.433Z
Learning: In apps/admin-x-settings/src/hooks/useLimiter.tsx, the useMemo dependency array intentionally includes the entire `config` object rather than just `config.hostSettings?.limits` to enforce re-renders when anything in the config changes, particularly for subscription/plan change scenarios.

Applied to files:

  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Lint
  • GitHub Check: Build & Push
🔇 Additional comments (6)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (6)

21-44: LGTM!

The utility correctly handles the various empty states for Lexical content: null/undefined, JSON parse failures, empty children arrays, and the common case of a single empty paragraph node.


108-121: LGTM!

The keyboard shortcut implementation is correct. The effect properly adds/removes the listener and includes handleSave in the dependency array to avoid stale closure issues.


123-124: LGTM!

The fallback logic correctly uses defaultEmailAddress when the automated email doesn't have specific sender/reply-to values configured.


174-179: LGTM!

The Save button correctly integrates with okProps from the form hook and provides sensible fallbacks.


189-196: LGTM!

Conditionally hiding Reply-to when it matches the sender email reduces visual noise and follows the original static UI design.


197-208: LGTM!

Subject validation errors are properly wired to the TextField component.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 8, 2025

React E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20041724283 -n playwright-report-react -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 8, 2025

Ember E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20041724283 -n playwright-report-ember -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 8, 2025

Ember E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20042071700 -n playwright-report-ember -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 8, 2025

React E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20042071700 -n playwright-report-react -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 8, 2025

React E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20042230592 -n playwright-report-react -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 8, 2025

Ember E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20042230592 -n playwright-report-ember -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

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: 0

Caution

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

⚠️ Outside diff range comments (1)
e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts (1)

90-93: Update saveWelcomeEmail to align with new “Save keeps modal open” behavior

The helper still assumes that clicking Save closes the modal:

await this.modalSaveButton.click();
await this.welcomeEmailModal.waitFor({state: 'hidden'});

Per the PR objective, Save now shows “Saved” instead of closing the modal, so this waitFor({state: 'hidden'}) is likely to hang/fail. It would be better to wait for a post-save UI signal (e.g., the “Saved” label) instead of modal disappearance, for example:

 async saveWelcomeEmail(): Promise<void> {
-    await this.modalSaveButton.click();
-    await this.welcomeEmailModal.waitFor({state: 'hidden'});
+    await this.modalSaveButton.click();
+    await this.welcomeEmailModal
+        .getByRole('button', {name: 'Saved'})
+        .waitFor({state: 'visible'});
 }

Adjust the guard to whichever “save complete” indicator the modal guarantees so tests don’t depend on old close-on-save behavior.

♻️ Duplicate comments (1)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (1)

21-44: Lexical “empty content” validation error is never surfaced to the user

onValidate correctly sets errors.lexical when isEmptyLexical(state.lexical) is true, but the UI never reads or renders errors.lexical. Only errors.subject is wired into the Subject TextField:

  • isEmptyLexical + lexical validation: lines 21–44, 76–78
  • Subject field error wiring: lines 200–207
  • MemberEmailEditor usage: lines 213–220

This creates a broken UX: attempting to save an empty welcome email silently fails (validation blocks save) but shows no explanation near the editor.

Consider surfacing errors.lexical below MemberEmailEditor (or by styling the editor container) similar to how the subject hint is handled, e.g.:

                 <MemberEmailEditor
                     key={automatedEmail?.id || 'new'}
                     nodes='DEFAULT_NODES'
                     placeholder='Write your welcome email content...'
                     singleParagraph={false}
                     value={formState.lexical}
                     onChange={lexical => updateForm(state => ({...state, lexical}))}
                 />
+                {errors.lexical && (
+                    <p className="mt-2 text-sm text-red">
+                        {errors.lexical}
+                    </p>
+                )}

Adjust styling to match existing form error patterns, but the key is to read errors.lexical and make the validation reason visible.

Also applies to: 69-81, 213-220

🧹 Nitpick comments (3)
e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts (1)

27-27: Subject locator should avoid raw CSS selector and .first(); prefer semantic or test-id based targeting

this.modalSubjectInput = this.welcomeEmailModal.locator('input').first(); relies on generic CSS and DOM order, which is fragile (e.g., if the Test dropdown input is rendered first) and conflicts with the e2e guideline to avoid CSS selectors in favor of semantic locators or data-testid.

Consider adding a dedicated test id to the subject field in the modal (e.g., data-testid="welcome-email-subject") and using getByTestId here instead:

-        this.modalSubjectInput = this.welcomeEmailModal.locator('input').first();
+        this.modalSubjectInput = this.welcomeEmailModal.getByTestId('welcome-email-subject');

This will be more stable and aligned with the e2e locator guidelines.

Based on learnings, expose stable public readonly locators using semantic or test-id based selectors.

apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (2)

59-68: onSave assumes automatedEmail is always defined despite optional prop type

WelcomeEmailModalProps declares automatedEmail?: AutomatedEmail, but onSave spreads it with a non-null assertion cast:

onSave: async (state) => {
    await editAutomatedEmail({...automatedEmail as AutomatedEmail, ...state});
},

If this modal is ever used without an automatedEmail (e.g., a “create new welcome email” path), ...automatedEmail will throw at runtime. Either:

  • Make automatedEmail required in the props for this modal, or
  • Guard in onSave (and/or assert early) and decide what should happen when it’s missing (e.g., call a “create” API instead of edit, or bail with a clear error).

Example minimal guard:

-        onSave: async (state) => {
-            await editAutomatedEmail({...automatedEmail as AutomatedEmail, ...state});
-        },
+        onSave: async (state) => {
+            if (!automatedEmail) {
+                // Option A: throw, Option B: no-op, Option C: call a "create" path
+                throw new Error('automatedEmail is required to save a welcome email');
+            }
+            await editAutomatedEmail({...automatedEmail, ...state});
+        },

Align this with how the modal is actually instantiated so the runtime invariant is explicit.


53-58: Guard against missing default email / title when rendering “From” and “Reply‑to”

senderEmail and replyToEmail fall back to defaultEmailAddress, and sender_name falls back to siteTitle:

const [defaultEmailAddress] = getSettingValues<string>(settings, ['default_email_address']);
const {localSettings} = useSettingGroup();
const [siteTitle] = getSettingValues(localSettings, ['title']) as string[];

const senderEmail = automatedEmail?.sender_email || defaultEmailAddress;
const replyToEmail = automatedEmail?.sender_reply_to || defaultEmailAddress;

If either setting is missing, the UI can end up rendering something like From: <undefined> or a blank name, which is confusing in an email configuration modal.

Worth adding a small safety fallback (e.g., to currentUser?.email, or a placeholder like “(no address configured)”), and/or defensive defaults:

-const senderEmail = automatedEmail?.sender_email || defaultEmailAddress;
-const replyToEmail = automatedEmail?.sender_reply_to || defaultEmailAddress;
+const senderEmail = automatedEmail?.sender_email || defaultEmailAddress || currentUser?.email || '';
+const replyToEmail = automatedEmail?.sender_reply_to || defaultEmailAddress || senderEmail;

and optionally avoid rendering the angle‑bracketed part if senderEmail is empty. This keeps the header robust even when settings are misconfigured.

Also applies to: 123-125, 183-195

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f85948dca06e4525005798a2912a6b0efaf6ba65 and f6f1a7488e550b04918062f5ee1a0dcb538ec4cb.

📒 Files selected for processing (3)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (5 hunks)
  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts (1 hunks)
  • e2e/tests/admin/settings/member-welcome-emails.test.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • e2e/tests/admin/settings/member-welcome-emails.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
e2e/**/*.{ts,tsx}

📄 CodeRabbit inference engine (e2e/AGENTS.md)

Prefer less comments and give things clear names

Files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
e2e/helpers/pages/**/*.ts

📄 CodeRabbit inference engine (e2e/AGENTS.md)

e2e/helpers/pages/**/*.ts: Page Objects should be located in helpers/pages/ directory
Expose locators as public readonly in Page Objects when used with assertions
Page Object methods should use semantic names (e.g., login() instead of clickLoginButton())
Use waitFor() for guards in Page Objects, never use expect() in page objects

Files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
e2e/**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

E2E tests should use Playwright with Docker container isolation

Files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
🧠 Learnings (5)
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/helpers/pages/**/*.ts : Expose locators as `public readonly` in Page Objects when used with assertions

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Prefer semantic locators (`getByRole`, `getByLabel`, `getByText`) over test IDs and never use CSS selectors, XPath, nth-child, or class names

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Use `getByTestId()` only when semantic locators are unavailable, and suggest adding `data-testid` to Ghost codebase when needed

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Never use CSS/XPath selectors - only use semantic locators or data-testid

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
📚 Learning: 2025-09-11T07:31:40.433Z
Learnt from: aileen
Repo: TryGhost/Ghost PR: 24879
File: apps/admin-x-settings/src/hooks/useLimiter.tsx:125-125
Timestamp: 2025-09-11T07:31:40.433Z
Learning: In apps/admin-x-settings/src/hooks/useLimiter.tsx, the useMemo dependency array intentionally includes the entire `config` object rather than just `config.hostSettings?.limits` to enforce re-renders when anything in the config changes, particularly for subscription/plan change scenarios.

Applied to files:

  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Lint
  • GitHub Check: Build & Push
🔇 Additional comments (1)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (1)

108-121: Global Cmd/Ctrl+S handler and Save button wiring look consistent with useForm pattern

The useEffect that registers a window keydown listener for Cmd/Ctrl+S and the Save button’s onClick both delegate to handleSave({fakeWhenUnchanged: true}), sharing the same behavior and respecting okProps / saveState for button state:

  • useEffect with handleSave dependency and cleanup: lines 108–121
  • Save button: lines 175–179

This matches the intended “Cmd+S saves without closing” behavior and plugs into the form framework cleanly. No issues spotted here.

Also applies to: 175-179

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 8, 2025

Ember E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20043424711 -n playwright-report-ember -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 8, 2025

React E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20043424711 -n playwright-report-react -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

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: 0

🧹 Nitpick comments (2)
e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts (2)

26-32: Subject input locator may be brittle; prefer semantic locator if available

this.modalSubjectInput = this.welcomeEmailModal.locator('input').first(); will break if another input is added before the subject field. If the subject input has an accessible label/placeholder or a data-testid, consider targeting that instead, e.g.:

-        this.modalSubjectInput = this.welcomeEmailModal.locator('input').first();
+        this.modalSubjectInput = this.welcomeEmailModal.getByRole('textbox', {name: 'Subject'});
+        // or use a specific data-testid if that’s what the modal exposes
+        // this.modalSubjectInput = this.welcomeEmailModal.getByTestId('welcome-email-subject');

Adjust the locator to whatever semantic hook the modal actually exposes.
Based on learnings, prefer stable semantic locators over positional ones where possible.


11-17: Saved‑state locator aligns with new UX; just confirm it’s stable enough for waitFor

Adding modalSavedButton and waiting for it after clicking Save matches the new “stay open & show Saved” behavior and keeps the guard nicely encapsulated in saveWelcomeEmail(). The only thing to double‑check is that the “Saved” state isn’t too transient; if the button quickly flips back to “Save”, this waitFor({state: 'visible'}) could become flaky.

If the “Saved” label is short‑lived in practice, consider making the success indicator more durable (e.g., keeping “Saved” visible for a minimum time or exposing a more stable attribute/test id) and point this locator at that. Otherwise, this change looks good.

Also applies to: 26-32, 92-95

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6f1a7488e550b04918062f5ee1a0dcb538ec4cb and 0964b3837d11f07f8f89c3372aedec6aa614db70.

📒 Files selected for processing (1)
  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
e2e/**/*.{ts,tsx}

📄 CodeRabbit inference engine (e2e/AGENTS.md)

Prefer less comments and give things clear names

Files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
e2e/helpers/pages/**/*.ts

📄 CodeRabbit inference engine (e2e/AGENTS.md)

e2e/helpers/pages/**/*.ts: Page Objects should be located in helpers/pages/ directory
Expose locators as public readonly in Page Objects when used with assertions
Page Object methods should use semantic names (e.g., login() instead of clickLoginButton())
Use waitFor() for guards in Page Objects, never use expect() in page objects

Files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
e2e/**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

E2E tests should use Playwright with Docker container isolation

Files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
🧠 Learnings (4)
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/helpers/pages/**/*.ts : Expose locators as `public readonly` in Page Objects when used with assertions

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Prefer semantic locators (`getByRole`, `getByLabel`, `getByText`) over test IDs and never use CSS selectors, XPath, nth-child, or class names

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Use `getByTestId()` only when semantic locators are unavailable, and suggest adding `data-testid` to Ghost codebase when needed

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Never use CSS/XPath selectors - only use semantic locators or data-testid

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Lint
  • GitHub Check: Build & Push

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 8, 2025

React E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20044027739 -n playwright-report-react -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

Copy link
Copy Markdown
Collaborator

@cmraible cmraible left a comment

Choose a reason for hiding this comment

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

Nice! Definitely good call on useForm, that's my bad for not checking our existing patterns for form handling. Everything's working nicely for me locally, and I've confirmed that it fixes the issues mentioned in the PR description.

Left a couple suggestions that are worth looking at before merge, but they're all pretty minor so I'll go ahead and approve so you're not blocked getting this merged in the AM 🙌

@troyciesco troyciesco force-pushed the NY-856_welcome-email-modal-use-form branch 3 times, most recently from 252fdf0 to ade31c4 Compare December 9, 2025 16:58
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: 1

♻️ Duplicate comments (1)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (1)

175-179: Consider delegating Save button disabled state entirely to okProps.

useForm already computes okProps.disabled (including the saving state), so disabled={okProps.disabled ?? saveState === 'saving'} is likely redundant and slightly duplicates that logic. You could simplify to:

-                                disabled={okProps.disabled ?? saveState === 'saving'}
+                                disabled={okProps.disabled}

to keep all button-state rules centralized in useForm.

In TryGhost/Ghost’s admin-x-framework `useForm` hook, does `okProps.disabled` already cover the saving state, making extra `saveState === 'saving'` checks redundant?
🧹 Nitpick comments (2)
e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts (1)

11-17: Saved-state locator and save flow look good; consider a more semantic subject locator

The new modalSavedButton locator and the updated saveWelcomeEmail() flow (clicking Save then waiting for the Saved button state) align well with the new UX where the modal stays open and shows a “Saved” state. Naming and usage are clear and consistent with the rest of the Page Object.

One small robustness suggestion: modalSubjectInput = this.welcomeEmailModal.locator('input').first(); is still positional. If the modal ever gains another input before Subject, this could start targeting the wrong element. If possible, consider switching to a more semantic or stable locator (e.g., by label, placeholder, or data-testid) to match how other locators in this class are defined. As per coding guidelines, this will make the tests less brittle over time.

Also applies to: 26-31, 92-95

apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (1)

52-55: Sender/Reply‑To display and subject binding match the intended behavior; minor placeholder tweak possible.

Deriving From/Reply‑to from automatedEmail with a fallback to default_email_address, hiding Reply‑to when it matches From, and binding subject to formState.subject all look correct and align with the PR description. One small UX improvement would be to avoid a potential "Welcome to undefined" placeholder if siteTitle is unavailable:

-                                placeholder={`Welcome to ${siteTitle}`}
+                                placeholder={`Welcome to ${siteTitle || 'your site'}`}

Optional, but keeps the placeholder clean in edge cases.

Also applies to: 123-125, 184-196, 205-207

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f35178035847c92640c09f2f83f6b50057f6bf4 and 252fdf082ebd5a3e8c1f6011f50e8b75687265cb.

📒 Files selected for processing (4)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx (1 hunks)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (4 hunks)
  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts (3 hunks)
  • e2e/tests/admin/settings/member-welcome-emails.test.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • e2e/tests/admin/settings/member-welcome-emails.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx
🧰 Additional context used
📓 Path-based instructions (3)
e2e/**/*.{ts,tsx}

📄 CodeRabbit inference engine (e2e/AGENTS.md)

Prefer less comments and give things clear names

Files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
e2e/helpers/pages/**/*.ts

📄 CodeRabbit inference engine (e2e/AGENTS.md)

e2e/helpers/pages/**/*.ts: Page Objects should be located in helpers/pages/ directory
Expose locators as public readonly in Page Objects when used with assertions
Page Object methods should use semantic names (e.g., login() instead of clickLoginButton())
Use waitFor() for guards in Page Objects, never use expect() in page objects

Files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
e2e/**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

E2E tests should use Playwright with Docker container isolation

Files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
🧠 Learnings (5)
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/helpers/pages/**/*.ts : Expose locators as `public readonly` in Page Objects when used with assertions

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Prefer semantic locators (`getByRole`, `getByLabel`, `getByText`) over test IDs and never use CSS selectors, XPath, nth-child, or class names

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Use `getByTestId()` only when semantic locators are unavailable, and suggest adding `data-testid` to Ghost codebase when needed

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Never use CSS/XPath selectors - only use semantic locators or data-testid

Applied to files:

  • e2e/helpers/pages/admin/settings/sections/member-welcome-emails-section.ts
📚 Learning: 2025-09-11T07:31:40.433Z
Learnt from: aileen
Repo: TryGhost/Ghost PR: 24879
File: apps/admin-x-settings/src/hooks/useLimiter.tsx:125-125
Timestamp: 2025-09-11T07:31:40.433Z
Learning: In apps/admin-x-settings/src/hooks/useLimiter.tsx, the useMemo dependency array intentionally includes the entire `config` object rather than just `config.hostSettings?.limits` to enforce re-renders when anything in the config changes, particularly for subscription/plan change scenarios.

Applied to files:

  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
🧬 Code graph analysis (1)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (6)
apps/admin-x-framework/src/providers/routing-provider.tsx (1)
  • useRouting (188-190)
apps/admin-x-framework/src/api/current-user.ts (1)
  • useCurrentUser (14-31)
apps/admin-x-framework/src/api/automated-emails.ts (2)
  • useEditAutomatedEmail (41-50)
  • AutomatedEmail (4-16)
apps/admin-x-settings/src/components/providers/global-data-provider.tsx (1)
  • useGlobalData (75-75)
apps/admin-x-framework/src/api/settings.ts (1)
  • getSettingValues (85-87)
ghost/core/core/server/services/koenig/node-renderers/header-v1-renderer.js (1)
  • div (55-55)
🔇 Additional comments (3)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (3)

5-12: Imports and hook usage look consistent.

All newly added imports (design system components, useForm/useHandleError, settings/global data hooks) are used and align with existing admin-x patterns.


20-43: Lexical/content validation and error surfacing are wired correctly.

isEmptyLexical plus the onValidate checks, border color switch, and red Hint ensure both subject and body must be non-empty and give clear feedback when Save is blocked, rather than failing silently.

Also applies to: 66-78, 213-224


105-121: Cmd/Ctrl+S handler with useRef is a solid pattern.

Storing handleSave in a ref and registering the keydown listener once (empty deps) avoids both stale closures and repeatedly adding/removing listeners on each keystroke.

@troyciesco troyciesco force-pushed the NY-856_welcome-email-modal-use-form branch from ade31c4 to 5f1f76a Compare December 9, 2025 17:04
@troyciesco troyciesco force-pushed the NY-856_welcome-email-modal-use-form branch from 5f1f76a to d2ce740 Compare December 9, 2025 17:10
@troyciesco troyciesco merged commit 61a739b into main Dec 9, 2025
35 checks passed
@troyciesco troyciesco deleted the NY-856_welcome-email-modal-use-form branch December 9, 2025 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants