dotCMS / dotCMS/core

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

Open
#37,307 1 comment 0 reactions 1 assignee View on GitHub

@oidacra is already working on this.

Since Sep 10, 2026.

dotCMS : Experiments Team : Falcon Type : Task
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:

  1. User filter — filter experiments by who created them.
  2. Time 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. 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 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.

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 schedule is 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 schedule and restores the unfiltered list.

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.tsDEFAULT_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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.