Skip to content

Sync with upstream Ghost v6.58.0 - #21

Open
andreascreten wants to merge 191 commits into
mainfrom
chore/sync-v6.58.0
Open

Sync with upstream Ghost v6.58.0#21
andreascreten wants to merge 191 commits into
mainfrom
chore/sync-v6.58.0

Conversation

@andreascreten

Copy link
Copy Markdown
Member

Syncing fork to upstream release v6.58.0.

github-actions Bot and others added 30 commits August 10, 2026 20:25
…t#29873)

no ref
- makes the core build consistent with admin/e2e builds
no ref

First slice of the settings NiceModal burn-down (follows the router swap
TryGhost#29780 and un-portal TryGhost#29789).

`ConfirmationModal` and `LimitModal` were NiceModal-shown from **54 call
sites across 27 files** — the bulk of settings' remaining NiceModal
usage. They now go through a settings-local `ConfirmationProvider`:
ref https://linear.app/ghost/issue/BER-3816

A member import too large to run inside the request is accepted straight away and
reported by email once the background job finishes, and when that job failed no
email was ever sent. The publisher was told to expect one and nothing ever came,
leaving a failed import indistinguishable from one still running. Nobody else
found out either, since a queued import runs in-process where a thrown error
never reaches the error tracker.

The import now runs in three phases and each says what it may do. Gathering what
the rows are written against is the only phase allowed to throw, so a throw
means nothing was written; writing reports rather than throws, since once a row
has committed an import that failed halfway is not one that never ran; and
settling up afterwards guards each step on its own, so a failed lookup cannot
skip archiving a Stripe price that nothing else will ever clean up. The job then
always tells the publisher something, sending a distinct email for an import
that could not be completed at all.

Whether a row failed for a reason the publisher can act on is not asked. A row
that failed is a row that failed, and it goes into the report they receive with
the reason beside it. Nothing inspects those errors, which is also why a row's
values -- which a database driver inlines into the message of the statement that
failed -- cannot reach a log or an error tracker from here. What does reach them
is the run itself failing, a cleanup that did not finish, or a notification that
could not be sent, none of which carry a row.
no ref
- Added an additive Shade `CodeEditor` with lazy CodeMirror loading.
- Adopted it only for the header and footer fields on the flagged React
tag-details screen.
- Added Storybook states for default, focused, error, and disabled
behavior.
- Portalled Shade autocomplete into a transparent body-level host so
suggestions escape clipped editor and accordion containers.
- Connected hint and error copy to the editable element and reset shared
focus state when a focused editor unmounts.
- Added real-browser coverage for editing, saving, autocomplete
visibility, positioning stability, and portal paint safety.
ref BER-3863

An investigation into whether a member could reach custom fields through the
members API found that they cannot, and never could. Every member-facing
surface serialises from a field whitelist that predates the feature, and the
member update path drops unknown keys twice before it reaches any custom-field
code. Nothing asserted that, so these two tests pin it: a member's own response
carries no custom fields, and custom fields a member sends are dropped while the
rest of the update still applies, the same way email already behaves on that
endpoint.
ref https://linear.app/ghost/issue/BER-3862

A key is typed by hand into member filters, CSV columns, email replacement
strings and, before long, themes and editor cards. The hyphen a slug separates
with is the one character those readers disagree about: NQL will not parse a
hyphen in a property path, and a replacement string matches word characters
only, so a hyphenated field reaches the reader as literal text with nothing to
say it was never substituted. The convention contradicted itself as well, since
the parts of a composite field were already underscored and a name typed with
underscores kept them, leaving a single export column carrying both.

Underneath that the format had no definition of its own. It was whatever slugify
produced, so a rule a growing number of surfaces depend on lived in a helper that
answers to URLs and could widen on a version bump. The characters a key may
contain are now stated where its readers are known, as an allowlist rather than a
list of things to strip out, and the libraries keep only the transliteration and
invisible-character stripping they own. Trimming the ends falls out of that rule
and makes __proto__ unmintable under every spelling, so it no longer needs
reserving alongside constructor.

A key is minted once and never changes, so a definition created earlier keeps its
old key for good, and rewriting one in place would leave its stored values and
every reference to it pointing somewhere else. Those definitions are discarded
with their values instead: custom fields sit behind a private flag and have never
been released, so only a site that deliberately opted in can hold one, and
re-creating the field re-mints the key. The migration measures a key against the
shape this release mints rather than looking for a hyphen, because the previous
minting passed underscores through untouched and the definitions endpoint shipped
four days before the reserved-key guard did.
no ref
- Clear populated CodeMirror fields through their own `ControlOrMeta+A`
and Backspace key handling.
- Wait for the empty document before filling each replacement, then
verify the rendered value before saving.
- Reuse the expected replacement values in the exact request-body
assertions.
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@tanstack/react-query](https://tanstack.com/query)
([source](https://redirect.github.com/TanStack/query/tree/HEAD/packages/react-query))
| [`5.101.2` →
`5.101.4`](https://renovatebot.com/diffs/npm/@tanstack%2freact-query/5.101.2/5.101.4)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@tanstack%2freact-query/5.101.4?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tanstack%2freact-query/5.101.2/5.101.4?slim=true)
|

---

### Release Notes

<details>
<summary>TanStack/query (@&TryGhost#8203;tanstack/react-query)</summary>

###
[`v5.101.4`](https://redirect.github.com/TanStack/query/blob/HEAD/packages/react-query/CHANGELOG.md#51014)

[Compare
Source](https://redirect.github.com/TanStack/query/compare/@tanstack/react-query@5.101.3...@tanstack/react-query@5.101.4)

##### Patch Changes

- Updated dependencies \[]:
-
[@&TryGhost#8203;tanstack/query-core](https://redirect.github.com/tanstack/query-core)@&#8203;5.101.4

###
[`v5.101.3`](https://redirect.github.com/TanStack/query/blob/HEAD/packages/react-query/CHANGELOG.md#51013)

[Compare
Source](https://redirect.github.com/TanStack/query/compare/@tanstack/react-query@5.101.2...@tanstack/react-query@5.101.3)

##### Patch Changes

- Updated dependencies
\[[`7e3c822`](https://redirect.github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]:
-
[@&TryGhost#8203;tanstack/query-core](https://redirect.github.com/tanstack/query-core)@&#8203;5.101.3

</details>

---

### Configuration

📅 **Schedule**: (in timezone Etc/UTC)

- Branch creation
  - Only on Sunday and Saturday (`* * * * 0,6`)
- Between 11:00 PM and 11:59 PM, Monday through Friday (`* 23 * * 1-5`)
- Between 12:00 AM and 04:59 AM, Monday through Saturday (`* 0-4 * *
1-6`)
- Automerge
  - Only on Sunday and Saturday (`* * * * 0,6`)
- Between 11:00 PM and 11:59 PM, Monday through Friday (`* 23 * * 1-5`)
- Between 12:00 AM and 04:59 AM, Monday through Saturday (`* 0-4 * *
1-6`)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODAuMyIsInVwZGF0ZWRJblZlciI6IjQzLjI4MC4zIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: tryghost-renovate[bot] <269725441+tryghost-renovate[bot]@users.noreply.github.com>
ref https://linear.app/ghost/issue/BER-3861

A value is addressed by key everywhere it matters: the write names the
field by key, a segment filter names it by key, and the key is immutable.
Carrying the key directly on the values row lets the read and filter paths
drop the id-to-key join, so the foreign key now references the field's key
column rather than its id.
Two migrations read custom_field_id in their up: the 6.57 leaf-rows migration and the 6.58 underscore-keys reset. The value re-key drops that column, and the idempotency check re-runs every current-major up against the final schema, so each guards its custom_field_id work on the column still being present. A forward install always sees the column, so this changes nothing an install actually applies.
…e holes (TryGhost#29817)

ref https://linear.app/ghost/project/moving-routesyaml-off-of-disk-9887873946a1/

- removed comments describing code we already deleted: they point at an eager
  URL service and a macrotask queue that no longer exist, and one names the
  wrong place for where `{field}` placeholders get rewritten
- removed `unmountRoute` - it had no callers and didn't work anyway, matching
  `item.path` when a mounted route's path lives at `layer.route.path`
- removed the `resourceAlias` fallback and the `filter` key on static pages,
  neither of which is set anywhere, so both were already fixed values
- dropped the query defaults in `fetch-data`, since both callers now pass a
  complete query and nothing was being defaulted
- built the default post query from the API adapter instead of repeating
  `postsPublic` by hand, so renaming it in `config.ts` can't quietly point the
  frontend at the Admin endpoint
- fixed `Omit<Route, 'path'>` dropping `filter`, `order`, `limit`, `rss` and
  `contentType` - `Omit` over a union keeps only the shared keys, so the parser
  could leave one out and still compile
- added a `satisfies` check so a resource added without a `QUERY` entry fails
  the build instead of erroring on every request through that route
- fixed three fixtures stubbing `api.posts` where production passes
  `postsPublic`, and ran the FileStore against the store contract it was the
  only adapter missing
- no behaviour change: every path removed was already unreachable
Hardens the internal-package migration workflow based on its first
cold-start use.

Requires isolated, freshly based worktrees and fail-fast checks around
subtree splitting and importing. Verifies the source checkout is clean,
the destination is a linked worktree, and the resulting merge topology
preserves and reaches the recorded source history.

Adds guidance for assessing public consumers, mapping workspace
dependencies under Ghost's strict catalog policy, and minimally
integrating legacy CommonJS packages without mixing migration with
modernization.

Expands verification to cover package resolution, Nx tests, consumer
tests, the full build, and release-archive contents.
Removes the permanently enabled `llmsTxt` labs flag now that llms.txt
support is generally available. LLMs.txt is still off by default, and can be
enabled in settings.
Removes the permanently enabled `explore` Labs flag from Explore Ping
now that the feature is generally available. Explore is still configurable in 
settings.
Updates the settings API response header snapshot after the merged
Labs flag removals. Minimal change required to get main green.
…ryGhost#29884)

no issue

- the config API snapshot embedded the exact labs flag list, and the
  settings API Stripe test pinned an exact content-length that varies
  with the size of the labs setting — both churned every time a feature
  flag was added or removed, causing avoidable snapshot conflicts
  between PRs
- labs is now matched as any object, with an explicit assertion keeping
  the meaningful coverage: the flags must be a non-empty plain object of
  booleans; the settings header match now uses anyContentLength like the
  other settings tests
no issue

- the flag has been permanently enabled via GA_FEATURES for a while, so
  the conditional it gated was dead in practice
- the Featurebase widget remains gated by the `featurebase` config, which
  was already the real on/off switch everywhere else (token endpoint,
  config serializer)
Removes the permanently enabled `dangerZoneResetAuth` labs flag now that
resetting all authentication is generally available. The Danger Zone action
is always shown, and the tests no longer need to opt in to the flag.
ref https://linear.app/ghost/issue/BER-3850

Definitions came back in whatever order they happened to be created in, and
every surface reads that one list: the settings list, a member's details, the
import mapping's targets, the export's columns. A sort_order column now carries
the order, added without a backfill because the read tie-breaks on created_at,
so a site that never reorders keeps the order it has. The order is set by
PUTting the whole collection in the order it should have rather than by editing
one field, because where a field sits is a fact about the list and not about the
field, so no rank is exposed on a definition anywhere in the API or the domain.
That list has to name every definition exactly once, and the rows are locked in
key order so two publishers dragging at the same moment cannot deadlock.
ref https://linear.app/ghost/issue/BER-3850

The read that assembles a member's values sorted its rows by field, on the
hottest query in the domain: every member of a page, joined against their
values. It could never deliver that order to anyone, because the rows become an
object keyed by field and an object carries no order, since JSON gives member
order no meaning and JavaScript enumerates any key that looks like an array
index first. Path ordering stays, so the parts of one composite field still
assemble the same way every time.
ref https://linear.app/ghost/issue/BER-3850

The API could take an order but nothing could give it one. The active tab in
Settings is now a sortable list, following the newsletters list it borrows its
component from. A drag is applied to the whole list rather than the tab it was
made in, because a reorder names every definition and the archived ones keep
their places around the move. The order just drawn is held on the screen that
drew it and rendered from immediately, so letting go is the end of the
interaction rather than the start of a round-trip; the response then settles the
cached lists directly, since a reorder only succeeds when it named exactly the
fields the site has and a refetch would learn nothing. Each cached list is put
into the new order rather than replaced, because several live under one key and
they do not hold the same fields.
…#29845)

no ref

*I recommend [reviewing this with whitespace changes
disabled](https://github.com/TryGhost/Ghost/pull/29845/changes?w=1).*

What
----

Before this change, there was a class called
`EmailAnalyticsProviderMailgun`, which basically just exposed a single
function that fetched Mailgun events.

After this change, there's a function called `fetchMailgunEvents` that
does this.

Why
---

There was no reason for this to be a stateful class. The function is
simpler.

Test plan
---------

- Automated tests
- Made sure newsletter opened events still worked with Mailgun. Sent a
newsletter, opened it in Gmail, and verified that it appeared in the UI
no issue

- removed the `commentsThreads` and `commentsPinning` labs flags now that both features are GA, along with all flag conditionals
- deleted the unreachable non-threaded comments UI: the flat reply layout, "Replied to:" snippets, and top-level reply collapsing
- made pinned-first ordering and pin moderation unconditional, removing the comments service's labs dependency entirely
- bumped comments-ui to 1.6 so already-deployed bundles that still read the flags stay paired with backends that serve them
…ost#29846)

ref TryGhost#29845

This change should have no user impact.



---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
towards https://linear.app/ghost/issue/NY-1473

This change should have no user impact.

I think this is a useful change on its own, but it'll make [an upcoming
change][0] easier.

[0]: https://linear.app/ghost/issue/NY-1473

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
values-service hardcoded the members_custom_fields.key length as 191 while its sibling definitions-service already reads it from the canonical schema. This reads it from the same source so the two services cannot disagree, and neither can drift from the column if the width ever changes.
The static schema restates the field-type and status lists because it cannot import them, guarded only by a keep-in-sync comment. This asserts the schema's isIn lists equal FIELD_TYPE_IDS and FIELD_STATUS, so a divergence fails the build rather than shipping a schema that accepts or rejects the wrong values.
…schema

The presentation catalog listed an address's sub-field labels as a loose Record of string to string, so a mistyped or omitted part compiled and silently lost its label. Typing it against the shared Address value schema's keys makes a part added, removed, or renamed upstream a compile error here instead of a missing label at runtime.
rob-ghost and others added 29 commits August 18, 2026 11:07
The members custom-field filter matches a field and its value on the same leaf
row through mongo-knex's $elemMatch operator, pulled in via @tryghost/nql.
Pinning the newest patch (nql 0.13.4 / mongo-knex 0.11.2) keeps the filter on the
latest fixes rather than the oldest version its range accepts.
The members filter needs a custom field's operator control to live inside that field's own renderer rather than in the filter framework, so the operator can react to the chosen field's type. This adds the segmented select and input primitives a custom renderer composes into one cohesive filter pill, and threads an operator-change callback through the value selector so a renderer can present and own its operator while still reading as a native filter row.
Members can now be filtered and segmented by their custom field values, behind the membersCustomFields flag. A segment names a field by its stable key and matches on its value; the values reach the query through a custom_fields relation that mongo-knex resolves as a correlated subquery, so a custom-field predicate composes with every other member filter. The public key/value/path grammar is rewritten onto the leaf-row columns at one choke point on the Member model, the method every members query routes its filter through, so the same saved segment behaves identically across the list, CSV export, bulk actions, member count, and email audiences without any of those paths wiring it up themselves.
The members filter now lists each custom field as its own entry under a named, searchable section, the way newsletters appear. Choosing a field shows an operator and value control that the field's renderer owns, covering equality and contains matches, is-set and is-not-set for a whole field, and for a composite field like an address the same for an individual part. The chosen predicate serialises to the key/value grammar the backend understands and parses back from it, so a saved segment reopens as the filter that created it. Behind the membersCustomFields flag.
The field picker resolved both its dropdown list and every restored filter's
pill from one flattened config map, so a long list of custom fields could not
be shortened without dropping fields from resolution and breaking the saved
segments that referenced them. A group now carries an optional previewLimit
that shortens only what the picker lists, leaving every field resolvable and
findable by search, and the members custom fields group opts in at five with
the rest behind a Show more.
A recognised (key + value) compound only rejected an unpaired value or path
clause. A compound that also carried a clause naming no leaf column had that
clause silently dropped instead, leaving a wider match on the key alone. The
transformer now fails closed on an unsupported, duplicate, or conflicting clause
the same way, so a hand-crafted filter cannot quietly widen its own result.
The composed segments announced generic "Field part", "Operator", and "Value"
labels, so two custom field pills on one row were indistinguishable to a screen
reader. Each segment's aria-label now carries the field's own name.
A saved segment on an archived field kept filtering the list while its pill
vanished, because the picker only knows active fields. Archived fields the
current filter references are now hydrated back as read-only pills: the operator
and value stay visible as static segments so the filter reads clearly, but the
field is gone from the picker so the pill can only be removed. Read-only is a
mode on the shade filter primitives, where the segments render as static text
through the same chrome, and the operator control now shares one dropdown
implementation with them. The picker's overflow copy routes through the pattern's
i18n, and Storybook covers the new segments and states. Custom text fields
default to contains, and the review tidy-ups ride along: a stable empty-array
reference, removal of dead icon branches, dropping a forbidden dark variant, and
restored test globals.
Add a guide for testing Ghost on physical devices and with non-default
development URLs: HTTPS, subdirectory installs, and a separate Admin hostname.
…yGhost#30024)

ref https://linear.app/ghost/issue/MIG-1437/

- adds a testid attribute to the existing file upload dropbox
description
- adds test to ensure existing behaviour is not changed when the
`csvContentImporter` flag is enabled
- adds tests for when the `csvContentImporter` is enabled
- adds a conditional for the file drop placeholder text
Ghost Admin composes a filter from its own field catalogue and sends NQL,
and nothing on either side can tell whether the server honours what was
written: admin cannot see the SQL, and the query layer cannot see what admin
meant. The only place the two are observable together is the browse
endpoint, so these run the NQL admin emits against a real database and
assert exactly which members come back.

Derived fields are the reason this matters more than it looks. A column
filter is its own evidence, but `subscribed` is rewritten to a newsletter
relation before it reaches the model, `email_disabled` combines with it to
mean something neither column says alone, and a label is a join. Those
shapes are agreed between two codebases and pinned nowhere.

This lands ahead of the filter engine refactor so the safety net exists
before anything moves.
…reen (TryGhost#29962)

closes https://linear.app/ghost/issue/NY-1520

Automation workflows could be panned completely out of view, while graph or zoom changes could leave the viewport outside its updated bounds.

Centralized viewport measurement, zoom configuration and constraint handling so workflows remain recoverable across canvas, graph and zoom changes. Updated the controls to match the supported 50–100% zoom range and disable actions at their limits.
…ryGhost#30061)

closes TryGhost#30040

The file upload allowlist introduced with the file type validation was
derived from observed usage and did not include Linux package formats,
so sites distributing software could no longer attach `.rpm` or `.deb`
files while sibling archive formats like `.tar.gz` continued to work.
…29733)

no ref

- `free`: kostenlos → kostenloses
- `complimentary`: kostenlos → kostenloses (kept identical to `free`,
mirroring `de` which uses one word for both)
- `Subscription details`: Abo Details → Abo-Details (German orthography
requires hyphenated compounds)
- `This email address will not be used.`: E-Mail Adresse →
E-Mail-Adresse (same hyphenation rule)
…30014)

no ref

The code injection, webhook, labs YAML editor, feature-toggle
confirmation and universal import dialogs were the last
`NiceModal.create` dialogs under `settings/advanced`. They now take an
`onClose` prop and are rendered by the component that opens them, using
`SettingsModal`'s controlled `onClose` path that the routed settings
dialogs already use. This removes five of the remaining NiceModal call
sites in settings so the provider and dependency can be torn down once
the theme and membership dialogs follow.
no ref

The invalid-theme, theme-installed, theme-editor confirm and
theme-editor input dialogs were the remaining `NiceModal.create` dialogs
in the theme settings area. They now render in-tree from the component
that opens them: the two `ConfirmationModalContent` dialogs take an
`onClose` prop, and the two promise-style editor dialogs take an
`onResolve` prop, with the code editor keeping its await-based
`requestConfirmation`/`requestInput` helpers via a small pending-request
state instead of `NiceModal.show`.

The theme code editor's dialogs are rendered as fragment siblings of its
dark, backdrop-blurred root so they keep the same theme and stacking
they had when NiceModal rendered them at the provider.
ref https://linear.app/ghost/issue/HKG-1914

Ghost's background jobs are moving onto a class-based interface whose transport
is swappable, and this package defines the seam every backend implements. A
backend extends JobsBackendBase - a frozen start/enqueue/scheduleRecurring/
shutdown contract - and only ever sees the serialised {type, payload} envelope
and a single delivery-processor callback, never a live job instance. Keeping the
backend blind to job instances is what makes jobs serialisable end to end and
lets a durable backend drop in for the in-memory one without touching a call
site. The package also exports a backend-agnostic contract test suite, so the
in-memory reference backend and any future durable backend are held to the same
acceptance, delivery, drain and bounded-shutdown behaviour. It is a private
workspace package, matching the other adapter-base-* packages.
ref https://linear.app/ghost/issue/HKG-1914

This is the first working layer on top of the backend contract, and the point at
which jobs can be registered and dispatched at all. The class-based jobs service
owns everything the transport does not: the handler registry, the JSON envelope
boundary, rehydration, cron validation and error reporting. InMemoryJobsBackend
is the reference implementation - delivery runs through a bounded fastq queue
whose concurrency comes from adapter config, shutdown drains in-flight work
within a bounded time, and a recurring schedule is first-write-wins so a
re-registration never disturbs one that is already running. A handler failure is
reported and rethrown so the backend sees a rejected delivery as a failed one;
the in-memory backend defensively logs and drops it, matching the legacy
in-process queue with no redelivery yet. Because the backend is selected through
adapter config, a durable backend is a drop-in replacement. Boot constructs the
service, runs an initially-empty central registration step so there is a single
place to wire handlers, then starts delivery.
To quote [ESLint's docs][0]:

> If a variable is never reassigned, using the `const` declaration is
better.

This enables the `prefer-const` ESLint rule and fixes all violations.

[0]: https://eslint.org/docs/latest/rules/prefer-const
no ref

We had a bunch of code like this in a test:

```js
assertExists(result);
assert.equal(result, 123);
```

That `assertExists` can be removed.

(This also fixes the fact that `assertExists` was imported strangely.)
…30037)

closes https://linear.app/ghost/issue/NY-1529
ref ce08414

This adds `stats.last_run_created_at` to each automation, which we'll
display in the UI soon.

This was built by Claude Opus 5 with the following prompt:

> Commit `ce0841435c44a24bd444277cc587e3a18adc052d` adds a "last run
created at" stat to the automation browse endpoint.
>
> I want a new key, `total_run_count`, which is a count of all the runs
for that automation.
>
> * Update `AutomationBrowseResult`.
> * Update `AutomationBrowseRow`.
> * Update `loadAutomations`.
> * Update `buildAutomationBrowseResult`.
> * Update `ghost/core/test/e2e-api/admin/automations.test.js`.
> * Update
`ghost/core/test/unit/server/services/automations/automations-repository.test.ts`.
> * Re-generate the snapshots with `UPDATE_SNAPSHOTS=1` and running the
necessary tests.
>
> Use red/green TDD.
>
> This should be a fairly straightforward change.

In addition to unit and E2E tests, I also verified that the data
appeared by checking the Network tab of the Firefox devtools:

![devtools
screenshot](https://github.com/user-attachments/assets/de9bb167-ac6d-487c-9196-37d7528ba6bd)

Co-authored-by: Claude <noreply@anthropic.com>
ref TryGhost#23361

This PR completes the Serbian (`sr`) locale. I'm a native Serbian
speaker, and I've filled in all the previously-empty strings across
`comments.json`, `ghost.json`, and `portal.json` — including the Serbian
plural forms (`{count} month_*` / `{count} year_*`, which need the
`_few` paucal form).
no ref

This change should have no user impact.

[`@ts-ignore` should never be used.][0] This removes all usages. Most of
the time, we could simply delete it. Sometimes, we needed to replace it
with something.

All of the functionality changes were (1) only in tests (2) minor.

[0]: https://evanhahn.com/ts-ignore-is-almost-always-the-worst-option/
no ref

[`@ts-ignore` should never be used.][0] ESLint now errors if you do.
`@ts-nocheck` is also forbidden. `@ts-expect-error` is allowed with a
description.

No "real" code changes were necessary after this.

[0]: https://evanhahn.com/ts-ignore-is-almost-always-the-worst-option/
towards https://linear.app/ghost/issue/PLA-137

This installs Oxfmt in the Ghost repo. It doesn't add any scripts or
enforcement, but it enables a workflow where we can run `pnpm exec oxfmt
my-file.ts`, which will ease the future transition.

---------

Co-authored-by: Steve Larson <9larsons@gmail.com>
ref https://linear.app/ghost/issue/BER-3876/refine-the-field-picker-in-the-members-import-mapping

The trigger named the kind of every field it showed, putting "Membership field"
under nearly every row to answer a question almost none of them raise. The kind
is named now only where a custom field's name is one a native field already has,
and with the second line gone the icon leaves its tile, so the control stands at
the height every other one does.

Building that turned up a bug. The list identified its items by their label, so a
custom field named "Name" and the native "Name" were one item to cmdk: both drew
as selected, and since the active item is resolved by querying the first match in
the DOM, Enter took the native one whichever had been arrowed to. Identity moves
to the targets, already namespaced apart as `name` and `custom_fields.name`, and
the label moves to keywords — which needs a filter of its own, since cmdk scores
an item's value and its keywords as one joined string and a match is otherwise
free to run from one into the other.

A composite's parts differ only in their last few words, so truncating the label
as one string took away the only thing telling those rows apart. The name gives
way first now and the part stays whole.
…t#30016)

no ref

Uses the settings `DialogPortal` from TryGhost#30014.

The custom field, welcome email and welcome email customize dialogs were
the last `NiceModal.create` dialogs in the membership and email settings
areas. They now take an `onClose` prop and are rendered by the component
that opens them: the two `SettingsModal` dialogs through the settings
dialog portal so they paint above the settings chrome, and the customize
dialog directly, since its `EmailDesignModal` is already a portalled
Shade `Dialog`.

With these converted (plus TryGhost#30014 and TryGhost#30015), no settings dialog is
created or shown through NiceModal any more, which clears the way to
remove the provider, the dependency and the compatibility bridges.
@andreascreten andreascreten self-assigned this Aug 19, 2026
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.