@delmaredigital/payload-experiments 0.4.0-rc.1 → 0.4.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +10 -8
  2. package/dist/admin/TestsDashboardCards.d.ts +1 -0
  3. package/dist/admin/TestsDashboardCards.js +4 -8
  4. package/dist/admin/TestsNavLink.d.ts +1 -0
  5. package/dist/admin/TestsNavLink.js +4 -10
  6. package/dist/admin/api.d.ts +7 -0
  7. package/dist/admin/api.js +27 -0
  8. package/dist/admin/blocks/BlockPanel.js +65 -19
  9. package/dist/admin/blocks/formApi.d.ts +3 -0
  10. package/dist/admin/blocks/formApi.js +9 -3
  11. package/dist/admin/blocks/statusCache.d.ts +2 -0
  12. package/dist/admin/blocks/statusCache.js +11 -2
  13. package/dist/admin/copy/blocks.d.ts +16 -0
  14. package/dist/admin/copy/blocks.js +27 -1
  15. package/dist/admin/copy/common.d.ts +1 -0
  16. package/dist/admin/copy/common.js +1 -1
  17. package/dist/admin/copy/pages.d.ts +2 -4
  18. package/dist/admin/copy/pages.js +3 -5
  19. package/dist/admin/copy/readiness.d.ts +1 -0
  20. package/dist/admin/copy/readiness.js +1 -0
  21. package/dist/admin/goals/XpGoalField.d.ts +2 -0
  22. package/dist/admin/goals/XpGoalField.js +51 -35
  23. package/dist/admin/goals/xpGoalField.css +48 -0
  24. package/dist/admin/paths.d.ts +3 -0
  25. package/dist/admin/paths.js +3 -0
  26. package/dist/admin/surface/TestsHome.js +2 -4
  27. package/dist/admin/surface/TestsSurface.js +2 -5
  28. package/dist/admin/surface/goals/GoalForm.js +4 -10
  29. package/dist/admin/surface/goals/GoalsList.js +2 -1
  30. package/dist/admin/surface/home.model.d.ts +2 -2
  31. package/dist/admin/surface/newTest/NewTest.js +2 -4
  32. package/dist/admin/surface/newTest/PageWizard.js +7 -2
  33. package/dist/admin/surface/pagePicker/CopyRoute.d.ts +1 -1
  34. package/dist/admin/surface/pagePicker/{PagePicker.d.ts → PageCombobox.d.ts} +3 -3
  35. package/dist/admin/surface/pagePicker/PageCombobox.js +395 -0
  36. package/dist/admin/surface/pagePicker/pagePicker.css +10 -0
  37. package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +2 -0
  38. package/dist/admin/surface/readiness/readiness.model.js +1 -0
  39. package/dist/admin/surface/results/ResultsHeader.js +3 -8
  40. package/dist/admin/surface/results/useResults.d.ts +10 -0
  41. package/dist/admin/surface/test/GoalSection.js +4 -7
  42. package/dist/admin/surface/test/TestPage.js +3 -8
  43. package/dist/admin/surface/test/pageParts.js +10 -39
  44. package/dist/admin/surface/test/testPage.css +0 -5
  45. package/dist/admin/surface/useTestDocument.d.ts +4 -0
  46. package/dist/admin/surfaceUrls.d.ts +7 -0
  47. package/dist/admin/surfaceUrls.js +19 -0
  48. package/dist/admin/theme.css +2 -0
  49. package/dist/admin/useApi.d.ts +10 -0
  50. package/dist/admin/views/CollectionListRedirect.d.ts +1 -0
  51. package/dist/admin/views/CollectionListRedirect.js +1 -1
  52. package/dist/admin/views/ExperimentsRootView.js +18 -165
  53. package/dist/admin/views/GoalEditRedirect.d.ts +4 -0
  54. package/dist/admin/views/GoalEditRedirect.js +14 -0
  55. package/dist/admin/views/GoalsCollectionListView.d.ts +2 -0
  56. package/dist/admin/views/GoalsCollectionListView.js +4 -0
  57. package/dist/admin/views/TestsCollectionListView.d.ts +2 -0
  58. package/dist/admin/views/TestsCollectionListView.js +4 -0
  59. package/dist/admin/views/TestsNavLinkServer.d.ts +1 -0
  60. package/dist/admin/views/TestsNavLinkServer.js +1 -1
  61. package/dist/admin/views/buildSurfaceProps.d.ts +8 -0
  62. package/dist/admin/views/buildSurfaceProps.js +143 -0
  63. package/dist/admin/views/renderCollectionList.d.ts +2 -0
  64. package/dist/admin/views/renderCollectionList.js +34 -0
  65. package/dist/admin/views/renderTestsSurface.d.ts +3 -0
  66. package/dist/admin/views/renderTestsSurface.js +34 -0
  67. package/dist/collections/experiments/config.js +9 -5
  68. package/dist/collections/metrics.js +16 -4
  69. package/dist/exports/adminRsc.d.ts +3 -0
  70. package/dist/exports/adminRsc.js +3 -0
  71. package/dist/fields/xpGoal.js +2 -14
  72. package/dist/goals/validate.d.ts +3 -0
  73. package/dist/goals/validate.js +14 -0
  74. package/dist/hooks/puckGoals.d.ts +3 -0
  75. package/dist/hooks/puckGoals.js +38 -0
  76. package/dist/plugin.js +12 -18
  77. package/dist/puck/editor/editor.chrome.css +13 -0
  78. package/dist/puck/editor/editor.css +105 -0
  79. package/dist/puck/editor/xpGoalPuckEditorField.js +28 -20
  80. package/dist/sanitizeReadinessDetail.js +2 -0
  81. package/dist/transitions/checkStart.d.ts +2 -1
  82. package/dist/transitions/checkStart.js +3 -1
  83. package/dist/transitions/decide.js +1 -0
  84. package/dist/transitions/start.js +2 -1
  85. package/dist/types/endpoints.d.ts +1 -1
  86. package/dist/ui/ui.css +4 -0
  87. package/package.json +1 -1
  88. package/dist/admin/surface/pagePicker/PagePicker.js +0 -172
@@ -0,0 +1,395 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useId, useMemo, useRef, useState } from 'react';
4
+ import { createApi } from '../../api.js';
5
+ import { PAGE_COPY as C, pageFieldCopy, pageNotFoundCopy } from '../../copy/pages.js';
6
+ import { TextInput } from '../../../ui/TextInput.js';
7
+ import { normalisePath } from '../../../runtime/normalisePath.js';
8
+ import { reasonFor } from '../test/fields.js';
9
+ import { disabledPageReason, isLatestPageRequest } from './pagePicker.model.js';
10
+ import './pagePicker.css';
11
+ const display = (row)=>`${row.title ? `${row.title} · ` : ''}${row.path ?? ''}${row.published === false ? ` · ${C.draft}` : ''}`;
12
+ /** One input owns focus; list options are navigated through aria-activedescendant. */ export function PageCombobox({ collection, test, armKey, apiRoute }) {
13
+ const api = useMemo(()=>createApi(apiRoute), [
14
+ apiRoute
15
+ ]);
16
+ const id = useId();
17
+ const root = useRef(null);
18
+ const arm = test.doc?.arms.find((item)=>item.key === armKey);
19
+ const target = arm?.pageTarget || '';
20
+ const matches = (path)=>Boolean(target && path != null && normalisePath(path) === normalisePath(target));
21
+ const remembered = arm && test.chosenPages[arm.key];
22
+ const chosen = remembered && matches(remembered.path) ? remembered : undefined;
23
+ const placement = test.preflight?.pagePlacements?.find((entry)=>entry.arm === armKey && entry.collection === collection.slug && entry.id != null && matches(entry.path));
24
+ const resolved = chosen ?? placement;
25
+ const needsLookup = Boolean(target && !resolved);
26
+ const lookupKey = JSON.stringify([
27
+ collection.slug,
28
+ target
29
+ ]);
30
+ const lookups = useRef({});
31
+ const [, updateLookup] = useState(0);
32
+ const lookupRevision = useRef(0);
33
+ useEffect(()=>{
34
+ if (!needsLookup || lookups.current[lookupKey]) return;
35
+ const controller = new AbortController();
36
+ const request = ++lookupRevision.current;
37
+ const pending = {};
38
+ const discardPending = ()=>{
39
+ if (lookups.current[lookupKey] === pending) delete lookups.current[lookupKey];
40
+ };
41
+ // Defer dispatch so StrictMode's discarded effect cannot cache an aborted attempt.
42
+ void Promise.resolve().then(async ()=>{
43
+ if (controller.signal.aborted) return;
44
+ lookups.current[lookupKey] = pending;
45
+ const result = await api.pages({
46
+ collection: collection.slug,
47
+ path: target
48
+ }, controller.signal);
49
+ if (controller.signal.aborted || !isLatestPageRequest(request, lookupRevision.current)) {
50
+ discardPending();
51
+ return;
52
+ }
53
+ lookups.current[lookupKey] = result.ok && result.data ? {
54
+ row: result.data.docs[0],
55
+ missing: result.data.docs.length === 0
56
+ } : {};
57
+ updateLookup((value)=>value + 1);
58
+ });
59
+ return ()=>{
60
+ controller.abort();
61
+ discardPending();
62
+ };
63
+ }, [
64
+ api,
65
+ collection.slug,
66
+ target,
67
+ lookupKey,
68
+ needsLookup
69
+ ]);
70
+ const lookup = lookups.current[lookupKey];
71
+ const value = resolved ? display(resolved) : !target ? '' : lookup?.missing ? pageNotFoundCopy(target, collection.label) : lookup?.row ? lookup.row.title ? display(lookup.row) : target : target;
72
+ const currentId = resolved?.id ?? lookup?.row?.id;
73
+ const [open, setOpen] = useState(false);
74
+ const [search, setSearch] = useState('');
75
+ const [q, setQuery] = useState('');
76
+ const [page, setPage] = useState(1);
77
+ const [refresh, setRefresh] = useState(0);
78
+ const [rows, setRows] = useState([]);
79
+ const [more, setMore] = useState(false);
80
+ const [reveal, setReveal] = useState(8);
81
+ const [active, setActive] = useState(null);
82
+ const [loading, setLoading] = useState(false);
83
+ const [error, setError] = useState('');
84
+ const revision = useRef(0);
85
+ const firstNew = useRef(null);
86
+ const locked = reasonFor(test, 'arms');
87
+ const saving = test.saving === 'saving';
88
+ const blocked = Boolean(locked || saving);
89
+ const reset = ()=>{
90
+ setPage(1);
91
+ setReveal(8);
92
+ setActive(null);
93
+ firstNew.current = null;
94
+ setRefresh((value)=>value + 1);
95
+ };
96
+ const close = ()=>{
97
+ setOpen(false);
98
+ setActive(null);
99
+ };
100
+ const show = (query = '')=>{
101
+ if (blocked || open) return;
102
+ setSearch(query);
103
+ setQuery(query);
104
+ setRows([]);
105
+ setMore(false);
106
+ setActive(null);
107
+ reset();
108
+ setOpen(true);
109
+ };
110
+ useEffect(()=>{
111
+ if (search === q) return;
112
+ const timer = setTimeout(()=>{
113
+ setQuery(search);
114
+ setPage(1);
115
+ setReveal(8);
116
+ }, 250);
117
+ return ()=>clearTimeout(timer);
118
+ }, [
119
+ search,
120
+ q
121
+ ]);
122
+ useEffect(()=>{
123
+ if (!open) return;
124
+ let lastReturn = -Infinity;
125
+ const focus = ()=>{
126
+ if (document.visibilityState !== 'visible' || Date.now() - lastReturn < 100) return;
127
+ lastReturn = Date.now();
128
+ reset();
129
+ };
130
+ const visible = ()=>{
131
+ if (document.visibilityState === 'visible') focus();
132
+ };
133
+ const outside = (event)=>{
134
+ if (!root.current?.contains(event.target)) close();
135
+ };
136
+ window.addEventListener('focus', focus);
137
+ document.addEventListener('visibilitychange', visible);
138
+ document.addEventListener('pointerdown', outside);
139
+ document.addEventListener('click', outside);
140
+ return ()=>{
141
+ window.removeEventListener('focus', focus);
142
+ document.removeEventListener('visibilitychange', visible);
143
+ document.removeEventListener('pointerdown', outside);
144
+ document.removeEventListener('click', outside);
145
+ };
146
+ }, [
147
+ open
148
+ ]);
149
+ useEffect(()=>{
150
+ if (!open) return;
151
+ const controller = new AbortController();
152
+ const request = ++revision.current;
153
+ setLoading(true);
154
+ setError('');
155
+ void api.pages({
156
+ collection: collection.slug,
157
+ q,
158
+ page
159
+ }, controller.signal).then((result)=>{
160
+ if (controller.signal.aborted || !isLatestPageRequest(request, revision.current)) return;
161
+ if (result.ok && result.data) {
162
+ // These rows belong to this request; a reset/search aborts it. Commit the
163
+ // pending highlight with the rows, before a final page can remove More.
164
+ const combined = page === 1 ? result.data.docs : [
165
+ ...rows,
166
+ ...result.data.docs
167
+ ];
168
+ const nextRows = combined.filter((row, index)=>combined.findIndex((item)=>item.id === row.id) === index);
169
+ setRows(nextRows);
170
+ if (firstNew.current != null) {
171
+ const row = nextRows[firstNew.current];
172
+ setActive(row ? String(row.id) : null);
173
+ firstNew.current = null;
174
+ }
175
+ setMore(result.data.hasNextPage);
176
+ } else {
177
+ setMore(false);
178
+ setError(C.error);
179
+ firstNew.current = null;
180
+ }
181
+ setLoading(false);
182
+ });
183
+ return ()=>controller.abort();
184
+ }, [
185
+ api,
186
+ collection.slug,
187
+ q,
188
+ page,
189
+ refresh,
190
+ open
191
+ ]);
192
+ const visibleRows = rows.slice(0, reveal);
193
+ const hasMore = rows.length > reveal || more;
194
+ const optionKeys = [
195
+ ...visibleRows.map((row)=>String(row.id)),
196
+ ...hasMore ? [
197
+ 'more'
198
+ ] : []
199
+ ];
200
+ const activeKey = active != null && optionKeys.includes(active) ? active : undefined;
201
+ useEffect(()=>{
202
+ if (open && activeKey) root.current?.ownerDocument.getElementById(`${id}-option-${activeKey}`)?.scrollIntoView?.({
203
+ block: 'nearest'
204
+ });
205
+ }, [
206
+ open,
207
+ activeKey,
208
+ id
209
+ ]);
210
+ const disabled = (row)=>disabledPageReason(row, armKey, test.doc?.arms ?? [], test.preflight?.pagePlacements, test.chosenPages, collection.label, collection.slug);
211
+ const choose = (key)=>{
212
+ if (blocked) return;
213
+ if (key === 'more') {
214
+ if (loading) return;
215
+ const offset = visibleRows.length;
216
+ const nextReveal = offset + 8;
217
+ setReveal(nextReveal);
218
+ if (rows[offset]) setActive(String(rows[offset].id));
219
+ else firstNew.current = offset;
220
+ if (more && rows.length < nextReveal) {
221
+ setLoading(true);
222
+ setPage((count)=>count + 1);
223
+ }
224
+ return;
225
+ }
226
+ const row = rows.find((item)=>String(item.id) === key);
227
+ if (!arm || !row || row.path === null || disabled(row)) return;
228
+ void test.choosePage(arm.key, {
229
+ id: row.id,
230
+ path: row.path,
231
+ title: row.title,
232
+ published: row.published
233
+ });
234
+ close();
235
+ root.current?.querySelector('input')?.focus();
236
+ };
237
+ const label = pageFieldCopy(arm?.label ?? armKey);
238
+ return /*#__PURE__*/ _jsxs("div", {
239
+ ref: root,
240
+ className: "xp-page-combobox",
241
+ children: [
242
+ /*#__PURE__*/ _jsx(TextInput, {
243
+ id: id,
244
+ label: label,
245
+ role: "combobox",
246
+ "aria-autocomplete": "list",
247
+ autoComplete: "off",
248
+ "aria-expanded": open,
249
+ "aria-controls": `${id}-list`,
250
+ "aria-activedescendant": open && activeKey ? `${id}-option-${activeKey}` : undefined,
251
+ placeholder: C.choose,
252
+ value: open ? search : value,
253
+ disabled: Boolean(locked),
254
+ readOnly: saving,
255
+ "aria-disabled": saving || undefined,
256
+ onClick: ()=>show(),
257
+ onBlur: (event)=>{
258
+ if (!root.current?.contains(event.relatedTarget)) close();
259
+ },
260
+ onPaste: (event)=>{
261
+ if (blocked || open) return;
262
+ event.preventDefault();
263
+ show(event.clipboardData.getData('text/plain'));
264
+ },
265
+ onChange: (event)=>{
266
+ if (blocked) return;
267
+ if (!open) {
268
+ show(event.nativeEvent.data ?? '');
269
+ return;
270
+ }
271
+ const query = event.currentTarget.value;
272
+ if (query === q) reset();
273
+ setSearch(query);
274
+ setRows([]);
275
+ setMore(false);
276
+ setActive(null);
277
+ setReveal(8);
278
+ setLoading(true);
279
+ setError('');
280
+ firstNew.current = null;
281
+ revision.current++;
282
+ },
283
+ onKeyDown: (event)=>{
284
+ if (event.key === 'Escape' || event.key === 'Tab' || event.altKey && event.key === 'ArrowUp') {
285
+ if (open) {
286
+ if (event.key !== 'Tab') event.preventDefault();
287
+ close();
288
+ }
289
+ return;
290
+ }
291
+ if (blocked) return;
292
+ if (!open && event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
293
+ event.preventDefault();
294
+ show(event.key);
295
+ return;
296
+ }
297
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
298
+ event.preventDefault();
299
+ if (!open) {
300
+ show();
301
+ return;
302
+ }
303
+ const index = activeKey ? optionKeys.indexOf(activeKey) : event.key === 'ArrowDown' ? -1 : optionKeys.length;
304
+ setActive(optionKeys[Math.max(0, Math.min(optionKeys.length - 1, index + (event.key === 'ArrowDown' ? 1 : -1)))] ?? null);
305
+ } else if (open && activeKey && (event.key === 'Home' || event.key === 'End')) {
306
+ event.preventDefault();
307
+ setActive(optionKeys[event.key === 'Home' ? 0 : optionKeys.length - 1] ?? null);
308
+ } else if (open && activeKey && event.key === 'Enter' && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229) {
309
+ event.preventDefault();
310
+ choose(activeKey);
311
+ }
312
+ }
313
+ }),
314
+ open ? /*#__PURE__*/ _jsxs("div", {
315
+ className: "xp-page-combobox-popup",
316
+ children: [
317
+ /*#__PURE__*/ _jsxs("div", {
318
+ id: `${id}-list`,
319
+ role: "listbox",
320
+ "aria-label": label,
321
+ children: [
322
+ visibleRows.map((row)=>{
323
+ const reason = disabled(row);
324
+ const selected = currentId != null ? String(row.id) === String(currentId) : matches(row.path);
325
+ return /*#__PURE__*/ _jsxs("div", {
326
+ role: "option",
327
+ id: `${id}-option-${row.id}`,
328
+ "aria-selected": selected,
329
+ "aria-disabled": Boolean(reason) || undefined,
330
+ "data-active": activeKey === String(row.id) || undefined,
331
+ className: "xp-page-option",
332
+ onMouseDown: (event)=>event.preventDefault(),
333
+ onClick: ()=>choose(String(row.id)),
334
+ children: [
335
+ /*#__PURE__*/ _jsx("strong", {
336
+ children: row.title
337
+ }),
338
+ selected ? /*#__PURE__*/ _jsx("span", {
339
+ className: "xp-page-option-check",
340
+ "aria-hidden": "true",
341
+ children: "✓"
342
+ }) : null,
343
+ /*#__PURE__*/ _jsxs("span", {
344
+ className: "xp-page-option-detail",
345
+ children: [
346
+ row.path,
347
+ row.published === false ? ` · ${C.draft}` : '',
348
+ reason ? /*#__PURE__*/ _jsxs(_Fragment, {
349
+ children: [
350
+ /*#__PURE__*/ _jsx("br", {}),
351
+ reason
352
+ ]
353
+ }) : null
354
+ ]
355
+ })
356
+ ]
357
+ }, row.id);
358
+ }),
359
+ hasMore ? /*#__PURE__*/ _jsx("div", {
360
+ role: "option",
361
+ id: `${id}-option-more`,
362
+ className: "xp-page-option",
363
+ "data-active": activeKey === 'more' || undefined,
364
+ "aria-disabled": loading || undefined,
365
+ onMouseDown: (event)=>event.preventDefault(),
366
+ onClick: ()=>choose('more'),
367
+ children: C.more
368
+ }) : null
369
+ ]
370
+ }),
371
+ loading && !rows.length ? /*#__PURE__*/ _jsx("p", {
372
+ role: "status",
373
+ children: C.loading
374
+ }) : null,
375
+ !loading && !error && !rows.length ? /*#__PURE__*/ _jsx("p", {
376
+ role: "status",
377
+ children: C.empty
378
+ }) : null,
379
+ error ? /*#__PURE__*/ _jsxs("div", {
380
+ role: "alert",
381
+ children: [
382
+ error,
383
+ " ",
384
+ /*#__PURE__*/ _jsx("button", {
385
+ type: "button",
386
+ onClick: reset,
387
+ children: C.retry
388
+ })
389
+ ]
390
+ }) : null
391
+ ]
392
+ }) : null
393
+ ]
394
+ });
395
+ }
@@ -0,0 +1,10 @@
1
+ .xp-page-combobox { position: relative; width: 100%; min-width: 0; }
2
+ .xp-page-combobox input { width: 100%; }
3
+ .xp-page-combobox-popup { position: absolute; inset-inline: 0; z-index: 5; max-height: 24rem; overflow-y: auto; background: var(--xp-surface); color: var(--xp-text); border: 1px solid var(--xp-border-strong); border-radius: var(--xp-radius-control); padding: var(--xp-space-1); }
4
+ .xp-page-option { position: relative; display: block; width: 100%; box-sizing: border-box; text-align: left; padding: var(--xp-space-3); padding-inline-end: var(--xp-space-6); border: 2px solid transparent; border-radius: var(--xp-radius-control); cursor: pointer; color: var(--xp-text); background: var(--xp-surface); overflow-wrap: anywhere; }
5
+ .xp-page-option-detail { display: block; color: var(--xp-text-2); font: var(--xp-text-small); }
6
+ .xp-page-option[aria-selected='true'] { border-color: var(--xp-accent); }
7
+ .xp-page-option-check { position: absolute; inset-inline-end: var(--xp-space-2); top: var(--xp-space-3); color: var(--xp-accent); }
8
+ .xp-page-option[data-active='true'] { background: var(--xp-surface-2); outline: 2px solid var(--xp-focus); outline-offset: -4px; }
9
+ .xp-page-option[aria-disabled='true'] { cursor: not-allowed; border-color: var(--xp-text-2); border-style: dashed; }
10
+ .xp-page-option[aria-disabled='true'] strong { font-weight: 400; }
@@ -6,6 +6,8 @@ export type PageCollection = NonNullable<TestsSurfaceProps['pageCollections']>[n
6
6
  export type PageSelections = Partial<Record<Arm['key'], {
7
7
  id: DocId;
8
8
  path: string;
9
+ title?: string;
10
+ published?: boolean;
9
11
  }>>;
10
12
  /** The server resolves legacy collections; the client consumes its placements verbatim. */
11
13
  export declare function testPageCollection(collections: readonly PageCollection[] | undefined, doc: {
@@ -30,6 +30,7 @@ const versions = row('versions', (detail)=>versionsNext(detailText(detail, C.ver
30
30
  },
31
31
  'invalid-status': row('header', ()=>null),
32
32
  'no-placements': row('what', (detail)=>detail === undefined ? C.noPlacements : placementFailureCopy(detail)),
33
+ 'placement-removed': row('what', ()=>C.placementRemoved),
33
34
  'placement-unpublished': {
34
35
  ...row('versions', (detail)=>(Array.isArray(detail) && detail.length ? detail : [
35
36
  detail
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { surfaceUrls } from '../../surfaceUrls.js';
3
4
  import { useEffect, useState } from 'react';
4
5
  import { formatAdminURL } from 'payload/shared';
5
6
  import { SurfaceShell } from '../SurfaceShell.js';
@@ -69,10 +70,7 @@ export function ResultsHeader({ test, surface, resultsVersion, children, actions
69
70
  crumbs: [
70
71
  {
71
72
  label: C.tests,
72
- href: formatAdminURL({
73
- adminRoute: surface.adminRoute,
74
- path: '/experiments'
75
- })
73
+ href: surface.canManage ? surfaceUrls.home(surface.adminRoute) : undefined
76
74
  },
77
75
  {
78
76
  label: doc.name,
@@ -157,10 +155,7 @@ export function ReaderResultsShell({ surface, children, data }) {
157
155
  crumbs: [
158
156
  {
159
157
  label: C.tests,
160
- href: formatAdminURL({
161
- adminRoute: surface.adminRoute,
162
- path: '/experiments'
163
- })
158
+ href: surface.canManage ? surfaceUrls.home(surface.adminRoute) : undefined
164
159
  },
165
160
  {
166
161
  label: title
@@ -29,6 +29,13 @@ export declare function useResults(id: DocId, { apiRoute, canReadResults, canMan
29
29
  canManage?: boolean;
30
30
  }): {
31
31
  api: {
32
+ findTestIdBySlug: (slug: string) => Promise<Extract<ApiResult<DocId>, {
33
+ ok: false;
34
+ }> | {
35
+ ok: true;
36
+ data: DocId;
37
+ }>;
38
+ deleteTest: (id: DocId) => Promise<ApiResult<unknown>>;
32
39
  wrapBlock: (body: import("../../../types/endpoints.js").WrapBlockBody) => Promise<ApiResult<Record<string, unknown>>>;
33
40
  pages: (query: {
34
41
  collection: string;
@@ -40,11 +47,13 @@ export declare function useResults(id: DocId, { apiRoute, canReadResults, canMan
40
47
  start: (id: DocId) => Promise<ApiResult<import("../../../types/endpoints.js").TransitionSuccess>>;
41
48
  allocate: (id: DocId, body: import("../../../types/endpoints.js").AllocateBody, options?: {
42
49
  notFound?: string;
50
+ forbidden?: string;
43
51
  codes?: Readonly<Record<string, string>>;
44
52
  fallback?: string;
45
53
  }) => Promise<ApiResult<import("../../../types/endpoints.js").TransitionSuccess>>;
46
54
  addMetric: (id: DocId, body: import("../../../types/endpoints.js").AddMetricBody, options?: {
47
55
  notFound?: string;
56
+ forbidden?: string;
48
57
  codes?: Readonly<Record<string, string>>;
49
58
  fallback?: string;
50
59
  }) => Promise<ApiResult<import("../../../types/endpoints.js").AddMetricResult>>;
@@ -64,6 +73,7 @@ export declare function useResults(id: DocId, { apiRoute, canReadResults, canMan
64
73
  series?: boolean;
65
74
  } & {
66
75
  notFound?: string;
76
+ forbidden?: string;
67
77
  codes?: Readonly<Record<string, string>>;
68
78
  fallback?: string;
69
79
  }) => Promise<ApiResult<ResultsResponse>>;
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { fetchAllDocs } from '../../../lib/fetchAllDocs.js';
4
4
  import { Button } from '../../../ui/Button.js';
5
5
  import { useEffect, useMemo, useState } from 'react';
6
- import { formatAdminURL } from 'payload/shared';
6
+ import { surfaceUrls } from '../../surfaceUrls.js';
7
7
  import { TEST_PAGE as C } from '../../copy/testPage.js';
8
8
  import { METRIC_KIND_LABELS } from '../../copy/metrics.js';
9
9
  import { isClickSource } from '../../../goals/key.js';
@@ -217,12 +217,9 @@ export function GoalSection({ test, surface }) {
217
217
  children: /*#__PURE__*/ _jsx(Button, {
218
218
  variant: "link",
219
219
  size: "sm",
220
- href: formatAdminURL({
221
- adminRoute: surface.adminRoute,
222
- path: `/experiments/goals${!reason ? `?${new URLSearchParams({
223
- return: String(doc.id)
224
- })}` : ''}`
225
- }),
220
+ href: surfaceUrls.goals(surface.adminRoute, !reason ? {
221
+ return: String(doc.id)
222
+ } : undefined),
226
223
  children: C.manageGoals
227
224
  })
228
225
  }),
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { surfaceUrls } from '../../surfaceUrls.js';
3
4
  import { useEffect, useState } from 'react';
4
5
  import { formatAdminURL } from 'payload/shared';
5
6
  import { useTestDocument } from '../useTestDocument.js';
@@ -38,10 +39,7 @@ export function TestPage({ id, surface, onTitleChange }) {
38
39
  crumbs: [
39
40
  {
40
41
  label: C.tests,
41
- href: formatAdminURL({
42
- adminRoute: surface.adminRoute,
43
- path: '/experiments'
44
- })
42
+ href: surfaceUrls.home(surface.adminRoute)
45
43
  },
46
44
  {
47
45
  label: C.test
@@ -72,10 +70,7 @@ export function TestPage({ id, surface, onTitleChange }) {
72
70
  crumbs: [
73
71
  {
74
72
  label: C.tests,
75
- href: formatAdminURL({
76
- adminRoute: surface.adminRoute,
77
- path: '/experiments'
78
- })
73
+ href: surfaceUrls.home(surface.adminRoute)
79
74
  },
80
75
  {
81
76
  label: test.doc.name
@@ -1,15 +1,14 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { TEST_PAGE as C } from '../../copy/testPage.js';
4
4
  import { pageTargetIssue, setPageTarget, showsPageTarget } from '../../arms/pageTarget.model.js';
5
- import { useState } from 'react';
5
+ import { normalisePath } from '../../../runtime/normalisePath.js';
6
6
  import { CopyRoute } from '../pagePicker/CopyRoute.js';
7
- import { PagePicker } from '../pagePicker/PagePicker.js';
7
+ import { PageCombobox } from '../pagePicker/PageCombobox.js';
8
8
  import { testPageCollection } from '../pagePicker/pagePicker.model.js';
9
9
  import { PAGE_COPY as P } from '../../copy/pages.js';
10
10
  import { FieldErrors, reasonFor, save, TextEdit } from './fields.js';
11
11
  export function VersionContent({ test, surface, armIndex }) {
12
- const [open, setOpen] = useState(false);
13
12
  const arm = test.doc?.arms[armIndex];
14
13
  if (!arm || !test.doc) return null;
15
14
  if (!showsPageTarget(test.doc.kind, arm.kind)) return /*#__PURE__*/ _jsx("span", {
@@ -17,14 +16,13 @@ export function VersionContent({ test, surface, armIndex }) {
17
16
  children: C.holdoutContent
18
17
  });
19
18
  const collection = testPageCollection(surface.pageCollections, test.doc, test.preflight?.pagePlacements);
20
- const placement = test.preflight?.pagePlacements?.find((entry)=>entry.arm === arm.key);
19
+ const placement = test.preflight?.pagePlacements?.find((entry)=>entry.arm === arm.key && entry.collection === collection?.slug && entry.id != null && arm.pageTarget && normalisePath(entry.path) === normalisePath(arm.pageTarget));
21
20
  if (collection) {
22
- const picker = /*#__PURE__*/ _jsx(PagePicker, {
21
+ const picker = /*#__PURE__*/ _jsx(PageCombobox, {
23
22
  collection: collection,
24
23
  test: test,
25
24
  armKey: arm.key,
26
- apiRoute: surface.apiRoute,
27
- onChoose: ()=>setOpen(false)
25
+ apiRoute: surface.apiRoute
28
26
  });
29
27
  return /*#__PURE__*/ _jsxs("div", {
30
28
  className: "xp-version-content xp-version-content--page",
@@ -32,27 +30,6 @@ export function VersionContent({ test, surface, armIndex }) {
32
30
  /*#__PURE__*/ _jsx("strong", {
33
31
  children: P.where
34
32
  }),
35
- /*#__PURE__*/ _jsx("p", {
36
- children: placement?.id != null ? /*#__PURE__*/ _jsxs(_Fragment, {
37
- children: [
38
- placement.title,
39
- " · ",
40
- placement.path,
41
- !placement.published ? /*#__PURE__*/ _jsxs(_Fragment, {
42
- children: [
43
- " · ",
44
- P.draft
45
- ]
46
- }) : null
47
- ]
48
- }) : arm.pageTarget ? /*#__PURE__*/ _jsxs(_Fragment, {
49
- children: [
50
- arm.pageTarget,
51
- " · ",
52
- P.draft
53
- ]
54
- }) : P.noPage
55
- }),
56
33
  placement?.id != null ? /*#__PURE__*/ _jsxs("div", {
57
34
  children: [
58
35
  placement.editorUrl ? /*#__PURE__*/ _jsx("a", {
@@ -70,16 +47,10 @@ export function VersionContent({ test, surface, armIndex }) {
70
47
  }) : null
71
48
  ]
72
49
  }) : null,
73
- /*#__PURE__*/ _jsx("button", {
74
- type: "button",
75
- disabled: !!reasonFor(test, 'arms'),
76
- onClick: ()=>setOpen((value)=>!value),
77
- children: open ? P.close : arm.pageTarget ? P.change : P.choose
78
- }),
79
- open ? arm.kind === 'variant' ? /*#__PURE__*/ _jsx(CopyRoute, {
80
- collection: collection,
81
- children: picker
82
- }) : picker : null,
50
+ arm.kind === 'variant' ? /*#__PURE__*/ _jsx(CopyRoute, {
51
+ collection: collection
52
+ }) : null,
53
+ picker,
83
54
  /*#__PURE__*/ _jsx(FieldErrors, {
84
55
  test: test,
85
56
  path: `arms.${armIndex}.pageTarget`
@@ -16,11 +16,6 @@
16
16
  .xp-version-content { display: flex; flex-wrap: nowrap; gap: var(--xp-space-2); align-items: center; min-width: 0; }
17
17
  .xp-version-content .xp-ui-button { white-space: nowrap; }
18
18
  .xp-version-content--page { flex-direction: column; align-items: stretch; gap: var(--xp-space-3); }
19
- .xp-version-content--page > button { align-self: flex-start; }
20
- .xp-page-picker input[type="search"] { width: 100%; }
21
- .xp-page-picker ul { list-style: none; padding: 0; margin-block: var(--xp-space-3); }
22
- .xp-page-picker li > button { display: block; width: 100%; text-align: left; }
23
- .xp-page-picker li > p { color: var(--xp-text-2); font: var(--xp-text-small); }
24
19
  .xp-copy-route ol { list-style: decimal; padding-inline-start: var(--xp-space-6); }
25
20
  .xp-content-chip { flex-shrink: 0; display: inline-flex; width: fit-content; border-radius: var(--xp-radius-chip); padding: var(--xp-space-half) var(--xp-space-2); font: var(--xp-text-small); background: var(--xp-surface-2); color: var(--xp-text-2); }
26
21
  .xp-content-chip.xp-content-ready { background: var(--xp-success-bg); color: var(--xp-success); }
@@ -13,10 +13,14 @@ export declare function useTestDocument(id: DocId, { apiRoute, refetchPreflightO
13
13
  choosePage: (arm: Arm["key"], page: {
14
14
  id: DocId;
15
15
  path: string;
16
+ title?: string;
17
+ published?: boolean;
16
18
  }) => Promise<void>;
17
19
  chosenPages: Partial<Record<Arm["key"], {
18
20
  id: DocId;
19
21
  path: string;
22
+ title?: string;
23
+ published?: boolean;
20
24
  }>>;
21
25
  doc: TestDocument | null;
22
26
  preflight: PreflightResult | null;