Experiments listing: user and time filters, Created By column, and Content Drive-aligned pagination
@oidacra is already working on this.
Since Sep 10, 2026.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Description
Feedback from the Global Sprint demo on the new Experiments portlet. Three additions to the listing screen:
- User filter — filter experiments by who created them.
- Time filter — filter experiments by scheduled start date.
- "Created By" column — surface the creator in the listing table.
- 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:
- https://dotcms.slack.com/archives/C09LGH4HNR5/p1788194416605279
- https://dotcms.slack.com/archives/C09BJJ28THR/p1788192537515369
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
filterquery 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. Time filter
Single-select (unlike the other chips) with exactly these options:
| Option | Rule |
|---|---|
| Any schedule | No date constraint — default |
| Scheduled in the last month | scheduling.startDate > now − 1 month |
| Scheduled in the last 3 months | scheduling.startDate > now − 3 months |
| Scheduled in the last 6 months | scheduling.startDate > now − 6 months |
| Scheduled in the last 12 months | scheduling.startDate > now − 12 months |
- No upper bound. The comparison is a lower bound only, so experiments scheduled to start in the future match every option. "Last 3 months" means "scheduled from 3 months ago onward".
- Experiments with no schedule (Drafts) are excluded whenever an option other than "Any schedule" is selected. They reappear under "Any schedule".
3. "Created By" column
- New column in the listing table, header
Created By. - Renders
experiment.createdByUserName— the field added by #37304. - This part is blocked by #37304. The filter work is not: it matches on
createdBy(the user ID), which the API already returns.
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
filterquery param, debounced, and resets to page 0. - Multiple users can be selected at once; selecting users narrows the table to experiments whose
createdBymatches 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/filterrequest surfaces throughDotHttpErrorManagerServiceand 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
createdByrefers to a deleted/unresolvable user do not break the filter.
Time filter
- A time chip filter is present with exactly:
Any schedule,Scheduled in the last month,Scheduled in the last 3 months,Scheduled in the last 6 months,Scheduled in the last 12 months. - Only one option can be selected at a time; picking a new one replaces the previous.
-
Any scheduleis the default and applies no date constraint. - With an option selected, an experiment matches when its scheduled start date is later than
now − N months. - An experiment scheduled to start in the future matches every non-default option (no upper bound).
- An experiment with no schedule (e.g. a Draft) is excluded under every option except
Any schedule. - The time filter combines with search, Status, Goal, and the user filter as an AND.
- Clearing the time filter returns it to
Any scheduleand restores the unfiltered list.
Created By column
- The listing table has a
Created Bycolumn rendering the creator's name fromcreatedByUserName. - 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
createdByUserNameis absent from the payload, the cell renders the existing placeholder rather thanundefined.
Shared / cross-cutting
- Both new filters persist in the URL and rehydrate on reload and browser back/forward, matching the existing
filter/selectedStatuses/selectedGoalsbehavior in the store. - Changing either filter resets pagination to page 1 and
totalRecordsreflects the filtered count. - The
$hasActiveFilters()/ "clear filters" empty-state action clears the new filters too. - All new labels go through the
| dmmessage 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_PAGEis 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.
-
lazyLoadOnInitis bound consistently with the table's lazy flag, so the first page loads once and not twice. -
showFirstLastIcon,showPageLinks, and thePage {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 + tablecore-web/libs/portlets/dot-experiments/portlet/src/lib/store/dot-experiments-list.store.ts— the client-side filter chain to extendcore-web/libs/portlets/dot-experiments/portlet/src/lib/components/dot-experiment-list-filter/— existing chip filter wrappercore-web/libs/ui/src/lib/components/dot-chip-filter/— shared content-drive chipcore-web/libs/ui/src/lib/components/dot-folder-list-view/— Content Drive's table; the pagination reference to matchcore-web/libs/portlets/dot-experiments/portlet/src/lib/shared/constants.ts—DEFAULT_EXPERIMENTS_LIST_PER_PAGE,ROWS_PER_PAGE_OPTIONSdotCMS/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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.