@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
@@ -0,0 +1,4 @@
1
+ import type { TextFieldClientProps } from 'payload';
2
+ import '../theme.css';
3
+ import './xpGoalField.css';
4
+ export declare function XpGoalField({ path, field, readOnly }: TextFieldClientProps): import("react").JSX.Element;
@@ -0,0 +1,168 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Button, FieldError, FieldLabel, SelectInput, TextInput, useAuth, useConfig, useField } from '@payloadcms/ui';
4
+ import { useId } from 'react';
5
+ import { useClickGoalPicker } from '../../goals/picker/useClickGoalPicker.js';
6
+ import { CLICK_GOAL_COPY as C } from '../../goals/picker/copy.js';
7
+ import '../theme.css';
8
+ import './xpGoalField.css';
9
+ export function XpGoalField({ path, field, readOnly }) {
10
+ const { value, setValue, showError } = useField({
11
+ path
12
+ });
13
+ const { permissions } = useAuth();
14
+ const { config } = useConfig();
15
+ const access = permissions?.collections?.['xp-experiments'];
16
+ const locked = Boolean(readOnly || field.admin?.readOnly);
17
+ const picker = useClickGoalPicker(config.routes.api, {
18
+ enabled: access?.update === true,
19
+ onSelect: setValue
20
+ });
21
+ const id = useId(), namePath = `${id}-name`, slugPath = `${id}-slug`;
22
+ const stopEnter = (event)=>{
23
+ if (event.key === 'Enter') {
24
+ event.preventDefault();
25
+ event.stopPropagation();
26
+ }
27
+ };
28
+ return /*#__PURE__*/ _jsxs("div", {
29
+ className: "field-type xp-goal-field",
30
+ children: [
31
+ /*#__PURE__*/ _jsx(TextInput, {
32
+ path: path,
33
+ label: field.label ?? C.label,
34
+ description: field.admin?.description,
35
+ required: field.required,
36
+ readOnly: locked,
37
+ value: value ?? '',
38
+ showError: showError,
39
+ Error: /*#__PURE__*/ _jsx(FieldError, {
40
+ path: path,
41
+ showError: showError
42
+ }),
43
+ onChange: (event)=>{
44
+ if (!locked) setValue(event.currentTarget.value);
45
+ }
46
+ }),
47
+ picker.canManage && !locked ? /*#__PURE__*/ _jsxs(_Fragment, {
48
+ children: [
49
+ /*#__PURE__*/ _jsxs("div", {
50
+ className: "xp-goal-field__row",
51
+ children: [
52
+ /*#__PURE__*/ _jsx(SelectInput, {
53
+ name: `${path}__xpGoalPicker`,
54
+ path: `${path}__xpGoalPicker`,
55
+ className: "xp-goal-field__select",
56
+ value: picker.goals.some((goal)=>goal.slug === value) ? value : undefined,
57
+ options: picker.goals.map((goal)=>({
58
+ value: goal.slug,
59
+ label: goal.name
60
+ })),
61
+ placeholder: C.choose,
62
+ isClearable: false,
63
+ label: C.choose,
64
+ onChange: (option)=>{
65
+ if (option && !Array.isArray(option)) setValue(String(option.value));
66
+ }
67
+ }),
68
+ !picker.panel.open ? /*#__PURE__*/ _jsx(Button, {
69
+ type: "button",
70
+ margin: false,
71
+ buttonStyle: "secondary",
72
+ onClick: picker.open,
73
+ children: C.newGoal
74
+ }) : null
75
+ ]
76
+ }),
77
+ picker.undefinedKey(value ?? '') ? /*#__PURE__*/ _jsx("p", {
78
+ role: "status",
79
+ children: C.undefinedKey
80
+ }) : null,
81
+ picker.panel.open ? /*#__PURE__*/ _jsxs("section", {
82
+ className: "xp-goal-field__panel",
83
+ "aria-labelledby": `${id}-title`,
84
+ children: [
85
+ /*#__PURE__*/ _jsx("h3", {
86
+ id: `${id}-title`,
87
+ children: C.title
88
+ }),
89
+ /*#__PURE__*/ _jsxs("div", {
90
+ className: "field-type text",
91
+ children: [
92
+ /*#__PURE__*/ _jsx(FieldLabel, {
93
+ path: namePath,
94
+ label: C.name
95
+ }),
96
+ /*#__PURE__*/ _jsx("input", {
97
+ id: `field-${namePath}`,
98
+ value: picker.panel.name,
99
+ disabled: picker.panel.pending,
100
+ onKeyDown: stopEnter,
101
+ onChange: (event)=>picker.setName(event.currentTarget.value)
102
+ })
103
+ ]
104
+ }),
105
+ /*#__PURE__*/ _jsxs("details", {
106
+ open: picker.panel.advanced,
107
+ onToggle: (event)=>picker.setAdvanced(event.currentTarget.open),
108
+ children: [
109
+ /*#__PURE__*/ _jsx("summary", {
110
+ children: C.advanced
111
+ }),
112
+ /*#__PURE__*/ _jsxs("div", {
113
+ className: "field-type text",
114
+ children: [
115
+ /*#__PURE__*/ _jsx(FieldLabel, {
116
+ path: slugPath,
117
+ label: C.slug
118
+ }),
119
+ /*#__PURE__*/ _jsx("input", {
120
+ id: `field-${slugPath}`,
121
+ value: picker.panel.slug,
122
+ disabled: picker.panel.pending,
123
+ onKeyDown: stopEnter,
124
+ "aria-invalid": Boolean(picker.panel.errors.slug) || undefined,
125
+ "aria-describedby": picker.panel.errors.slug ? `${id}-slug-error` : undefined,
126
+ onChange: (event)=>picker.setSlug(event.currentTarget.value)
127
+ }),
128
+ picker.panel.errors.slug ? /*#__PURE__*/ _jsx("p", {
129
+ id: `${id}-slug-error`,
130
+ role: "alert",
131
+ children: picker.panel.errors.slug
132
+ }) : null
133
+ ]
134
+ })
135
+ ]
136
+ }),
137
+ picker.panel.errors.general ? /*#__PURE__*/ _jsx("p", {
138
+ role: "alert",
139
+ children: picker.panel.errors.general
140
+ }) : null,
141
+ /*#__PURE__*/ _jsxs("div", {
142
+ className: "xp-goal-field__actions",
143
+ children: [
144
+ /*#__PURE__*/ _jsx(Button, {
145
+ type: "button",
146
+ margin: false,
147
+ buttonStyle: "primary",
148
+ disabled: picker.panel.pending || !picker.panel.name.trim(),
149
+ onClick: ()=>void picker.create(),
150
+ children: C.createGoal
151
+ }),
152
+ /*#__PURE__*/ _jsx(Button, {
153
+ type: "button",
154
+ margin: false,
155
+ buttonStyle: "secondary",
156
+ disabled: picker.panel.pending,
157
+ onClick: picker.cancel,
158
+ children: C.cancel
159
+ })
160
+ ]
161
+ })
162
+ ]
163
+ }) : null
164
+ ]
165
+ }) : null
166
+ ]
167
+ });
168
+ }
@@ -0,0 +1,48 @@
1
+ .xp-goal-field {
2
+ display: grid;
3
+ gap: var(--xp-field-gap);
4
+ }
5
+
6
+ .xp-goal-field__row {
7
+ display: flex;
8
+ flex-wrap: wrap;
9
+ align-items: end;
10
+ gap: var(--xp-space-2);
11
+ }
12
+
13
+ .xp-goal-field__select {
14
+ flex: 1 1 auto;
15
+ min-width: 0;
16
+ }
17
+
18
+ .xp-goal-field__panel {
19
+ display: grid;
20
+ gap: var(--xp-field-gap);
21
+ border: 1px solid var(--xp-border);
22
+ border-radius: var(--xp-radius-control);
23
+ padding: var(--xp-space-4);
24
+ }
25
+
26
+ .xp-goal-field > p,
27
+ .xp-goal-field__panel > h3,
28
+ .xp-goal-field__panel > p {
29
+ margin: 0;
30
+ }
31
+
32
+ .xp-goal-field__panel details[open] > summary {
33
+ margin-bottom: var(--xp-field-gap);
34
+ }
35
+
36
+ .xp-goal-field__actions {
37
+ display: flex;
38
+ flex-wrap: wrap;
39
+ gap: var(--xp-space-2);
40
+ }
41
+
42
+ /* Payload halves the gap between fields below 768px; match it inside the field. */
43
+ @media (max-width: 768px) {
44
+ .xp-goal-field,
45
+ .xp-goal-field__panel {
46
+ gap: calc(var(--xp-field-gap) / 2);
47
+ }
48
+ }
@@ -5,3 +5,5 @@ export declare function metricSourceOptions(sources: readonly SerializedMetricSo
5
5
  badge: "count" | "yes/no" | "amount";
6
6
  }[];
7
7
  export declare function metricSourceHint(source?: SerializedMetricSource): string | null;
8
+ /** Click goal definitions share the serialized click-source option. */
9
+ export declare function sourceFor(sourceId: string | undefined, metricSources: readonly SerializedMetricSource[]): SerializedMetricSource | undefined;
@@ -9,3 +9,7 @@ export function metricSourceOptions(sources) {
9
9
  export function metricSourceHint(source) {
10
10
  return source?.kind === 'revenue' ? METRICS_COPY.revenueHint : null;
11
11
  }
12
+ /** Click goal definitions share the serialized click-source option. */ export function sourceFor(sourceId, metricSources) {
13
+ const id = sourceId?.startsWith('click:') ? 'click:' : sourceId;
14
+ return metricSources.find((source)=>source.id === id);
15
+ }
@@ -0,0 +1,10 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { ExperimentsPageCollection } from '../types/contracts.js';
3
+ export declare function serializePageCollections(collections: ExperimentsPageCollection[], req: PayloadRequest): {
4
+ copyUrl: string;
5
+ copyLabel: string;
6
+ copyHint?: string | undefined;
7
+ slug: string;
8
+ label: string;
9
+ contentChecks: boolean;
10
+ }[];
@@ -0,0 +1,20 @@
1
+ import { formatAdminURL } from 'payload/shared';
2
+ import { collectionLabel } from '../collections/labels.js';
3
+ export function serializePageCollections(collections, req) {
4
+ return collections.map((entry)=>{
5
+ const label = collectionLabel(req.payload.config.collections.find((collection)=>collection.slug === entry.slug)?.labels.plural, entry.slug, req);
6
+ return {
7
+ slug: entry.slug,
8
+ label,
9
+ contentChecks: typeof entry.contentOf === 'function',
10
+ ...entry.copyHint ? {
11
+ copyHint: entry.copyHint
12
+ } : {},
13
+ copyUrl: entry.copyUrl ?? formatAdminURL({
14
+ adminRoute: req.payload.config.routes.admin,
15
+ path: `/collections/${entry.slug}`
16
+ }),
17
+ copyLabel: entry.copyLabel ?? `Open ${label}`
18
+ };
19
+ });
20
+ }
@@ -8,3 +8,12 @@ export declare const XP_TESTS_NAV_LINK = "@delmaredigital/payload-experiments/ad
8
8
  export declare const XP_TESTS_NAV_CLIENT = "@delmaredigital/payload-experiments/admin/client#TestsNavLink";
9
9
  export declare const XP_TESTS_SURFACE = "@delmaredigital/payload-experiments/admin/client#TestsSurface";
10
10
  export declare const XP_TESTS_DASHBOARD_CARDS = "@delmaredigital/payload-experiments/admin/client#TestsDashboardCards";
11
+ export declare const XP_PLACEMENT_STATUS = "@delmaredigital/payload-experiments/admin/client#PlacementStatus";
12
+ export declare const XP_TEST_BLOCK = "@delmaredigital/payload-experiments/admin/client#TestBlockField";
13
+ export declare const XP_BLOCK_STATUS = "@delmaredigital/payload-experiments/admin/client#BlockStatusField";
14
+ export declare const XP_BLOCK_ROW_LABEL = "@delmaredigital/payload-experiments/admin/client#BlockSlotRowLabel";
15
+ export declare const XP_COPY_ORIGINAL = "@delmaredigital/payload-experiments/admin/client#CopyOriginalField";
16
+ export declare const XP_GOAL_FIELD = "@delmaredigital/payload-experiments/admin/client#XpGoalField";
17
+ export declare const XP_TESTS_COLLECTION_LIST = "@delmaredigital/payload-experiments/admin/rsc#TestsCollectionListView";
18
+ export declare const XP_GOALS_COLLECTION_LIST = "@delmaredigital/payload-experiments/admin/rsc#GoalsCollectionListView";
19
+ export declare const XP_GOAL_EDIT_REDIRECT = "@delmaredigital/payload-experiments/admin/rsc#GoalEditRedirect";
@@ -8,3 +8,12 @@ export const XP_TESTS_NAV_LINK = '@delmaredigital/payload-experiments/admin/rsc#
8
8
  export const XP_TESTS_NAV_CLIENT = '@delmaredigital/payload-experiments/admin/client#TestsNavLink';
9
9
  export const XP_TESTS_SURFACE = '@delmaredigital/payload-experiments/admin/client#TestsSurface';
10
10
  export const XP_TESTS_DASHBOARD_CARDS = '@delmaredigital/payload-experiments/admin/client#TestsDashboardCards';
11
+ export const XP_PLACEMENT_STATUS = '@delmaredigital/payload-experiments/admin/client#PlacementStatus';
12
+ export const XP_TEST_BLOCK = '@delmaredigital/payload-experiments/admin/client#TestBlockField';
13
+ export const XP_BLOCK_STATUS = '@delmaredigital/payload-experiments/admin/client#BlockStatusField';
14
+ export const XP_BLOCK_ROW_LABEL = '@delmaredigital/payload-experiments/admin/client#BlockSlotRowLabel';
15
+ export const XP_COPY_ORIGINAL = '@delmaredigital/payload-experiments/admin/client#CopyOriginalField';
16
+ export const XP_GOAL_FIELD = '@delmaredigital/payload-experiments/admin/client#XpGoalField';
17
+ export const XP_TESTS_COLLECTION_LIST = '@delmaredigital/payload-experiments/admin/rsc#TestsCollectionListView';
18
+ export const XP_GOALS_COLLECTION_LIST = '@delmaredigital/payload-experiments/admin/rsc#GoalsCollectionListView';
19
+ export const XP_GOAL_EDIT_REDIRECT = '@delmaredigital/payload-experiments/admin/rsc#GoalEditRedirect';
@@ -0,0 +1,2 @@
1
+ import type { PageTestStatus } from '../types/endpoints.js';
2
+ export declare function pageStatusCopy(test: PageTestStatus): string | null;
@@ -0,0 +1,19 @@
1
+ export function pageStatusCopy(test) {
2
+ if (test.pagesReleased) return null;
3
+ const winner = `${test.winnerLabel ?? 'the winner'}${test.winnerTitle ? ` (${test.winnerTitle})` : ''}`;
4
+ switch(test.status){
5
+ case 'draft':
6
+ return `This page is ${test.armLabel} in the draft test ${test.name}.`;
7
+ case 'stopped':
8
+ {
9
+ const visitors = test.role === 'original' ? 'Visitors see this page.' : !test.originalPath ? 'Visitors see the Original.' : `Visitors to ${test.originalPath === '/' ? 'the homepage (/)' : test.originalPath} see the Original.`;
10
+ return `This page is ${test.armLabel} in the stopped test ${test.name}. ${visitors}`;
11
+ }
12
+ case 'running':
13
+ return `This page is the ${test.armLabel} of a running test: ${test.name}.`;
14
+ case 'decided':
15
+ return `This page is ${test.armLabel} of a decided test; ${test.rollout ? `most visitors see ${winner}; a holdout still sees the Original` : `visitors see ${winner}`}.`;
16
+ case 'archived':
17
+ return test.winner ? `This page is also shown at the other versions' addresses because test ${test.name} made it the winner. To free it, open the test and choose "Release pages".` : test.role === 'original' ? `This page's address now shows the winner of test ${test.name}. To free it, open the test and choose "Release pages".` : `This page's address now shows ${winner}. To show this page again, open the test and choose "Release pages".`;
18
+ }
19
+ }
@@ -0,0 +1,11 @@
1
+ import type { DocId } from '../../types/contracts.js';
2
+ import type { PreflightResult } from '../../types/endpoints.js';
3
+ export { placementDetailKey, placementDetail, fallbackPlacementLabel } from './placementLabels.model.js';
4
+ export type { PlacementLabel } from './placementLabels.model.js';
5
+ import type { PlacementLabel } from './placementLabels.model.js';
6
+ export declare function resolvedPlacementLabel(value: unknown, match: {
7
+ id?: DocId | null;
8
+ title?: string;
9
+ editorUrl?: string;
10
+ } | undefined, adminRoute: string): PlacementLabel;
11
+ export declare function preflightPlacementLabel(value: unknown, preflight: PreflightResult, adminRoute: string): PlacementLabel;
@@ -0,0 +1,21 @@
1
+ import { formatAdminURL } from 'payload/shared';
2
+ import { placementDetail } from './placementLabels.model.js';
3
+ export { placementDetailKey, placementDetail, fallbackPlacementLabel } from './placementLabels.model.js';
4
+ export function resolvedPlacementLabel(value, match, adminRoute) {
5
+ const detail = placementDetail(value);
6
+ const id = match?.id ?? detail.id;
7
+ return {
8
+ title: match?.title?.trim() || detail.path || 'This page',
9
+ ...id != null && detail.collection ? {
10
+ href: match?.editorUrl || formatAdminURL({
11
+ adminRoute,
12
+ path: `/collections/${encodeURIComponent(detail.collection)}/${encodeURIComponent(String(id))}`
13
+ })
14
+ } : {}
15
+ };
16
+ }
17
+ export function preflightPlacementLabel(value, preflight, adminRoute) {
18
+ const detail = placementDetail(value);
19
+ const match = detail.collection ? detail.id != null ? preflight.placements.find((item)=>item.collection === detail.collection && String(item.id) === String(detail.id)) : typeof detail.path === 'string' ? preflight.pagePlacements?.find((item)=>item.collection === detail.collection && item.path === detail.path) : undefined : undefined;
20
+ return resolvedPlacementLabel(value, match, adminRoute);
21
+ }
@@ -0,0 +1,13 @@
1
+ import type { DocId } from '../../types/contracts.js';
2
+ export type PlacementDetail = {
3
+ collection?: string;
4
+ id?: DocId;
5
+ path?: string;
6
+ };
7
+ export type PlacementLabel = {
8
+ title: string;
9
+ href?: string;
10
+ };
11
+ export declare const placementDetail: (detail: unknown) => PlacementDetail;
12
+ export declare function placementDetailKey(value: unknown): string;
13
+ export declare const fallbackPlacementLabel: (value: unknown) => PlacementLabel;
@@ -0,0 +1,12 @@
1
+ export const placementDetail = (detail)=>detail && typeof detail === 'object' && !Array.isArray(detail) ? detail : {};
2
+ export function placementDetailKey(value) {
3
+ const detail = placementDetail(value);
4
+ return JSON.stringify([
5
+ detail.collection,
6
+ detail.id == null ? null : String(detail.id),
7
+ detail.path
8
+ ]);
9
+ }
10
+ export const fallbackPlacementLabel = (value)=>({
11
+ title: placementDetail(value).path || 'This page'
12
+ });
@@ -0,0 +1,12 @@
1
+ import type { PreflightResult } from '../../types/endpoints.js';
2
+ import type { PlacementLabel } from './placementLabels.js';
3
+ export { placementDetailKey } from './placementLabels.js';
4
+ /** The view owns a cache, including misses, so repeated warnings never repeat reads. */
5
+ export declare function usePlacementLabels({ details, preflight, apiRoute, adminRoute, canManage, collectionTitles }: {
6
+ details: unknown[];
7
+ preflight?: PreflightResult | null;
8
+ apiRoute: string;
9
+ adminRoute: string;
10
+ canManage: boolean;
11
+ collectionTitles?: Record<string, string>;
12
+ }): Map<string, PlacementLabel>;
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { createApi } from '../api.js';
4
+ import { placementDetail, placementDetailKey, preflightPlacementLabel, resolvedPlacementLabel } from './placementLabels.js';
5
+ export { placementDetailKey } from './placementLabels.js';
6
+ /** The view owns a cache, including misses, so repeated warnings never repeat reads. */ export function usePlacementLabels({ details, preflight, apiRoute, adminRoute, canManage, collectionTitles }) {
7
+ const titlesSignature = JSON.stringify(collectionTitles);
8
+ const cache = useMemo(()=>new Map(), [
9
+ apiRoute,
10
+ adminRoute,
11
+ titlesSignature
12
+ ]);
13
+ const mounted = useRef(true);
14
+ useEffect(()=>{
15
+ mounted.current = true;
16
+ return ()=>{
17
+ mounted.current = false;
18
+ };
19
+ }, []);
20
+ const [, refresh] = useState(0);
21
+ const signature = JSON.stringify(details.map(placementDetail));
22
+ useEffect(()=>{
23
+ if (!canManage || preflight) return;
24
+ const pending = [
25
+ ...new Map(JSON.parse(signature).map((detail)=>[
26
+ placementDetailKey(detail),
27
+ detail
28
+ ])).values()
29
+ ].filter((detail)=>!cache.has(placementDetailKey(detail)));
30
+ const blocks = new Map();
31
+ const jobs = [];
32
+ const save = (value, label)=>{
33
+ cache.set(placementDetailKey(value), label);
34
+ };
35
+ for (const value of pending){
36
+ const detail = placementDetail(value);
37
+ // Reserve immediately: repeated renders do not issue duplicate requests.
38
+ save(value, resolvedPlacementLabel(value, undefined, adminRoute));
39
+ if (!detail.collection) continue;
40
+ if (detail.id != null) {
41
+ blocks.set(detail.collection, [
42
+ ...blocks.get(detail.collection) ?? [],
43
+ value
44
+ ]);
45
+ continue;
46
+ }
47
+ if (!detail.path) continue;
48
+ jobs.push(createApi(apiRoute).pages({
49
+ collection: detail.collection,
50
+ path: detail.path
51
+ }).then((result)=>{
52
+ if (result.ok) save(value, resolvedPlacementLabel(value, result.data?.docs[0], adminRoute));
53
+ }));
54
+ }
55
+ for (const [collection, values] of blocks)jobs.push((async ()=>{
56
+ try {
57
+ const ids = [
58
+ ...new Set(values.map((value)=>String(placementDetail(value).id)))
59
+ ];
60
+ const query = new URLSearchParams({
61
+ 'where[id][in]': ids.join(','),
62
+ depth: '0',
63
+ limit: String(ids.length)
64
+ });
65
+ const response = await fetch(`${apiRoute.replace(/\/$/, '')}/${encodeURIComponent(collection)}?${query}`, {
66
+ credentials: 'include'
67
+ });
68
+ if (!response.ok) return;
69
+ const data = await response.json();
70
+ const titleField = collectionTitles?.[collection];
71
+ for (const value of values){
72
+ const doc = data.docs?.find((doc)=>String(doc.id) === String(placementDetail(value).id));
73
+ if (doc) save(value, resolvedPlacementLabel(value, {
74
+ id: placementDetail(value).id,
75
+ title: titleField && typeof doc[titleField] === 'string' ? doc[titleField] : undefined
76
+ }, adminRoute));
77
+ }
78
+ } catch {}
79
+ })());
80
+ void Promise.all(jobs).then(()=>{
81
+ if (mounted.current && jobs.length) refresh((value)=>value + 1);
82
+ });
83
+ }, [
84
+ signature,
85
+ preflight,
86
+ apiRoute,
87
+ adminRoute,
88
+ canManage,
89
+ collectionTitles,
90
+ cache
91
+ ]);
92
+ return new Map(details.map((detail)=>{
93
+ const label = preflight ? preflightPlacementLabel(detail, preflight, adminRoute) : cache.get(placementDetailKey(detail)) ?? resolvedPlacementLabel(detail, undefined, adminRoute);
94
+ // Results-only readers may not be able to open the collection, so they get the name without a link.
95
+ return [
96
+ placementDetailKey(detail),
97
+ canManage ? label : {
98
+ title: label.title
99
+ }
100
+ ];
101
+ }));
102
+ }
@@ -41,7 +41,10 @@ export function DataQualityPanel({ items, problem = false }) {
41
41
  }) : null,
42
42
  /*#__PURE__*/ _jsx("p", {
43
43
  className: "xp-quality-next",
44
- children: item.nextStep
44
+ children: item.href ? /*#__PURE__*/ _jsx("a", {
45
+ href: item.href,
46
+ children: item.nextStep
47
+ }) : item.nextStep
45
48
  })
46
49
  ]
47
50
  }, item.id)) : /*#__PURE__*/ _jsx("p", {
@@ -2,6 +2,7 @@ import type { DocId, Status } from '../../types/contracts.js';
2
2
  import type { PhaseSummary, ResultsResponse } from '../../types/endpoints.js';
3
3
  import type { Snapshot } from '../../types/snapshot.js';
4
4
  import { type DisplaySnapshot } from './snapshot.js';
5
+ import type { PlacementLabel } from '../placements/placementLabels.model.js';
5
6
  type Metric = Snapshot['metrics'][number];
6
7
  type Arm = Metric['perArm'][number];
7
8
  type Decision = {
@@ -88,6 +89,7 @@ interface RoundLineInput {
88
89
  }
89
90
  export declare function powerLine({ snapshot, phaseKind, status }: RoundLineInput): string | null;
90
91
  export interface ResultsContext {
92
+ placementLabels?: Map<string, PlacementLabel>;
91
93
  status?: string;
92
94
  phase?: PhaseSummary;
93
95
  minExposuresPerArm?: number;
@@ -102,6 +104,7 @@ export interface DataQualityItem {
102
104
  message: string;
103
105
  detail?: string;
104
106
  nextStep: string;
107
+ href?: string;
105
108
  shares?: string[];
106
109
  }
107
110
  export declare function dataQuality(snapshot: DisplaySnapshot, ctx?: ResultsContext): DataQualityItem[];
@@ -1,9 +1,10 @@
1
- import { READINESS_COPY, versionPageUnavailableCopy, versionPageUncheckedCopy } from '../copy/readiness.js';
1
+ import { READINESS_COPY, placementUncheckedCopy, versionPageUnavailableCopy, versionPageUncheckedCopy } from '../copy/readiness.js';
2
2
  import { armLabel, phaseKindLabel, verdictCodeLabel } from '../labels.js';
3
3
  import { RESULTS_COPY, cappedCount, collectingCopy, conflictCopy, decidedTitle, exclusionCopy, ignoredCopy, metricUnavailable, moreDays, roundLabel, shareCopy, shippingTitle, srmP, uncappedTotal } from '../copy/results.js';
4
4
  import { formatCI, formatCount, formatMetric, formatPercent, formatProbability } from './format.js';
5
5
  import { legacyMinimum, legacyRolloutLine, legacyTooFew } from './legacy.js';
6
6
  import { isLegacySnapshot } from './snapshot.js';
7
+ import { placementDetailKey } from '../placements/placementLabels.model.js';
7
8
  export function defaultRound({ phases, decision, status }) {
8
9
  if ((status === 'decided' || status === 'archived') && decision) return decision.phase;
9
10
  return phases.reduce((latest, phase)=>latest === null || phase.phase > latest ? phase.phase : latest, null);
@@ -132,9 +133,12 @@ function smallest(values) {
132
133
  }
133
134
  export function dataQuality(snapshot, ctx = {}) {
134
135
  const items = (snapshot.health ?? []).map((item)=>({
135
- id: `${item.code}:${item.path}`,
136
- message: READINESS_COPY.needsAttention,
137
- nextStep: item.code === 'version-page-unavailable' ? versionPageUnavailableCopy(item.path, ctx.status) : versionPageUncheckedCopy(item.path, ctx.status)
136
+ id: item.code === 'placement-unchecked' ? `${item.code}:${JSON.stringify(item.detail)}` : `${item.code}:${'path' in item ? item.path : item.metricId}`,
137
+ message: item.code === 'placement-unchecked' ? placementUncheckedCopy(ctx.placementLabels?.get(placementDetailKey(item.detail))?.title ?? item.detail.path ?? 'This page') : item.code === 'goal-in-some-versions' ? `${item.name} now appears in some versions but not all, so this result is no longer a fair comparison. Add it to every version, or remove it.` : READINESS_COPY.needsAttention,
138
+ ...item.code === 'placement-unchecked' ? {
139
+ href: ctx.placementLabels?.get(placementDetailKey(item.detail))?.href
140
+ } : {},
141
+ nextStep: item.code === 'placement-unchecked' ? `Open ${ctx.placementLabels?.get(placementDetailKey(item.detail))?.title ?? item.detail.path ?? 'the page'}` : item.code === 'goal-in-some-versions' ? 'Edit the versions' : item.code === 'version-page-unavailable' ? versionPageUnavailableCopy(item.path, ctx.status) : versionPageUncheckedCopy(item.path, ctx.status)
138
142
  }));
139
143
  // Test snapshots store null by design; only rollout snapshots record the shipped winner.
140
144
  if (!isLegacySnapshot(snapshot) && snapshot.phaseKind === 'rollout' && snapshot.winnerArm === null) items.push({