@delmaredigital/payload-experiments 0.3.0-rc.4 → 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 (299) hide show
  1. package/README.md +122 -27
  2. package/dist/admin/MetricSourceSelect.js +3 -3
  3. package/dist/admin/PlacementStatus.d.ts +3 -0
  4. package/dist/admin/PlacementStatus.js +54 -0
  5. package/dist/admin/PreviewLinks.d.ts +2 -1
  6. package/dist/admin/PreviewLinks.js +2 -2
  7. package/dist/admin/TestsDashboardCards.d.ts +1 -0
  8. package/dist/admin/TestsDashboardCards.js +4 -8
  9. package/dist/admin/TestsNavLink.d.ts +1 -0
  10. package/dist/admin/TestsNavLink.js +4 -10
  11. package/dist/admin/actions/ConfirmDialog.d.ts +2 -1
  12. package/dist/admin/actions/ConfirmDialog.js +2 -2
  13. package/dist/admin/actions/DecideDialog.d.ts +2 -1
  14. package/dist/admin/actions/DecideDialog.js +2 -2
  15. package/dist/admin/api.d.ts +18 -2
  16. package/dist/admin/api.js +47 -2
  17. package/dist/admin/blocks/BlockPanel.d.ts +4 -0
  18. package/dist/admin/blocks/BlockPanel.js +199 -0
  19. package/dist/admin/blocks/TestBlockField.d.ts +5 -0
  20. package/dist/admin/blocks/TestBlockField.js +163 -0
  21. package/dist/admin/blocks/formApi.d.ts +46 -0
  22. package/dist/admin/blocks/formApi.js +159 -0
  23. package/dist/admin/blocks/inject.d.ts +7 -0
  24. package/dist/admin/blocks/inject.js +41 -0
  25. package/dist/admin/blocks/model.d.ts +17 -0
  26. package/dist/admin/blocks/model.js +45 -0
  27. package/dist/admin/blocks/statusCache.d.ts +9 -0
  28. package/dist/admin/blocks/statusCache.js +92 -0
  29. package/dist/admin/blocks/useBlockForm.d.ts +17 -0
  30. package/dist/admin/blocks/useBlockForm.js +46 -0
  31. package/dist/admin/copy/actions.d.ts +2 -0
  32. package/dist/admin/copy/actions.js +9 -0
  33. package/dist/admin/copy/blocks.d.ts +43 -0
  34. package/dist/admin/copy/blocks.js +64 -0
  35. package/dist/admin/copy/common.d.ts +1 -0
  36. package/dist/admin/copy/common.js +1 -1
  37. package/dist/admin/copy/goals.d.ts +2 -0
  38. package/dist/admin/copy/goals.js +4 -1
  39. package/dist/admin/copy/newTest.d.ts +10 -0
  40. package/dist/admin/copy/newTest.js +10 -0
  41. package/dist/admin/copy/pages.d.ts +24 -0
  42. package/dist/admin/copy/pages.js +24 -0
  43. package/dist/admin/copy/readiness.d.ts +23 -3
  44. package/dist/admin/copy/readiness.js +24 -3
  45. package/dist/admin/copy/results.d.ts +1 -0
  46. package/dist/admin/copy/results.js +1 -0
  47. package/dist/admin/copy/testPage.d.ts +4 -0
  48. package/dist/admin/copy/testPage.js +4 -0
  49. package/dist/admin/goals/XpGoalField.d.ts +4 -0
  50. package/dist/admin/goals/XpGoalField.js +168 -0
  51. package/dist/admin/goals/xpGoalField.css +48 -0
  52. package/dist/admin/metricSource.model.d.ts +2 -0
  53. package/dist/admin/metricSource.model.js +4 -0
  54. package/dist/admin/pageCollections.d.ts +10 -0
  55. package/dist/admin/pageCollections.js +20 -0
  56. package/dist/admin/paths.d.ts +9 -0
  57. package/dist/admin/paths.js +9 -0
  58. package/dist/admin/placementStatus.model.d.ts +2 -0
  59. package/dist/admin/placementStatus.model.js +19 -0
  60. package/dist/admin/placements/placementLabels.d.ts +11 -0
  61. package/dist/admin/placements/placementLabels.js +21 -0
  62. package/dist/admin/placements/placementLabels.model.d.ts +13 -0
  63. package/dist/admin/placements/placementLabels.model.js +12 -0
  64. package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
  65. package/dist/admin/placements/usePlacementLabels.js +102 -0
  66. package/dist/admin/results/DataQualityPanel.js +4 -1
  67. package/dist/admin/results/results.model.d.ts +3 -0
  68. package/dist/admin/results/results.model.js +8 -4
  69. package/dist/admin/surface/TestsHome.js +39 -10
  70. package/dist/admin/surface/TestsSurface.js +7 -6
  71. package/dist/admin/surface/contracts.d.ts +6 -5
  72. package/dist/admin/surface/goals/GoalForm.js +71 -44
  73. package/dist/admin/surface/goals/GoalsList.js +5 -4
  74. package/dist/admin/surface/goals/data.js +4 -1
  75. package/dist/admin/surface/goals/goals.model.d.ts +1 -2
  76. package/dist/admin/surface/goals/goals.model.js +1 -3
  77. package/dist/admin/surface/home.model.d.ts +3 -3
  78. package/dist/admin/surface/home.model.js +8 -4
  79. package/dist/admin/surface/newTest/NewTest.js +56 -17
  80. package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
  81. package/dist/admin/surface/newTest/PageWizard.js +214 -0
  82. package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
  83. package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
  84. package/dist/admin/surface/pagePicker/PageCombobox.d.ts +10 -0
  85. package/dist/admin/surface/pagePicker/PageCombobox.js +395 -0
  86. package/dist/admin/surface/pagePicker/pagePicker.css +10 -0
  87. package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +27 -0
  88. package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
  89. package/dist/admin/surface/readiness/ActionDialogs.js +99 -64
  90. package/dist/admin/surface/readiness/Banner.js +30 -6
  91. package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
  92. package/dist/admin/surface/readiness/readiness.model.js +92 -9
  93. package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
  94. package/dist/admin/surface/results/ReleasePages.js +3 -7
  95. package/dist/admin/surface/results/ResultsBody.js +21 -2
  96. package/dist/admin/surface/results/ResultsHeader.js +5 -13
  97. package/dist/admin/surface/results/useResults.d.ts +20 -1
  98. package/dist/admin/surface/test/AdvancedSection.js +111 -104
  99. package/dist/admin/surface/test/GoalSection.js +94 -14
  100. package/dist/admin/surface/test/LengthSection.js +1 -1
  101. package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
  102. package/dist/admin/surface/test/PageHoldout.js +67 -0
  103. package/dist/admin/surface/test/TestPage.js +5 -9
  104. package/dist/admin/surface/test/VersionsSection.js +12 -5
  105. package/dist/admin/surface/test/fields.d.ts +13 -4
  106. package/dist/admin/surface/test/fields.js +46 -14
  107. package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
  108. package/dist/admin/surface/test/jumpToSection.js +4 -3
  109. package/dist/admin/surface/test/pageParts.d.ts +4 -1
  110. package/dist/admin/surface/test/pageParts.js +49 -1
  111. package/dist/admin/surface/test/testPage.css +3 -1
  112. package/dist/admin/surface/testDocument.model.d.ts +1 -0
  113. package/dist/admin/surface/testDocument.model.js +2 -1
  114. package/dist/admin/surface/useTestDocument.d.ts +15 -2
  115. package/dist/admin/surface/useTestDocument.js +53 -15
  116. package/dist/admin/surfaceUrls.d.ts +7 -0
  117. package/dist/admin/surfaceUrls.js +19 -0
  118. package/dist/admin/theme.css +2 -0
  119. package/dist/admin/useApi.d.ts +21 -2
  120. package/dist/admin/useApi.js +3 -5
  121. package/dist/admin/views/CollectionListRedirect.d.ts +1 -0
  122. package/dist/admin/views/CollectionListRedirect.js +1 -1
  123. package/dist/admin/views/ExperimentsRootView.js +28 -133
  124. package/dist/admin/views/GoalEditRedirect.d.ts +4 -0
  125. package/dist/admin/views/GoalEditRedirect.js +14 -0
  126. package/dist/admin/views/GoalsCollectionListView.d.ts +2 -0
  127. package/dist/admin/views/GoalsCollectionListView.js +4 -0
  128. package/dist/admin/views/TestsCollectionListView.d.ts +2 -0
  129. package/dist/admin/views/TestsCollectionListView.js +4 -0
  130. package/dist/admin/views/TestsNavLinkServer.d.ts +1 -0
  131. package/dist/admin/views/TestsNavLinkServer.js +1 -1
  132. package/dist/admin/views/buildSurfaceProps.d.ts +8 -0
  133. package/dist/admin/views/buildSurfaceProps.js +143 -0
  134. package/dist/admin/views/renderCollectionList.d.ts +2 -0
  135. package/dist/admin/views/renderCollectionList.js +34 -0
  136. package/dist/admin/views/renderTestsSurface.d.ts +3 -0
  137. package/dist/admin/views/renderTestsSurface.js +34 -0
  138. package/dist/blocks/bindings.d.ts +3 -0
  139. package/dist/blocks/bindings.js +34 -0
  140. package/dist/blocks/createExperimentBlock.d.ts +6 -0
  141. package/dist/blocks/createExperimentBlock.js +77 -0
  142. package/dist/blocks/formState.d.ts +31 -0
  143. package/dist/blocks/formState.js +146 -0
  144. package/dist/blocks/model.d.ts +41 -0
  145. package/dist/blocks/model.js +34 -0
  146. package/dist/client/ExperimentGroup.d.ts +6 -0
  147. package/dist/client/ExperimentGroup.js +9 -0
  148. package/dist/client/ExperimentsProvider.js +20 -4
  149. package/dist/client/clickGoals.d.ts +4 -0
  150. package/dist/client/clickGoals.js +43 -0
  151. package/dist/collections/experiments/config.js +21 -7
  152. package/dist/collections/experiments/deleteGuard.d.ts +2 -1
  153. package/dist/collections/experiments/deleteGuard.js +12 -1
  154. package/dist/collections/experiments/editability.js +4 -2
  155. package/dist/collections/experiments/pickSlug.d.ts +2 -0
  156. package/dist/collections/experiments/pickSlug.js +8 -0
  157. package/dist/collections/labels.d.ts +3 -0
  158. package/dist/collections/labels.js +20 -0
  159. package/dist/collections/metrics.js +28 -6
  160. package/dist/collections/phases.js +1 -0
  161. package/dist/collections/serialize.js +7 -0
  162. package/dist/db/clickGoals.d.ts +9 -0
  163. package/dist/db/clickGoals.js +22 -0
  164. package/dist/db/delete.d.ts +1 -0
  165. package/dist/db/delete.js +1 -0
  166. package/dist/db/locks.d.ts +8 -0
  167. package/dist/db/locks.js +27 -4
  168. package/dist/db/names.d.ts +4 -0
  169. package/dist/db/names.js +4 -0
  170. package/dist/db/pageBindings.d.ts +4 -0
  171. package/dist/db/pageBindings.js +17 -0
  172. package/dist/db/pageBusy.d.ts +2 -0
  173. package/dist/db/pageBusy.js +10 -0
  174. package/dist/db/pageContent.d.ts +10 -0
  175. package/dist/db/pageContent.js +79 -0
  176. package/dist/db/placements.d.ts +33 -0
  177. package/dist/db/placements.js +54 -0
  178. package/dist/db/slugs.d.ts +8 -0
  179. package/dist/db/slugs.js +40 -0
  180. package/dist/db/snapshots.d.ts +6 -0
  181. package/dist/db/snapshots.js +8 -5
  182. package/dist/endpoints/clickKeys.d.ts +5 -0
  183. package/dist/endpoints/clickKeys.js +17 -0
  184. package/dist/endpoints/events.js +4 -1
  185. package/dist/endpoints/index.js +28 -0
  186. package/dist/endpoints/pages.d.ts +5 -0
  187. package/dist/endpoints/pages.js +120 -0
  188. package/dist/endpoints/placementStatus.d.ts +6 -0
  189. package/dist/endpoints/placementStatus.js +238 -0
  190. package/dist/endpoints/readBody.d.ts +2 -0
  191. package/dist/endpoints/readBody.js +24 -0
  192. package/dist/endpoints/results.js +5 -1
  193. package/dist/endpoints/summary.js +11 -4
  194. package/dist/endpoints/wrapBlock.d.ts +6 -0
  195. package/dist/endpoints/wrapBlock.js +76 -0
  196. package/dist/errors.d.ts +3 -3
  197. package/dist/errors.js +4 -4
  198. package/dist/events/rows.d.ts +1 -1
  199. package/dist/events/rows.js +5 -3
  200. package/dist/exports/adminClient.d.ts +5 -0
  201. package/dist/exports/adminClient.js +5 -0
  202. package/dist/exports/adminRsc.d.ts +3 -0
  203. package/dist/exports/adminRsc.js +3 -0
  204. package/dist/exports/blocks.d.ts +2 -0
  205. package/dist/exports/blocks.js +1 -0
  206. package/dist/exports/client.d.ts +1 -0
  207. package/dist/exports/client.js +1 -0
  208. package/dist/exports/index.d.ts +6 -1
  209. package/dist/exports/index.js +3 -0
  210. package/dist/exports/puck.d.ts +3 -1
  211. package/dist/exports/puck.js +2 -0
  212. package/dist/exports/puckEditor.d.ts +3 -0
  213. package/dist/exports/puckEditor.js +2 -0
  214. package/dist/exports/rsc.d.ts +3 -0
  215. package/dist/exports/rsc.js +2 -0
  216. package/dist/fields/xpGoal.d.ts +3 -0
  217. package/dist/fields/xpGoal.js +16 -0
  218. package/dist/globals/state.js +10 -0
  219. package/dist/goals/cache.d.ts +3 -0
  220. package/dist/goals/cache.js +35 -0
  221. package/dist/goals/fairness.d.ts +40 -0
  222. package/dist/goals/fairness.js +200 -0
  223. package/dist/goals/key.d.ts +4 -0
  224. package/dist/goals/key.js +3 -0
  225. package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
  226. package/dist/goals/picker/clickGoalPicker.model.js +45 -0
  227. package/dist/goals/picker/copy.d.ts +16 -0
  228. package/dist/goals/picker/copy.js +15 -0
  229. package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
  230. package/dist/goals/picker/useClickGoalPicker.js +220 -0
  231. package/dist/goals/validate.d.ts +3 -0
  232. package/dist/goals/validate.js +14 -0
  233. package/dist/hooks/pageGuard.d.ts +5 -1
  234. package/dist/hooks/pageGuard.js +157 -95
  235. package/dist/hooks/puckGoals.d.ts +3 -0
  236. package/dist/hooks/puckGoals.js +38 -0
  237. package/dist/onInit.js +29 -0
  238. package/dist/options.d.ts +2 -0
  239. package/dist/options.js +41 -1
  240. package/dist/plugin.js +50 -43
  241. package/dist/puck/copy.d.ts +1 -0
  242. package/dist/puck/copy.js +1 -0
  243. package/dist/puck/editor/editor.chrome.css +14 -0
  244. package/dist/puck/editor/editor.css +106 -0
  245. package/dist/puck/editor/editor.model.d.ts +4 -2
  246. package/dist/puck/editor/editor.model.js +6 -3
  247. package/dist/puck/editor/integration.d.ts +10 -0
  248. package/dist/puck/editor/integration.js +30 -0
  249. package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
  250. package/dist/puck/editor/xpGoalPuckEditorField.js +142 -0
  251. package/dist/puck/integration.d.ts +6 -0
  252. package/dist/puck/integration.js +21 -0
  253. package/dist/puck/xpGoalField.d.ts +3 -0
  254. package/dist/puck/xpGoalField.js +6 -0
  255. package/dist/results/health.d.ts +4 -0
  256. package/dist/results/health.js +27 -0
  257. package/dist/results/snapshot.js +15 -2
  258. package/dist/sanitizeReadinessDetail.js +43 -0
  259. package/dist/server/pageKindSeoContract.d.ts +27 -0
  260. package/dist/server/pageKindSeoContract.js +80 -0
  261. package/dist/server/recordContentChange.js +58 -8
  262. package/dist/server/resolveExperimentBlocks.d.ts +5 -0
  263. package/dist/server/resolveExperimentBlocks.js +54 -0
  264. package/dist/server/runResults.js +56 -15
  265. package/dist/server/toClientAssignments.js +2 -1
  266. package/dist/transitions/addMetric.d.ts +2 -1
  267. package/dist/transitions/addMetric.js +28 -1
  268. package/dist/transitions/checkStart.d.ts +14 -1
  269. package/dist/transitions/checkStart.js +92 -8
  270. package/dist/transitions/decide.js +25 -0
  271. package/dist/transitions/deleteExperiment.d.ts +3 -1
  272. package/dist/transitions/deleteExperiment.js +27 -1
  273. package/dist/transitions/kernel.js +4 -0
  274. package/dist/transitions/makeContent.d.ts +5 -0
  275. package/dist/transitions/makeContent.js +39 -11
  276. package/dist/transitions/pageCollection.d.ts +10 -0
  277. package/dist/transitions/pageCollection.js +20 -0
  278. package/dist/transitions/pageVersions.d.ts +10 -0
  279. package/dist/transitions/pageVersions.js +44 -0
  280. package/dist/transitions/phaseRecord.d.ts +1 -0
  281. package/dist/transitions/placementInteractions.d.ts +40 -0
  282. package/dist/transitions/placementInteractions.js +94 -0
  283. package/dist/transitions/placements.d.ts +8 -0
  284. package/dist/transitions/placements.js +148 -1
  285. package/dist/transitions/preflightData.d.ts +2 -2
  286. package/dist/transitions/preflightData.js +137 -5
  287. package/dist/transitions/resolveCaps.d.ts +1 -0
  288. package/dist/transitions/resolveCaps.js +6 -2
  289. package/dist/transitions/start.js +46 -1
  290. package/dist/types/admin.d.ts +1 -1
  291. package/dist/types/assignments.d.ts +1 -0
  292. package/dist/types/contracts.d.ts +15 -0
  293. package/dist/types/endpoints.d.ts +92 -5
  294. package/dist/types/index.d.ts +3 -0
  295. package/dist/types/puck.d.ts +7 -1
  296. package/dist/types/snapshot.d.ts +17 -4
  297. package/dist/types/surface.d.ts +13 -0
  298. package/dist/ui/ui.css +4 -0
  299. package/package.json +8 -1
@@ -1,7 +1,7 @@
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, useMemo, useState } from 'react';
4
- import { Link } from '@payloadcms/ui';
5
5
  import { formatAdminURL } from 'payload/shared';
6
6
  import { fetchAllDocs } from '../../lib/fetchAllDocs.js';
7
7
  import { createApi } from '../api.js';
@@ -10,6 +10,7 @@ import { Banner, EmptyState, StatusChip } from '../../ui/Feedback.js';
10
10
  import { Button } from '../../ui/Button.js';
11
11
  import { SearchInput } from '../../ui/Inputs.js';
12
12
  import { SurfaceShell } from './SurfaceShell.js';
13
+ import { sanitizeReadinessDetail } from '../../sanitizeReadinessDetail.js';
13
14
  import { groupTests, homeCard } from './home.model.js';
14
15
  const HOME_FIELDS = [
15
16
  'name',
@@ -52,11 +53,40 @@ export function TestsHome({ adminRoute, apiRoute, canManage = false, crumbs, fla
52
53
  const docs = response.docs.sort((a, b)=>(Date.parse(b.updatedAt ?? '') || 0) - (Date.parse(a.updatedAt ?? '') || 0));
53
54
  if (!active) return;
54
55
  const summary = await api.summary(docs.map((doc)=>doc.id));
56
+ const summaries = summary.ok ? summary.data ?? {} : {};
57
+ const ids = [
58
+ ...new Set(docs.flatMap((doc)=>{
59
+ const item = summaries[doc.id];
60
+ if (!canManage || ![
61
+ 'draft',
62
+ 'stopped'
63
+ ].includes(doc.status) || item?.ready === true || item?.next?.code !== 'goal-in-some-versions') return [];
64
+ const detail = sanitizeReadinessDetail(item.next.code, item.next.detail);
65
+ return detail?.map((row)=>String(row.metricId)) ?? [];
66
+ }))
67
+ ];
68
+ const goalNames = {};
69
+ if (ids.length) {
70
+ try {
71
+ const query = new URLSearchParams({
72
+ 'where[id][in]': ids.join(','),
73
+ depth: '0',
74
+ limit: String(ids.length)
75
+ });
76
+ const response = await fetch(`${apiRoute.replace(/\/$/, '')}/xp-metrics?${query}`, {
77
+ credentials: 'include',
78
+ signal: controller.signal
79
+ });
80
+ const body = response.ok ? await response.json() : undefined;
81
+ for (const goal of body?.docs ?? [])if (typeof goal.name === 'string' && goal.name.trim() && ids.includes(String(goal.id))) goalNames[String(goal.id)] = goal.name;
82
+ } catch {}
83
+ }
55
84
  if (active) setState({
56
85
  loading: false,
57
86
  error: false,
58
87
  tests: docs,
59
- summaries: summary.ok ? summary.data ?? {} : {},
88
+ goalNames,
89
+ summaries,
60
90
  summaryError: !summary.ok
61
91
  });
62
92
  } catch {
@@ -74,7 +104,8 @@ export function TestsHome({ adminRoute, apiRoute, canManage = false, crumbs, fla
74
104
  }, [
75
105
  api,
76
106
  apiRoute,
77
- attempt
107
+ attempt,
108
+ canManage
78
109
  ]);
79
110
  const retry = /*#__PURE__*/ _jsx(Button, {
80
111
  size: "sm",
@@ -124,10 +155,7 @@ export function TestsHome({ adminRoute, apiRoute, canManage = false, crumbs, fla
124
155
  ]
125
156
  }),
126
157
  canManage ? /*#__PURE__*/ _jsx(Button, {
127
- href: formatAdminURL({
128
- adminRoute,
129
- path: '/experiments/goals'
130
- }),
158
+ href: surfaceUrls.goals(adminRoute),
131
159
  children: SURFACE_COPY.goals
132
160
  }) : null,
133
161
  newTest
@@ -153,6 +181,7 @@ export function TestsHome({ adminRoute, apiRoute, canManage = false, crumbs, fla
153
181
  }) : /*#__PURE__*/ _jsx(TestGroups, {
154
182
  tests: state.tests,
155
183
  summaries: state.summaries,
184
+ goalNames: state.goalNames,
156
185
  adminRoute: adminRoute,
157
186
  flagPoints: flagPoints,
158
187
  search: search,
@@ -162,7 +191,7 @@ export function TestsHome({ adminRoute, apiRoute, canManage = false, crumbs, fla
162
191
  })
163
192
  });
164
193
  }
165
- function TestGroups({ tests, summaries, adminRoute, flagPoints, search, clear }) {
194
+ function TestGroups({ tests, summaries, goalNames, adminRoute, flagPoints, search, clear }) {
166
195
  const groups = groupTests(tests, summaries, search);
167
196
  if (!groups.some((group)=>group.tests.length)) return /*#__PURE__*/ _jsxs("div", {
168
197
  className: "xp-tests-no-matches",
@@ -184,7 +213,7 @@ function TestGroups({ tests, summaries, adminRoute, flagPoints, search, clear })
184
213
  const rows = /*#__PURE__*/ _jsx("div", {
185
214
  className: "xp-tests-list",
186
215
  children: group.tests.map((test)=>{
187
- const card = homeCard(test, summaries[test.id], adminRoute, flagPoints);
216
+ const card = homeCard(test, summaries[test.id], adminRoute, flagPoints, goalNames);
188
217
  return /*#__PURE__*/ _jsxs("article", {
189
218
  className: "xp-tests-row",
190
219
  children: [
@@ -194,7 +223,7 @@ function TestGroups({ tests, summaries, adminRoute, flagPoints, search, clear })
194
223
  /*#__PURE__*/ _jsxs("div", {
195
224
  className: "xp-tests-row-name",
196
225
  children: [
197
- /*#__PURE__*/ _jsx(Link, {
226
+ /*#__PURE__*/ _jsx("a", {
198
227
  href: card.href,
199
228
  children: test.name
200
229
  }),
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { surfaceUrls } from '../surfaceUrls.js';
3
4
  import { useState } from 'react';
4
- import { formatAdminURL } from 'payload/shared';
5
5
  import { SURFACE_COPY } from '../copy/surface.js';
6
6
  import { NewTest } from './newTest/NewTest.js';
7
7
  import { TestPage } from './test/TestPage.js';
@@ -28,7 +28,11 @@ function SurfacePage(props) {
28
28
  flagPoints: props.flagPoints,
29
29
  attributeKeys: props.attributeKeys,
30
30
  metricSources: props.metricSources,
31
- descriptions: props.admin?.descriptions
31
+ admin: props.admin,
32
+ pageCollections: props.pageCollections,
33
+ collectionTitles: props.collectionTitles,
34
+ userId: props.userId,
35
+ userCollection: props.userCollection
32
36
  };
33
37
  const crumbs = [
34
38
  {
@@ -36,10 +40,7 @@ function SurfacePage(props) {
36
40
  }
37
41
  ];
38
42
  if (props.route !== 'home') {
39
- crumbs[0].href = formatAdminURL({
40
- adminRoute: props.adminRoute,
41
- path: '/experiments'
42
- });
43
+ crumbs[0].href = props.canManage ? surfaceUrls.home(props.adminRoute) : undefined;
43
44
  crumbs.push({
44
45
  label: props.route === 'new' ? SURFACE_COPY.newTest : props.route === 'results' ? SURFACE_COPY.resultsTitle : SURFACE_COPY.test
45
46
  });
@@ -1,4 +1,5 @@
1
1
  import type { ComponentType } from 'react';
2
+ import type { TestsSurfaceProps } from '../../types/surface.js';
2
3
  import type { SerializedAttributeKeys, SerializedFlagPoint, SerializedMetricSource } from '../../types/admin.js';
3
4
  import type { useTestDocument } from './useTestDocument.js';
4
5
  /** Sections of the test page, in readiness order (surface spec §6.2, §8). */
@@ -10,17 +11,17 @@ export type TestDocApi = ReturnType<typeof useTestDocument>;
10
11
  export interface SurfaceProps {
11
12
  adminRoute: string;
12
13
  apiRoute: string;
14
+ collectionTitles?: Record<string, string>;
15
+ userId?: string | number;
16
+ userCollection?: string;
13
17
  canManage: boolean;
14
18
  canReadResults: boolean;
15
19
  isAdmin: boolean;
16
20
  flagPoints: SerializedFlagPoint[];
17
21
  attributeKeys: SerializedAttributeKeys;
18
22
  metricSources: SerializedMetricSource[];
19
- descriptions?: {
20
- block?: string;
21
- document?: string;
22
- setting?: string;
23
- };
23
+ pageCollections?: TestsSurfaceProps['pageCollections'];
24
+ admin?: TestsSurfaceProps['admin'];
24
25
  }
25
26
  export interface SectionProps {
26
27
  test: TestDocApi;
@@ -1,11 +1,14 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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, useRef, useState } from 'react';
4
5
  import { useRouter } from 'next/navigation.js';
5
6
  import { formatAdminURL } from 'payload/shared';
6
7
  import { GOALS_COPY as C, goalDeleteLocked, goalInUse } from '../../copy/goals.js';
7
- import { metricSourceHint, metricSourceOptions } from '../../metricSource.model.js';
8
+ import { metricSourceHint, metricSourceOptions, sourceFor } from '../../metricSource.model.js';
8
9
  import { validateCappingType, validateCappingValue } from '../../../collections/metricValidation.js';
10
+ import { CLICK_KEY } from '../../../goals/key.js';
11
+ import { clickGoalSlug } from '../../../goals/picker/clickGoalPicker.model.js';
9
12
  import { goalSlug, goalUsage } from './goals.model.js';
10
13
  import { GoalResponseError, goalPages, goalResponse } from './data.js';
11
14
  import { SURFACE_COPY } from '../../copy/surface.js';
@@ -55,17 +58,13 @@ export function GoalForm({ surface, id, returnToTest }) {
55
58
  const advanced = useRef(null);
56
59
  const [confirmDelete, setConfirmDelete] = useState(false);
57
60
  useEffect(()=>{
58
- if (advanced.current && (fieldErrors.capType || fieldErrors.capValue || fieldErrors.warningPercent)) advanced.current.open = true;
61
+ if (advanced.current && (fieldErrors.slug || fieldErrors.capType || fieldErrors.capValue || fieldErrors.warningPercent)) advanced.current.open = true;
59
62
  }, [
60
63
  fieldErrors
61
64
  ]);
62
- const returnQuery = returnToTest ? `?${new URLSearchParams({
65
+ const listUrl = surfaceUrls.goals(surface.adminRoute, returnToTest ? {
63
66
  return: returnToTest
64
- })}` : '';
65
- const listUrl = formatAdminURL({
66
- adminRoute: surface.adminRoute,
67
- path: `/experiments/goals${returnQuery}`
68
- });
67
+ } : undefined);
69
68
  useEffect(()=>{
70
69
  const controller = new AbortController();
71
70
  setReady(false);
@@ -84,7 +83,7 @@ export function GoalForm({ surface, id, returnToTest }) {
84
83
  ]);
85
84
  if (loaded.id == null) throw new Error(C.missing);
86
85
  if (controller.signal.aborted) return;
87
- const defaultCap = surface.metricSources.some((source)=>source.id === loaded.source && source.kind === 'revenue') && loaded.capping?.type == null && loaded.capping?.value == null;
86
+ const defaultCap = sourceFor(loaded.source, surface.metricSources)?.kind === 'revenue' && loaded.capping?.type == null && loaded.capping?.value == null;
88
87
  setGoal(loaded);
89
88
  setUsedBy(goalUsage(tests).get(loaded.id) ?? 0);
90
89
  setDraft({
@@ -107,7 +106,8 @@ export function GoalForm({ surface, id, returnToTest }) {
107
106
  id,
108
107
  attempt
109
108
  ]);
110
- const source = surface.metricSources.find((source)=>source.id === draft.source);
109
+ const source = sourceFor(draft.source, surface.metricSources);
110
+ const click = draft.source.startsWith('click:');
111
111
  const hint = metricSourceHint(source);
112
112
  const options = metricSourceOptions(surface.metricSources);
113
113
  const set = (key, value)=>setDraft((current)=>({
@@ -116,11 +116,14 @@ export function GoalForm({ surface, id, returnToTest }) {
116
116
  }));
117
117
  async function submit() {
118
118
  if (inFlight.current || !ready || !surface.canManage) return;
119
- const capValue = draft.capValue.trim() === '' ? null : Number(draft.capValue);
120
- const typeError = validateCappingType(draft.capType || null, capValue, source?.kind);
121
- const valueError = validateCappingValue(capValue, draft.capType || null);
119
+ const capValue = click || draft.capValue.trim() === '' ? null : Number(draft.capValue);
120
+ const typeError = validateCappingType(click ? null : draft.capType || null, capValue, source?.kind);
121
+ const valueError = validateCappingValue(capValue, click ? null : draft.capType || null);
122
122
  const warningPercent = draft.warningPercent.trim() === '' ? 5 : Number(draft.warningPercent);
123
123
  const errors = {
124
+ ...click && !CLICK_KEY.test(draft.slug) ? {
125
+ slug: C.slugRule
126
+ } : {},
124
127
  ...typeError !== true ? {
125
128
  capType: typeError
126
129
  } : {},
@@ -141,12 +144,12 @@ export function GoalForm({ surface, id, returnToTest }) {
141
144
  const body = {
142
145
  name: draft.name,
143
146
  slug: usedBy && goal ? goal.slug : draft.slug,
144
- source: usedBy && goal ? goal.source : draft.source,
145
- direction: draft.direction,
147
+ source: usedBy && goal ? goal.source : click ? `click:${draft.slug}` : draft.source,
148
+ direction: click ? 'increase' : draft.direction,
146
149
  attributionWindowHours: draft.attributionWindowHours,
147
150
  minSampleSize: draft.minSampleSize,
148
151
  capping: {
149
- type: draft.capType || null,
152
+ type: click ? null : draft.capType || null,
150
153
  value: capValue
151
154
  },
152
155
  guardrailThreshold: warningPercent / 100
@@ -170,11 +173,11 @@ export function GoalForm({ surface, id, returnToTest }) {
170
173
  })}`
171
174
  }) : listUrl);
172
175
  } catch (cause) {
173
- setError(cause instanceof Error ? cause.message : C.saveError);
176
+ setError(cause instanceof GoalResponseError ? cause.message : C.saveError);
174
177
  if (cause instanceof GoalResponseError) setFieldErrors(Object.fromEntries(Object.entries(cause.fields).flatMap(([path, message])=>fieldNames[path] ? [
175
178
  [
176
179
  fieldNames[path],
177
- message
180
+ path === 'slug' ? C.duplicateSlug : message
178
181
  ]
179
182
  ] : [])));
180
183
  inFlight.current = false;
@@ -209,10 +212,7 @@ export function GoalForm({ surface, id, returnToTest }) {
209
212
  crumbs: [
210
213
  {
211
214
  label: SURFACE_COPY.title,
212
- href: formatAdminURL({
213
- adminRoute: surface.adminRoute,
214
- path: '/experiments'
215
- })
215
+ href: surfaceUrls.home(surface.adminRoute)
216
216
  },
217
217
  {
218
218
  label: C.title,
@@ -272,41 +272,54 @@ export function GoalForm({ surface, id, returnToTest }) {
272
272
  ...current,
273
273
  name,
274
274
  ...!slugEdited && !usedBy ? {
275
- slug: goalSlug(name)
275
+ slug: click ? clickGoalSlug(name) : goalSlug(name)
276
276
  } : {}
277
277
  }));
278
+ if (click && !slugEdited && !usedBy) setFieldErrors((current)=>({
279
+ ...current,
280
+ slug: clickGoalSlug(name) ? undefined : C.slugRule
281
+ }));
278
282
  }
279
283
  }),
280
- /*#__PURE__*/ _jsx(TextInput, {
281
- id: "xp-goal-slug",
282
- label: C.slug,
283
- required: true,
284
- value: draft.slug,
285
- readOnly: usedBy > 0,
286
- help: usedBy ? C.slugLocked : C.slugHelp,
287
- error: fieldErrors.slug,
288
- onChange: (event)=>{
289
- setSlugEdited(true);
290
- set('slug', event.currentTarget.value);
291
- }
292
- }),
284
+ !click ? /*#__PURE__*/ _jsx(_Fragment, {
285
+ children: /*#__PURE__*/ _jsx(TextInput, {
286
+ id: "xp-goal-slug",
287
+ label: C.slug,
288
+ required: true,
289
+ value: draft.slug,
290
+ readOnly: usedBy > 0,
291
+ help: usedBy ? C.slugLocked : C.slugHelp,
292
+ error: fieldErrors.slug,
293
+ onChange: (event)=>{
294
+ setSlugEdited(true);
295
+ set('slug', event.currentTarget.value);
296
+ }
297
+ })
298
+ }) : null,
293
299
  /*#__PURE__*/ _jsxs(Select, {
294
300
  id: "xp-goal-source",
295
301
  label: C.source,
296
302
  required: true,
297
- value: draft.source,
303
+ value: click ? 'click:' : draft.source,
298
304
  disabled: usedBy > 0,
299
305
  help: usedBy ? C.sourceLocked : hint || undefined,
300
306
  error: fieldErrors.source,
301
307
  onChange: (event)=>{
302
308
  const sourceId = event.currentTarget.value;
303
- const revenue = surface.metricSources.find((source)=>source.id === sourceId)?.kind === 'revenue';
309
+ const revenue = sourceFor(sourceId, surface.metricSources)?.kind === 'revenue';
304
310
  setDraft((current)=>({
305
311
  ...current,
306
312
  source: sourceId,
313
+ ...!slugEdited && !usedBy ? {
314
+ slug: sourceId === 'click:' ? clickGoalSlug(current.name) : goalSlug(current.name)
315
+ } : {},
307
316
  capType: revenue ? 'percentile' : '',
308
317
  capValue: revenue ? '0.99' : ''
309
318
  }));
319
+ if (sourceId === 'click:' && !slugEdited && !usedBy) setFieldErrors((current)=>({
320
+ ...current,
321
+ slug: clickGoalSlug(draft.name) ? undefined : C.slugRule
322
+ }));
310
323
  },
311
324
  children: [
312
325
  /*#__PURE__*/ _jsx("option", {
@@ -315,15 +328,15 @@ export function GoalForm({ surface, id, returnToTest }) {
315
328
  }),
316
329
  options.map((option)=>/*#__PURE__*/ _jsx("option", {
317
330
  value: option.value,
318
- children: option.label
331
+ children: option.value === 'click:' ? sourceFor(option.value, surface.metricSources)?.label : option.label
319
332
  }, option.value)),
320
- draft.source && !options.some((option)=>option.value === draft.source) ? /*#__PURE__*/ _jsx("option", {
333
+ !click && draft.source && !options.some((option)=>option.value === draft.source) ? /*#__PURE__*/ _jsx("option", {
321
334
  value: draft.source,
322
335
  children: draft.source
323
336
  }) : null
324
337
  ]
325
338
  }),
326
- /*#__PURE__*/ _jsxs(Select, {
339
+ !click ? /*#__PURE__*/ _jsxs(Select, {
327
340
  id: "xp-goal-direction",
328
341
  label: C.direction,
329
342
  value: draft.direction,
@@ -339,7 +352,7 @@ export function GoalForm({ surface, id, returnToTest }) {
339
352
  children: C.decrease
340
353
  })
341
354
  ]
342
- }),
355
+ }) : null,
343
356
  /*#__PURE__*/ _jsx(NumberInput, {
344
357
  id: "xp-goal-window",
345
358
  label: C.window,
@@ -370,7 +383,21 @@ export function GoalForm({ surface, id, returnToTest }) {
370
383
  /*#__PURE__*/ _jsxs("div", {
371
384
  className: "xp-goal-advanced-fields",
372
385
  children: [
373
- /*#__PURE__*/ _jsxs("fieldset", {
386
+ click ? /*#__PURE__*/ _jsx(_Fragment, {
387
+ children: /*#__PURE__*/ _jsx(TextInput, {
388
+ id: "xp-goal-slug",
389
+ label: C.slug,
390
+ required: true,
391
+ value: draft.slug,
392
+ readOnly: usedBy > 0,
393
+ help: usedBy ? C.slugLocked : C.slugHelp,
394
+ error: fieldErrors.slug,
395
+ onChange: (event)=>{
396
+ setSlugEdited(true);
397
+ set('slug', event.currentTarget.value);
398
+ }
399
+ })
400
+ }) : /*#__PURE__*/ _jsxs("fieldset", {
374
401
  children: [
375
402
  /*#__PURE__*/ _jsx("legend", {
376
403
  children: C.capping
@@ -1,7 +1,8 @@
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';
4
+ import { sourceFor } from '../../metricSource.model.js';
3
5
  import { useEffect, useState } from 'react';
4
- import { Link } from '@payloadcms/ui';
5
6
  import { formatAdminURL } from 'payload/shared';
6
7
  import { Banner, EmptyState } from '../../../ui/Feedback.js';
7
8
  import { Button } from '../../../ui/Button.js';
@@ -60,7 +61,7 @@ export function GoalsList({ surface, returnToTest }) {
60
61
  crumbs: [
61
62
  {
62
63
  label: SURFACE_COPY.title,
63
- href: url('/experiments')
64
+ href: surfaceUrls.home(surface.adminRoute)
64
65
  },
65
66
  {
66
67
  label: C.title
@@ -111,7 +112,7 @@ export function GoalsList({ surface, returnToTest }) {
111
112
  className: "xp-tests-list",
112
113
  children: goals.map((goal)=>{
113
114
  const count = usage.get(goal.id) ?? 0;
114
- const kind = surface.metricSources.find((source)=>source.id === goal.source)?.kind;
115
+ const kind = sourceFor(goal.source, surface.metricSources)?.kind;
115
116
  const label = kind && Object.hasOwn(METRIC_KIND_LABELS, kind) ? METRIC_KIND_LABELS[kind] : undefined;
116
117
  const href = url(`/experiments/goals/${encodeURIComponent(String(goal.id))}${returnQuery}`);
117
118
  return /*#__PURE__*/ _jsxs("article", {
@@ -123,7 +124,7 @@ export function GoalsList({ surface, returnToTest }) {
123
124
  /*#__PURE__*/ _jsxs("div", {
124
125
  className: "xp-tests-row-name",
125
126
  children: [
126
- /*#__PURE__*/ _jsx(Link, {
127
+ /*#__PURE__*/ _jsx("a", {
127
128
  href: href,
128
129
  children: goal.name
129
130
  }),
@@ -17,7 +17,10 @@ export class GoalResponseError extends Error {
17
17
  function goalRefusal(body, status, fallback, options) {
18
18
  const fields = payloadFieldErrors(body, '_section', fallback);
19
19
  const first = body?.errors?.[0];
20
- throw new GoalResponseError(refusalMessage(first?.code, status, Object.values(fields).join(' ') || fallback, options), fields);
20
+ throw new GoalResponseError(refusalMessage(first?.code, status, undefined, {
21
+ ...options,
22
+ fallback
23
+ }), fields);
21
24
  }
22
25
  export async function goalPages(base, collection, signal) {
23
26
  const result = await fetchAllDocs(`${base}/${collection}?depth=0`, {
@@ -26,5 +26,4 @@ export interface GoalUsageTest {
26
26
  }
27
27
  /** Count each experiment once per goal, retaining the API's id types. */
28
28
  export declare function goalUsage(tests: readonly GoalUsageTest[]): Map<DocId, number>;
29
- export declare function goalSlug(name: string): string;
30
- export {};
29
+ export { goalSlug } from '../../../goals/picker/clickGoalPicker.model.js';
@@ -21,6 +21,4 @@
21
21
  tests.size
22
22
  ]));
23
23
  }
24
- export function goalSlug(name) {
25
- return name.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
26
- }
24
+ export { goalSlug } from '../../../goals/picker/clickGoalPicker.model.js';
@@ -23,11 +23,11 @@ type Summary = SummaryResponse[DocId] | undefined;
23
23
  export type Health = 'red' | 'amber' | 'green';
24
24
  export declare function healthDot(test: HomeTest, summary: Summary): Health | undefined;
25
25
  export declare function groupTests(tests: readonly HomeTest[], summaries: SummaryResponse, search?: string): {
26
- label: "Needs attention" | "Running" | "Decided" | "Draft" | "Archived" | "Ready to start";
26
+ label: "Needs attention" | "Running" | "Decided" | "Ready to start" | "Draft" | "Archived";
27
27
  tests: HomeTest[];
28
28
  }[];
29
- export declare function homeCard(test: HomeTest, summary: Summary, adminRoute: string, flagPoints?: ReadinessContext['flagPoints']): {
30
- kind: "Whole page" | "Page block" | "Setting";
29
+ export declare function homeCard(test: HomeTest, summary: Summary, adminRoute: string, flagPoints?: ReadinessContext['flagPoints'], goalNames?: Readonly<Record<string, string>>): {
30
+ kind: "Page block" | "Setting" | "Whole page";
31
31
  pill: {
32
32
  label: string;
33
33
  tone: import("../status.model.js").PillTone;
@@ -3,7 +3,7 @@ import { formatDate } from '../format/date.js';
3
3
  import { armLabel } from '../labels.js';
4
4
  import { statusPill } from '../status.model.js';
5
5
  import { decidedLine, runningLine, SURFACE_COPY } from '../copy/surface.js';
6
- import { nextCopy } from '../copy/readiness.js';
6
+ import { asymmetricGoalsCopy, nextCopy, READINESS_COPY } from '../copy/readiness.js';
7
7
  import { READINESS } from './readiness/readiness.model.js';
8
8
  import { sanitizeReadinessDetail } from '../../sanitizeReadinessDetail.js';
9
9
  export function healthDot(test, summary) {
@@ -30,7 +30,7 @@ export function groupTests(tests, summaries, search = '') {
30
30
  const number = (value)=>value != null && Number.isFinite(value) ? new Intl.NumberFormat('en-US', {
31
31
  maximumFractionDigits: 0
32
32
  }).format(value) : '—';
33
- export function homeCard(test, summary, adminRoute, flagPoints) {
33
+ export function homeCard(test, summary, adminRoute, flagPoints, goalNames = {}) {
34
34
  const results = [
35
35
  'running',
36
36
  'stopped',
@@ -45,12 +45,16 @@ export function homeCard(test, summary, adminRoute, flagPoints) {
45
45
  const next = summary?.next;
46
46
  if ((test.status === 'draft' || test.status === 'stopped') && summary?.ready !== true && next && Object.hasOwn(READINESS, next.code)) {
47
47
  // Keep unexpected text out of authored copy, including responses from older servers.
48
- const sentence = READINESS[next.code].next(sanitizeReadinessDetail(next.code, next.detail), {
48
+ const detail = sanitizeReadinessDetail(next.code, next.detail);
49
+ const first = Array.isArray(detail) ? detail[0] : undefined;
50
+ const sentence = next.code === 'goal-in-some-versions' ? asymmetricGoalsCopy({
51
+ name: first?.metricId == null ? undefined : goalNames[String(first.metricId)]
52
+ }) : READINESS[next.code].next(detail, {
49
53
  ...test,
50
54
  arms: test.arms ?? [],
51
55
  flagPoints
52
56
  });
53
- if (sentence) line = nextCopy(sentence);
57
+ if (sentence) line = nextCopy(next.code === 'page-no-targets' ? READINESS_COPY.choosePages : sentence);
54
58
  }
55
59
  if (test.status === 'running') line = runningLine(number(summary?.days), number(test.analysis?.plannedDurationDays), number(summary?.exposures));
56
60
  if (test.status === 'decided') {