Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8fadace
Changed email analytics result merging to linear time (#30667)
jonatansberg Sep 30, 2026
6df192e
Improved React editor publish flow (#31123)
peterzimon Sep 30, 2026
a326cbe
Added automation entry history and status statistics (#31051)
troyciesco Sep 30, 2026
68a5306
Improved email sending status in post analytics and the posts list (#…
weylandswart Sep 30, 2026
e72fbd7
Added error reporting for embed previews that fail to load (#31134)
acburdine Sep 30, 2026
c519f9a
Fixed admin acceptance specs and UI races exposed by slow requests (#…
kevinansfield Sep 30, 2026
7020afa
Always rendered server-sent automation description (#30863)
EvanHahn Sep 30, 2026
8ff6447
Fixed Mailgun error logging in automation polling (#31059)
cmraible Sep 30, 2026
372d160
🎨 Increased max automation actions from 20 to 50 (#31099)
EvanHahn Sep 30, 2026
ab265eb
Added the post analytics export button back to the self-serve export …
sam-lord Sep 30, 2026
7fa872c
Fixed a refetch letting the React editor overwrite another writer's c…
9larsons Sep 30, 2026
cd95247
Run automations from trigger data, not hard-coded slug (#30835)
EvanHahn Sep 30, 2026
a6db721
🎨 Improved display of YouTube video images in emails (#31093)
kevinansfield Sep 30, 2026
033c0ac
Added user status to the SSO adapter repository (#31135)
acburdine Sep 30, 2026
3c3c0a3
Tested running all migrations from scratch (#31138)
EvanHahn Sep 30, 2026
6722ca0
TypeScriptified 52 unit tests (#31139)
EvanHahn Sep 30, 2026
a6b13c0
🐛 Fixed the editor freezing when adding a YouTube embed (#31140)
acburdine Sep 30, 2026
b89dfe5
TypeScriptified private site access code unit tests (#31141)
EvanHahn Sep 30, 2026
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
5 changes: 5 additions & 0 deletions .changeset/brave-embeds-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tryghost/koenig-lexical": patch
---

Reported embed renderer load failures to the editor's `onError` handler
5 changes: 5 additions & 0 deletions .changeset/calm-frames-settle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tryghost/koenig-lexical": patch
---

Fixed the editor freezing on embed previews when the browser shows scrollbars that take up space
5 changes: 5 additions & 0 deletions .changeset/preview-sso-user-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tryghost/adapter-base-sso": patch
---

Exposed optional user status so SSO adapters can reject inactive owners before first-run setup.
5 changes: 5 additions & 0 deletions .changeset/thick-jobs-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tryghost/kg-default-nodes": patch
---

Added URL transforms for embed card thumbnails so self-hosted thumbnails are stored with the site URL placeholder
4 changes: 2 additions & 2 deletions apps/admin-x-framework/src/api/automations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { Meta, createMutation, createQuery, createQueryWithId } from '../utils/a
import type { ReadonlyDeep } from 'type-fest';

export type AutomationStatus = 'active' | 'inactive';
export const MAX_AUTOMATION_ACTIONS = 20;
export const MAX_AUTOMATION_ACTIONS = 50;

export type Automation = {
id: string;
name: string;
description?: string;
description: string;
/** @deprecated `slug` will be removed in the future. */
slug?: null | string;
status: AutomationStatus;
Expand Down
1 change: 1 addition & 0 deletions apps/admin-x-framework/test/unit/api/automations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const baseDetail = (
id: 'a1',
slug: 'welcome',
name: 'Welcome',
description: 'Welcome new members.',
status: 'active',
created_at: '2026-05-05T00:00:00.000Z',
updated_at: '2026-05-05T00:00:00.000Z',
Expand Down
2 changes: 2 additions & 0 deletions apps/admin/src/automations/automations.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const automations = [
{
id: 'automation-id-1',
name: 'Free member welcome flow',
description: 'Greet new free members.',
slug: 'member-welcome-email-free',
status: 'active' as const,
stats: {
Expand All @@ -81,6 +82,7 @@ const automations = [
{
id: 'automation-id-2',
name: 'Paid member welcome flow',
description: 'Welcome new paid members.',
slug: 'member-welcome-email-paid',
status: 'inactive' as const,
stats: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const automations = [
{
id: 'automation-id-2',
name: 'Paid member welcome flow',
description: 'Welcome new paid members after they start their subscription.',
slug: 'member-welcome-email-paid',
status: 'inactive' as const,
stats: {
Expand All @@ -31,6 +32,7 @@ const automations = [
{
id: 'automation-id-3',
name: 'Another automation',
description: '',
status: 'inactive' as const,
stats: {
last_run_created_at: null,
Expand Down Expand Up @@ -68,15 +70,12 @@ describe('AutomationsList', () => {
vi.useRealTimers();
});

it('renders fetched automations with API and fallback descriptions and status labels', () => {
it('renders server descriptions and status labels', () => {
renderWithRouter(<AutomationsList automations={automations} />);

expect(screen.getAllByTestId('automation-list-row')).toHaveLength(3);
expect(screen.getByText('Free member welcome flow')).toBeInTheDocument();
expect(screen.getByText('Greet new free members.')).toBeInTheDocument();
expect(
screen.queryByText('Welcome new free members after they sign up.'),
).not.toBeInTheDocument();
expect(screen.getByText('Paid member welcome flow')).toBeInTheDocument();
expect(
screen.getByText('Welcome new paid members after they start their subscription.'),
Expand Down
15 changes: 1 addition & 14 deletions apps/admin/src/automations/components/automations-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,6 @@ import {
} from '@tryghost/shade/components';
import { cn, formatNumber, formatTimestamp } from '@tryghost/shade/utils';

const AUTOMATION_DESCRIPTIONS: Record<string, string> = {
'member-welcome-email-free': 'Welcome new free members after they sign up.',
'member-welcome-email-paid': 'Welcome new paid members after they start their subscription.',
};

const getAutomationDescription = (automation: AutomationBrowseItem): string | undefined => {
if (automation.description !== undefined) {
return automation.description;
}

return automation.slug ? AUTOMATION_DESCRIPTIONS[automation.slug] : undefined;
};

// Widths are scoped to `lg` because below that the stats lay out on the row's
// grid rather than in table cells, where a fixed width would fight the columns.
const AUTOMATION_STAT_COLUMNS = [
Expand Down Expand Up @@ -145,7 +132,7 @@ const AutomationsList: React.FC<AutomationsListProps> = ({
)}
<TableBody className="flex flex-col lg:table-row-group">
{automations.map((automation) => {
const description = getAutomationDescription(automation);
const description = automation.description.trim();
const lastEntry = automation.stats?.last_run_created_at;
const totalEntries = automation.stats?.total_run_count ?? 0;
const inProgressEntries = automation.stats?.in_progress_run_count ?? 0;
Expand Down
1 change: 1 addition & 0 deletions apps/admin/src/automations/editor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ const automationDetail: AutomationDetail = {
id: 'automation-id-1',
slug: 'member-welcome-email-free',
name: 'Free member welcome flow',
description: 'Welcome new free members.',
status: 'active',
created_at: '2026-05-05T00:00:00.000Z',
updated_at: '2026-05-05T00:00:00.000Z',
Expand Down
11 changes: 11 additions & 0 deletions apps/admin/src/editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ members" while the email is on its way and "Published and sent to N members"
once the flow's email confirmation finds it submitted; an email-only send reads
"Sent to N members" throughout.

After successful completion, the editor follows the publish flow's celebration
handoff to the destination screen. Pages return to `/pages`; scheduled posts
and posts without email return to `/posts`. Immediately published posts with
email, including email-only sends and posts that were emailed previously, open
`/posts/analytics/:id`. Failed saves and failed sends keep the flow open so the
writer can retry.

## Adding a settings section

1. Add the section's id to `SETTINGS_SECTION_ORDER` in `settings/sections.ts`,
Expand Down Expand Up @@ -89,6 +96,10 @@ specific to the editor lives in `apps/admin/test-utils/acceptance/editor.ts`:
server that rewrites what it was sent.
- `submittedPost(capture, index)` reads the fields of a captured save, the most
recent one by default.
- `editorReadLanded(queryClient, version)` resolves once a read of `version` at
its `updated_at` is in the query cache and the editor has handled it, which is
the only sign of a read the editor refuses. `renderAdminApp` resolves with the
`queryClient`.
- `fakeUnsplashPhotos()` serves one photo in the shape the picker lays out and
inserts, and `UNSPLASH_PICKED` is the rendition the picker asks for.

Expand Down
10 changes: 9 additions & 1 deletion apps/admin/src/editor/date-time-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface DateTimePickerProps {
/** Calendar days after this instant's site-timezone day are not selectable. */
maxDate?: string | null;
disabled?: boolean;
className?: string;
dateLabel: string;
timeLabel: string;
dateTestId: string;
Expand All @@ -47,6 +48,7 @@ export function DateTimePicker({
minDate,
maxDate,
disabled = false,
className,
dateLabel,
timeLabel,
dateTestId,
Expand Down Expand Up @@ -108,7 +110,13 @@ export function DateTimePicker({
const describedByProps = describedBy ? { 'aria-describedby': describedBy } : {};

return (
<Grid aria-labelledby={labelledBy} columns={2} gap="sm" role={labelledBy ? 'group' : undefined}>
<Grid
aria-labelledby={labelledBy}
className={className}
columns={2}
gap="sm"
role={labelledBy ? 'group' : undefined}
>
<InputGroup className="min-w-0" data-disabled={disabled}>
<InputGroupAddon>
<LucideIcon.CalendarDays />
Expand Down
45 changes: 42 additions & 3 deletions apps/admin/src/editor/editor-header-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useCallback, useState } from 'react';
import { useEmberOwnedRouteMatcher } from '@/routes';
import { useNavigate } from '@tryghost/admin-x-framework';
import { Button } from '@tryghost/shade/components';
import { useShade } from '@tryghost/shade/app';
import { PageHeader } from '@tryghost/shade/patterns';
Expand Down Expand Up @@ -88,7 +90,12 @@ export function EditorHeaderActions({
const isDraft = post.status === 'draft';

usePreviewShortcut(
useCallback(() => setPreviewOpen((open) => !open), []),
useCallback(() => {
setPreviewOpen(!previewOpen);
if (previewOpen) {
onOpenFlow('none');
}
}, [onOpenFlow, previewOpen]),
isDraft && persistedId !== null,
);

Expand Down Expand Up @@ -199,6 +206,8 @@ function PublishActions({
onOpenFlow,
onPreview,
}: PublishActionsProps) {
const navigate = useNavigate();
const isEmberOwned = useEmberOwnedRouteMatcher();
const { isAdmin7 } = useShade();
const inputs = usePublishInputs();
const limits = usePublishLimits();
Expand All @@ -210,6 +219,7 @@ function PublishActions({
});
// A refetch of any input must not unmount an open flow, so readiness latches once.
const [everReady, setEverReady] = useState(false);
const [openedFromPreview, setOpenedFromPreview] = useState(false);

if (inputs.isReady && !everReady) {
setEverReady(true);
Expand All @@ -226,10 +236,26 @@ function PublishActions({
onOpenFlow('none');
void session.dispatchPublish({ kind: 'revert' });
}, [onOpenFlow, session]);
const closeFlow = useCallback(() => onOpenFlow('none'), [onOpenFlow]);
const openPublishFlow = useCallback(() => onOpenFlow('publish'), [onOpenFlow]);
const closeFlow = useCallback(() => {
setOpenedFromPreview(false);
onOpenFlow('none');
}, [onOpenFlow]);
const openPublishFlow = useCallback(() => {
setOpenedFromPreview(false);
onOpenFlow('publish');
}, [onOpenFlow]);
const { onOpenChange: setPreviewOpen } = preview;
const changePreviewOpen = useCallback(
(open: boolean) => {
setPreviewOpen(open);
if (!open) {
closeFlow();
}
},
[closeFlow, setPreviewOpen],
);
const publishFromPreview = useCallback(() => {
setOpenedFromPreview(true);
setPreviewOpen(false);
onOpenFlow('publish');
}, [onOpenFlow, setPreviewOpen]);
Expand Down Expand Up @@ -275,7 +301,9 @@ function PublishActions({
</Button>
<PostPreviewModal
{...preview}
animate={openFlow !== 'publish'}
publishDisabled={!inputs.isReady}
onOpenChange={changePreviewOpen}
onPublish={publishFromPreview}
/>
</>
Expand Down Expand Up @@ -306,17 +334,28 @@ function PublishActions({

{openFlow === 'publish' && everReady ? (
<PublishFlowModal
animate={!openedFromPreview}
dispatch={session.dispatchPublish}
limits={limits}
paywallImprovements={paywallImprovements}
post={post}
showCompletion={false}
site={inputs.site}
siteTitle={siteTitle}
timezone={inputs.timezone}
tkCount={tkCount}
user={inputs.user}
onBeforePublish={saveBeforePublish}
onClose={closeFlow}
onCompleted={({ postId, isScheduled, hasEmail }) => {
const destination =
post.displayName === 'page'
? '/pages'
: !isScheduled && (hasEmail || post.email || post.emailOnly)
? `/posts/analytics/${postId}`
: '/posts';
navigate(destination, { crossApp: isEmberOwned(destination) });
}}
onPreview={onPreview}
onRevertToDraft={revertToDraft}
/>
Expand Down
Loading
Loading