Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/create-database-migration/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ See [add source columns to emails table](../../../ghost/core/core/server/data/mi
See [add member track source setting](../../../ghost/core/core/server/data/migrations/versions/5.21/2022-10-27-09-50-add-member-track-source-setting.js)

## Manipulate data

See [update newsletter subscriptions](../../../ghost/core/core/server/data/migrations/versions/5.31/2022-12-05-09-56-update-newsletter-subscriptions.js).
2 changes: 1 addition & 1 deletion .agents/skills/create-database-migration/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Once migrations are on the `main` branch, they're final. If you need to make fur

## Use utility functions

Wherever possible, use the utility functions in `ghost/core/core/server/data/migrations/utils`. These util functions have been tested and already include protections for idempotency, as well as log statements where appropriate to make migrations easier to debug.
Wherever possible, use the utility functions in `ghost/core/core/server/data/migrations/utils`, such as `addTable`, `createTransactionalMigration`, and `addSetting`. These util functions have been tested and already include protections for idempotency, as well as log statements where appropriate to make migrations easier to debug.

## Migration PRs should be as minimal as possible

Expand Down
1 change: 1 addition & 0 deletions apps/admin-x-framework/src/api/automated-email-design.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type AutomatedEmailDesign = {
background_color: string;
header_background_color: string;
header_image: string | null;
show_header_icon: boolean;
show_header_title: boolean;
footer_content: string | null;
button_color: string | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('automated-email-design api', () => {
background_color: 'light',
header_background_color: 'transparent',
header_image: null,
show_header_icon: true,
show_header_title: true,
footer_content: null,
button_color: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,13 @@ const ColorPickerField: React.FC<ColorPickerFieldProps> = ({title, value, onChan
</div>
</div>
</PopoverTrigger>
<PopoverContent align="end" className="w-auto p-4">
<PopoverContent
align="end"
className="w-auto p-4"
onEscapeKeyDown={(event) => {
event.stopPropagation();
}}
>
<div
onInputCapture={() => {
allowPickerChanges.current = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export const BodyFontField = () => {
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent
onEscapeKeyDown={(event) => {
event.stopPropagation();
}}
>
{FONT_OPTIONS.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ export const HeadingFontField = () => {
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent
onEscapeKeyDown={(event) => {
event.stopPropagation();
}}
>
{FONT_OPTIONS.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ export const HeadingWeightField = () => {
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent
onEscapeKeyDown={(event) => {
event.stopPropagation();
}}
>
{weightOptions.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ const EmailDesignModal: React.FC<EmailDesignModalProps> = ({
'top-[50%] left-[50%] h-[calc(100vh-8vmin)] w-[calc(100vw-8vmin)] max-w-none translate-x-[-50%] translate-y-[-50%] gap-0 overflow-hidden p-0'
)}
data-testid={testId}
onEscapeKeyDown={(event) => {
event.preventDefault();
event.stopPropagation();
handleClose();
}}
>
<div className="flex h-full min-h-0">
{/* Left: Preview */}
Expand Down Expand Up @@ -104,7 +109,11 @@ const EmailDesignModal: React.FC<EmailDesignModalProps> = ({
</DialogContent>
</Dialog>
<AlertDialog open={showDirtyConfirm} onOpenChange={setShowDirtyConfirm}>
<AlertDialogContent>
<AlertDialogContent
onEscapeKeyDown={(event) => {
event.stopPropagation();
}}
>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure you want to leave this page?</AlertDialogTitle>
<AlertDialogDescription asChild>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface EmailPreviewProps {
showRecipientLine?: boolean;
showSubjectLine?: boolean;
headerImage?: string;
showPublicationIcon?: boolean;
showPublicationTitle?: boolean;
showBadge?: boolean;
emailFooter?: string;
Expand Down Expand Up @@ -64,12 +65,14 @@ const EnvelopeHeader: React.FC<{
};

const PublicationHeader: React.FC<{
iconUrl?: string | null;
showIcon: boolean;
showTitle: boolean;
siteTitle?: string;
backgroundColor?: string;
textColor: string;
}> = ({showTitle, siteTitle, backgroundColor, textColor}) => {
if (!showTitle || !siteTitle) {
}> = ({iconUrl, showIcon, showTitle, siteTitle, backgroundColor, textColor}) => {
if (!showIcon && (!showTitle || !siteTitle)) {
return null;
}

Expand All @@ -78,12 +81,21 @@ const PublicationHeader: React.FC<{
className="px-[7rem] py-3 text-center"
style={{backgroundColor: backgroundColor === 'transparent' ? undefined : backgroundColor}}
>
<h4
className="mb-1 text-[1.6rem] leading-tight font-bold tracking-tight uppercase"
style={{color: textColor}}
>
{siteTitle}
</h4>
{showIcon && iconUrl && (
<img
alt={siteTitle || 'Publication icon'}
className="mx-auto mb-3 h-12 w-12 rounded"
src={iconUrl}
/>
)}
{showTitle && siteTitle && (
<h4
className="mb-1 text-[1.6rem] leading-tight font-bold tracking-tight uppercase"
style={{color: textColor}}
>
{siteTitle}
</h4>
)}
</div>
);
};
Expand Down Expand Up @@ -115,9 +127,9 @@ const Footer: React.FC<{siteTitle?: string; footerLinkText?: string; emailFooter

// --- Main component ---

const EmailPreview: React.FC<EmailPreviewProps> = ({settings, senderName, senderEmail, replyToEmail, subject, showRecipientLine = true, showSubjectLine = true, headerImage, showPublicationTitle = true, showBadge = true, emailFooter, footerLinkText, children}) => {
const EmailPreview: React.FC<EmailPreviewProps> = ({settings, senderName, senderEmail, replyToEmail, subject, showRecipientLine = true, showSubjectLine = true, headerImage, showPublicationIcon = false, showPublicationTitle = true, showBadge = true, emailFooter, footerLinkText, children}) => {
const {settings: globalSettings, siteData} = useGlobalData();
const [siteTitle] = getSettingValues<string>(globalSettings, ['title']);
const [siteTitle, icon] = getSettingValues<string>(globalSettings, ['title', 'icon']);
const accentColor = siteData.accent_color;

const colors = resolveAllColors(settings, accentColor);
Expand All @@ -140,6 +152,8 @@ const EmailPreview: React.FC<EmailPreviewProps> = ({settings, senderName, sender

<PublicationHeader
backgroundColor="transparent"
iconUrl={icon}
showIcon={showPublicationIcon && Boolean(icon)}
showTitle={showPublicationTitle}
siteTitle={siteTitle}
textColor={colors.headerTextColor}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface GeneralSettings {
senderEmail: string;
replyToEmail: string;
headerImage: string;
showPublicationIcon: boolean;
showPublicationTitle: boolean;
showBadge: boolean;
emailFooter: string;
Expand All @@ -55,6 +56,7 @@ const NON_DESIGN_FIELDS = new Set([
'created_at',
'updated_at',
'header_image',
'show_header_icon',
'show_header_title',
'show_badge',
'footer_content'
Expand All @@ -67,6 +69,7 @@ const PREVIEW_ONLY_FIELDS = new Set([
interface GeneralTabProps {
generalSettings: GeneralSettings;
onGeneralChange: (updates: Partial<GeneralSettings>) => void;
showPublicationIconToggle: boolean;
senderNamePlaceholder: string;
senderEmailPlaceholder: string;
replyToEmailPlaceholder: string;
Expand All @@ -79,6 +82,7 @@ interface GeneralTabProps {
const GeneralTab: React.FC<GeneralTabProps> = ({
generalSettings,
onGeneralChange,
showPublicationIconToggle,
senderNamePlaceholder,
senderEmailPlaceholder,
replyToEmailPlaceholder,
Expand Down Expand Up @@ -135,6 +139,16 @@ const GeneralTab: React.FC<GeneralTabProps> = ({
value={generalSettings.headerImage}
onChange={url => onGeneralChange({headerImage: url})}
/>
{showPublicationIconToggle && (
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Publication icon</span>
<Switch
checked={generalSettings.showPublicationIcon}
size='sm'
onCheckedChange={checked => onGeneralChange({showPublicationIcon: checked})}
/>
</div>
)}
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Publication title</span>
<Switch
Expand Down Expand Up @@ -204,6 +218,7 @@ export const DesignTab: React.FC = () => (
interface SidebarProps {
generalSettings: GeneralSettings;
onGeneralChange: (updates: Partial<GeneralSettings>) => void;
showPublicationIconToggle: boolean;
senderNamePlaceholder: string;
senderEmailPlaceholder: string;
replyToEmailPlaceholder: string;
Expand All @@ -218,6 +233,7 @@ interface SidebarProps {
const Sidebar: React.FC<SidebarProps> = ({
generalSettings,
onGeneralChange,
showPublicationIconToggle,
senderNamePlaceholder,
senderEmailPlaceholder,
replyToEmailPlaceholder,
Expand Down Expand Up @@ -252,6 +268,7 @@ const Sidebar: React.FC<SidebarProps> = ({
senderEmailPlaceholder={senderEmailPlaceholder}
senderNameError={senderNameError}
senderNamePlaceholder={senderNamePlaceholder}
showPublicationIconToggle={showPublicationIconToggle}
showSenderEmailInput={showSenderEmailInput}
onGeneralChange={onGeneralChange}
/>
Expand All @@ -268,19 +285,20 @@ const Sidebar: React.FC<SidebarProps> = ({
* Maps API response fields to the frontend GeneralSettings shape.
* Note: senderName, senderEmail and replyToEmail are not part of the design endpoint.
*
* @param {Pick<AutomatedEmailDesign, 'header_image' | 'show_header_title' | 'show_badge' | 'footer_content'>} apiData - Subset of design fields used for general settings
* @param {Pick<AutomatedEmailDesign, 'header_image' | 'show_header_icon' | 'show_header_title' | 'show_badge' | 'footer_content'>} apiData - Subset of design fields used for general settings
* @param {GeneralSettings} defaults - Carries forward sender fields, which are not part of the design API
* @returns {GeneralSettings} General settings populated from the API response
*/
function mapApiToGeneralSettings(
apiData: Pick<AutomatedEmailDesign, 'header_image' | 'show_header_title' | 'show_badge' | 'footer_content'>,
apiData: Pick<AutomatedEmailDesign, 'header_image' | 'show_header_icon' | 'show_header_title' | 'show_badge' | 'footer_content'>,
defaults: GeneralSettings
): GeneralSettings {
return {
senderName: defaults.senderName,
senderEmail: defaults.senderEmail,
replyToEmail: defaults.replyToEmail,
headerImage: apiData.header_image || '',
showPublicationIcon: apiData.show_header_icon,
showPublicationTitle: apiData.show_header_title,
showBadge: apiData.show_badge,
emailFooter: apiData.footer_content || ''
Expand Down Expand Up @@ -316,6 +334,7 @@ export function buildAutomatedEmailDesignPayload(state: WelcomeEmailCustomizeFor
return {
...persistedDesign,
header_image: state.generalSettings.headerImage || null,
show_header_icon: state.generalSettings.showPublicationIcon,
show_header_title: state.generalSettings.showPublicationTitle,
show_badge: state.generalSettings.showBadge,
footer_content: state.generalSettings.emailFooter || null
Expand All @@ -336,7 +355,7 @@ const normalizeSenderValue = (value: string | null | undefined) => {
const WelcomeEmailCustomizeModal = NiceModal.create(() => {
const modal = useModal();
const {siteData, settings: globalSettings} = useGlobalData();
const [siteTitle, defaultEmailAddress] = getSettingValues<string>(globalSettings, ['title', 'default_email_address']);
const [siteTitle, defaultEmailAddress, icon] = getSettingValues<string>(globalSettings, ['title', 'default_email_address', 'icon']);

const handleError = useHandleError();
const {data: designData, isLoading, isError} = useReadAutomatedEmailDesign();
Expand Down Expand Up @@ -364,6 +383,7 @@ const WelcomeEmailCustomizeModal = NiceModal.create(() => {
senderEmail: senderEmailInput,
replyToEmail: replyToEmailInput,
headerImage: '',
showPublicationIcon: true,
showPublicationTitle: true,
showBadge: true,
emailFooter: ''
Expand Down Expand Up @@ -532,6 +552,7 @@ const WelcomeEmailCustomizeModal = NiceModal.create(() => {
senderName={generalSettings.senderName || senderNamePlaceholder || siteTitle || 'Your site'}
settings={designSettings}
showBadge={generalSettings.showBadge}
showPublicationIcon={generalSettings.showPublicationIcon && Boolean(icon)}
showPublicationTitle={generalSettings.showPublicationTitle}
showRecipientLine={false}
showSubjectLine={false}
Expand All @@ -551,6 +572,7 @@ const WelcomeEmailCustomizeModal = NiceModal.create(() => {
senderEmailPlaceholder={senderEmailPlaceholder}
senderNameError={errors.senderName}
senderNamePlaceholder={senderNamePlaceholder}
showPublicationIconToggle={Boolean(icon)}
showSenderEmailInput={showSenderEmailInput}
onGeneralChange={handleGeneralChange}
/>
Expand Down
Loading
Loading