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
{{ message }}
Repository navigation
Experiments Portlet — Screen 1: portlet base + site-wide List #36989
This is the issue that makes the portlet exist: the site-wide/ subtree, the /experiments route, and the List screen from the approved design — search, status filter with counts, pagination, sorting, row actions — listing every experiment on the current site. No backend dependency: it ships against GET /api/v1/experiments as it exists today (see Interim below).
Portlet base (additive — three existing files, one line each)
New subtree libs/portlets/dot-experiments/portlet/src/lib/site-wide/ with its own lib.routes.ts exporting dotExperimentsPortletRoutes ('', new, :id/configuration, :id/results — the last three are placeholders wired by their own issues).
Export the new route array from the lib src/index.ts (the existing dotExperimentsRoutes export untouched; edit-ema keeps consuming it).
Route entry in apps/dotcms-ui/src/app/app.routes.ts (PORTLETS_ANGULAR, copy the tags entry). The path must be experiments: MenuGuardService validates the first URL segment against /api/v1/menu.
No registration UpgradeTask and no flag guard — the portlet is reachable only where a cms_layouts_portlets row was inserted manually (dev/QA): one row against an existing layout (e.g. Marketing); nothing in portlet, layouts_cms_roles or cms_layout. Roles & Tools on the admin role works too.
List screen
Table copied from dot-tags (libs/portlets/dot-tags/src/lib/dot-tags-list/): p-toolbar + p-iconField search (300 ms debounce), lazy p-table + paginator + sort, per-row skeletons, dot-empty-container with the science icon. Cross-check dot-users (same skeleton, newest iteration).
Status tag: portlet-local p-tag + Record<DotExperimentStatus, severity> map (pattern: dot-publishing-status-chip).
Status filter chip: reuse DotChipFilterComponent / DotFilterListItemComponent / CHIP_FILTER_*_PT from @dotcms/portlets/content-drive/ui (precedent: dot-users-filter-by). Multi-select, per-status counts, Clear/Done. Default excludes ARCHIVED.
Primary row action (derived): status ∈ {RUNNING, ENDED} → View Results, else Configure. Hidden until Screens 2 and 3 land — the new list must not route into the old screens.
Row actions — full parity with the current UVE list (dot-experiments-list-store.ts:483-640): Archive/Restore inline icons per the design (archive-from-DRAFT and restore enabled once Experiments API gaps for the standalone portlet: archive/restore, PATCH side effects, goal operators, variant cap #36988 lands, via the new archiveDraft/restore keys), plus a kebab with cancelSchedule, end, abort, delete, pushPublish, addToBundle — all gated by AllowedActionsByExperimentStatus, each with confirm + toast. Push-publish/add-to-bundle need DotPushPublishEnvironmentsResolver on the route and reuse dot-add-to-bundle.
Site scoping: the site selector drives the list; switching site reloads it.
Store — NgRx Signal Store + Events plugin
experiments-list.events.ts with eventGroup; state transitions in withReducer(on(...)); HTTP in withEventHandlers with Request → Succeeded → Failed triples and switchMap (re-trigger cancels in-flight). Components dispatch via injectDispatch — no store methods for state changes.
Errors: catchError → DotHttpErrorManagerService.handle; load failure → status: 'error', CRUD failure → back to 'loaded'.
Reference: libs/image-editor/src/lib/store/. Note: the async hook is withEventHandlers in the installed @ngrx/signals — examples naming withEffects will not compile.
Add an events-pattern section to libs/portlets/CLAUDE.md in this PR, pointing at libs/image-editor, so reviewers have a written standard.
URL state
All navigable state in the URL: page, per_page, orderby, direction, filter, status (repeatable). Hydrate from route.snapshot.queryParamsbefore the first fetch; write back via effect with queryParamsHandling: 'merge', null clearing defaults (a bare /experiments is the default view), guarded against location.path(true) to avoid redundant history entries. Reference: dot-content-drive-shell.component.ts:454-494.
Interim: client-side list operations (temporary by design)
GET /api/v1/experiments with no params returns every experiment, so this issue computes paging, sorting, text/status/site filtering and status counts client-side. Acceptable at current volumes; safe because the portlet is unregistered for customers; sidesteps two SQL bugs that only fire on server-side filtering. The follow-up "swap to the server-side contract" replaces this when #36823 lands — the URL param contract is final from day one, so the swap changes where params are applied, not what they are called. Known gap: without #36823's enriched rows the API returns pageId, not the page path — resolve titles client-side in one bulk lookup (fallback: show the id). Shape the data-access service so the swap changes only the service and the store's load handler.
Hazard: dot-experiments.model.ts:204-215 runs getComputedStyle(document.body) at import time — stub it in the new specs (lazy-getter fix belongs to the migration issue).
Acceptance Criteria
/experiments renders the site-wide list for the current site (manual layout-row insert documented in the PR).
Search filters by name and page path (substring, case-insensitive), debounced 300 ms.
Status chip shows the 5 statuses with counts for the whole filtered set; default view excludes ARCHIVED.
Table pages, sorts (default modDate DESC) and shows the design's columns + Modified.
Kebab exposes cancelSchedule / end / abort / delete / pushPublish / addToBundle exactly where AllowedActionsByExperimentStatus allows them; each has confirm + toast; the list reloads after.
Filter + page + sort state round-trips through the URL: copying the URL into a new tab restores the identical view; back/forward walks the states; a bare /experiments has no params.
Switching site via the site selector reloads the list.
The existing UVE experiments UI is untouched: dotExperimentsRoutes, the nav item and dot-ema-shell.component.spec.ts:117-120 all unchanged.
libs/portlets/CLAUDE.md documents the events-store pattern.
Jest specs: store (dispatch events, assert signals; error paths assert DotHttpErrorManagerService.handle), list component, filter chip, URL sync (hydration + write-back).
Out of scope: Configure/Results navigation targets (own issues); server-side paging/filtering (swap issue); registration UpgradeTask/starter bump; metrics in the list (GET /{id}/results costs 2 CubeJS round-trips + a Monte Carlo per experiment, @NoCache, no batch form — the design deliberately shows none).
E2E for this screen (including deep-links) lives in the dedicated portlet E2E issue.
Description
This is the issue that makes the portlet exist: the
site-wide/subtree, the/experimentsroute, and the List screen from the approved design — search, status filter with counts, pagination, sorting, row actions — listing every experiment on the current site. No backend dependency: it ships againstGET /api/v1/experimentsas it exists today (see Interim below).Design: approved prototype,
listscreen.Portlet base (additive — three existing files, one line each)
libs/portlets/dot-experiments/portlet/src/lib/site-wide/with its ownlib.routes.tsexportingdotExperimentsPortletRoutes('',new,:id/configuration,:id/results— the last three are placeholders wired by their own issues).src/index.ts(the existingdotExperimentsRoutesexport untouched;edit-emakeeps consuming it).apps/dotcms-ui/src/app/app.routes.ts(PORTLETS_ANGULAR, copy thetagsentry). The path must beexperiments:MenuGuardServicevalidates the first URL segment against/api/v1/menu.cms_layouts_portletsrow was inserted manually (dev/QA): one row against an existing layout (e.g. Marketing); nothing inportlet,layouts_cms_rolesorcms_layout. Roles & Tools on the admin role works too.List screen
dot-tags(libs/portlets/dot-tags/src/lib/dot-tags-list/):p-toolbar+p-iconFieldsearch (300 ms debounce), lazyp-table+ paginator + sort, per-row skeletons,dot-empty-containerwith thescienceicon. Cross-checkdot-users(same skeleton, newest iteration).—) · Variants (count) · Schedule (Jun 25, 2026 → Jul 9, 2026/→ Open/Not scheduled) · Status (tag) · Modified (sortable, default DESC) · Actions.p-tag+Record<DotExperimentStatus, severity>map (pattern:dot-publishing-status-chip).DotChipFilterComponent/DotFilterListItemComponent/CHIP_FILTER_*_PTfrom@dotcms/portlets/content-drive/ui(precedent:dot-users-filter-by). Multi-select, per-status counts, Clear/Done. Default excludesARCHIVED.status ∈ {RUNNING, ENDED}→ View Results, else Configure. Hidden until Screens 2 and 3 land — the new list must not route into the old screens.dot-experiments-list-store.ts:483-640): Archive/Restore inline icons per the design (archive-from-DRAFT and restore enabled once Experiments API gaps for the standalone portlet: archive/restore, PATCH side effects, goal operators, variant cap #36988 lands, via the newarchiveDraft/restorekeys), plus a kebab withcancelSchedule,end,abort,delete,pushPublish,addToBundle— all gated byAllowedActionsByExperimentStatus, each with confirm + toast. Push-publish/add-to-bundle needDotPushPublishEnvironmentsResolveron the route and reusedot-add-to-bundle.Store — NgRx Signal Store + Events plugin
experiments-list.events.tswitheventGroup; state transitions inwithReducer(on(...)); HTTP inwithEventHandlerswith Request → Succeeded → Failed triples andswitchMap(re-trigger cancels in-flight). Components dispatch viainjectDispatch— no store methods for state changes.catchError→DotHttpErrorManagerService.handle; load failure →status: 'error', CRUD failure → back to'loaded'.libs/image-editor/src/lib/store/. Note: the async hook iswithEventHandlersin the installed@ngrx/signals— examples namingwithEffectswill not compile.libs/portlets/CLAUDE.mdin this PR, pointing atlibs/image-editor, so reviewers have a written standard.URL state
All navigable state in the URL:
page,per_page,orderby,direction,filter,status(repeatable). Hydrate fromroute.snapshot.queryParamsbefore the first fetch; write back via effect withqueryParamsHandling: 'merge',nullclearing defaults (a bare/experimentsis the default view), guarded againstlocation.path(true)to avoid redundant history entries. Reference:dot-content-drive-shell.component.ts:454-494.Interim: client-side list operations (temporary by design)
GET /api/v1/experimentswith no params returns every experiment, so this issue computes paging, sorting, text/status/site filtering and status counts client-side. Acceptable at current volumes; safe because the portlet is unregistered for customers; sidesteps two SQL bugs that only fire on server-side filtering. The follow-up "swap to the server-side contract" replaces this when #36823 lands — the URL param contract is final from day one, so the swap changes where params are applied, not what they are called. Known gap: without #36823's enriched rows the API returnspageId, not the page path — resolve titles client-side in one bulk lookup (fallback: show the id). Shape the data-access service so the swap changes only the service and the store's load handler.Hazard:
dot-experiments.model.ts:204-215runsgetComputedStyle(document.body)at import time — stub it in the new specs (lazy-getter fix belongs to the migration issue).Acceptance Criteria
/experimentsrenders the site-wide list for the current site (manual layout-row insert documented in the PR).modDateDESC) and shows the design's columns + Modified.AllowedActionsByExperimentStatusallows them; each has confirm + toast; the list reloads after./experimentshas no params.dotExperimentsRoutes, the nav item anddot-ema-shell.component.spec.ts:117-120all unchanged.libs/portlets/CLAUDE.mddocuments the events-store pattern.DotHttpErrorManagerService.handle), list component, filter chip, URL sync (hydration + write-back).Priority
High — everything else in #36987 hangs off this.
Additional Context
GET /{id}/resultscosts 2 CubeJS round-trips + a Monte Carlo per experiment,@NoCache, no batch form — the design deliberately shows none).pnpm nx lint|test portlets-dot-experiments-portlet·pnpm nx test portlets-edit-ema-portlet·pnpm nx test dotcms-ui --testPathPattern=app.routes·pnpm nx format:write·pnpm nx build dotcms-ui