Skip to content

Support label chips, click-to-filter, and label-aware saved filters#178

Merged
xispa merged 14 commits into
2.xfrom
feature/sample-label-chips
Jun 23, 2026
Merged

Support label chips, click-to-filter, and label-aware saved filters#178
xispa merged 14 commits into
2.xfrom
feature/sample-label-chips

Conversation

@ramonski

@ramonski ramonski commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Description of the issue/feature this PR addresses

Companion to senaite/senaite.core#2958. That PR adds the labels infrastructure (permissions, content-type color, listing chip rendering, REST endpoints, modal). This PR is the client-side half: chip styling, click-to-filter behavior, active filter chips above the search box, and label-aware saved filters.

Current behavior before PR

  • Labels assigned to a sample have no visual representation in listings.
  • There is no ?labels= URL filter; saved presets do not capture or apply label state.
  • The reset arrow next to the search box does not clear cross-listing URL filters.

Desired behavior after PR is merged

  • Row chip renders below the primary column value (Sample ID for SamplesView) and reads the inline color emitted by the server-side chip_style helper. Clicking a chip toggles its label in the ?labels= URL parameter — additive on multi-click; clicking the same chip a second time removes it.
  • An active filter row renders before the SearchBox when ?labels= is non-empty. Each chip is painted in the matching Label's color (fetched once from @@available_labels and cached). The X on each chip removes that label from the URL on click. The Reset button (the spinning arrow) also wipes ?labels= via navigation.
  • Saved filter presets ("Save current view") capture the active labels alongside review-state, sort, search term, etc. Loading a preset whose labels differ from the URL triggers a navigation so the server-side filter applies; same-labels presets apply via set_state exactly as before.
  • PRESET_URL_PARAMS includes labels, so a bookmarked share-link with ?labels= correctly suppresses the user's default-preset auto-apply.

Companion PR

senaite.core PR — senaite/senaite.core#2958

Commit walk-through

  1. Support label chips, click-to-filter, and label-aware saved filterslisting.coffee, listing.css, storage/preset_storage.js (source).
  2. Rebuild listing bundle — replaces the current 2.x content-hashed bundle with the rebuilt one.

Test plan

  • Reviewer: with the companion senaite.core PR installed, open a samples listing where samples carry labels — chips render under the Sample ID in their configured colors.
  • Reviewer: click a chip — URL gains ?labels=<name>, listing filters; click the same chip again, filter clears; click a second chip in the same row, both labels filter (AND).
  • Reviewer: the active filter chip row appears before the search box; each chip uses its Label color; clicking the chip's X removes that label from the URL.
  • Reviewer: click the search-box reset arrow — ?labels= is wiped along with the rest of the listing state.
  • Reviewer: with ?labels=Foo active, click the bookmark icon → "Save current view" → reload the page without ?labels= → click the saved preset → URL navigates back to ?labels=Foo and chips reappear.
  • Reviewer: existing listing flows that don't use labels (column filtering, sort, saved presets without labels) are unchanged.

I confirm I have tested this PR thoroughly and coded it according to PEP8 and Plone's Python styleguide standards.

ramonski added 5 commits June 21, 2026 11:13
Adds the client-side half of the labels feature implemented in
senaite/senaite.core#2958.

Listing controller and template:

- on_click delegates a 'sample-label.is-filterable' click into
  on_label_filter_click, which toggles the chip's label in the URL
  ?labels= comma list (additive: clicking a chip adds it, clicking
  again removes it) and reloads the page so the server-side filter
  applies.
- A new active-filter chip row renders before the SearchBox when
  ?labels= is non-empty. Each chip carries an X that removes that
  label from the URL on click. The chips are painted in the
  matching Label's color via @@available_labels (fetched once,
  cached), so the inline filter chip visually matches the row chip
  of the same name.
- resetView clears ?labels= via navigation when the URL carries it,
  in addition to the existing in-memory reset.

Listing CSS:

- .sample-label / .sample-label-text / .sample-id-with-labels:
  flat square-cornered pill (no badge-pill, no padding-fattening),
  block-level so chips stack under the primary value cell.
- .sample-label--lg: bigger variant used by the Label view's
  restyled <h1>.
- .label-color-swatch: 14px square inline before the Label title
  in the controlpanel listing.
- .manage-labels-grid / .manage-labels-toggle / .manage-labels-color
  / .manage-labels-presets / .manage-labels-preset: chip-grid and
  picker chrome for the Manage Labels modal. The 'is-selected' /
  'is-removed' classes mark the add / remove sets the user is
  about to apply.
- .active-label-filter / .active-label-filter__remove: inline
  filter chip rendered next to the SearchBox.

Saved filter preset payload (preset_storage.js):

- normalize_payload / capture_payload include 'labels' (sorted
  array), so payloads_equal compares two captures of the same UI
  state as equal regardless of label order.
- The ListingController's 'current' prop and applySavedFilter both
  flow the active labels through. Applying a preset whose labels
  differ from the URL triggers a navigation so the server-side
  filter takes effect; same-labels presets apply via set_state
  exactly as before.
- 'labels' is added to PRESET_URL_PARAMS so a bookmarked share-link
  with ?labels= correctly suppresses the user's default-preset
  auto-apply.
senaite.app.listing.f0772ab7711d8e304faf.js (current 2.x bundle)
replaced by senaite.app.listing.74a49469a8a88fc9761b.js with the
client-side label-chip support added in the previous commit.
Tracks senaite/senaite.core#2958 (LabelsAPI traversal view): the
listing controller now fetches the active-filter chip color map
from ./@@labels/available instead of ./@@available_labels. No
behavioral change; the response shape (name, color, description)
is identical.
Tracks the rename in senaite/senaite.core#2958: the listing
controller now fetches the active-filter chip color map from
./@@senaite_labels/available. No behavioral change; the rename
on the server side was made to avoid an icon-traversal collision
with the bare /labels path.
ramonski added 3 commits June 21, 2026 12:34
capture_payload now returns a 'labels' array alongside the
existing review_state / column_filters / sort / pagesize / filter
fields. The pre-existing 'canonical shape with defaults' test was
asserting the old shape and failed in CI; updated to include
labels: [] and added three positive tests covering the sort +
empty-strip behavior, the clone-on-capture guarantee, and the
fallback for missing / non-array labels.
The .sample-label--lg variant was a fixed .85rem chip used when the
LabelColorViewlet rendered a separate pill next to the heading.
Now that the heading itself IS the chip (the @@title override
wraps the <h1>'s content in the chip span), the chip should
inherit the H1's font-size so it reads as a proper page heading
rather than a small pill stuck where the title used to be.

font-size: inherit lets the cascade do its job; padding bumped to
.25rem .9rem and border-radius to 6px so the proportions hold at
the larger H1 size. The row-chip body (.sample-label without --lg)
is untouched.
Inheriting the H1 font-size let the chip take on the full default
Plone heading scale, which read as too dominant. 1.25rem lands
between the row-chip body size (.65rem) and the default H1 (~2rem)
— big enough to register as a heading, small enough to leave
breathing room above the description text.
@ramonski ramonski requested a review from xispa June 21, 2026 19:34
@ramonski ramonski added the Enhancement ✨ Improvement to existing functionality label Jun 21, 2026
ramonski added 6 commits June 21, 2026 21:39
A Label without a configured color rendered as a grey pill in the
listing row but as a blue accent pill above the search box,
because the .active-label-filter rule defaulted to the SENAITE
accent palette while .sample-label defaulted to the neutral
hairline palette. Same Label, two looks.

Aligns the .active-label-filter defaults to the row-chip neutral
palette so the two render identically when no color is set. The
listing controller still resolves the per-label color from
@@senaite_labels/available and sets an inline style that wins
over these fallbacks when a color exists.
Typing `label:Test` (or `labels:Foo,Bar`) into the search box and
hitting Enter now lifts the matched names into the URL ?labels=
filter — the same channel the click-to-filter chip uses — and
treats the remainder of the input as the regular search filter.
The active-filter chip row above the search box picks them up on
the next render, exactly like a chip click would.

filterBySearchterm peels the prefixes via parse_label_search_prefixes
and history.replaceState's a fresh URL so the next folderitems
fetch carries the new labels in QUERY_STRING (api.coffee already
reads location.search on every call). The residual term becomes
the new @State.filter, so a query like "label:Test water" filters
to samples labeled Test containing the word "water".

Whitespace inside a single label token is not supported — the
parser splits on /\s+/. Labels with spaces in the name should be
applied via chip click instead.
…search box

Two fixes around the cross-listing labels URL filter:

1. Applying a saved preset that carried a labels filter used to
   `window.location.assign` the new URL. The reload discarded the
   listing's local state, including `applied_preset_id`, so the
   preset visibly de-activated the moment it took effect. Selecting
   the same preset a second time worked because the URL labels
   already matched and the reload path was skipped.

   `applySavedFilter`, `resetView`, `on_label_filter_click` and the
   active-filter chip `×` handler now use `window.history.replaceState`
   to update the URL in place. The next folderitems fetch reads
   `location.search` fresh (api.coffee:#get_api_url), so the new
   labels still reach the server, but the rest of the React state
   (preset id, column filters, sort, pagination) survives the
   change. The fetch is triggered either by `set_state` (preset
   apply, reset) or by an explicit `fetch_folderitems()` +
   `forceUpdate?()` (chip click / chip ×).

2. The `.active-label-filter` chip was 1.6rem tall and visually
   shorter than the Bootstrap form-control-sm search box next to
   it. Height now uses the same `calc(1.5em + .5rem + 2px)`
   formula Bootstrap uses for the search input, font-size bumped
   to .875rem, padding adjusted so the text sits centered. Chip,
   bookmark icon and search input share one baseline.
Matching the search box height made the chip dominate the row,
especially with a saturated label color. Reverts to the previous
1.6rem / .75rem font footprint so the chip reads as a status
indicator riding alongside the input rather than a peer button.
align-items: center on the parent flex row already keeps the chip
mid-line with the search box, which is the alignment cue that
matters.
When a saved preset was flagged as the user's default and that
preset carried labels, the constructor's
apply_default_preset_unless_url_state hook mutated the React
state fields (filter, review_state, sort, ...) but never touched
the URL's ?labels= query. Labels live outside React state — every
folderitems fetch reads them fresh from location.search — so the
first listing render came up with no label filter applied. The
user then had to open the SavedFilters menu and click the preset
explicitly to push the labels into the URL.

The constructor now history.replaceState's the URL with the
preset's labels before @State is built (no listener wired yet, so
this is a silent setup step). The first folderitems fetch picks
them up like any other ?labels= filter; the active-filter chip
row above the search box renders on the first mount.
# Conflicts:
#	src/senaite/app/listing/browser/static/bundles/senaite.app.listing.8c375ee0d292e8fef0a4.js
#	src/senaite/app/listing/browser/static/bundles/senaite.app.listing.8c375ee0d292e8fef0a4.js.LICENSE.txt
#	src/senaite/app/listing/browser/static/bundles/senaite.app.listing.9adf3bddf1be9d10a643.js.LICENSE.txt
#	src/senaite/app/listing/browser/static/bundles/senaite.app.listing.f0772ab7711d8e304faf.js.LICENSE.txt
#	src/senaite/app/listing/browser/static/resources.pt

@xispa xispa left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent thanks!

@xispa xispa merged commit e9d3510 into 2.x Jun 23, 2026
2 checks passed
@xispa xispa deleted the feature/sample-label-chips branch June 23, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement ✨ Improvement to existing functionality

Development

Successfully merging this pull request may close these issues.

2 participants