Support label chips, click-to-filter, and label-aware saved filters#178
Merged
Conversation
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.
Merged
6 tasks
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.
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
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
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=URL filter; saved presets do not capture or apply label state.Desired behavior after PR is merged
chip_stylehelper. Clicking a chip toggles its label in the?labels=URL parameter — additive on multi-click; clicking the same chip a second time removes it.?labels=is non-empty. Each chip is painted in the matching Label's color (fetched once from@@available_labelsand cached). TheXon each chip removes that label from the URL on click. The Reset button (the spinning arrow) also wipes?labels=via navigation.set_stateexactly as before.PRESET_URL_PARAMSincludeslabels, 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
listing.coffee,listing.css,storage/preset_storage.js(source).2.xcontent-hashed bundle with the rebuilt one.Test plan
?labels=<name>, listing filters; click the same chip again, filter clears; click a second chip in the same row, both labels filter (AND).Xremoves that label from the URL.?labels=is wiped along with the rest of the listing state.?labels=Fooactive, click the bookmark icon → "Save current view" → reload the page without?labels=→ click the saved preset → URL navigates back to?labels=Fooand chips reappear.I confirm I have tested this PR thoroughly and coded it according to PEP8 and Plone's Python styleguide standards.