[pull] main from TryGhost:main - #1428
Merged
Merged
Conversation
no ref Fourth chunk out of `settings/app/`: `email/*` and the shared `email-design/*` now live at `settings/email` and `settings/email-design`, beside the email acceptance tests. Moving them enables the type-aware lint rules the rest of admin uses, so that debt is cleared for these files: - async handlers passed to void callbacks are wrapped; fire-and-forget promises get `void` - `searchKeywords` → `email/search-keywords.ts` (email-settings) and `email/emails-search-keywords.ts` (emails) - `EmailDesignContext` split the way admin's other contexts are: context + `useEmailDesign` in `email-design-context.ts`, `EmailDesignProvider` in `email-design-provider.tsx` - two `act(async …)` callbacks with no awaits made sync in the customize-modal unit test
…0112) ref TryGhost/renovate-config#31 Removes the local `Require human review for CSS preprocessor updates` rule from `.github/renovate.json5`. It existed only to mirror the shared preset's CSS/style toolchain exclusion so the pnpm-catalog automerge rule wouldn't override it; the shared exclusion is being removed. ## Why Minor/patch updates to PostCSS, Sass, Stylelint, Tailwind, etc. don't cause visual regressions in practice, and the exclusion was backing up PRs. Majors for these packages still require dashboard approval via the existing `Require dashboard approval for major updates` rule.
no ref Fifth area out of `settings/app/`: site components now live beside their acceptance tests in `settings/site`. Moving them enables the type-aware lint rules the rest of admin uses, so that debt is cleared for these files: - async handlers passed to void callbacks are wrapped; fire-and-forget promises get `void`; async functions that never await become sync - `searchKeywords` → `site/search-keywords.ts`; `FatalErrors` + `getIssuesFromFatalErrors`/`getIssuesFromInstalledTheme` → `theme/theme-validation-issues.ts`, so component files only export components - the three identical icon/logo/cover upload handlers in `global-settings.tsx` collapse into one `uploadSettingImage` - `getThemeLabel` builds the default/legacy label as a string instead of `+=` onto a `ReactNode` - `design-modal.tsx`'s `useForm` gets an explicit type parameter instead of the `as Dirtyable<…>` cast (the lint autofix strips that cast and breaks `dirty` typing — same trap as #30082's portal modal)
ref https://linear.app/ghost/issue/BER-3851 Gift delivery needs the same UTC timestamp conversion as automations and seeders. Keeping separate implementations risks differences in timezone, precision and validation behaviour when handling raw Knex values. A single Luxon-backed conversion path beside the DbDate codec keeps MySQL and SQLite behaviour consistent and rejects invalid inputs explicitly.
ref https://linear.app/ghost/issue/BER-3851 Transactional messages carrying private redemption links must be able to override publication tracking settings. Mailgun transports drop boolean false values, so the adapters use Mailgun's explicit string form and keep the transport-specific detail behind their existing interfaces.
no ref The settings shell and the shared `components/`, `hooks/`, `utils/`, `data/`, `assets/` and `providers/` now live directly under `settings/`, leaving only `advanced/` in `settings/app/` until the PRs touching it land (#30018, #30019, #29916, #30054, #30099, #30110, #28368). Big diff, but ~300 of the 325 files are import-line rewrites; the substantive changes are the lint-debt fixes the move surfaces: - shell files grouped in `settings/layout/`: `app.tsx` (provider tree), `main-content.tsx`, `sidebar.tsx`, `settings-sections.tsx` (was `components/settings.tsx`); root `settings.tsx` route entry unchanged - contexts split the way admin's other contexts are — hooks + context in `*-context.ts`, provider component in its own file — for global data, settings app, confirmations, scroll sections; `withErrorBoundary` → `with-error-boundary.tsx` - `loadKoenig` → `components/koenig-loader.ts`; the untyped `@tryghost/koenig-lexical` bundle gets a minimal `KoenigLexicalModule` type instead of `any` - `@tryghost/limit-service` and `@tryghost/nql` get typed ambient declarations in `vite-env.d.ts` (clears ~25 `no-unsafe-*` in `use-limiter`) - `iframe-buffering` reuses `utils/debounce`; `TopLevelGroup.saveState` uses the framework's `SaveState`; the usual `void`-wrapping of async handlers
Fixes #30102 - Maps the exact source path `css/header_v2.css` to the documented public `header` card when building the card asset manifest. - Keeps the versioned source files separate while combining both stylesheets under one logical manifest key. - Preserves the existing source order and separator so the default all-card bundle remains byte-for-byte identical. - Makes the manifest builder importable without executing it and adds regression coverage for mapping, inclusion, exclusion, and asset-type isolation.
no ref Last area out of `settings/app/`: advanced components now live beside their acceptance tests in `settings/advanced`. With it gone `settings/app/` is empty and **the ESLint quarantine override is deleted** — every settings file now lints under the same type-aware rules as the rest of admin.
ref https://linear.app/ghost/issue/BER-3851 Delayed payment methods can complete a Checkout Session before Stripe marks its payment as paid. Routing completed and asynchronous payment events through one guarded path prevents legacy gift purchases from being finalized early while leaving donation behavior unchanged.
no ref
Tail of the settings integration:
- `admin-x-settings-{content,sidebar,sidebar-scroller,scroller}` element
ids → `settings-*`; the `admin-x-base admin-x-settings` wrapper class →
`settings-app` (and the heading line-height rule in `index.css`
follows). Nothing outside `apps/admin/src/settings` referenced any of
them (checked Ember, e2e, shade, test-utils); no id collisions with
Ember's DOM.
- `SettingsAppProvider` typed every context field as an optional prop
but overwrote
`sortingState`/`setSortingState`/`offersShowArchived`/`setOffersShowArchived`
after spreading them (the CodeRabbit note on #30113 — pre-existing). It
now takes only `upgradeStatus`, the one prop its caller passes.
The list read a column's value by switching on the column key in a function far from where that column was declared, so a field naming a column and the code filling it had to be kept in step by hand. A column now carries its reader, attached where the column is built, and a cell asks the column for a member's value instead of working back from its key. What the list must ask the API to include is declared on the field alongside the column rather than derived from the columns that resolve, because whether a value is needed follows from the filter alone while naming a column can wait on data still in flight, and a column can be dropped by the display budget while its values are still wanted. Custom fields are addressed under one custom_fields namespace named in a single place, so a field reads the same as a filter key and as a column key. No behaviour change.
A filter told a publisher who matched but not what they said, so reading the data they had just filtered on meant opening members one at a time. Labels, tiers and the subscription filters already append a column for that reason; custom fields are the case where it matters most, since a field exists precisely because its value differs member to member. Every custom field filtered on now brings its own column, whatever the operator, the way Label does. The column layer already lets a field declare a column and how to read it. What is new is that a field whose key is a pattern stands for many columns rather than one, so `custom_fields.:key` resolves its column per instance from the matched key, hydrated from the fields the API loaded -- the same hydration the filter list already does for newsletters, tiers and offers. A name it cannot resolve is a column it does not show, which is also what the flag being off looks like from there. Asking the API for the values does not wait on those names, so the list does not fetch them twice, and the fetch that names them waits for a filter rather than riding every visit to the list. Rendering a value as one line was the member detail screen's, and is now the framework's user-type catalog, next to the part labels and CSV column names it already owns. A scalar reads as it stands and a composite joins its parts the way that type reads; the detail screen renders through the same function, so the two cannot drift, and the catalog is total over the field types so a new composite cannot arrive without a line to read as. Values a publisher collected have no length a column can be sized to, so a dynamic cell carries its full text as a native title. That is a fallback for the edge, not a tooltip component per row across a virtualised list.
closes https://linear.app/ghost/issue/NY-1525/check-how-the-caching-works-for-the-automations-endpoint-and-make-sure Automation run counts change independently of Admin mutations. Refetching the browse query whenever the list mounts prevents the five-minute client freshness window from leaving summary stats behind the detail view while retaining cached data for rendering.
#30119) no ref Closing a dirty settings dialog through Escape, its buttons, the exit button or a sidebar link already confirms before discarding changes; the browser **back/forward buttons did not** — the router swap lost the route-transition guard Ember settings had, and tags/members already guard this with `useBlocker`. - `layout/dirty-navigation-guard.tsx`: `useBlocker` on the global dirty state, wired to the shared `DirtyConfirmDialog` (Leave → `proceed`, Stay → `reset`). Only `POP` navigations are blocked — every in-app exit already runs its own dirty confirmation, so blocking PUSH/REPLACE would double-prompt. Also registers `useConfirmUnload` while dirty, like the other guarded screens. Mounted beside `<Outlet />` in `layout/app.tsx` so contributors' profile dialog is covered too. - Blocks only when the entry being left was **router-created** (`history.state.key`). React Router can only undo a POP from an entry it created; from a native hash-navigation entry it miscounts the delta and can call `history.go(0)` (reload) — found during review, so the guard deliberately stays out of that case. - Compares matched dialog routes, not pathnames: sibling routes rendered by one dialog instance declare `handle: {dialogGroup}` in `settings/routes.tsx` (currently the staff-profile tabs) so back between tabs doesn't prompt; every other route change out of a dialog does. Offers and design/theme containers swap child components per route, so they are intentionally *not* grouped.
ref https://linear.app/ghost/project/4b2edbd66469/ Each commit references the issue individually This PR covers various issues, all linked in the individual commits. It adds basic processing of post content CSV files, limits imports to 100 posts (while in development, this will be removed later), and lays the foundation for future milestones. - **CSV import engine** — Uploading a title/html/published_at CSV to `POST /posts/upload/` now creates real posts instead of a no-op 202. Rows are parsed in-request, then written by an in-process background job via `models.Post.add` under `{importing: true}`; html is converted to lexical, slugs are set as a slugified title, and a file that can't be parsed as CSV at all is rejected with a 422. - **Temporary 100-post cap** — Files with more than 100 rows are rejected with a clear 422 before anything is written. A hard-coded constant with a single check, deliberately with no config surface — the whole limit goes away when the durable job system lands. - **Dates come from the CSV** — The one date column sets `published_at`, `created_at` and `updated_at`, so imported archives look written when they were written, not created at import time. - **In-memory outcome tracking** — Each import registers a run in an in-memory store recording one outcome per row (status, source line number, title, post id/URL); the 202 body returns the run's `import_id` and row total. This is what the completion-report milestone will render from; nothing is persisted until the durable job system. - **Bad rows don't kill the run** — A malformed row (missing/overlong title, invalid date, unconvertible html) is skipped on its own with an actionable reason; the rest of the file still imports. Skipped (fix the file) is distinguished from failed (write attempted and lost). - **Defaults for absent fields** — Imported posts land published, public, type `post`, authored by the site owner, with two internal batch tags: a date stamp (matching the JSON importer's format) and a unique `#Import Run <id>` tag the report milestones key on. - **Zero side-effects, pinned** — A regression suite proves a bulk import sends no newsletter emails and fires no per-post webhooks, with a positive control so the assertion can't pass vacuously.
) ref https://linear.app/ghost/issue/BER-3851 - Added labs-gated recipient details, buyer names and personal messages to gift checkout. - Pre-created payment-pending gifts and optional delivery records before Stripe Checkout, keeping recipient data inside Ghost. - Sent only the internal gift ID to Stripe and preserved the locally calculated amount and currency, because Stripe totals may include tax. - Completed immediate and delayed payments idempotently while retaining support for legacy link-gift checkout sessions. - Delivered recipient emails through atomically claimed jobs with Mailgun tracking disabled, recording success only after provider acceptance. - Recovered pending and interrupted deliveries during boot and scheduled cleanup, and cancelled unsent delivery when a gift became unusable. - Deleted abandoned payment-pending gifts and recipient data after 30 days. - Added the persistence schema, service boundaries, email templates and translations required for the new delivery flow.
no ref Ghost shuts down slowly on some sites and the cleanup tasks were anonymous closures, so there was no way to tell which task (or the HTTP server stop itself) was hanging. This labels each cleanup task and logs how long it and _stopServer take, so a single SIGTERM on the affected environment names the culprit instead of requiring a bisect.
…outer (#30122) no ref Follow-up from the review of #30119. The admin sidebar rendered every destination as a plain `<a href="#/…">`. Those navigations bypass the React router, so the history entries they create carry no router state and React Router refuses to block a browser back onto them (`delta == null`) — the `useBlocker` guards on tags, members and settings never fire when the screen was entered from the sidebar, and from some entry shapes the router miscounts the delta and calls `history.go(0)` (reload) instead of stepping back. - `NavMenuLink` renders a router `Link` for React-owned routes and keeps a native anchor for Ember-owned ones. Ember's `HashLocation` only follows `hashchange`, which the router's `pushState` doesn't fire, so Ember targets must stay anchors. - Ownership comes from the route table at runtime (`isEmberOwnedRoute(path)` in `routes.tsx`: leaf route served by `EmberFallback` / `EmberListWithGiftLinks` / `TagDetailGate`), so flag-gated routes follow the same rule without a hand-maintained list. Query strings (`posts?type=…`, member views) are preserved either way. - External (`target="_blank"`) links unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )