@delmaredigital/payload-experiments 0.3.0-rc.4 → 0.4.0-rc.1

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 (265) hide show
  1. package/README.md +113 -20
  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/actions/ConfirmDialog.d.ts +2 -1
  8. package/dist/admin/actions/ConfirmDialog.js +2 -2
  9. package/dist/admin/actions/DecideDialog.d.ts +2 -1
  10. package/dist/admin/actions/DecideDialog.js +2 -2
  11. package/dist/admin/api.d.ts +11 -2
  12. package/dist/admin/api.js +20 -2
  13. package/dist/admin/blocks/BlockPanel.d.ts +4 -0
  14. package/dist/admin/blocks/BlockPanel.js +153 -0
  15. package/dist/admin/blocks/TestBlockField.d.ts +5 -0
  16. package/dist/admin/blocks/TestBlockField.js +163 -0
  17. package/dist/admin/blocks/formApi.d.ts +43 -0
  18. package/dist/admin/blocks/formApi.js +153 -0
  19. package/dist/admin/blocks/inject.d.ts +7 -0
  20. package/dist/admin/blocks/inject.js +41 -0
  21. package/dist/admin/blocks/model.d.ts +17 -0
  22. package/dist/admin/blocks/model.js +45 -0
  23. package/dist/admin/blocks/statusCache.d.ts +7 -0
  24. package/dist/admin/blocks/statusCache.js +83 -0
  25. package/dist/admin/blocks/useBlockForm.d.ts +17 -0
  26. package/dist/admin/blocks/useBlockForm.js +46 -0
  27. package/dist/admin/copy/actions.d.ts +2 -0
  28. package/dist/admin/copy/actions.js +9 -0
  29. package/dist/admin/copy/blocks.d.ts +27 -0
  30. package/dist/admin/copy/blocks.js +38 -0
  31. package/dist/admin/copy/goals.d.ts +2 -0
  32. package/dist/admin/copy/goals.js +4 -1
  33. package/dist/admin/copy/newTest.d.ts +10 -0
  34. package/dist/admin/copy/newTest.js +10 -0
  35. package/dist/admin/copy/pages.d.ts +26 -0
  36. package/dist/admin/copy/pages.js +26 -0
  37. package/dist/admin/copy/readiness.d.ts +22 -3
  38. package/dist/admin/copy/readiness.js +23 -3
  39. package/dist/admin/copy/results.d.ts +1 -0
  40. package/dist/admin/copy/results.js +1 -0
  41. package/dist/admin/copy/testPage.d.ts +4 -0
  42. package/dist/admin/copy/testPage.js +4 -0
  43. package/dist/admin/goals/XpGoalField.d.ts +2 -0
  44. package/dist/admin/goals/XpGoalField.js +152 -0
  45. package/dist/admin/metricSource.model.d.ts +2 -0
  46. package/dist/admin/metricSource.model.js +4 -0
  47. package/dist/admin/pageCollections.d.ts +10 -0
  48. package/dist/admin/pageCollections.js +20 -0
  49. package/dist/admin/paths.d.ts +6 -0
  50. package/dist/admin/paths.js +6 -0
  51. package/dist/admin/placementStatus.model.d.ts +2 -0
  52. package/dist/admin/placementStatus.model.js +19 -0
  53. package/dist/admin/placements/placementLabels.d.ts +11 -0
  54. package/dist/admin/placements/placementLabels.js +21 -0
  55. package/dist/admin/placements/placementLabels.model.d.ts +13 -0
  56. package/dist/admin/placements/placementLabels.model.js +12 -0
  57. package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
  58. package/dist/admin/placements/usePlacementLabels.js +102 -0
  59. package/dist/admin/results/DataQualityPanel.js +4 -1
  60. package/dist/admin/results/results.model.d.ts +3 -0
  61. package/dist/admin/results/results.model.js +8 -4
  62. package/dist/admin/surface/TestsHome.js +37 -6
  63. package/dist/admin/surface/TestsSurface.js +5 -1
  64. package/dist/admin/surface/contracts.d.ts +6 -5
  65. package/dist/admin/surface/goals/GoalForm.js +67 -34
  66. package/dist/admin/surface/goals/GoalsList.js +3 -3
  67. package/dist/admin/surface/goals/data.js +4 -1
  68. package/dist/admin/surface/goals/goals.model.d.ts +1 -2
  69. package/dist/admin/surface/goals/goals.model.js +1 -3
  70. package/dist/admin/surface/home.model.d.ts +1 -1
  71. package/dist/admin/surface/home.model.js +8 -4
  72. package/dist/admin/surface/newTest/NewTest.js +54 -13
  73. package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
  74. package/dist/admin/surface/newTest/PageWizard.js +209 -0
  75. package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
  76. package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
  77. package/dist/admin/surface/pagePicker/PagePicker.d.ts +10 -0
  78. package/dist/admin/surface/pagePicker/PagePicker.js +172 -0
  79. package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +25 -0
  80. package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
  81. package/dist/admin/surface/readiness/ActionDialogs.js +99 -64
  82. package/dist/admin/surface/readiness/Banner.js +30 -6
  83. package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
  84. package/dist/admin/surface/readiness/readiness.model.js +91 -9
  85. package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
  86. package/dist/admin/surface/results/ReleasePages.js +3 -7
  87. package/dist/admin/surface/results/ResultsBody.js +21 -2
  88. package/dist/admin/surface/results/ResultsHeader.js +2 -5
  89. package/dist/admin/surface/results/useResults.d.ts +10 -1
  90. package/dist/admin/surface/test/AdvancedSection.js +111 -104
  91. package/dist/admin/surface/test/GoalSection.js +90 -7
  92. package/dist/admin/surface/test/LengthSection.js +1 -1
  93. package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
  94. package/dist/admin/surface/test/PageHoldout.js +67 -0
  95. package/dist/admin/surface/test/TestPage.js +2 -1
  96. package/dist/admin/surface/test/VersionsSection.js +12 -5
  97. package/dist/admin/surface/test/fields.d.ts +13 -4
  98. package/dist/admin/surface/test/fields.js +46 -14
  99. package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
  100. package/dist/admin/surface/test/jumpToSection.js +4 -3
  101. package/dist/admin/surface/test/pageParts.d.ts +4 -1
  102. package/dist/admin/surface/test/pageParts.js +79 -2
  103. package/dist/admin/surface/test/testPage.css +8 -1
  104. package/dist/admin/surface/testDocument.model.d.ts +1 -0
  105. package/dist/admin/surface/testDocument.model.js +2 -1
  106. package/dist/admin/surface/useTestDocument.d.ts +11 -2
  107. package/dist/admin/surface/useTestDocument.js +53 -15
  108. package/dist/admin/useApi.d.ts +11 -2
  109. package/dist/admin/useApi.js +3 -5
  110. package/dist/admin/views/ExperimentsRootView.js +58 -16
  111. package/dist/blocks/bindings.d.ts +3 -0
  112. package/dist/blocks/bindings.js +34 -0
  113. package/dist/blocks/createExperimentBlock.d.ts +6 -0
  114. package/dist/blocks/createExperimentBlock.js +77 -0
  115. package/dist/blocks/formState.d.ts +31 -0
  116. package/dist/blocks/formState.js +146 -0
  117. package/dist/blocks/model.d.ts +41 -0
  118. package/dist/blocks/model.js +34 -0
  119. package/dist/client/ExperimentGroup.d.ts +6 -0
  120. package/dist/client/ExperimentGroup.js +9 -0
  121. package/dist/client/ExperimentsProvider.js +20 -4
  122. package/dist/client/clickGoals.d.ts +4 -0
  123. package/dist/client/clickGoals.js +43 -0
  124. package/dist/collections/experiments/config.js +12 -2
  125. package/dist/collections/experiments/deleteGuard.d.ts +2 -1
  126. package/dist/collections/experiments/deleteGuard.js +12 -1
  127. package/dist/collections/experiments/editability.js +4 -2
  128. package/dist/collections/experiments/pickSlug.d.ts +2 -0
  129. package/dist/collections/experiments/pickSlug.js +8 -0
  130. package/dist/collections/labels.d.ts +3 -0
  131. package/dist/collections/labels.js +20 -0
  132. package/dist/collections/metrics.js +12 -2
  133. package/dist/collections/phases.js +1 -0
  134. package/dist/collections/serialize.js +7 -0
  135. package/dist/db/clickGoals.d.ts +9 -0
  136. package/dist/db/clickGoals.js +22 -0
  137. package/dist/db/delete.d.ts +1 -0
  138. package/dist/db/delete.js +1 -0
  139. package/dist/db/locks.d.ts +8 -0
  140. package/dist/db/locks.js +27 -4
  141. package/dist/db/names.d.ts +4 -0
  142. package/dist/db/names.js +4 -0
  143. package/dist/db/pageBindings.d.ts +4 -0
  144. package/dist/db/pageBindings.js +17 -0
  145. package/dist/db/pageBusy.d.ts +2 -0
  146. package/dist/db/pageBusy.js +10 -0
  147. package/dist/db/pageContent.d.ts +10 -0
  148. package/dist/db/pageContent.js +79 -0
  149. package/dist/db/placements.d.ts +33 -0
  150. package/dist/db/placements.js +54 -0
  151. package/dist/db/slugs.d.ts +8 -0
  152. package/dist/db/slugs.js +40 -0
  153. package/dist/db/snapshots.d.ts +6 -0
  154. package/dist/db/snapshots.js +8 -5
  155. package/dist/endpoints/clickKeys.d.ts +5 -0
  156. package/dist/endpoints/clickKeys.js +17 -0
  157. package/dist/endpoints/events.js +4 -1
  158. package/dist/endpoints/index.js +28 -0
  159. package/dist/endpoints/pages.d.ts +5 -0
  160. package/dist/endpoints/pages.js +120 -0
  161. package/dist/endpoints/placementStatus.d.ts +6 -0
  162. package/dist/endpoints/placementStatus.js +238 -0
  163. package/dist/endpoints/readBody.d.ts +2 -0
  164. package/dist/endpoints/readBody.js +24 -0
  165. package/dist/endpoints/results.js +5 -1
  166. package/dist/endpoints/summary.js +11 -4
  167. package/dist/endpoints/wrapBlock.d.ts +6 -0
  168. package/dist/endpoints/wrapBlock.js +76 -0
  169. package/dist/errors.d.ts +3 -3
  170. package/dist/errors.js +4 -4
  171. package/dist/events/rows.d.ts +1 -1
  172. package/dist/events/rows.js +5 -3
  173. package/dist/exports/adminClient.d.ts +5 -0
  174. package/dist/exports/adminClient.js +5 -0
  175. package/dist/exports/blocks.d.ts +2 -0
  176. package/dist/exports/blocks.js +1 -0
  177. package/dist/exports/client.d.ts +1 -0
  178. package/dist/exports/client.js +1 -0
  179. package/dist/exports/index.d.ts +6 -1
  180. package/dist/exports/index.js +3 -0
  181. package/dist/exports/puck.d.ts +3 -1
  182. package/dist/exports/puck.js +2 -0
  183. package/dist/exports/puckEditor.d.ts +3 -0
  184. package/dist/exports/puckEditor.js +2 -0
  185. package/dist/exports/rsc.d.ts +3 -0
  186. package/dist/exports/rsc.js +2 -0
  187. package/dist/fields/xpGoal.d.ts +3 -0
  188. package/dist/fields/xpGoal.js +28 -0
  189. package/dist/globals/state.js +10 -0
  190. package/dist/goals/cache.d.ts +3 -0
  191. package/dist/goals/cache.js +35 -0
  192. package/dist/goals/fairness.d.ts +40 -0
  193. package/dist/goals/fairness.js +200 -0
  194. package/dist/goals/key.d.ts +4 -0
  195. package/dist/goals/key.js +3 -0
  196. package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
  197. package/dist/goals/picker/clickGoalPicker.model.js +45 -0
  198. package/dist/goals/picker/copy.d.ts +16 -0
  199. package/dist/goals/picker/copy.js +15 -0
  200. package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
  201. package/dist/goals/picker/useClickGoalPicker.js +220 -0
  202. package/dist/hooks/pageGuard.d.ts +5 -1
  203. package/dist/hooks/pageGuard.js +157 -95
  204. package/dist/onInit.js +29 -0
  205. package/dist/options.d.ts +2 -0
  206. package/dist/options.js +41 -1
  207. package/dist/plugin.js +41 -28
  208. package/dist/puck/copy.d.ts +1 -0
  209. package/dist/puck/copy.js +1 -0
  210. package/dist/puck/editor/editor.chrome.css +1 -0
  211. package/dist/puck/editor/editor.css +1 -0
  212. package/dist/puck/editor/editor.model.d.ts +4 -2
  213. package/dist/puck/editor/editor.model.js +6 -3
  214. package/dist/puck/editor/integration.d.ts +10 -0
  215. package/dist/puck/editor/integration.js +30 -0
  216. package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
  217. package/dist/puck/editor/xpGoalPuckEditorField.js +134 -0
  218. package/dist/puck/integration.d.ts +6 -0
  219. package/dist/puck/integration.js +21 -0
  220. package/dist/puck/xpGoalField.d.ts +3 -0
  221. package/dist/puck/xpGoalField.js +6 -0
  222. package/dist/results/health.d.ts +4 -0
  223. package/dist/results/health.js +27 -0
  224. package/dist/results/snapshot.js +15 -2
  225. package/dist/sanitizeReadinessDetail.js +41 -0
  226. package/dist/server/pageKindSeoContract.d.ts +27 -0
  227. package/dist/server/pageKindSeoContract.js +80 -0
  228. package/dist/server/recordContentChange.js +58 -8
  229. package/dist/server/resolveExperimentBlocks.d.ts +5 -0
  230. package/dist/server/resolveExperimentBlocks.js +54 -0
  231. package/dist/server/runResults.js +56 -15
  232. package/dist/server/toClientAssignments.js +2 -1
  233. package/dist/transitions/addMetric.d.ts +2 -1
  234. package/dist/transitions/addMetric.js +28 -1
  235. package/dist/transitions/checkStart.d.ts +13 -1
  236. package/dist/transitions/checkStart.js +89 -7
  237. package/dist/transitions/decide.js +24 -0
  238. package/dist/transitions/deleteExperiment.d.ts +3 -1
  239. package/dist/transitions/deleteExperiment.js +27 -1
  240. package/dist/transitions/kernel.js +4 -0
  241. package/dist/transitions/makeContent.d.ts +5 -0
  242. package/dist/transitions/makeContent.js +39 -11
  243. package/dist/transitions/pageCollection.d.ts +10 -0
  244. package/dist/transitions/pageCollection.js +20 -0
  245. package/dist/transitions/pageVersions.d.ts +10 -0
  246. package/dist/transitions/pageVersions.js +44 -0
  247. package/dist/transitions/phaseRecord.d.ts +1 -0
  248. package/dist/transitions/placementInteractions.d.ts +40 -0
  249. package/dist/transitions/placementInteractions.js +94 -0
  250. package/dist/transitions/placements.d.ts +8 -0
  251. package/dist/transitions/placements.js +148 -1
  252. package/dist/transitions/preflightData.d.ts +2 -2
  253. package/dist/transitions/preflightData.js +137 -5
  254. package/dist/transitions/resolveCaps.d.ts +1 -0
  255. package/dist/transitions/resolveCaps.js +6 -2
  256. package/dist/transitions/start.js +45 -1
  257. package/dist/types/admin.d.ts +1 -1
  258. package/dist/types/assignments.d.ts +1 -0
  259. package/dist/types/contracts.d.ts +15 -0
  260. package/dist/types/endpoints.d.ts +92 -5
  261. package/dist/types/index.d.ts +3 -0
  262. package/dist/types/puck.d.ts +7 -1
  263. package/dist/types/snapshot.d.ts +17 -4
  264. package/dist/types/surface.d.ts +13 -0
  265. package/package.json +8 -1
@@ -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') {
@@ -6,10 +6,13 @@ import { useRouter } from 'next/navigation.js';
6
6
  import { formatAdminURL } from 'payload/shared';
7
7
  import { NEW_TEST_COPY as C } from '../../copy/newTest.js';
8
8
  import { CreateRefused, createTest } from '../../../puck/editor/createTest.js';
9
+ import { PageWizard } from './PageWizard.js';
9
10
  import './newTest.css';
10
11
  export function NewTest({ surface, pages }) {
11
12
  const router = useRouter();
12
13
  const [step, setStep] = useState('kind');
14
+ const [kind, setKind] = useState('flag');
15
+ const [created, setCreated] = useState();
13
16
  const [pointId, setPointId] = useState('');
14
17
  const point = surface.flagPoints.find((item)=>item.id === pointId);
15
18
  const [unitType, setUnitType] = useState('visitor');
@@ -50,23 +53,32 @@ export function NewTest({ surface, pages }) {
50
53
  layerRetry
51
54
  ]);
52
55
  async function submit() {
53
- if (inFlight.current || !point || !name.trim()) return;
56
+ if (inFlight.current || kind === 'flag' && !point || !name.trim()) return;
54
57
  inFlight.current = true;
55
58
  setPending(true);
56
59
  setError('');
57
60
  try {
58
61
  const { id } = await createTest(surface.apiRoute, {
59
- kind: 'flag',
60
- unitType,
62
+ kind,
63
+ unitType: kind === 'page' ? 'visitor' : unitType,
61
64
  name,
62
- flagPoint: point.id,
63
- ...layer === undefined ? {} : {
65
+ ...kind === 'flag' ? {
66
+ flagPoint: point.id
67
+ } : surface.pageCollections?.length === 1 ? {
68
+ pageCollection: surface.pageCollections[0].slug
69
+ } : {},
70
+ ...kind !== 'flag' || layer === undefined ? {} : {
64
71
  layer
65
72
  },
66
73
  ...hypothesis.trim() ? {
67
74
  hypothesis
68
75
  } : {}
69
76
  });
77
+ if (kind === 'page') {
78
+ setCreated(id);
79
+ setPending(false);
80
+ return;
81
+ }
70
82
  router.push(formatAdminURL({
71
83
  adminRoute: surface.adminRoute,
72
84
  path: `/experiments/${encodeURIComponent(String(id))}`
@@ -77,6 +89,10 @@ export function NewTest({ surface, pages }) {
77
89
  setPending(false);
78
90
  }
79
91
  }
92
+ if (created !== undefined) return /*#__PURE__*/ _jsx(PageWizard, {
93
+ id: created,
94
+ surface: surface
95
+ });
80
96
  const collections = [
81
97
  ...new Set(pages?.map((page)=>page.collection))
82
98
  ];
@@ -102,15 +118,37 @@ export function NewTest({ surface, pages }) {
102
118
  /*#__PURE__*/ _jsx("span", {
103
119
  children: C.blockMeaning
104
120
  }),
105
- surface.descriptions?.block ? /*#__PURE__*/ _jsx("span", {
106
- children: surface.descriptions.block
121
+ surface.admin?.descriptions?.block ? /*#__PURE__*/ _jsx("span", {
122
+ children: surface.admin?.descriptions?.block
123
+ }) : null
124
+ ]
125
+ }) : null,
126
+ surface.pageCollections?.length ? /*#__PURE__*/ _jsxs("button", {
127
+ type: "button",
128
+ className: "xp-new-test-card",
129
+ onClick: ()=>{
130
+ setKind('page');
131
+ setStep('name');
132
+ },
133
+ children: [
134
+ /*#__PURE__*/ _jsx("strong", {
135
+ children: C.page
136
+ }),
137
+ /*#__PURE__*/ _jsx("span", {
138
+ children: C.pageMeaning
139
+ }),
140
+ surface.admin?.descriptions?.page ? /*#__PURE__*/ _jsx("span", {
141
+ children: surface.admin?.descriptions?.page
107
142
  }) : null
108
143
  ]
109
144
  }) : null,
110
145
  surface.flagPoints.length > 0 ? /*#__PURE__*/ _jsxs("button", {
111
146
  type: "button",
112
147
  className: "xp-new-test-card",
113
- onClick: ()=>setStep('setting'),
148
+ onClick: ()=>{
149
+ setKind('flag');
150
+ setStep('setting');
151
+ },
114
152
  children: [
115
153
  /*#__PURE__*/ _jsx("strong", {
116
154
  children: C.setting
@@ -118,14 +156,14 @@ export function NewTest({ surface, pages }) {
118
156
  /*#__PURE__*/ _jsx("span", {
119
157
  children: C.settingMeaning
120
158
  }),
121
- surface.descriptions?.setting ? /*#__PURE__*/ _jsx("span", {
122
- children: surface.descriptions.setting
159
+ surface.admin?.descriptions?.setting ? /*#__PURE__*/ _jsx("span", {
160
+ children: surface.admin?.descriptions.setting
123
161
  }) : null
124
162
  ]
125
163
  }) : null
126
164
  ]
127
165
  }),
128
- pages === undefined && !surface.flagPoints.length ? /*#__PURE__*/ _jsx("p", {
166
+ pages === undefined && !surface.flagPoints.length && !surface.pageCollections?.length ? /*#__PURE__*/ _jsx("p", {
129
167
  children: C.unavailable
130
168
  }) : null
131
169
  ]
@@ -156,7 +194,10 @@ export function NewTest({ surface, pages }) {
156
194
  /*#__PURE__*/ _jsx("a", {
157
195
  href: page.editorUrl,
158
196
  children: C.open
159
- })
197
+ }),
198
+ page.editor === 'blocks' ? /*#__PURE__*/ _jsx("p", {
199
+ children: C.blocksHandoff
200
+ }) : null
160
201
  ]
161
202
  }, `${typeof page.id}:${page.id}`))
162
203
  }),
@@ -337,7 +378,7 @@ export function NewTest({ surface, pages }) {
337
378
  }));
338
379
  else {
339
380
  setError('');
340
- setStep(step === 'name' ? 'setting' : 'kind');
381
+ setStep(step === 'name' && kind === 'flag' ? 'setting' : 'kind');
341
382
  }
342
383
  },
343
384
  children: C.back
@@ -0,0 +1,7 @@
1
+ import type { DocId } from '../../../types/contracts.js';
2
+ import type { SurfaceProps } from '../contracts.js';
3
+ /** One session stays mounted across Collection, all page steps and Review. */
4
+ export declare function PageWizard({ id, surface }: {
5
+ id: DocId;
6
+ surface: SurfaceProps;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,209 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { useRouter } from 'next/navigation.js';
5
+ import { formatAdminURL } from 'payload/shared';
6
+ import { useTestDocument } from '../useTestDocument.js';
7
+ import { CopyRoute } from '../pagePicker/CopyRoute.js';
8
+ import { PagePicker } from '../pagePicker/PagePicker.js';
9
+ import { testPageCollection, controlFirst } from '../pagePicker/pagePicker.model.js';
10
+ import { NEW_TEST_COPY as C } from '../../copy/newTest.js';
11
+ import { READINESS_COPY } from '../../copy/readiness.js';
12
+ import { TEST_PAGE } from '../../copy/testPage.js';
13
+ import { SURFACE_COPY } from '../../copy/surface.js';
14
+ import { pageStepCopy } from '../../copy/pages.js';
15
+ import { READINESS, nextStep, pageReadinessContext, readinessArm } from '../readiness/readiness.model.js';
16
+ import { SaveStatus } from '../readiness/SaveStatus.js';
17
+ import { createApi } from '../../api.js';
18
+ import { useTransition } from '../readiness/useTransition.js';
19
+ /** One session stays mounted across Collection, all page steps and Review. */ export function PageWizard({ id, surface }) {
20
+ const router = useRouter();
21
+ const collections = surface.pageCollections ?? [];
22
+ const [step, setStep] = useState(collections.length > 1 ? 'collection' : 'control');
23
+ const test = useTestDocument(id, {
24
+ apiRoute: surface.apiRoute,
25
+ refetchPreflightOnFocus: step === 'review'
26
+ });
27
+ const testUrl = formatAdminURL({
28
+ adminRoute: surface.adminRoute,
29
+ path: `/experiments/${encodeURIComponent(String(id))}`
30
+ });
31
+ const transition = useTransition(test, ()=>router.push(testUrl));
32
+ useEffect(()=>{
33
+ if (step === 'review') void test.refreshPreflight();
34
+ }, [
35
+ step,
36
+ test.refreshPreflight
37
+ ]);
38
+ const leave = ()=>router.push(testUrl);
39
+ if (test.loadState === 'loading') return /*#__PURE__*/ _jsx("p", {
40
+ role: "status",
41
+ children: TEST_PAGE.loading
42
+ });
43
+ if (!test.doc || test.loadState !== 'ready') return /*#__PURE__*/ _jsxs("div", {
44
+ children: [
45
+ /*#__PURE__*/ _jsx("p", {
46
+ role: "alert",
47
+ children: test.loadState === 'forbidden' ? TEST_PAGE.forbidden : test.loadState === 'missing' ? TEST_PAGE.missing : TEST_PAGE.error
48
+ }),
49
+ /*#__PURE__*/ _jsx("button", {
50
+ type: "button",
51
+ onClick: ()=>void test.reload(),
52
+ children: TEST_PAGE.retry
53
+ }),
54
+ /*#__PURE__*/ _jsx("button", {
55
+ type: "button",
56
+ onClick: leave,
57
+ children: C.backToTest
58
+ })
59
+ ]
60
+ });
61
+ const doc = test.doc;
62
+ const collection = testPageCollection(collections, doc, test.preflight?.pagePlacements);
63
+ const arms = controlFirst(doc.arms).filter((arm)=>arm.kind !== 'holdout');
64
+ const steps = [
65
+ ...collections.length > 1 ? [
66
+ 'collection'
67
+ ] : [],
68
+ ...arms.map((arm)=>arm.key),
69
+ 'review'
70
+ ];
71
+ const arm = arms.find((item)=>item.key === step);
72
+ const ctx = pageReadinessContext(doc, test.preflight, surface);
73
+ const next = nextStep(test.preflight, ctx);
74
+ const ready = Boolean(test.preflight && !test.preflightPending && !test.preflightError && test.preflight.checks.every((check)=>check.ok));
75
+ const busy = test.hasUnsaved || test.saving === 'saving' || transition.busy;
76
+ const jump = (section, key)=>{
77
+ if (section === 'versions') setStep(key ?? arms.find((item)=>!item.pageTarget)?.key ?? 'control');
78
+ else router.push(`${testUrl}#section-${section}`);
79
+ };
80
+ const picker = arm && collection ? /*#__PURE__*/ _jsx(PagePicker, {
81
+ collection: collection,
82
+ test: test,
83
+ armKey: arm.key,
84
+ apiRoute: surface.apiRoute
85
+ }, `${collection.slug}/${arm.key}`) : null;
86
+ return /*#__PURE__*/ _jsxs("div", {
87
+ className: "xp-new-test xp-page-wizard",
88
+ children: [
89
+ /*#__PURE__*/ _jsx("h2", {
90
+ children: step === 'collection' ? C.collection : step === 'review' ? C.review : arm?.kind === 'control' ? C.original : pageStepCopy(arm?.label ?? '')
91
+ }),
92
+ /*#__PURE__*/ _jsx(SaveStatus, {
93
+ test: test
94
+ }),
95
+ test.errors._section ? /*#__PURE__*/ _jsx("p", {
96
+ role: "alert",
97
+ children: test.errors._section
98
+ }) : null,
99
+ step === 'collection' ? /*#__PURE__*/ _jsxs("fieldset", {
100
+ children: [
101
+ /*#__PURE__*/ _jsx("legend", {
102
+ children: C.collection
103
+ }),
104
+ collections.map((entry)=>/*#__PURE__*/ _jsxs("label", {
105
+ children: [
106
+ /*#__PURE__*/ _jsx("input", {
107
+ type: "radio",
108
+ name: "page-collection",
109
+ checked: doc.pageCollection === entry.slug,
110
+ disabled: busy,
111
+ onChange: ()=>{
112
+ test.stage({
113
+ pageCollection: entry.slug
114
+ });
115
+ void test.commit();
116
+ }
117
+ }),
118
+ entry.label
119
+ ]
120
+ }, entry.slug))
121
+ ]
122
+ }) : step === 'review' ? /*#__PURE__*/ _jsxs(_Fragment, {
123
+ children: [
124
+ test.preflightError ? /*#__PURE__*/ _jsx("p", {
125
+ role: "alert",
126
+ children: test.preflightError
127
+ }) : null,
128
+ /*#__PURE__*/ _jsx("ul", {
129
+ children: test.preflight?.checks.map((check, index)=>{
130
+ if (!Object.hasOwn(READINESS, check.code)) return check.ok ? null : /*#__PURE__*/ _jsx("li", {
131
+ children: SURFACE_COPY.next
132
+ }, `${check.code}/${index}`);
133
+ const row = READINESS[check.code];
134
+ const sentence = row.next(check.detail, ctx);
135
+ const label = typeof row.nextStep === 'function' ? row.nextStep(check.detail, ctx) : row.nextStep;
136
+ return sentence && !check.ok ? /*#__PURE__*/ _jsxs("li", {
137
+ children: [
138
+ sentence,
139
+ label ? /*#__PURE__*/ _jsx("button", {
140
+ type: "button",
141
+ onClick: ()=>jump(row.section, readinessArm(check.code, check.detail, ctx)),
142
+ children: label
143
+ }) : null
144
+ ]
145
+ }, `${check.code}/${index}`) : null;
146
+ })
147
+ }),
148
+ ready ? /*#__PURE__*/ _jsx("p", {
149
+ children: READINESS_COPY.ready
150
+ }) : null,
151
+ /*#__PURE__*/ _jsx("button", {
152
+ type: "button",
153
+ disabled: busy || test.preflightPending || !test.preflight,
154
+ onClick: ()=>{
155
+ if (ready) void transition.run(()=>createApi(surface.apiRoute).start(id));
156
+ else if (next) jump(next.section, next.arm);
157
+ },
158
+ children: ready ? C.start : C.finishSetup
159
+ }),
160
+ transition.error ? /*#__PURE__*/ _jsx("p", {
161
+ role: "alert",
162
+ children: transition.error
163
+ }) : null
164
+ ]
165
+ }) : arm?.kind === 'control' ? /*#__PURE__*/ _jsxs(_Fragment, {
166
+ children: [
167
+ /*#__PURE__*/ _jsx("p", {
168
+ children: C.originalHelp
169
+ }),
170
+ picker
171
+ ]
172
+ }) : collection ? /*#__PURE__*/ _jsx(CopyRoute, {
173
+ collection: collection,
174
+ children: picker
175
+ }) : null,
176
+ /*#__PURE__*/ _jsxs("div", {
177
+ className: "xp-new-test-actions",
178
+ children: [
179
+ /*#__PURE__*/ _jsx("button", {
180
+ type: "button",
181
+ disabled: busy,
182
+ onClick: ()=>{
183
+ const previous = steps[steps.indexOf(step) - 1];
184
+ if (previous) setStep(previous);
185
+ else leave();
186
+ },
187
+ children: C.back
188
+ }),
189
+ /*#__PURE__*/ _jsx("button", {
190
+ type: "button",
191
+ disabled: busy,
192
+ onClick: leave,
193
+ children: C.backToTest
194
+ }),
195
+ step !== 'review' ? /*#__PURE__*/ _jsx("button", {
196
+ type: "button",
197
+ disabled: busy || step === 'collection' && !collection,
198
+ onClick: ()=>setStep(steps[steps.indexOf(step) + 1] ?? 'review'),
199
+ children: C.next
200
+ }) : null
201
+ ]
202
+ }),
203
+ test.errors.pageCollection ? /*#__PURE__*/ _jsx("p", {
204
+ role: "alert",
205
+ children: test.errors.pageCollection
206
+ }) : null
207
+ ]
208
+ });
209
+ }
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { PageCollection } from './pagePicker.model.js';
3
+ export declare function CopyRoute({ collection, children }: {
4
+ collection: PageCollection;
5
+ children: ReactNode;
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { PAGE_COPY as C, openCollectionCopy, openOriginalCopy } from '../../copy/pages.js';
4
+ export function CopyRoute({ collection, children }) {
5
+ const link = /*#__PURE__*/ _jsx("a", {
6
+ href: collection.copyUrl,
7
+ target: "_blank",
8
+ rel: "noopener",
9
+ children: collection.copyLabel ?? openCollectionCopy(collection.label)
10
+ });
11
+ return /*#__PURE__*/ _jsxs("div", {
12
+ className: "xp-copy-route",
13
+ children: [
14
+ /*#__PURE__*/ _jsx("div", {
15
+ className: "xp-copy-route-action"
16
+ }),
17
+ collection.copyHint ? /*#__PURE__*/ _jsxs(_Fragment, {
18
+ children: [
19
+ /*#__PURE__*/ _jsxs("p", {
20
+ children: [
21
+ collection.copyHint,
22
+ ' ',
23
+ link
24
+ ]
25
+ }),
26
+ /*#__PURE__*/ _jsx("p", {
27
+ children: C.ownAddress
28
+ })
29
+ ]
30
+ }) : /*#__PURE__*/ _jsxs("ol", {
31
+ children: [
32
+ /*#__PURE__*/ _jsxs("li", {
33
+ children: [
34
+ openOriginalCopy(collection.label),
35
+ ' ',
36
+ link
37
+ ]
38
+ }),
39
+ /*#__PURE__*/ _jsx("li", {
40
+ children: C.duplicateStep
41
+ }),
42
+ /*#__PURE__*/ _jsx("li", {
43
+ children: C.ownAddress
44
+ })
45
+ ]
46
+ }),
47
+ /*#__PURE__*/ _jsx("p", {
48
+ children: C.thenChoose
49
+ }),
50
+ children
51
+ ]
52
+ });
53
+ }
@@ -0,0 +1,10 @@
1
+ import type { TestDocApi } from '../contracts.js';
2
+ import type { PageCollection } from './pagePicker.model.js';
3
+ /** Mount on open; every reopen and return from another tab refreshes the current query. */
4
+ export declare function PagePicker({ collection, test, armKey, apiRoute, onChoose }: {
5
+ collection: PageCollection;
6
+ test: TestDocApi;
7
+ armKey: string;
8
+ apiRoute: string;
9
+ onChoose?: () => void;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,172 @@
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 } from '../../copy/pages.js';
6
+ import { reasonFor } from '../test/fields.js';
7
+ import { disabledPageReason, isLatestPageRequest } from './pagePicker.model.js';
8
+ /** Mount on open; every reopen and return from another tab refreshes the current query. */ export function PagePicker({ collection, test, armKey, apiRoute, onChoose }) {
9
+ const api = useMemo(()=>createApi(apiRoute), [
10
+ apiRoute
11
+ ]);
12
+ const id = useId();
13
+ const [search, setSearch] = useState('');
14
+ const [q, setQuery] = useState('');
15
+ const [page, setPage] = useState(1);
16
+ const [refresh, setRefresh] = useState(0);
17
+ const [rows, setRows] = useState([]);
18
+ const [more, setMore] = useState(false);
19
+ const [loading, setLoading] = useState(true);
20
+ const [error, setError] = useState('');
21
+ const revision = useRef(0);
22
+ const reload = ()=>{
23
+ setPage(1);
24
+ setRefresh((value)=>value + 1);
25
+ };
26
+ useEffect(()=>{
27
+ if (search === q) return;
28
+ const timer = setTimeout(()=>{
29
+ setQuery(search);
30
+ setPage(1);
31
+ setRows([]);
32
+ setMore(false);
33
+ setLoading(true);
34
+ setError('');
35
+ }, 250);
36
+ return ()=>clearTimeout(timer);
37
+ }, [
38
+ search,
39
+ q
40
+ ]);
41
+ useEffect(()=>{
42
+ let lastReturn = -Infinity;
43
+ const focus = ()=>{
44
+ if (document.visibilityState !== 'visible' || Date.now() - lastReturn < 100) return;
45
+ lastReturn = Date.now();
46
+ setPage(1);
47
+ setRefresh((value)=>value + 1);
48
+ };
49
+ const visible = ()=>{
50
+ if (document.visibilityState === 'visible') focus();
51
+ };
52
+ window.addEventListener('focus', focus);
53
+ document.addEventListener('visibilitychange', visible);
54
+ return ()=>{
55
+ window.removeEventListener('focus', focus);
56
+ document.removeEventListener('visibilitychange', visible);
57
+ };
58
+ }, []);
59
+ useEffect(()=>{
60
+ const controller = new AbortController();
61
+ const request = ++revision.current;
62
+ setLoading(true);
63
+ setError('');
64
+ void api.pages({
65
+ collection: collection.slug,
66
+ q,
67
+ page
68
+ }, controller.signal).then((result)=>{
69
+ if (controller.signal.aborted || !isLatestPageRequest(request, revision.current)) return;
70
+ if (result.ok && result.data) {
71
+ const docs = result.data.docs;
72
+ setRows((current)=>{
73
+ const combined = page === 1 ? docs : [
74
+ ...current,
75
+ ...docs
76
+ ];
77
+ return combined.filter((row, index)=>combined.findIndex((item)=>item.id === row.id) === index);
78
+ });
79
+ setMore(result.data.hasNextPage);
80
+ } else {
81
+ setMore(false);
82
+ setError(C.error);
83
+ }
84
+ setLoading(false);
85
+ });
86
+ return ()=>controller.abort();
87
+ }, [
88
+ api,
89
+ collection.slug,
90
+ q,
91
+ page,
92
+ refresh
93
+ ]);
94
+ const reason = reasonFor(test, 'arms');
95
+ return /*#__PURE__*/ _jsxs("div", {
96
+ className: "xp-page-picker",
97
+ children: [
98
+ /*#__PURE__*/ _jsx("label", {
99
+ htmlFor: id,
100
+ children: C.search
101
+ }),
102
+ /*#__PURE__*/ _jsx("input", {
103
+ id: id,
104
+ type: "search",
105
+ value: search,
106
+ onChange: (event)=>setSearch(event.currentTarget.value)
107
+ }),
108
+ loading && !rows.length ? /*#__PURE__*/ _jsx("p", {
109
+ role: "status",
110
+ children: C.loading
111
+ }) : null,
112
+ error ? /*#__PURE__*/ _jsxs("p", {
113
+ role: "alert",
114
+ children: [
115
+ error,
116
+ " ",
117
+ /*#__PURE__*/ _jsx("button", {
118
+ type: "button",
119
+ onClick: reload,
120
+ children: C.retry
121
+ })
122
+ ]
123
+ }) : null,
124
+ !rows.length ? !loading && !error ? /*#__PURE__*/ _jsx("p", {
125
+ children: C.empty
126
+ }) : null : /*#__PURE__*/ _jsx("ul", {
127
+ children: rows.map((row)=>{
128
+ const disabled = reason ?? disabledPageReason(row, armKey, test.doc?.arms ?? [], test.preflight?.pagePlacements, test.chosenPages, collection.label, collection.slug);
129
+ return /*#__PURE__*/ _jsxs("li", {
130
+ children: [
131
+ /*#__PURE__*/ _jsxs("button", {
132
+ type: "button",
133
+ disabled: !!disabled || test.saving === 'saving',
134
+ onClick: ()=>{
135
+ const arm = test.doc?.arms.find((item)=>item.key === armKey);
136
+ if (!arm || row.path === null || disabled) return;
137
+ void test.choosePage(arm.key, {
138
+ id: row.id,
139
+ path: row.path
140
+ });
141
+ onChoose?.();
142
+ },
143
+ children: [
144
+ /*#__PURE__*/ _jsx("strong", {
145
+ children: row.title
146
+ }),
147
+ ' ',
148
+ row.path,
149
+ !row.published ? /*#__PURE__*/ _jsxs(_Fragment, {
150
+ children: [
151
+ " · ",
152
+ C.draft
153
+ ]
154
+ }) : null
155
+ ]
156
+ }),
157
+ disabled ? /*#__PURE__*/ _jsx("p", {
158
+ children: disabled
159
+ }) : null
160
+ ]
161
+ }, row.id);
162
+ })
163
+ }),
164
+ more ? /*#__PURE__*/ _jsx("button", {
165
+ type: "button",
166
+ disabled: loading,
167
+ onClick: ()=>setPage((value)=>value + 1),
168
+ children: C.more
169
+ }) : null
170
+ ]
171
+ });
172
+ }
@@ -0,0 +1,25 @@
1
+ import type { Arm, DocId } from '../../../types/contracts.js';
2
+ import type { PagesResult, PreflightResult } from '../../../types/endpoints.js';
3
+ import type { TestsSurfaceProps } from '../../../types/surface.js';
4
+ export type PageRow = PagesResult['docs'][number];
5
+ export type PageCollection = NonNullable<TestsSurfaceProps['pageCollections']>[number];
6
+ export type PageSelections = Partial<Record<Arm['key'], {
7
+ id: DocId;
8
+ path: string;
9
+ }>>;
10
+ /** The server resolves legacy collections; the client consumes its placements verbatim. */
11
+ export declare function testPageCollection(collections: readonly PageCollection[] | undefined, doc: {
12
+ pageCollection?: string | null;
13
+ }, placements: PreflightResult['pagePlacements']): {
14
+ slug: string;
15
+ label: string;
16
+ contentChecks: boolean;
17
+ copyHint?: string;
18
+ copyUrl?: string;
19
+ copyLabel?: string;
20
+ } | undefined;
21
+ export declare const controlFirst: (arms: readonly Arm[]) => Arm[];
22
+ /** Identity wins over address, so distinct documents sharing an address get actionable copy. */
23
+ export declare function disabledPageReason(row: PageRow, ownKey: string, arms: readonly Arm[], placements: PreflightResult['pagePlacements'], chosen: PageSelections, collection: string, collectionSlug?: string): string | null;
24
+ /** Pure request revision comparison, also used after an aborted transport resolves. */
25
+ export declare const isLatestPageRequest: (response: number, latest: number) => boolean;