You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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
New column in the listing table, header Created By.
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 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
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).
Description
Feedback from the Global Sprint demo on the new Experiments portlet. Three additions to the listing screen:
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-driveDotChipFilterComponentfrom@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
DotExperimentsListStoreas a chain of computed signals:GET /v1/experimentsaccepts onlypageId,name, andstatusand returns the full list, so no backend filtering work is required — the two new filters extend this client-side chain.1. User filter
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.filterquery param (server-side search, debounced).createdBy(which holds the user ID).2. Schedule filter
Chip filter whose popover holds a calendar in range mode: a period is picked by selecting its two ends.
The period travels in the address as
schedule_fromandschedule_to, each a local calendar date inyyyy-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-filterin@dotcms/uithat 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
Created By.experiment.createdByUserName— the field added by Expose the experiment creator's username in the Experiments API #37304.createdBy(the user ID), which the API already returns.4. Pagination alignment
The listing already paginates:
DotExperimentsListStoreexposes apagedExperimentscomputed that slices the sorted list client-side, and the table runs[lazy]="true"with a PrimeNG paginator bound tototalRecords. BecauseGET /v1/experimentsreturns 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:[10, 25, 50][20, 40, 60][20, 40, 60]paginatorDropdownAppendTo="body"lazyLoadOnInitShared already-matching behavior to preserve:
showFirstLastIcon=false,showPageLinks=false, and thePage {currentPage}report template.Acceptance Criteria
User filter — happy path
DotChipFilterComponent/DotExperimentListFilterComponent.GET /api/v1/users/filter.filterquery param, debounced, and resets to page 0.createdBymatches any selected user (OR within the filter).User filter — sad path / edge cases
/api/v1/users/filterrequest surfaces throughDotHttpErrorManagerServiceand leaves the table's current results intact — it does not blank the listing.createdByrefers to a deleted/unresolvable user do not break the filter.Schedule filter
schedule_fromandschedule_toinyyyy-MM-dd, and either may appear without the other.Created By column
Created Bycolumn rendering the creator's name fromcreatedByUserName.createdByUserNameis absent from the payload, the cell renders the existing placeholder rather thanundefined.Shared / cross-cutting
filter/selectedStatuses/selectedGoalsbehavior in the store.totalRecordsreflects the filtered count.$hasActiveFilters()/ "clear filters" empty-state action clears the new filters too.| dmmessage pipe — no hardcoded strings.Pagination
DEFAULT_EXPERIMENTS_LIST_PER_PAGEis updated accordingly.paginatorDropdownAppendTo="body"so its overlay escapes the table's scroll container and is never clipped.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.totalRecords.Tests
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.byTestId) cover: infinite-scroll paging, debounced search, multi-select, chip clear, and the Created By column rendering.Priority
Medium
Additional Context
Blocked by #37304 — the
Created Bycolumn needs thecreatedByUserNamefield that issue adds. The two filters are not blocked and can proceed in parallel.Decisions made during refinement:
createdByUserNamefrom #37304 (not FE-side name resolution)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/filterImplementation note:
DotChipFilterComponentrenders 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 toDotExperimentListFilterComponent(whose single-select mode for the time filter is also a new variant).Questions: filters/chips → Jalison. Users endpoint → Humberto.