Skip to content

Experiments listing: user and time filters, Created By column, and Content Drive-aligned pagination #37307

Description

@erickgonzalez

Description

Feedback from the Global Sprint demo on the new Experiments portlet. Three additions to the listing screen:

  1. User filter — filter experiments by who created them.
  2. Schedule filter — filter experiments by scheduled start date.
  3. "Created By" column — surface the creator in the listing table.
  4. Pagination aligned with Content Drive — the listing paginates, but not the way the rest of the product does.

Design prototype: https://claude.ai/design/p/36aed5a6-ca42-4624-b187-a465a23ce909?via=share&file=Experiments.dc.html

Slack threads:


Current state

The listing already has a search input plus two chip filters (Status, Goal) built on DotExperimentListFilterComponent, which wraps the shared content-drive DotChipFilterComponent from @dotcms/ui. The new filters follow that same pattern — the component is deliberately generic (its own docblock notes it "knows nothing about statuses or goals"), so adding a filter is mostly a matter of supplying an option list.

All filtering happens client-side in DotExperimentsListStore as a chain of computed signals:

siteScopedExperiments → searchedExperiments → statusFilteredExperiments
  → goalFilteredExperiments → filteredExperiments → (paginated)

GET /v1/experiments accepts only pageId, name, and status and returns the full list, so no backend filtering work is required — the two new filters extend this client-side chain.


1. User filter

  • Chip filter, multi-select, sitting alongside the existing Status and Goal chips.
  • Options come from GET /api/v1/users/filter, the endpoint Modernization already uses. It supports everything needed: filter (search across id, first/last name, email, full name), page, perPage, orderBy, direction.
  • Infinite scroll inside the popover — page in more users as the list is scrolled, rather than loading everything up front.
  • Search inside the popover, passed through as the filter query param (server-side search, debounced).
  • Lists all users in the system, not just those who have created an experiment.
  • Matching is on user ID: the selected users' IDs are compared against each experiment's createdBy (which holds the user ID).
  • No result counts on this chip's options — users are paged from the server, so a count cannot be computed for users not yet fetched. This is a deliberate, documented divergence from the Status/Goal chips.

2. Schedule filter

Chip filter whose popover holds a calendar in range mode: a period is picked by selecting its two ends.

Rule Behaviour
Default No period. The filter reads as unfiltered and applies no date constraint.
Match An experiment matches when its scheduled start falls inside the period, both bounds inclusive.
Open bounds Either end may be left open. A start alone means "on or after", an end alone "on or before".
Whole days Bounds cover whole local days, first instant of the start day to last instant of the end day.
Inverted range A period whose end precedes its start is reported to the user and not applied. Only a hand-edited or stale address can produce one.
Unscheduled Experiments with no scheduled start are excluded whenever either bound is in force, and reappear when the filter is cleared.

The period travels in the address as schedule_from and schedule_to, each a local calendar date in yyyy-MM-dd. Either may appear without the other.

This replaces the five relative windows first specified here (any schedule, and the last 1, 3, 6 and 12 months). Content Drive already filters dates with an absolute from-to calendar, the shared dot-field-filter in @dotcms/ui that the asset picker and the relationship field also render. A second, different way to filter by date would have created exactly the divergence the pagination part of this issue exists to close. The range also covers every window listed above, plus periods that are not "the last N months". The accepted cost is that a shared link freezes the period instead of continuing to mean "the last three months", which is also what makes such a link reproducible.

Argued in full in the approved spec (User Story 2, FR-017, FR-020 to FR-022, FR-049a) and in the description of PR #37514.

3. "Created By" column

4. Pagination alignment

The listing already paginates: DotExperimentsListStore exposes a pagedExperiments computed that slices the sorted list client-side, and the table runs [lazy]="true" with a PrimeNG paginator bound to totalRecords. Because GET /v1/experiments returns everything in one shot, that pagination is simulated — the store plays the role the server plays elsewhere.

What it does not do is behave like Content Drive's dot-folder-list-view. Close these gaps so both portlets read as one product:

Experiments (today) Content Drive Action
Default rows per page 25 20 Adopt 20
Rows-per-page options [10, 25, 50] [20, 40, 60] Adopt [20, 40, 60]
Dropdown overlay not set — clips inside the table's scroll container paginatorDropdownAppendTo="body" Add it
Paginator visibility always rendered rendered only when lazy, or when items exceed one page Match Content Drive
lazyLoadOnInit not set bound to the lazy flag Match Content Drive

Shared already-matching behavior to preserve: showFirstLastIcon=false, showPageLinks=false, and the Page {currentPage} report template.

Acceptance Criteria

User filter — happy path

  • A "Created By" (user) chip filter appears in the listing toolbar alongside the existing Status and Goal chips, reusing DotChipFilterComponent / DotExperimentListFilterComponent.
  • Opening the chip loads the first page of users from GET /api/v1/users/filter.
  • Scrolling to the bottom of the user list loads the next page (infinite scroll) — no full up-front load of all users.
  • Typing in the popover's search box filters users server-side via the filter query param, debounced, and resets to page 0.
  • Multiple users can be selected at once; selecting users narrows the table to experiments whose createdBy matches any selected user (OR within the filter).
  • The user filter combines with search, Status, and Goal as an AND across filters, consistent with the existing chain.
  • The chip's remove (×) control clears the whole user selection and restores the unfiltered list.
  • Selected users stay selected while scrolling loads further pages.
  • The user filter's options render without result counts.

User filter — sad path / edge cases

  • Selecting a user who has created no experiments yields the empty state with the "clear filters" action, not an error.
  • A failed /api/v1/users/filter request surfaces through DotHttpErrorManagerService and leaves the table's current results intact — it does not blank the listing.
  • Scrolling past the last page of users stops requesting — no infinite request loop at the end of the list.
  • Experiments whose createdBy refers to a deleted/unresolvable user do not break the filter.

Schedule filter

  • A schedule chip filter is present, and its popover holds a calendar in range mode rather than a list of relative windows.
  • No period is the default, and in that state the filter applies no date constraint.
  • One period applies at a time: picking a new one replaces the previous.
  • With a period in force, an experiment matches when its scheduled start falls inside it, both bounds inclusive.
  • A period with only a start matches "on or after" it; a period with only an end matches "on or before" it.
  • Bounds cover whole local days, so a single-day period matches an experiment scheduled at any time that day.
  • A period whose end precedes its start is reported to the user and not applied.
  • An experiment with no scheduled start is excluded whenever either bound is in force, and reappears when the filter is cleared.
  • The period travels in the address as schedule_from and schedule_to in yyyy-MM-dd, and either may appear without the other.
  • The schedule filter combines with search, Status, Goal, and the user filter as an AND.

Created By column

  • The listing table has a Created By column rendering the creator's name from createdByUserName.
  • The column is placed per the design prototype.
  • Long names truncate with a tooltip carrying the full value, consistent with the Name and Page columns.
  • When createdByUserName is absent from the payload, the cell renders the existing placeholder rather than undefined.

Shared / cross-cutting

  • Both new filters persist in the URL and rehydrate on reload and browser back/forward, matching the existing filter / selectedStatuses / selectedGoals behavior in the store.
  • Changing either filter resets pagination to page 1 and totalRecords reflects the filtered count.
  • The $hasActiveFilters() / "clear filters" empty-state action clears the new filters too.
  • All new labels go through the | dm message pipe — no hardcoded strings.
  • Existing Status and Goal chip counts are unaffected by the new filters.

Pagination

  • Default page size is 20 rows; DEFAULT_EXPERIMENTS_LIST_PER_PAGE is updated accordingly.
  • The rows-per-page selector offers 20 / 40 / 60, matching Content Drive.
  • The rows-per-page dropdown renders with paginatorDropdownAppendTo="body" so its overlay escapes the table's scroll container and is never clipped.
  • The paginator is hidden when the filtered result set fits on a single page, and shown otherwise — same rule Content Drive applies.
  • lazyLoadOnInit is bound consistently with the table's lazy flag, so the first page loads once and not twice.
  • showFirstLastIcon, showPageLinks, and the Page {currentPage} report template are unchanged.
  • Changing the page or the page size updates the rows shown without refetching from the server — pagination stays client-side over the already-loaded list.
  • Applying or clearing any filter (search, Status, Goal, user, time) resets to page 1 and recomputes totalRecords.
  • Sorting a column re-sorts the whole filtered set, not just the visible page.
  • Paging preserves the active sort and every active filter.

Tests

  • Store unit tests cover: user filter matching by createdBy, multi-user OR semantics, time-filter boundaries (just inside / just outside each window), future-dated start dates matching, unscheduled experiments excluded, and AND-combination with existing filters.
  • Component tests (Spectator, byTestId) cover: infinite-scroll paging, debounced search, multi-select, chip clear, and the Created By column rendering.
  • Pagination tests cover: default page size of 20, the 20/40/60 options, the paginator hiding on a single-page result, page-1 reset on filter change, and sort applying across the full filtered set rather than the visible page.

Priority

Medium

Additional Context

Blocked by #37304 — the Created By column needs the createdByUserName field that issue adds. The two filters are not blocked and can proceed in parallel.

Decisions made during refinement:

Question Decision
Created By data source Bind to createdByUserName from #37304 (not FE-side name resolution)
Unscheduled experiments under a time filter Excluded unless "Any schedule"
Future-dated start dates Included — lower bound only, no upper bound
User-filter chip counts None — users are server-paged, counts can't be computed
Backend filtering Not needed — filtering stays client-side in the store
Pagination Already exists and already simulates server-side paging — this is an alignment task, not net-new
Rows per page Adopt Content Drive's 20 / 40 / 60 (default 20), replacing 10 / 25 / 50

Relevant files:

  • core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-list/dot-experiments-list.component.html — toolbar + table
  • core-web/libs/portlets/dot-experiments/portlet/src/lib/store/dot-experiments-list.store.ts — the client-side filter chain to extend
  • core-web/libs/portlets/dot-experiments/portlet/src/lib/components/dot-experiment-list-filter/ — existing chip filter wrapper
  • core-web/libs/ui/src/lib/components/dot-chip-filter/ — shared content-drive chip
  • core-web/libs/ui/src/lib/components/dot-folder-list-view/ — Content Drive's table; the pagination reference to match
  • core-web/libs/portlets/dot-experiments/portlet/src/lib/shared/constants.ts — DEFAULT_EXPERIMENTS_LIST_PER_PAGE, ROWS_PER_PAGE_OPTIONS
  • dotCMS/src/main/java/com/dotcms/rest/api/v1/user/UserResource.java:439 — /api/v1/users/filter

Implementation note: DotChipFilterComponent renders only the chip/trigger; the popover body is projected by the consumer. The existing wrapper projects a plain PrimeNG listbox — the user filter will need its own popover body with a virtual/infinite scroller and a search box, so expect a sibling component rather than a change to DotExperimentListFilterComponent (whose single-select mode for the time filter is also a new variant).

Questions: filters/chips → Jalison. Users endpoint → Humberto.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions