Skip to content

[pull] main from TryGhost:main - #1424

Merged
pull[bot] merged 13 commits into
code:mainfrom
TryGhost:main
Aug 18, 2026
Merged

pull[bot] merged 13 commits into
code:mainfrom
TryGhost:main

Conversation

@pull

@pull pull Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

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

ErisDS and others added 13 commits August 18, 2026 07:48
Make Shade's Storybook pages the human-readable source of truth for shared
contribution, testing, and review guidance.

Keep `apps/shade/AGENTS.md` focused on routing, execution constraints,
and recurring agent mistakes, and keep it super short.

Update Shade skills and layer-specific docs so they no longer treat
the agent file as a competing source of truth.
## What changed

- moved View posts and Delete tag into a tag actions menu
- added the INTERNAL badge for internal tags
- replaced the native color input with Shade’s color picker
- refined advanced-section typography and tag image actions
- introduced a responsive 2:1 resource-editor layout
- reorganized the core tag fields for the wider left card
- made advanced settings a one-at-a-time sidebar accordion with Meta
data open by default and vertical form/preview composition

## Why

The React tag details screen needed clearer action hierarchy, more
consistent controls, and a layout that separates everyday tag data from
advanced metadata while remaining responsive.

## Validation

- tag detail acceptance tests: 28/28 passed
- Admin typecheck passed
- focused ESLint passed
- full Admin acceptance suite: 441/443 passed; the two unrelated
settings failures both passed when rerun independently (22/22)
- visually checked the responsive 2:1 layout and every advanced-settings
panel on the local Admin screen

ref https://linear.app/ghost/issue/PLA-342/design-refinements
ref https://linear.app/ghost/issue/BER-3868

A member's name or email can contain a dollar sign, and a publisher can
reasonably filter on one. But `$` is also how a regex marks the end of a
pattern, and the admin filter engine used the same character for both.

Reading a saved filter back, the engine asked twice whether the pattern
ended in a `$` — once to work out which operator had been used, and again to
strip the anchor off the value. Neither check considered that the character
might have been escaped because it was part of the value, and neither knew
what the other had concluded. So a filter for names containing "5$" came
back as a filter for names ending in "5", with the value itself left mangled
as "5\". Starts-with came back as contains. This hit contains,
does-not-contain, starts-with and does-not-start-with on member name and
email, and on comment filters.

The anchors are now read once, in a single decomposition that also produces
the value, so the operator and the value can no longer disagree about what
the pattern held. That check is escape-aware: a trailing `$` anchors the
pattern only when it is not escaped.

Round-trip coverage was missing, so it is added across every codec and
operator. A browser test covers the step above them, since the reload is
where the filter stops being state the page holds and becomes NQL that has
to be parsed again — the same path a saved view takes, which is how the
misreading became permanent rather than cosmetic.

The query layer has a related but independent fault, where the same
characters are unescaped before the anchors are read, so the server answers
the wrong question even for a correctly written filter. That is fixed in
TryGhost/NQL#213 and reaches Ghost through a dependency bump.
## What changed

- aligned the tag Color control with the adjacent Name input
- replaced the custom label gap with the shared Shade spacing primitive

## Why

The Color control used a slightly smaller label-to-field gap, which
pulled it out of alignment with Name. Matching the shared field spacing
keeps the first row visually consistent.

## Validation

- tag detail acceptance tests: 28/28 passed
- Admin typecheck passed
- focused ESLint passed
- git diff check passed

ref https://linear.app/ghost/issue/PLA-342/design-refinements
Enforces a 150-line maximum for every tracked `AGENTS.md` as these 
files are commonly loaded into context on every turn. They should be short
and guide AI to the relevant docs and skills as needed.

Also adds some of our newer linting to lint-staged, so that failures are 
reported as early as possible, preferably before a long CI run!
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.
@pull pull Bot locked and limited conversation to collaborators Aug 18, 2026
@pull pull Bot added the ⤵️ pull label Aug 18, 2026
@pull
pull Bot merged commit 589d180 into code:main Aug 18, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants