[pull] main from TryGhost:main - #1424
Merged
Merged
Conversation
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.
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 : )