Skip to content

Feature/listing rework - #438

Open
nicklee wants to merge 13 commits into
migration-2026from
feature/listing-rework
Open

Feature/listing rework#438
nicklee wants to merge 13 commits into
migration-2026from
feature/listing-rework

Conversation

@nicklee

@nicklee nicklee commented Jun 22, 2026

Copy link
Copy Markdown
Member

Replaces the legacy single-select tag cloud (title-filters + ?filter=) on Work, News, and Events index pages with a shared multi-select dropdown filter UI, progressive enhancement via htmx, and a centralised listing backend.

What changed

UI / templates

  • New filter UI with dropdown checkboxes (Sector, Service, Culture on Work/News; When + Event type on Events)
  • Active filter pills with remove links and “Clear all filters”
  • Badge counts per dropdown, synced from the URL
  • Dropdowns with no options and no selection stay hidden (including after other filters are applied)
  • Faceted options — dropdown choices narrow based on other active filters (zero-result options hidden)
  • Empty states for dropdowns with no matching options
  • Reusable listing panel structure: #listing-panel, partial templates, OOB swaps for meta, pills, and option lists
  • Mobile spacing improvements (tighter dropdown gaps, reduced gap before results)

Backend (tbx/core/listing/)

  • New shared module: filters.py, mixins.py, events.py
  • Work, Blog, and Events index pages refactored to use shared context builders
  • Multi-param filtering: ?sector=, ?service=, ?timing=, ?type= (legacy ?filter= still supported)
  • Filter logic: AND between dimensions, OR within a dimension
  • SEO rules: single filter indexable; 2+ filters noindex; dynamic title/canonical via OOB meta swaps
  • Culture split: UI-only separation of culture-related services via CULTURE_SERVICE_SLUGS (both submit as service)
  • EventType gains a slug field (migration 0044_eventtype_slug)

JavaScript (listing.js + listing-filters.js)

  • New webpack entry listing.js with htmx
  • Manual htmx.ajax filter requests (debounced 200ms) — avoids stale state vs form hx-trigger
  • Dropdown open/close, URL sync, badge counts, visibility management
  • Pagination scroll: anchors to top of results on all htmx-enabled listings
  • Non-JS fallback: standard GET form with visible “Apply filters” button

Tests

  • 33 tests in tbx/core/listing/tests/ covering filter state, AND/OR logic, faceting, htmx partials/OOB, SEO, pagination, legacy params, culture split, and non-JS submission

Documentation

  • New docs/listing.md (full feature reference)
  • Updates to docs/navigation.md, docs/upgrading.md, mkdocs.yml

Other

  • Merges in feature/IA-new-nav changes (nav/header SCSS adjustments)
  • Adds htmx.org dependency

@nicklee
nicklee marked this pull request as ready for review June 22, 2026 15:40
@JakubMastalerz

Copy link
Copy Markdown
Collaborator

Hi @nicklee, just wanted to flag that some merge conflicts came up on staging with my #440 changes. cb5807b removes canonical tags from your listing_meta.html since it's now included in base. If you don't think it's the right approach, I'm happy to discuss :)

@tm-kn
tm-kn changed the base branch from main to migration-2026 July 2, 2026 14:13

@helenb helenb 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.

Hey @nicklee thanks for this - there are a few changes to look at from a front-end perspective.

I am hoping @tm-kn can review the js as I am less familiar with htmx - and also I've run out of time today. I can take another look next week if not.

One final comment: it's a bit confusing that if you choose the work listing filtered by public from the parent drop down, you get the charity branding. I am not sure how we'd avoid something like this as presumably you need to select one theme overall for the work listing. But might be something that causes confusion / comments from Lily and Lisa I think.

I would also recommend running the tbx-pr-review skill on this. It picked up a bunch of performance issues and missing tests when i tried it. It might be easier for you to run it than for me to copy and past them all here, but I can share the output if that would be helpful.

Comment thread docs/listing.md
| **Service** | `Service` snippets (non-culture) | `service` | Repeatable; excludes culture slugs (see below) |
| **Culture** | `Service` snippets (culture set) | `service` | UI-only split; same param as Service; badge counts culture selections |

Dropdowns with **no options on the unfiltered listing** and **no active selection** in that dimension are hidden (for example, Culture on Work when no culture-tagged work exists). Dropdown visibility is fixed at page load and does not change when facet narrowing removes options after other filters are applied.

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.

When I tested this on the news listing, I got a drop down for culture with a single checkbox labelled - would that not count as one with no options so should therefore be hidden?

Image

Comment thread docs/listing.md

---

## Filter dropdowns (Events)

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.

For events I was unable to deselect past events once I had selected them. (Appears to be using a radio button not a checkbox)

Screen.Recording.2026-07-03.at.08.26.49.mov

Comment thread docs/listing.md

Without JavaScript:

- The filter form submits via **GET** to the listing URL.

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.

The dropdowns don't appear to function without JavaScript, so it's impossible to select any filters. If we are going to support non-JS here we need to do it fully.

@@ -0,0 +1,5 @@
{% for option in listing_filters.culture %}
{% include "patterns/molecules/listing-filters/includes/filter-option.html" with param="service" value=option.value label=option.label selected=selected_culture %}

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.

There only seemed to be one option here, as mentioned above, and it matches the name of the dropdown. Perhaps the whole filter should be hidden in this scenario.

data-listing-filter-dropdown
{% if not show_dropdown %}hidden{% endif %}
>
<button

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.

Is there a reason you've used buttons and a list here rather than a select dropdown? Select would have the benefit of working without JS.

@use '../config' as *;

.listing-header {
grid-column: 2 / span 4;

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.

Could be moved to grid.scss

}

.listing-filters {
grid-column: 2 / span 4;

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.

same here

min-width: 0;

@include media-query(medium) {
min-width: 9rem;

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.

Is there a logic to this value? Might be worth an explanatory comment as otherwise feels a bit arbitrary.

top: calc(100% + 6px);
left: 0;
width: max-content;
min-width: max(100%, 20rem);

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.

I noticed at small mobile size that the panel is wider than the viewport - seems like the max-width isn't being applied for some reason:

Image

}
}

[id='listing-panel'] {

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.

Do we need this selector? Could it be done with a class instead?

@helenb

helenb commented Jul 3, 2026

Copy link
Copy Markdown
Member

@nicklee Could you use the mr template and tick off the relevant checks?

Comment thread docs/listing.md
| -------------- | --------------------------------- | ------------------- | ----------------------------------------------- |
| 0 | Page title | (default) | Base URL if `?page=` present; else page default |
| 1 | `{title} filtered by {label}` | indexable | Current filtered URL |
| 2+ | `{title} filtered by {a}, {b}, …` | `noindex, nofollow` | Base listing URL |

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.

I'm triple checking with Beth if this is right. I think we usually don't want to do canonical URLs for pages that will repeat content.

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.

return sorted(merged.values(), key=lambda item: item["label"].lower())


@dataclass(frozen=True)

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.

I think we should pivot and use django-filter instead as it's a more established framework for building filters. That would also avoid reading user input directly from request.GET, and use Django forms for sanitising and validating the values, rather than passing them straight to database.

@@ -0,0 +1,13 @@
{% load wagtailcore_tags %}

{# Always present so htmx OOB swaps can target these elements. #}

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.

What's the signifance of using HTMX to swap those tags? Why is this needed?

@helenb helenb removed the on staging label Jul 20, 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.

4 participants