Skip to content

CMS: Create shared FilterOptions component to use throughout overviews#1794

Open
johan-bell wants to merge 8 commits into
mainfrom
1266-cms-shared-filter-options-component
Open

CMS: Create shared FilterOptions component to use throughout overviews#1794
johan-bell wants to merge 8 commits into
mainfrom
1266-cms-shared-filter-options-component

Conversation

@johan-bell

@johan-bell johan-bell commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #1266, #1795

What

The same filter bar — search box, optional group multi-select with removable tag chips, and a responsive mobile/desktop layout — was independently reimplemented five times across the CMS's overview pages:

  • AuthProviderOverview.vue / AutoGroupMappingOverview.vue — inlined directly in the page (~130 near-identical lines each)
  • UserFilterOptions.vue + its own UserFilterOptionsMobile.vue/UserFilterOptionsDesktop.vue pair
  • GroupFilterOptions.vue + its own GroupFilterOptionsMobile.vue/GroupFilterOptionsDesktop.vue pair
  • RedirectOverview.vue — a simpler bare search box (no group filter)

Added one shared cms/src/components/common/FilterOptions.vue: search/selectedGroups v-models, an optional debounceMs, and an extra-filters slot (rendered inline on desktop and inside the mobile filter modal — with an optional extra-filters-mobile override) for page-specific controls like AutoGroupMapping's auth-provider select. All five pages above now use it; the old bespoke components are deleted.

ContentOverview (#1795)

ContentOverview/FilterOptions.vue was initially left untouched — its filter panel has enough extra dimensions (status, translation, tags, sort) and a fundamentally different search interaction (min-3-character + explicit "Go" button + separate "X" clear, vs. everywhere else's auto-debounce) that folding it into the same behavior wasn't a drop-in fit.

Rather than generalizing the shared component's behavior to match Content (a searchMode prop, as originally proposed in #1795), it turned out cleaner to reuse just its shell:

  • New #search slot — when provided, replaces the built-in debounced search input entirely. The search model is now optional (default "") since a page using this slot has no reason to bind it.
  • New #selected-filters slot — renders a page-owned chip list in the same below-the-row spot the built-in group-filter chips use.
  • extra-filters / extra-filters-mobile now carry all of Content's filter dimensions, including its own group filter — the shared component's built-in group filter/chips are unused for this page.

ContentOverview/FilterOptions.vue keeps 100% of its existing filter logic (status/translation/tags/sort state, the min-length Enter/Esc search, sessionStorage persistence) — only the component rendering the responsive shell changes. ContentOverview.vue's own props/model contract is untouched. FilterOptionsDesktop.vue / FilterOptionsMobile.vue are deleted.

Net effect

-838 deleted / +51 modified (5-page migration) + the shell-slot extension and Content migration = large net reduction while removing all six duplicated filter-bar implementations.

No behavior change on any page, other than: Redirect and AuthProvider/AutoGroupMapping now get a reset button consistently (Redirect didn't have one before); User's and Redirect's search debounce is now owned by the shared component's debounceMs prop instead of hand-rolled debouncedWatch in each page.

Tests

  • cms/src/components/common/FilterOptions.spec.ts (12 tests): group filter visibility, debounced vs. immediate search commit, tag removal, reset, mobile layout + modal, extra-filters slot (incl. mobile fallback), the search/selected-filters slot overrides.
  • Existing GroupOverview.spec.ts, UserOverview.spec.ts, RedirectOverview.spec.ts, ContentOverview.spec.ts all pass unmodified against the migrated pages.
  • AuthProviderOverview.vue and AutoGroupMappingOverview.vue have no dedicated spec files (pre-existing gap, not introduced here) — recommend a manual pass on both filter bars before merge.

Full cms suite: 932/932 passing. npm run lint and npm run type-check clean.

The same search-box + group-multiselect + responsive mobile/desktop
filter bar was independently reimplemented across five overview pages,
some as inline markup, some as separate Mobile/Desktop file pairs. Add
one shared component: search (with optional debounce), an optional
group filter with removable tag chips, and a slot for page-specific
extra filters. Not a fit for ContentOverview, which has enough extra
dimensions (status, translation, tags, sort, a min-length search) to
warrant its own dedicated filter panel.
Replaces ~130 lines of inline desktop/mobile filter markup with the
shared component. No behavior change.
Same ~130-line duplicate filter bar as AuthProviderOverview, plus one
extra control (the auth-provider select) wired through the extra-filters
slot. No behavior change.
Removes UserFilterOptions.vue and its Mobile/Desktop pair (232 lines)
in favor of the shared component; UserOverviewQueryOptions moves inline
since it was only consumed here. Search debounce (500ms) is now owned
by FilterOptions' debounceMs prop. Behavior-equivalent coverage moves
to FilterOptions.spec.ts.
Removes GroupFilterOptions.vue and its Mobile/Desktop pair (206 lines)
in favor of the shared component. Group-specific fields (orderBy,
orderDirection) stay owned by GroupOverview's own resetQueryOptions,
wired through FilterOptions' reset event.
…tions

Redirect only had a bare debounced search box (no group filter); swaps
it for FilterOptions in search-only mode (groups prop omitted), which
also picks up a reset button Redirect didn't previously have.
Lets a page reuse just the responsive shell (desktop row / mobile
Adjustments-button-and-modal / reset wiring) without adopting the
built-in search-debounce or group-filter/chip behavior: the search
slot replaces the built-in input entirely, selected-filters renders a
page-owned chip list in the same below-the-row spot the built-in group
chips use. The search model becomes optional (default "") since a page
using the search slot has no reason to bind it.
…shell

Removes FilterOptionsDesktop.vue and FilterOptionsMobile.vue (161
lines); their content moves into the shared component's search /
extra-filters / extra-filters-mobile / selected-filters slots.
ContentOverview.vue's own props/model contract is unchanged. All
filter logic (status, translation, tags, sort, the min-length
Enter/Esc search) is untouched — only which component renders the
responsive shell changes.
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.

CMS: Create shared FilterOptions component to use throughout overviews

2 participants