Skip to content

feat(experiments): add Experiments portlet with site-wide list screen - #37034

Open
oidacra wants to merge 41 commits into
mainfrom
oidacra/experiments-portlet-screen-1-portlet-base-site-w
Open

feat(experiments): add Experiments portlet with site-wide list screen#37034
oidacra wants to merge 41 commits into
mainfrom
oidacra/experiments-portlet-screen-1-portlet-base-site-w

Conversation

@oidacra

@oidacra oidacra commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary

Adds the Experiments portlet: a new /experiments route rendering a searchable, filterable, sortable and paginated list of every experiment on the current site. This is the issue that makes the portlet exist, and everything else in #36987 hangs off it.

Ships against GET /api/v1/experiments as it exists today. No backend dependency, no registration UpgradeTask, no feature flag.

CleanShot.2026-08-13.at.13.46.15.mp4

Closes #36989

How to read this

37 commits, rebased on main. Reviewing them in order is much easier than reading the combined diff — most of the later ones are single-concern fixes found by running the screen.

The feature

55723e4 Move legacy UVE screens under old/ — 110 files, zero content change
662a762 The portlet: routes, store, list screen, specs
4412dff Declare the portlet server-side + Analytics health gate
b6e656e Align the list row with the approved design

Three DI failures, all invisible to Jest and to the AOT build

805d1d8 f8caf74 Provide DotPushPublishEnvironmentsResolver on the route, DotExperimentsService on the component
859f51a DI smoke test so this class of bug fails in CI instead
8f687c2 Hold the list until the health check answers, so there is no layout flash

Shared UI (see "Scope beyond the issue")

26aad63 7cfc299 Align the status filter with content-drive; move the chip primitives into @dotcms/ui
8b82736 a9cd41b Move panel styling into the theme preset, then make it global
362ae0e Security: stop DotHighlightPipe injecting unescaped text as live markup

Store shape

5faf36d 9264d75 Split page/API events, extract helpers to util/, move URL write-back into the store
c223db5 Debounce the search with Angular 22's debounced()
2cc5718 Adopt the shared ComponentStatus, converted to as const

Behaviour and layout, driven by running it

f79a24b dddc0f8 Load-error state with retry; make the loading skeleton actually render
bc35ab4 e5ea193 2cc5718 Actions cell is one kebab, styled like content-drive's
76e8e58 Status filter starts empty, archived opt-in
4e7c853 7ffaad0 d5aa74a 5d0fc66 Column widths bounded to their content; table laid out fixed
956f9e9 Goal filter, generalising the chip filter behind it
e53d0a3 ee7af1e Search covers the description; clear control in the box
920cfee Sorting on Experiment, Page, Goal, Schedule, Status
5e6ecb3 Paginator matches content-drive
f7822c7 Three defects from review (below)
3bec27c Split the two empty states, centre them, soften row changes

Architecture notes

Events store, split by source. dotExperimentsListPageEvents is what the page asks for; dotExperimentsApiEvents is what the backend answered. The page dispatches only the first, handlers raise only the second, so the two halves of an async flow cannot be confused. Page events read as commands (loadExperiments, archiveExperiment) rather than …Requested. Both fold into one withReducer, so reading it tells you who caused each transition.

Handlers return their events via mapResponse; withEventHandlers dispatches whatever they emit, so no Dispatcher is injected there. Verified against the installed 21.1.1 source: auto-dispatch skips events carrying SOURCE_TYPE, which is stamped only on incoming events as a loop guard.

The async hook is withEventHandlerswithEffects does not exist in @ngrx/signals 21.1.1 and will not compile. Written down in libs/portlets/CLAUDE.md with the rest of the pattern.

Site scoping fails closed. DotExperiment carries no host field, only pageId. One bulk DotContentSearchService query resolves each page's url for the Page column and host for the site filter. An experiment whose page cannot be resolved is excluded rather than shown, so an unresolvable page can never leak another site's experiment into the list.

The store owns the whole URL contract. It parses on entry and on popstate, and writes back through toQueryParams, the inverse of parseViewState. Serialization used to live in the component, which split one contract across two files and invited the halves to drift.

Client-side list operations are interim by design. The endpoint returns the full set, so paging, sorting, filtering and counts are computed client-side until #36823 lands the server-side contract. The URL param contract is final from day one, so the swap changes only DotExperimentsService.getAllUnfiltered() and the store's load handler.

Analytics health gate. Mirrors what the legacy UVE routes get from AnalyticsAppGuard: only OK passes. The check runs before the first fetch and the list is never requested when it fails — including on a later site switch. A pending gate is its own render state, otherwise the list paints and is then replaced, which read as a flash on every entry.

No Enterprise licence gate. The legacy list gates push-publish and add-to-bundle behind hasEnterpriseLicense; deliberately not carried over.

Why portlet.xml changed

The issue's setup note said a cms_layouts_portlets row was enough. It isn't. MenuHelper.getMenuItems() resolves every layout portlet id through PortletAPI and silently skips what it cannot find. With no experiments portlet declared, findPortlet returned null, the menu never carried the entry, MenuGuardService rejected the route and redirected to the first portlet. Declaring it is what makes /experiments reachable at all.

This is a declaration, not a registration — still no UpgradeTask and no starter change, so the portlet stays invisible to customers until someone adds it to a layout by hand.

Defects found in review, fixed in f7822c7

All three were verified in the code before changing anything, and each fix has a test confirmed to fail without it.

  1. The page lookup truncated the list, silently. It limited to the number of pages, but Elasticsearch holds one document per identifier and language, so a multilingual site returned several documents per page and the response truncated at HTTP 200. Truncated pages resolved to nothing, the site filter fails closed, and their experiments vanished — with a total derived from the same set agreeing with the short list. Indistinguishable from reality on screen. The limit now allows headroom for languages, and a shortfall between resolved and requested ids is reported instead of passing silently.

  2. Retry after a failed health check bypassed the gate. healthCheckFailed leaves healthStatus null, so isMisconfigured stayed false and the generic error branch rendered; its Retry dispatched the list, which carries no health guard. Worse, $isLoading keys off that null, so the table sat on skeletons forever even once the list came back. Retry now re-runs the health check.

  3. A list where no experiment had a resolvable pageId stayed on LOADING — the lookup returned EMPTY, so nothing settled the status. Defensive, since pageId is typed non-optional, but it is one line standing between a bad payload and a permanent skeleton.

Empty states

"Nothing here" and "nothing matched" are different situations and now get different copy. Previously a list emptied by a Status filter still read "No experiments yet", which is wrong and offers nothing to do about it; the filtered case now says so and carries a Clear filters action. Which one shows keys off the search term and both chip selections together, so clearing one of three still reads correctly.

The message also used to render inside the table, so it sat in a short band under the header with the table's bottom border cutting across it while the rest of the viewport stayed blank. It now replaces the table and centres in the space the table would have filled — which also removed a paginator reading "Page 0" against no records.

Row changes fade in over 140ms instead of swapping in a single frame, dropped entirely under prefers-reduced-motion. Content-drive has no equivalent, so this is new rather than parity.

Scope beyond the issue

Each of these was requested during implementation; flagging them so they can be split out if preferred.

  1. Legacy screens moved to old/ — this portlet ultimately replaces the per-page UVE screens, so the new code takes the canonical names. Every legacy file moved with zero content change (a87445b reverts formatting that format:write had dragged in). dotExperimentsRoutes keeps its symbol name and barrel export, so edit-ema is untouched.
  2. Chip-filter primitives moved to @dotcms/ui — they lived in @dotcms/portlets/content-drive/ui, so every portlet wanting a filter chip imported from another portlet's UI lib. Seven consumers across four portlets already did.
  3. Panel styling moved into the theme preset, then made global — audited against all 24 popover and 13 listbox consumers. This restyles every popover and listbox in the app, not just filter panels; the two UVE selectors setting [unstyled]="true" needed adjusting.
  4. A Goal filter (956f9e9) that no AC asks for. Fully built, tested and URL-synced. Worth adding to Experiments Portlet — Screen 1: portlet base + site-wide List #36989 for traceability, or splitting out.
  5. DotHighlightPipe XSS fix (362ae0e) — pre-existing, in libs/ui, unrelated to this feature. Details below.

Security fix

DotHighlightPipe interpolated author-supplied text into an HTML string and handed the result to bypassSecurityTrustHtml, which switches Angular's sanitizer off. Verified before fixing rather than assumed: <img src=x onerror="alert(1)">Alpha rendered a live <img> element with its handler intact, through the pipe's own documented [innerHTML] usage.

Every part is now escaped before being wrapped, leaving the highlight span as the only markup. Its one existing consumer, dot-language-variable-selector, renders language variable keys through it, so the same input reached it. Four regression tests, each verified to fail with the escaping removed, including one asserting against the rendered DOM rather than the string.

Acceptance criteria

  • /experiments renders the list for the current site
  • Search filters name, description and page path, case-insensitive, debounced 300 ms
  • Status chip shows 5 statuses with counts over the filtered set, independent of selection; archived excluded until explicitly picked
  • Table pages and sorts, modDate DESC by default, design columns plus Modified
  • Kebab exposes cancelSchedule / end / abort / delete / pushPublish / addToBundle exactly where AllowedActionsByExperimentStatus allows, each with confirm and toast, list reloads after
  • Archive renders per current transitions; Restore renders disabled (no restore key or service method until Experiments API gaps for the standalone portlet: archive/restore, PATCH side effects, goal operators, variant cap #36988)
  • Filter, page and sort round-trip through the URL; defaults serialise to null so a bare /experiments has no params
  • Switching site reloads the list, keeping search/sort/status and resetting page to 1
  • Existing UVE UI untouched: dotExperimentsRoutes unchanged, portlets-edit-ema-portlet passes
  • libs/portlets/CLAUDE.md documents the events-store pattern
  • Jest specs: store, list component, filter chip, URL sync
  • [~] dotExperimentsPortletRoutes exports '' only — new, :id/configuration and :id/results are omitted rather than stubbed. A placeholder needs a component, and routing to the old UVE screens is forbidden by AC10 and would break anyway (they need the :pageId shell context). Omitted, an unimplemented deep link falls through to the app catch-all instead of rendering a blank screen.
  • [~] The Actions cell is one kebab. The design leads it with "View Results" and "Configure", but both screens land with feat(users): Profile tab, dialog shell & list CRUD wiring (#36717) #36990+, so a control that cannot go anywhere is noise. This changed during review — an earlier revision rendered the primary action disabled; it is now omitted entirely, matching content-drive, where the kebab is the only affordance and reveals on row hover.

Two deviations from the issue text, both decided during implementation:

  • The status filter applies each toggle immediately rather than behind a Clear/Done footer. No dot-chip-filter consumer in the app has a footer.
  • Sorting is enabled on Experiment, Page, Goal, Schedule and Status, not only Modified.

Setup for QA

The portlet is declared but not registered, so it must be added to a layout by hand. Easiest path, no SQL: Roles & Tools → admin role → add Experiments to a layout (e.g. Marketing). Or:

INSERT INTO cms_layouts_portlets (id, layout_id, portlet_id, portlet_order)
VALUES (gen_random_uuid()::text, '<layout_id>', 'experiments', 10);

Then open /dotAdmin/#/experiments. Requires a rebuilt image, since portlet.xml is a webapp resource.

If Analytics is not configured you will get the "Analytics not Enabled" notice instead of the list — that is the health gate working. The repo has a stack for it:

cd docker/docker-compose-examples/experiments
./start-experiments.sh --experiments-only

GET /api/v1/experiments/health returning OK is the single indicator that the list will render.

Verification

Check Result
pnpm nx lint portlets-dot-experiments-portlet pass
pnpm nx test portlets-dot-experiments-portlet 434 tests, 39 suites
pnpm nx test ui pass
pnpm nx test portlets-edit-ema-portlet pass
pnpm nx test portlets-content-drive pass
pnpm nx test dotcms-ui --testPathPatterns=app.routes pass
pnpm nx build dotcms-ui pass
nx affected -t lint --base=origin/main --exclude=tag:skip:lint 32 projects, pass
nx affected -t test --base=origin/main 29 projects, pass
nx format:check --base=origin/main pass

All re-run after rebasing onto main, not before.

The last three are the commands CI itself runs. A single-project lint had been hiding an import-order error in app.routes.spec.ts, which is what failed the first run on this branch — everything else in that run was cancelled by fail-fast rather than failing on its own.

Notes for the reviewer

  • Every fix in f7822c7, and the sort comparators, the URL write-back, the fixed table layout and the chip filter toggle, are mutation-checked: the change was reverted locally and the new tests confirmed to fail. Tests that pass without the code they cover are worse than none.
  • apps/dotcms-ui/src/app/app.routes.spec.ts did not exist, so the issue's app.routes verify command was silently finding zero tests. Added one pinning what AC3 needs.
  • Jest in this repo replaced --testPathPattern with --testPathPatterns; the issue's verify command uses the old flag.
  • archived is defined twice in Language.properties (line 345 archived=archived, line 5774 archived=Archived). Pre-existing and shared with the current UVE filter — left alone.
  • abort calls cancelSchedule in the store. Pre-existing behaviour in the legacy list, replicated for parity, not fixed. The specs assert the dispatched events rather than the HTTP call, so they keep passing when a real abort endpoint arrives.
  • LISTBOX_OPTION_HEIGHT = 40.6 is the measured row height and is now a shared constant rather than two hardcoded copies. Review has previously argued for an integer here; keeping the measured value was a deliberate call.

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @oidacra's task in 4m 34s —— View job


Review

I reviewed the store, list component, URL-sync utilities, the DotHighlightPipe security fix, the shared ComponentStatus change, routing, and the backend portlet declaration. This is a thorough, well-tested PR; most of what I checked holds up.

Verified sound (not flagged)

  • DotHighlightPipe (libs/ui/.../dot-highlight.pipe.ts) — every split part is escaped before wrapping, and the only two non-escaping return paths (!search → raw text; catch → raw text) return a plain string rather than a bypassSecurityTrustHtml result, so Angular's [innerHTML] sanitizer still runs on them. The fix is complete.
  • Health gate / site-switchsiteChanged sets status: LOADING without dispatching a load when Analytics is non-OK, but the template branches on isMisconfigured() first, so this renders the misconfiguration screen, not a permanent skeleton. On a first null → id site resolution the load is not double-fired in a way that leaves the list wrong (worst case is one extra fetch). OK.
  • Sort comparators (dot-experiments-list-store.util.ts) — modDate/startDate are typed number in DotExperiment, so a.modDate - b.modDate and startTimeOf arithmetic are safe. The Infinity - Infinity = NaN tie among unscheduled experiments is treated as 0 by the sort spec, so it's a harmless stable-order case, not a defect.
  • Lucene page lookup+identifier:(...) is built from system UUID pageIds, not user input; limit maps to the real EsQueryParamsSearch.limit param. The truncation fix (PAGE_LOOKUP_LANGUAGE_HEADROOM) plus the shortfall console.warn is a reasonable interim guard.
  • portlet.xml declaration + SerializationHelperTest bumped to 57 and pins experiments by id — matches the documented requirement.

New Issues

  • 🟡 Medium: core-web/libs/portlets/dot-experiments/portlet/src/lib/store/dot-experiments-list.store.ts:419 — the page-lookup limit is pageIds.length * PAGE_LOOKUP_LANGUAGE_HEADROOM (25×) with no absolute ceiling. Because the endpoint returns every experiment across all sites and site scoping is done client-side, a large instance loads all working htmlpageasset docs for every distinct page on every list entry. Assumption: this is the acknowledged interim client-side contract (documented on getAllUnfiltered() and slated for Add pagination, sorting, filtering and permission checks to the Experiments list endpoint #36823). What to verify: that the interim ES limit won't hit the server-side result cap on a large multi-site instance and silently truncate again — the shortfall console.warn will fire but the list still fails closed. Non-blocking given the documented interim design.

  • 🟡 Medium: core-web/libs/dotcms-models/src/lib/shared-models.ts:11ComponentStatus was converted from an enum to a const object + union type. This is a wide-blast-radius change touching every consumer of the shared status. It compiles and the affected-project tests are reported green, and value usage (ComponentStatus.LOADING) is runtime-identical to a string enum, so this is a heads-up rather than a defect: any consumer that used a member in type position (status: ComponentStatus.LOADING) would now be a compile error — worth a last confirmation that the full nx affected -t test/lint (not a partial run) covered every consumer, since the risk is entirely compile-time.

Nothing blocking. The events-store split, URL round-trip (parseViewState/toQueryParams inverse pair), fail-closed site scoping, and the three review-fix defects are all backed by tests.

``
· oidacra/experiments-portlet-screen-1-portlet-base-site-w

oidacra added 10 commits August 13, 2026 13:25
The per-page UVE experiments screens are superseded by the new site-wide
Experiments portlet. Move them wholesale under `src/lib/old/` so the new
portlet can take the canonical names, and keep them running untouched until
they are retired.

Pure file moves: no legacy file changed content. `dotExperimentsRoutes` keeps
its symbol name and is re-exported from the same barrel, so `edit-ema` and
every other consumer are unaffected.
Adds the portlet that makes Experiments reachable outside UVE: a new
`/experiments` route rendering a searchable, filterable, sortable and
paginated list of every experiment on the current site.

Store follows the NgRx Signals events plugin (`eventGroup` + `withReducer`
for state, `withEventHandlers` for HTTP with Request/Succeeded/Failed
triples). Components dispatch via `injectDispatch`; the store exposes no
state-changing methods and owns no UI.

Interim by design: `GET /api/v1/experiments` returns the full set, so paging,
sorting, filtering and status counts are computed client-side until #36823
lands the server-side contract. The URL param contract is final from day one,
so that swap changes only the service and the store's load handler.

Site scoping fails closed. `DotExperiment` carries no host, only `pageId`, so
one bulk `DotContentSearchService` lookup resolves each page's `url` (the Page
column) and `host` (the site filter). An experiment whose page cannot be
resolved is excluded rather than shown, so another site's experiment can never
leak into the list.

No registration UpgradeTask and no feature flag: the portlet is reachable only
where a `cms_layouts_portlets` row was inserted manually. No Enterprise licence
gate on push-publish or add-to-bundle.

Closes #36989
…ics health

Declaring the portlet in portlet.xml is what actually makes /experiments
reachable. MenuHelper resolves every layout portlet id through PortletAPI and
silently skips the ones it cannot find, so a cms_layouts_portlets row alone was
never enough: findPortlet('experiments') returned null, the menu never carried
the entry, MenuGuardService rejected the route and sent the user to the first
portlet instead.

This is a declaration, not a registration. There is still no UpgradeTask and no
starter change, so the portlet stays invisible to customers until someone adds
it to a layout by hand.

Also gates the list on the Analytics health check, matching the behaviour the
legacy UVE routes get from AnalyticsAppGuard. Only OK passes, same as the
guard. The check runs before the first fetch and the list is not requested at
all when it fails, including on a later site switch, so a broken install never
queries experiments behind the misconfiguration screen. URL hydration still
happens first, so a deep link survives once the install is healthy.

Fixes the legacy misconfiguration screen's header, which rendered a skeleton
forever: dot-experiments-ui-header defaults isLoading to true and that template
never opted out.
…ist route

`DotPushPublishEnvironmentsResolver` is `@Injectable()` with no
`providedIn: 'root'`, so naming it in `resolve` is not enough — the router
throws NG0201 "No provider found" the moment the portlet is opened. The legacy
UVE route works because `edit-ema` lists it in a route `providers` array; the
new route referenced it without providing it.

Neither the unit tests nor the AOT build catch this: the resolver is only
constructed on route activation. Adds a spec asserting every class resolver the
route references is also provided, so the next resolver added here cannot
reintroduce it.
Second NG0201 of the same shape: `DotExperimentsService` is `@Injectable()`
with no `providedIn` and is not among the app-wide providers in
`apps/dotcms-ui/src/app/providers.ts`, so the store could not inject it. The
legacy screens provide it in `old/dot-experiments-shell`; the new component now
does the same, scoped alongside the store it feeds.

`DotAddToBundleComponent` provides its own `AddToBundleService` and
`DotCurrentUserService`, and `PushPublishService`, `LoggerService`,
`DotHttpErrorManagerService`, `DotMessageService` and `DotMessageDisplayService`
are all in the app-wide providers, so this was the only remaining gap.
Entering the portlet rendered the full list chrome (toolbar, search, status
chip, table headers) and then replaced it with the "Analytics not Enabled"
notice a moment later, a visible layout jump on every entry.

The gate treated a null health status as "not misconfigured", so the list won
the first paint and lost it once the check resolved. A pending gate is now its
own state: neither branch renders until the status is known.
Two NG0201 errors shipped in a row because dotCMS has many `@Injectable()`
services with no `providedIn: 'root'`, kept alive by the app-level
`providers.ts` that a lazily-loaded standalone portlet does not inherit. Jest
missed both because the existing specs mock the store wholesale, and the AOT
build missed them because the failure only happens when the injector actually
constructs the chain.

This spec deliberately does NOT mock the store or `DotExperimentsService`; it
builds the component against the real injector and asserts it constructs.
Verified it earns its keep: removing `DotExperimentsService` from the
component's providers fails all four tests, restoring it makes them pass.
… chip filters

The Clear/Done footer came from the issue text, but no `dot-chip-filter`
consumer in content-drive has one: language, workflow, field and content-type
all apply each toggle immediately and clear through the chip's remove control.
An apply button made Experiments the only filter in the admin that behaves
differently.

Now mirrors `dot-content-drive-language-field`, the closest sibling: a
`linkedSignal` bound two-way to the listbox, an `onChange()` that emits, and
`onRemoveAll()` for the chip. Counts keep using `dot-filter-list-item`'s
`secondary` slot, which that component already does for ISO codes.

No search field: language and publishing-queue enable `[filter]` because they
list many options, and five statuses do not need one.
Read the design project directly rather than working from the issue summary.
The list row was missing two controls it specifies.

- New Experiment button in the toolbar. Creation lands with its own screen
  (#36990+), so it renders disabled with a tooltip rather than being absent.
- Primary row action leading the Actions cell: "View Results" where results
  exist, "Configure" otherwise. Derived from
  `AllowedActionsByExperimentStatus.results` (RUNNING, ENDED) rather than
  restating the statuses. Disabled for now: AC10 forbids routing into the
  legacy UVE screens until Screens 2 and 3 land, so the row matches the design
  without being able to navigate.
- Variants centred and Actions right-aligned, per the design.

Column order already matched: the design has no Modified column, and the issue
asked for "the design's columns + Modified", which is what ships.

Also drops the `dot-experiment-status-tag` wrapper: a component whose only job
was to render one `p-tag` earned nothing over inlining the tag and deriving
severity, icon and label on the row. Its severity coverage moved to the list
spec, so the mapping is still asserted for all five statuses.

Reuses `experiments.action.view.results`; adds keys only where the design's
copy genuinely differs from an existing string.
`DotChipFilterComponent`, `DotFilterListItemComponent` and the two
`CHIP_FILTER_*_PT` constants lived in `@dotcms/portlets/content-drive/ui`, so
every portlet that wanted a filter chip imported from another portlet's UI lib.
Seven consumers across content-drive, users, publishing-queue and experiments
already did exactly that. They now come from `@dotcms/ui`, where shared,
domain-agnostic UI belongs.

Pure move: no behaviour change. Both components had no content-drive coupling —
the only cross-lib import was `DotMessagePipe`, now relative. Only the two
`CHIP_FILTER_*` constants moved; `HEADER_COLUMNS`, `ALL_FOLDER` and the rest of
content-drive's constants stay put.

One test needed adjusting: `libs/ui` runs `@happy-dom/jest-environment` while
content-drive-ui runs jsdom, and Spectator's `dispatchKeyboardEvent` builds its
event with the legacy `initKeyboardEvent`, which happy-dom does not implement.
The two keydown tests now dispatch a real `KeyboardEvent`, which works under
both.
oidacra added 10 commits August 13, 2026 13:25
…slack to Experiment

Each column whose content is enumerable now carries that bound as its width: the
widest goal label ("Click on Element"), the Variants header (wider than any count
under it), two dates and a separator for Schedule, the "Scheduled" tag, one date
plus the sort icon for Modified, and a single kebab for the actions column.

Experiment is the only column with open-ended content, so it takes `w-full` and
absorbs whatever is left. Its cell is `max-w-0`, which is what lets the existing
truncation bite against the width the table hands it rather than against a fixed
cap — so the name and description get the leftover space instead of the dates
being padded out with it.

Page is open-ended too (paths can be arbitrarily long), so it is bounded and
truncates with a title tooltip rather than competing for that slack.

Goal also needed `whitespace-nowrap` on the body cell: only the header had it, so
labels wrapped onto a second line.
The width, wrap and alignment choices are legible from the classes themselves;
narrating them in HTML only adds lines that go stale when a number changes.

The three comments left all carry something the markup cannot: which follow-up
issue a disabled control is waiting on, and why the error state exists.
…wing the rows

The column widths were only hints: with the default `table-layout: auto` the
browser sizes each column to the content it can see. Filtering to Draft, whose
rows show "Not scheduled" instead of a date range, collapsed the Schedule column
and shifted everything after it — the widths changed with the filter.

`table-layout: fixed` makes the authored widths authoritative, so they no longer
depend on which rows are visible. Same approach content-drive already takes for
its list. The gutter problem it documents — fixed layout sharing leftover width
across every column — does not arise here because Experiment carries `w-full` and
claims the remainder.

Covered by a test asserting the layout, verified to fail when the style is
removed.
…hind it

The Goal chip narrows the list the same way Status does, and the two narrow
together: an experiment has to satisfy both, so one with no goal at all drops out
as soon as any goal is picked. Selection is URL-backed like every other part of
the view state, under a repeatable `goal` param, and unknown values are dropped
rather than trusted.

Goal counts sit beside the status counts and are computed over the same
site-plus-search set, independent of both selections — picking a value must not
move the numbers next to the values not yet picked.

Rather than copy the status filter, its component is now generic: it takes a
title, translated-and-counted options and a selection, and knows nothing about
statuses or goals. Adding the next filter is an option list, not another 130
lines. Values are plain strings for that reason and the list component owns the
narrower type. The old dot-experiment-status-filter is gone; its spec moves to
the generic component and keeps its cases.

Two UI decisions worth recording:

An empty selection means "no filter", i.e. everything, which the chip now says
out loud via an opt-in `emptyLabel` on the shared DotChipFilterComponent — the
chips read "Status: All" until something is picked. This is deliberately a
placeholder and not an `All` row: a row would be a second source of truth, since
checked-All plus a de-selected item is a state that contradicts itself.

PrimeNG's select-all is switched off (`showToggleAll`), which is what rendered
the unlabeled checkbox above the first option. It also duplicates "All" by
another route — ticking every value writes five params to mean what no params
already mean — and the chip's remove control is the affordance for getting back.

Verified past this portlet since the chip is shared: ui and content-drive pass,
dotcms-ui builds. 395 tests.
Search matched the name and the resolved page path but not the description, even
though the row renders it directly under the name — so a term the user could read
on screen found nothing.

Now every field the row shows as text is searched. A missing description is
tolerated rather than assumed present.

Covered by tests using a term that appears only in the description, verified to
fail when the field is dropped from the predicate.
The pipe interpolates the source text straight into an HTML string and hands the
result to `bypassSecurityTrustHtml`, which switches Angular's sanitizer off. The
text is author-supplied, so anything in it reached the DOM as real markup.

Verified before fixing, not assumed: `<img src=x onerror="alert(1)">Alpha`
rendered as a live <img> element with its handler intact, through the pipe's own
documented `[innerHTML]` usage.

Every part is now HTML-escaped before being wrapped, leaving the highlight span
as the only markup in the output. The existing 9 tests pass unchanged, since
escaping is a no-op for plain text.

The one current consumer, dot-language-variable-selector, renders language
variable keys through this pipe, so the same input reached it.

Four regression tests added, each verified to fail with the escaping removed —
including one that asserts against the rendered DOM rather than the string.
Emptying the box meant holding backspace. There is now an X inside the field,
shown only while there is something to clear, matching content-drive's search
input.

Clearing writes the same signal typing does rather than dispatching directly, so
it settles through the existing debounce — a direct dispatch would race the
pending debounced one and apply the empty term twice.

Reachable by keyboard as well as pointer, since it is an icon with no label.

Two things the tests had to account for, both verified by probing rather than
guessed: NgModel pushes the model back to the input on a microtask, so the DOM
value trails the signal by a tick; and the store mock's signals are plain
functions, so an applied term has to be typed rather than seeded through
`filter`, which a linkedSignal would never recompute from.
…ortable

Only Modified could be sorted; every other header was inert. Each column now has
a comparator, keyed by a shared `EXPERIMENTS_LIST_SORT_FIELDS` map whose values
double as the pSortableColumn field, the `orderby` URL param and the comparator
key, so the three cannot drift apart.

Choices worth naming:

- Text sorts are locale-aware and case-insensitive, so `apple` and `Zebra` order
  as a reader expects rather than by codepoint, which puts all uppercase first.
- Page sorts by the resolved path, not by pageId, which is what the column shows.
- Status sorts by lifecycle order — Draft, Scheduled, Running, Ended, Archived —
  taken from ExperimentsStatusList. Alphabetical would interleave them
  meaninglessly.
- Experiments with no goal, and unscheduled ones, sort to the end while ascending
  rather than clustering at the top as empty values.
- An unrecognised `orderby` leaves the API order untouched instead of throwing,
  since it is reachable by hand-editing the URL.

One existing test asserted that a non-modDate column keeps the API order; that
behaviour is what this change removes, so it is now an unrecognised-column test.
It had been passing only by coincidence — sorting those two fixtures by name
happens to match their modDate order.
…e import

The list showed first/last jumps and numbered page links; content-drive shows a
'Page N' report with prev and next only, and the same rows-per-page dropdown.
Aligned to that, reusing the existing global 'Page' key rather than adding one.

Also fixes a duplicate '@dotcms/dotcms-models' import in the sort comparators,
introduced in the previous commit — I read the lint output after the commit had
already been chained through, so it went out with the error in it.
…llow-ups

All three criticals verified in the code before changing anything, and each fix
has a test confirmed to fail without it.

1. The page lookup limited to the number of pages. Elasticsearch holds one
   document per identifier *and* language, so on a multilingual site the extra
   language documents consumed the limit and the response truncated at HTTP 200.
   Truncated pages got no entry, and the site filter fails closed — so their
   experiments vanished from the list, with a total derived from the same set
   that agreed with the short list. Indistinguishable from reality on screen.
   The limit now allows headroom for languages, and a shortfall between resolved
   and requested ids is reported rather than passing silently. Any language's
   document carries the host and url this lookup needs, and duplicates collapse
   by identifier, so over-asking costs nothing.

2. Retry after a failed health check skipped the gate.  leaves
    null, so  stayed false and the generic error
   branch rendered; its Retry dispatched the list, which has no health guard.
   Worse,  keys off that null, so the table sat on skeletons forever
   even once the list came back. Retry now re-runs the health check, which
   resolves the gate either way.

3. A list where no experiment had a resolvable pageId stayed on LOADING.
    moves to LOADING for any non-empty payload, but the lookup
   returned EMPTY when there were no ids, so nothing settled it. Defensive —
   pageId is typed non-optional — but it is one line for a permanent skeleton.

Also from the review: the portlet guide pointed at a  path that
no longer exists and taught tapResponse with manual dispatching, which the store
it references does not do; both corrected, and the sample now shows the page/API
event split. Deduped ExperimentPageInfo against the shared DotExperimentPageInfo.
Covered onLazyLoad (offsets, fallbacks, array sort fields, no-field case),
onStatusesChange, and the filter's multi-select toggle, which was untested — the
new tests drive it by clicking options so the model round-trip is real.
@oidacra
oidacra force-pushed the oidacra/experiments-portlet-screen-1-portlet-base-site-w branch from 4a9a009 to f7822c7 Compare August 13, 2026 17:28
…row changes

Three things, all visible on an empty list.

The message was rendered inside the table, so it sat in a short band directly
under the header with the table's bottom border cutting across it, and the rest
of the viewport stayed blank. The empty state now replaces the table instead of
living in it, so it centres in the space the table would have filled. That also
takes the paginator with it, which had been reading "Page 0" against no records.

"No experiments yet" was shown whether the site had none or the user's own
filters had hidden them all — the screenshot that prompted this had a Scheduled
filter applied and still claimed the site was empty. Filtered-empty now says so
and offers a way out, which the other case has no use for. Which of the two
shows keys off the search term and both chip selections together, so clearing
only one of three still reads correctly.

Row changes fade in over 140ms rather than swapping in a single frame. Filtering
and sorting replace the whole set, and the instant swap read as a jolt. Dropped
entirely under `prefers-reduced-motion`, since this is decoration.

The DI smoke test now flushes a row and its page lookup instead of an empty list.
It was asserting the table renders, which an empty list no longer does — but the
weaker reading is that it had been constructing none of the per-row children, and
those are exactly what a real injector is meant to be tested against here.
CI lints every affected project; I had been linting only the experiments portlet,
so this import-order error in the routes spec never showed up locally.

It was the single real failure in the run — every other job reported by
`gh pr checks` was cancelled by fail-fast, not failed on its own.
SerializationHelperTest asserts an exact number of declared portlets, so adding
experiments to portlet.xml turned it red: expected 56, was 57.

Bumped, and the new portlet is now pinned by id the way the others in this test
are — the count alone would pass if some unrelated portlet were swapped for it.

Verified locally on Java 25 rather than assumed: 9 tests, all passing.
…actually runs

Two things this branch learned the hard way, neither written down anywhere.

Declaring a portlet in portlet.xml requires bumping the exact count asserted by
SerializationHelperTest. That has been part of every portlet migration — users,
publishing-queue, velocity-playground, query-tool, es-search — and each one
presumably rediscovered it from a red CI run. The section also records why the
declaration is needed at all: MenuHelper silently skips portlet ids it cannot
resolve, so an undeclared portlet fails as a route that redirects elsewhere with
nothing in the console.

CI lints and tests every *affected* project. Linting a single project locally,
which is the obvious thing to do, is what let an import-order error in an app
reach CI from this branch. The exact commands are now in the guide.

Also fixes the generator snippet, which still said yarn; the repo is on pnpm.
…lazy load

Paging never advanced past 1, and a /experiments?page=3 deep link snapped back
to page 1 on load.

onLazyLoad dispatched sortChanged whenever the event carried a sortField, and
that reducer resets paging. PrimeNG's createLazyLoadMetadata() puts the current
sortField and sortOrder on every lazy-load event, not just on a sort — confirmed
in the installed source (primeng-table.mjs:2313), with onPageChange emitting
through it at :1549. So clicking page 2 dispatched pageChanged({page:2}) and then
a sort that had not changed, which sent the page straight back to 1. The table's
own first render did the same thing to the page hydrated from the URL.

Now dispatched only when the field or direction actually differs from the store.

The symptom was already visible in this repo: a spec comment noted that rendering
the table fires a lazy load carrying the current sort field. That was written to
work around the noise in an assertion, not read as the bug it was pointing at.

Two tests: a pagination event carrying the unchanged sort dispatches no sort, and
a real direction change still does. The first fails without the guard.
The Page column renders `resolvePagePath`, which falls back to the raw pageId
when a page has no url. Search compared against the url alone, so a row showing
an id could not be found by typing that id — and the comment directly above the
predicate claimed it covered every field the row shows, which was untrue.

Both now go through `resolvePagePath`, so the two cannot drift again.

Test verified to fail against the previous predicate.
@oidacra

oidacra commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Thanks — the critical one was real, and worse than "very likely". Confirmed against the installed PrimeNG rather than inferred: createLazyLoadMetadata() (primeng-table.mjs:2313) always includes sortField/sortOrder, and onPageChange (:1549) emits through it. So clicking page 2 dispatched pageChanged({page:2}) and then a sort that had not changed, which reset the page to 1 — pagination never advanced, and ?page=3 was undone by the table's own first render.

Fixed in eedb64f: sortChanged is dispatched only when the field or direction actually differs from the store. Two tests — a pagination event carrying the unchanged sort dispatches no sort, and a real direction change still does. The first fails without the guard.

Your point about why the tests missed it is the useful part. The spec comment at line 828 was written by me to explain away noise in an assertion, and it was in fact describing the bug. That is the second time on this PR that a comment I wrote to work around a symptom was pointing at a defect.

On the two notes:

Page path search — valid, fixed in a260dec. The column renders resolvePagePath, which falls back to the raw pageId, while the search compared the url alone, so a row displaying an id could not be found by typing it. Both go through the same helper now. It also made the comment above that predicate untrue, which is the part that bothered me more.

Page-lookup headroom — agreed it is a conscious trade-off, and left as is. The limit cannot be derived without knowing the language count, so the honest options were a generous limit or an extra round trip to count first. The shortfall warning is there to make the wrong guess visible instead of silent. #36823 removes the second request entirely, which is the actual fix.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Experiments portlet reachable at /experiments, centered on a site-wide list screen (search/filter/sort/page) and backed by the existing GET /api/v1/experiments endpoint, while also extracting shared chip-filter UI into @dotcms/ui, adjusting global theme styling for popovers/listboxes, and hardening DotHighlightPipe against XSS.

Changes:

  • Register the Experiments portlet end-to-end (server portlet.xml + menu/routing + i18n + route specs).
  • Introduce the Experiments list page store/event model and shared list-filter UI components.
  • Apply shared UI/theme updates (chip filters, listbox/popover styling) and fix DotHighlightPipe HTML injection.

Reviewed changes

Copilot reviewed 84 out of 173 changed files in this pull request and generated no comments.

Show a summary per file
File Description
dotCMS/src/test/java/com/dotmarketing/business/portal/SerializationHelperTest.java Update portlet.xml portlet count assertion
dotCMS/src/main/webapp/WEB-INF/portlet.xml Declare experiments portlet
dotCMS/src/main/webapp/WEB-INF/messages/Language.properties Add Experiments list i18n keys + portlet title
core-web/libs/ui/src/lib/theme/theme.config.ts Global popover/listbox styling + export listbox row height
core-web/libs/ui/src/lib/pipes/dot-highlight/dot-highlight.pipe.ts Escape untrusted text before bypassSecurityTrustHtml
core-web/libs/ui/src/lib/pipes/dot-highlight/dot-highlight.pipe.spec.ts Add regression tests for escaping + DOM safety
core-web/libs/ui/src/lib/components/dot-filter-list-item/dot-filter-list-item.component.ts Adjust list item layout classes
core-web/libs/ui/src/lib/components/dot-filter-list-item/dot-filter-list-item.component.spec.ts Add basic rendering tests
core-web/libs/ui/src/lib/components/dot-chip-filter/dot-chip-filter.component.ts Add emptyLabel support + local pipe import
core-web/libs/ui/src/lib/components/dot-chip-filter/dot-chip-filter.component.spec.ts Fix keyboard-event dispatch in happy-dom
core-web/libs/ui/src/lib/components/dot-chip-filter/dot-chip-filter.component.html Render empty label when inactive + class ordering
core-web/libs/ui/src/index.ts Export chip-filter + filter-list-item + theme barrel
core-web/libs/portlets/edit-ema/ui/src/lib/palette/components/dot-favorite-selector/dot-favorite-selector.component.html Remove unstyled popover; rely on global theme
core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/components/edit-ema-persona-selector/edit-ema-persona-selector.component.html Remove unstyled popover; rely on global theme
core-web/libs/portlets/dot-users/src/lib/dot-users-list/components/dot-users-filter-by/dot-users-filter-by.component.ts Switch chip-filter imports to @dotcms/ui
core-web/libs/portlets/dot-users/src/lib/dot-users-list/components/dot-users-filter-by/dot-users-filter-by.component.html Drop PT bindings; rely on theme CSS
core-web/libs/portlets/dot-publishing-queue/src/lib/components/dot-publishing-queue-status-filter/dot-publishing-queue-status-filter.component.ts Switch chip-filter imports to @dotcms/ui
core-web/libs/portlets/dot-publishing-queue/src/lib/components/dot-publishing-queue-status-filter/dot-publishing-queue-status-filter.component.html Drop PT bindings; rely on theme CSS
core-web/libs/portlets/dot-experiments/portlet/src/test-setup.ts Shim getComputedStyle/CSS vars for tests
core-web/libs/portlets/dot-experiments/portlet/src/lib/store/dot-experiments-list-page.events.ts Add page-intent event group
core-web/libs/portlets/dot-experiments/portlet/src/lib/store/dot-experiments-api.events.ts Add API-outcome event group
core-web/libs/portlets/dot-experiments/portlet/src/lib/shared/models.ts Add list view/table/filter model types
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-inline-edit-text/dot-experiments-inline-edit-text.component.html Class ordering/layout tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-header/dot-experiments-ui-header.component.ts Legacy header component moved under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-header/dot-experiments-ui-header.component.spec.ts Add header unit tests
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-header/dot-experiments-ui-header.component.html Add header template
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-goals-coming-soon/dot-experiments-goals-coming-soon.component.ts Add “coming soon” goals UI
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-goals-coming-soon/dot-experiments-goals-coming-soon.component.spec.ts Add goals “coming soon” tests
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-goals-coming-soon/dot-experiments-goals-coming-soon.component.html Add goals “coming soon” template
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-goal-configuration-url-parameter-component/dot-experiments-goal-configuration-url-parameter-component.component.ts Legacy goal config component under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-goal-configuration-url-parameter-component/dot-experiments-goal-configuration-url-parameter-component.component.html Legacy goal config template under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-goal-configuration-reach-page/dot-experiments-goal-configuration-reach-page.component.ts Legacy goal config component under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-goal-configuration-reach-page/dot-experiments-goal-configuration-reach-page.component.html Legacy goal config template under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-details-table/dot-experiments-details-table.component.ts Legacy details table under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiments-details-table/dot-experiments-details-table.component.html Legacy details table template under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiment-options/dot-experiment-options.component.ts Legacy options control under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiment-options/dot-experiment-options.component.spec.ts Add legacy options control tests
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiment-options/dot-experiment-options.component.scss Add CSS expand/collapse transition
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiment-options/dot-experiment-options.component.html Add legacy options template
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiment-options/directives/dot-experiment-options-item.directive.ts Add options item directive
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiment-options/directives/dot-experiment-option-content.directive.ts Add option content directive
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/ui/dot-experiment-options/components/dot-experiments-option-content-base-component/dot-experiments-option-content-base.component.ts Add base component for option content
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/shared/guards/dot-experiments-analytic-app.guard.ts Legacy analytics guard under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/lib.routes.ts Legacy UVE route tree moved under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-shell/store/dot-experiments.store.ts Legacy ComponentStore under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-shell/dot-experiments-shell.component.ts Legacy shell component under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-shell/dot-experiments-shell.component.spec.ts Add legacy shell tests
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-shell/dot-experiments-shell.component.html Legacy shell template
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-reports/dot-experiments-reports.component.html Legacy reports template under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-reports/components/dot-experiments-reports-skeleton/dot-experiments-reports-skeleton.component.html Legacy reports skeleton styling tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-reports/components/dot-experiments-reports-chart/dot-experiments-reports-chart.component.spec.ts Add chart tests under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-reports/components/dot-experiments-reports-chart/dot-experiments-reports-chart.component.html Legacy chart template tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-reports/components/dot-experiments-report-daily-details/dot-experiments-report-daily-details.component.ts Legacy daily details component under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-reports/components/dot-experiments-report-daily-details/dot-experiments-report-daily-details.component.html Legacy daily details template tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-reports/components/dot-experiments-experiment-summary/dot-experiments-experiment-summary.component.ts Legacy summary component under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-reports/components/dot-experiments-experiment-summary/dot-experiments-experiment-summary.component.html Legacy summary template under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-list/dot-experiments-list.component.html Legacy list template under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-list/components/dot-experiments-status-filter/dot-experiments-status-filter.component.ts Legacy status filter under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-list/components/dot-experiments-status-filter/dot-experiments-status-filter.component.spec.ts Add legacy status filter tests
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-list/components/dot-experiments-status-filter/dot-experiments-status-filter.component.html Legacy status filter template
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-list/components/dot-experiments-list-table/dot-experiments-list-table.component.ts Legacy list table under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-list/components/dot-experiments-list-table/dot-experiments-list-table.component.html Legacy list table template tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-list/components/dot-experiments-list-skeleton/dot-experiments-list-skeleton.component.ts Legacy list skeleton under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-list/components/dot-experiments-list-skeleton/dot-experiments-list-skeleton.component.html Legacy skeleton template tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-list/components/dot-experiments-create/dot-experiments-create.component.ts Legacy create drawer under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-list/components/dot-experiments-create/dot-experiments-create.component.html Legacy create drawer template
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/dot-experiments-configuration.component.html Legacy configuration template under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-variants-add/dot-experiments-configuration-variants-add.component.ts Legacy variants add component under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-variants-add/dot-experiments-configuration-variants-add.component.html Legacy variants add template tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-traffic/dot-experiments-configuration-traffic.component.ts Legacy traffic component under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-traffic/dot-experiments-configuration-traffic.component.html Legacy traffic template tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-traffic-allocation-add/dot-experiments-configuration-traffic-allocation-add.component.html Legacy traffic allocation template tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-targeting/dot-experiments-configuration-targeting.component.ts Legacy targeting placeholder under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-targeting/dot-experiments-configuration-targeting.component.spec.ts Add targeting placeholder tests
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-targeting/dot-experiments-configuration-targeting.component.html Legacy targeting placeholder template
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-skeleton/dot-experiments-configuration-skeleton.component.ts Legacy configuration skeleton under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-skeleton/dot-experiments-configuration-skeleton.component.html Legacy configuration skeleton template
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-scheduling/dot-experiments-configuration-scheduling.component.ts Legacy scheduling component under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-scheduling/dot-experiments-configuration-scheduling.component.html Legacy scheduling template tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-scheduling-add/dot-experiments-configuration-scheduling-add.component.html Legacy scheduling add template tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-items-count/dot-experiments-configuration-items-count.component.ts Legacy count component under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-items-count/dot-experiments-configuration-items-count.component.spec.ts Add count component tests
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-items-count/dot-experiments-configuration-items-count.component.html Add count component template
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-configuration/components/dot-experiments-configuration-goal-select/dot-experiments-configuration-goal-select.component.html Legacy goal select template tweak
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-analytic-app-misconfiguration/dot-experiments-analytic-app-misconfiguration.component.ts Legacy misconfiguration screen under old/
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-analytic-app-misconfiguration/dot-experiments-analytic-app-misconfiguration.component.spec.ts Add misconfiguration screen tests
core-web/libs/portlets/dot-experiments/portlet/src/lib/old/dot-experiments-analytic-app-misconfiguration/dot-experiments-analytic-app-misconfiguration.component.html Ensure header isn’t stuck loading
core-web/libs/portlets/dot-experiments/portlet/src/lib/lib.routes.ts New portlet routes (dotExperimentsPortletRoutes)
core-web/libs/portlets/dot-experiments/portlet/src/lib/lib.routes.spec.ts Spec for portlet route shape + resolver provisioning
core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-list/dot-experiments-list.component.scss Row transition + reduced-motion handling
core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-analytic-app-misconfiguration/dot-experiments-analytic-app-misconfiguration.component.html Remove outdated template (moved to old/)
core-web/libs/portlets/dot-experiments/portlet/src/lib/components/dot-experiment-list-filter/dot-experiment-list-filter.component.ts New reusable chip-filter popover component
core-web/libs/portlets/dot-experiments/portlet/src/lib/components/dot-experiment-list-filter/dot-experiment-list-filter.component.html New list-filter template
core-web/libs/portlets/dot-experiments/portlet/src/index.ts Export old + new route arrays
core-web/libs/portlets/dot-content-drive/ui/src/lib/shared/constants.ts Remove chip-filter PT constants (now themed)
core-web/libs/portlets/dot-content-drive/ui/src/index.ts Stop exporting chip-filter UI from content-drive
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-workflow-filter/dot-content-drive-workflow-filter.component.ts Switch chip-filter imports + drop PT
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-workflow-filter/dot-content-drive-workflow-filter.component.html Drop PT bindings; rely on theme
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-lazy-multiselect/dot-content-drive-lazy-multiselect.component.ts Use themed listbox option height constant
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-lazy-multiselect/dot-content-drive-lazy-multiselect.component.html Drop listbox PT binding
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-language-field/dot-content-drive-language-field.spec.ts Switch chip-filter import to @dotcms/ui
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-language-field/dot-content-drive-language-field.component.ts Switch chip-filter imports + drop PT
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-language-field/dot-content-drive-language-field.component.html Drop PT bindings; rely on theme
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-field-filter/dot-content-drive-field-filter.component.ts Switch filter UI imports + drop PT
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-field-filter/dot-content-drive-field-filter.component.html Drop PT bindings; rely on theme
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-field-filter-menu/dot-content-drive-field-filter-menu.component.ts Switch filter UI imports + drop PT
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-field-filter-menu/dot-content-drive-field-filter-menu.component.html Drop PT bindings; rely on theme
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-content-type-filter/dot-content-drive-content-type-filter.component.ts Use themed listbox option height + drop PT
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-content-type-filter/dot-content-drive-content-type-filter.component.spec.ts Switch chip-filter import to @dotcms/ui
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-content-type-filter/dot-content-drive-content-type-filter.component.html Drop PT bindings; rely on theme
core-web/libs/edit-content/src/lib/models/dot-edit-content-field.constant.ts Fix ComponentStatus typing after enum→const
core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.ts Adjust inferred types after enum→const
core-web/libs/dotcms-models/src/lib/shared-models.ts Convert ComponentStatus enum to as const + type
core-web/libs/data-access/src/lib/dot-experiments/dot-experiments.service.ts Add interim getAllUnfiltered() for portlet
core-web/apps/dotcms-ui/src/app/app.routes.ts Register /experiments portlet route
core-web/apps/dotcms-ui/src/app/app.routes.spec.ts Add spec pinning experiments route contract

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Experiments Portlet — Screen 1: portlet base + site-wide List

2 participants