@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
@@ -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
3
  import { useEffect, useId, useState } from 'react';
4
- import { TextInput, NumberInput, Textarea, Select, SearchInput, CheckboxList as UICheckboxList } from '../../../ui/Inputs.js';
4
+ import { TextInput, NumberInput, Textarea, Select, SearchInput, Checkbox, CheckboxList as UICheckboxList } from '../../../ui/Inputs.js';
5
5
  import { TEST_PAGE as C } from '../../copy/testPage.js';
6
6
  import { EDITABLE_BY_STATUS } from '../../../collections/experiments/editability.js';
7
7
  import { lockedReason } from '../../lockedNotice.model.js';
@@ -45,7 +45,7 @@ export function ReadOnly({ label, value, reason, labelAction }) {
45
45
  ]
46
46
  });
47
47
  }
48
- /** Local typing is intentionally separate from staging: build the current group on commit. */ export function TextEdit({ label, value, onCommit, reason, multiline, placeholder, type = 'text', min, max, required, onDraft, validate, invalidMessage, hideLabel = false, disabled = false, help }) {
48
+ /** Local typing is intentionally separate from staging: build the current group on commit. */ export function TextEdit({ label, value, onCommit, reason, multiline, placeholder, type = 'text', min, max, required, onDraft, isValid, invalidMessage, hideLabel = false, disabled = false, help }) {
49
49
  const id = useId();
50
50
  const [draft, setDraft] = useState(String(value));
51
51
  const [error, setError] = useState(null);
@@ -54,7 +54,8 @@ export function ReadOnly({ label, value, reason, labelAction }) {
54
54
  ]);
55
55
  const commit = ()=>{
56
56
  if (draft === String(value)) return;
57
- if (validate && !validate(draft)) {
57
+ // Named isValid, not validate: Payload 4's Local API has a validate method, which the access gate flags.
58
+ if (isValid && !isValid(draft)) {
58
59
  setError(draft.trim() ? invalidMessage ?? null : null);
59
60
  setDraft(String(value));
60
61
  onDraft?.(String(value));
@@ -96,7 +97,7 @@ export function ReadOnly({ label, value, reason, labelAction }) {
96
97
  });
97
98
  }
98
99
  export const positiveInteger = (raw)=>raw.trim() !== '' && Number.isInteger(Number(raw)) && Number(raw) > 0;
99
- export function SelectEdit({ label, value, options, reason, onCommit, readOnlyValue, labelAction }) {
100
+ export function SelectEdit({ label, value, options, groups, reason, onCommit, readOnlyValue, labelAction }) {
100
101
  const id = useId();
101
102
  if (reason) {
102
103
  return /*#__PURE__*/ _jsx(ReadOnly, {
@@ -112,13 +113,22 @@ export function SelectEdit({ label, value, options, reason, onCommit, readOnlyVa
112
113
  labelAction: labelAction,
113
114
  value: value,
114
115
  onChange: (e)=>onCommit(e.target.value),
115
- children: options.map((option)=>/*#__PURE__*/ _jsx("option", {
116
+ children: groups ? groups.map((group, index)=>group.label ? /*#__PURE__*/ _jsx("optgroup", {
117
+ label: group.label,
118
+ children: group.options.map((option)=>/*#__PURE__*/ _jsx("option", {
119
+ value: option.value,
120
+ children: option.label
121
+ }, option.value))
122
+ }, index) : group.options.map((option)=>/*#__PURE__*/ _jsx("option", {
123
+ value: option.value,
124
+ children: option.label
125
+ }, option.value))) : options.map((option)=>/*#__PURE__*/ _jsx("option", {
116
126
  value: option.value,
117
127
  children: option.label
118
128
  }, option.value))
119
129
  });
120
130
  }
121
- export function CheckboxList({ label, value, options, reason, onToggle, readOnlyValue, searchable = false }) {
131
+ export function CheckboxList({ label, value, options, groups, reason, onToggle, readOnlyValue, searchable = false }) {
122
132
  const id = useId();
123
133
  const [query, setQuery] = useState('');
124
134
  const visible = searchable ? options.filter((option)=>value.includes(option.value) || option.label.toLowerCase().includes(query.toLowerCase())) : options;
@@ -127,21 +137,43 @@ export function CheckboxList({ label, value, options, reason, onToggle, readOnly
127
137
  reason: reason,
128
138
  value: readOnlyValue ?? value.map((v)=>options.find((o)=>o.value === v)?.label ?? v).join(', ')
129
139
  });
130
- return /*#__PURE__*/ _jsx(UICheckboxList, {
140
+ return /*#__PURE__*/ _jsxs(UICheckboxList, {
131
141
  id: id,
132
142
  label: label,
133
143
  value: value,
134
- options: visible,
144
+ options: groups ? [] : visible,
135
145
  onChange: (next)=>{
136
146
  const changed = options.find((option)=>next.includes(option.value) !== value.includes(option.value));
137
147
  if (changed) onToggle(changed.value, next.includes(changed.value));
138
148
  },
139
- children: searchable ? /*#__PURE__*/ _jsx(SearchInput, {
140
- id: `${id}-search`,
141
- label: C.searchGoals,
142
- value: query,
143
- onChange: (event)=>setQuery(event.target.value)
144
- }) : null
149
+ children: [
150
+ searchable ? /*#__PURE__*/ _jsx(SearchInput, {
151
+ id: `${id}-search`,
152
+ label: C.searchGoals,
153
+ value: query,
154
+ onChange: (event)=>setQuery(event.target.value)
155
+ }) : null,
156
+ groups ? groups.map((group, index)=>{
157
+ const items = group.options.filter((option)=>visible.some((item)=>item.value === option.value));
158
+ return items.length ? /*#__PURE__*/ _jsxs("div", {
159
+ children: [
160
+ group.label ? /*#__PURE__*/ _jsx("h3", {
161
+ children: group.label
162
+ }) : null,
163
+ /*#__PURE__*/ _jsx("ul", {
164
+ children: items.map((option)=>/*#__PURE__*/ _jsx("li", {
165
+ children: /*#__PURE__*/ _jsx(Checkbox, {
166
+ id: `${id}-${index}-${option.value}`,
167
+ label: option.label,
168
+ checked: value.includes(option.value),
169
+ onChange: (event)=>onToggle(option.value, event.target.checked)
170
+ })
171
+ }, option.value))
172
+ })
173
+ ]
174
+ }, index) : null;
175
+ }) : null
176
+ ]
145
177
  });
146
178
  }
147
179
  export function TestName({ test }) {
@@ -1,3 +1,3 @@
1
1
  import type { SectionId } from '../contracts.js';
2
2
  /** Open disclosures before scrolling, then give keyboard users a meaningful landing. */
3
- export declare function jumpToSection(section: SectionId): void;
3
+ export declare function jumpToSection(section: SectionId, arm?: string): void;
@@ -1,10 +1,11 @@
1
- /** Open disclosures before scrolling, then give keyboard users a meaningful landing. */ export function jumpToSection(section) {
1
+ /** Open disclosures before scrolling, then give keyboard users a meaningful landing. */ export function jumpToSection(section, arm) {
2
2
  const target = document.getElementById(`section-${section}`);
3
3
  if (target instanceof HTMLDetailsElement) target.open = true;
4
- target?.scrollIntoView?.({
4
+ const row = arm ? document.getElementById(`xp-version-${arm}`) : null;
5
+ (row ?? target)?.scrollIntoView?.({
5
6
  block: 'start'
6
7
  });
7
- document.getElementById(`title-${section}`)?.focus({
8
+ (row ?? document.getElementById(`title-${section}`))?.focus({
8
9
  preventScroll: true
9
10
  });
10
11
  }
@@ -1,2 +1,5 @@
1
- import type { KindParts } from '../contracts.js';
1
+ import type { KindParts, SectionProps } from '../contracts.js';
2
+ export declare function VersionContent({ test, surface, armIndex }: SectionProps & {
3
+ armIndex: number;
4
+ }): import("react").JSX.Element | null;
2
5
  export declare const pageParts: KindParts;
@@ -1,15 +1,92 @@
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
3
  import { TEST_PAGE as C } from '../../copy/testPage.js';
4
4
  import { pageTargetIssue, setPageTarget, showsPageTarget } from '../../arms/pageTarget.model.js';
5
+ import { useState } from 'react';
6
+ import { CopyRoute } from '../pagePicker/CopyRoute.js';
7
+ import { PagePicker } from '../pagePicker/PagePicker.js';
8
+ import { testPageCollection } from '../pagePicker/pagePicker.model.js';
9
+ import { PAGE_COPY as P } from '../../copy/pages.js';
5
10
  import { FieldErrors, reasonFor, save, TextEdit } from './fields.js';
6
- function VersionContent({ test, armIndex }) {
11
+ export function VersionContent({ test, surface, armIndex }) {
12
+ const [open, setOpen] = useState(false);
7
13
  const arm = test.doc?.arms[armIndex];
8
14
  if (!arm || !test.doc) return null;
9
15
  if (!showsPageTarget(test.doc.kind, arm.kind)) return /*#__PURE__*/ _jsx("span", {
10
16
  className: "xp-content-chip",
11
17
  children: C.holdoutContent
12
18
  });
19
+ const collection = testPageCollection(surface.pageCollections, test.doc, test.preflight?.pagePlacements);
20
+ const placement = test.preflight?.pagePlacements?.find((entry)=>entry.arm === arm.key);
21
+ if (collection) {
22
+ const picker = /*#__PURE__*/ _jsx(PagePicker, {
23
+ collection: collection,
24
+ test: test,
25
+ armKey: arm.key,
26
+ apiRoute: surface.apiRoute,
27
+ onChoose: ()=>setOpen(false)
28
+ });
29
+ return /*#__PURE__*/ _jsxs("div", {
30
+ className: "xp-version-content xp-version-content--page",
31
+ children: [
32
+ /*#__PURE__*/ _jsx("strong", {
33
+ children: P.where
34
+ }),
35
+ /*#__PURE__*/ _jsx("p", {
36
+ children: placement?.id != null ? /*#__PURE__*/ _jsxs(_Fragment, {
37
+ children: [
38
+ placement.title,
39
+ " · ",
40
+ placement.path,
41
+ !placement.published ? /*#__PURE__*/ _jsxs(_Fragment, {
42
+ children: [
43
+ " · ",
44
+ P.draft
45
+ ]
46
+ }) : null
47
+ ]
48
+ }) : arm.pageTarget ? /*#__PURE__*/ _jsxs(_Fragment, {
49
+ children: [
50
+ arm.pageTarget,
51
+ " · ",
52
+ P.draft
53
+ ]
54
+ }) : P.noPage
55
+ }),
56
+ placement?.id != null ? /*#__PURE__*/ _jsxs("div", {
57
+ children: [
58
+ placement.editorUrl ? /*#__PURE__*/ _jsx("a", {
59
+ href: placement.editorUrl,
60
+ target: "_blank",
61
+ rel: "noopener",
62
+ children: P.open
63
+ }) : null,
64
+ ' ',
65
+ placement.url ? /*#__PURE__*/ _jsx("a", {
66
+ href: placement.url,
67
+ target: "_blank",
68
+ rel: "noopener",
69
+ children: P.preview
70
+ }) : null
71
+ ]
72
+ }) : null,
73
+ /*#__PURE__*/ _jsx("button", {
74
+ type: "button",
75
+ disabled: !!reasonFor(test, 'arms'),
76
+ onClick: ()=>setOpen((value)=>!value),
77
+ children: open ? P.close : arm.pageTarget ? P.change : P.choose
78
+ }),
79
+ open ? arm.kind === 'variant' ? /*#__PURE__*/ _jsx(CopyRoute, {
80
+ collection: collection,
81
+ children: picker
82
+ }) : picker : null,
83
+ /*#__PURE__*/ _jsx(FieldErrors, {
84
+ test: test,
85
+ path: `arms.${armIndex}.pageTarget`
86
+ })
87
+ ]
88
+ });
89
+ }
13
90
  const issue = pageTargetIssue(arm.pageTarget, arm.kind);
14
91
  return /*#__PURE__*/ _jsxs("div", {
15
92
  className: "xp-version-content",
@@ -15,6 +15,13 @@
15
15
  .xp-grid-field .xp-ui-field > label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
16
16
  .xp-version-content { display: flex; flex-wrap: nowrap; gap: var(--xp-space-2); align-items: center; min-width: 0; }
17
17
  .xp-version-content .xp-ui-button { white-space: nowrap; }
18
+ .xp-version-content--page { flex-direction: column; align-items: stretch; gap: var(--xp-space-3); }
19
+ .xp-version-content--page > button { align-self: flex-start; }
20
+ .xp-page-picker input[type="search"] { width: 100%; }
21
+ .xp-page-picker ul { list-style: none; padding: 0; margin-block: var(--xp-space-3); }
22
+ .xp-page-picker li > button { display: block; width: 100%; text-align: left; }
23
+ .xp-page-picker li > p { color: var(--xp-text-2); font: var(--xp-text-small); }
24
+ .xp-copy-route ol { list-style: decimal; padding-inline-start: var(--xp-space-6); }
18
25
  .xp-content-chip { flex-shrink: 0; display: inline-flex; width: fit-content; border-radius: var(--xp-radius-chip); padding: var(--xp-space-half) var(--xp-space-2); font: var(--xp-text-small); background: var(--xp-surface-2); color: var(--xp-text-2); }
19
26
  .xp-content-chip.xp-content-ready { background: var(--xp-success-bg); color: var(--xp-success); }
20
27
  .xp-content-chip.xp-content-empty { background: var(--xp-warning-bg); color: var(--xp-warning); }
@@ -66,7 +73,7 @@
66
73
  .xp-readiness--bar { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--xp-space-2) var(--xp-space-4); padding-block: var(--xp-space-3); min-height: 64px; border: 0; border-radius: 0; }
67
74
  .xp-readiness__state { display: flex; align-items: center; flex-wrap: wrap; gap: var(--xp-space-2); min-width: 0; }
68
75
  .xp-readiness__state p, .xp-readiness--bar > p { margin: 0; }
69
- .xp-readiness__state > p { font-weight: 500; }
76
+ .xp-readiness__state > p { font-weight: 500; white-space: pre-line; }
70
77
  .xp-readiness--bar .xp-save-status, .xp-readiness__separator { color: var(--xp-text-3); font: var(--xp-text-small); }
71
78
  .xp-readiness__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--xp-space-2); }
72
79
  .xp-readiness--bar > .xp-readiness__actions { grid-column: 2; grid-row: 1 / span 3; justify-content: flex-end; }
@@ -12,6 +12,7 @@ export interface TestDocument {
12
12
  arms: Arm[];
13
13
  targeting?: TargetingRule[] | null;
14
14
  allocation: number;
15
+ pageCollection?: string | null;
15
16
  flagPoint?: string | null;
16
17
  exposurePolicy: ExposurePolicy;
17
18
  metrics?: {
@@ -9,7 +9,8 @@ const EDITABLE_FIELDS = [
9
9
  'flagPoint',
10
10
  'metrics',
11
11
  'analysis',
12
- 'exposurePolicy'
12
+ 'exposurePolicy',
13
+ 'pageCollection'
13
14
  ];
14
15
  /** Drop unknown and locked keys even when the caller is untyped. Groups replace whole groups. */ export function editablePatch(status, partial) {
15
16
  return Object.fromEntries(EDITABLE_FIELDS.flatMap((field)=>Object.hasOwn(partial, field) && partial[field] !== undefined && isEditable(status, field) ? [
@@ -1,14 +1,23 @@
1
- import type { DocId } from '../../types/contracts.js';
1
+ import type { Arm, DocId } from '../../types/contracts.js';
2
2
  import type { PreflightResult } from '../../types/endpoints.js';
3
3
  import type { TestDocument, TestDocumentPatch } from './testDocument.model.js';
4
4
  /** Section data only: staging never saves. Call commit on blur or a section action. */
5
- export declare function useTestDocument(id: DocId, { apiRoute }: {
5
+ export declare function useTestDocument(id: DocId, { apiRoute, refetchPreflightOnFocus }: {
6
6
  apiRoute: string;
7
+ refetchPreflightOnFocus?: boolean;
7
8
  }): {
8
9
  stage: (partial: TestDocumentPatch) => void;
9
10
  commit: () => Promise<void>;
10
11
  reload: () => Promise<void>;
11
12
  refreshPreflight: () => Promise<void>;
13
+ choosePage: (arm: Arm["key"], page: {
14
+ id: DocId;
15
+ path: string;
16
+ }) => Promise<void>;
17
+ chosenPages: Partial<Record<Arm["key"], {
18
+ id: DocId;
19
+ path: string;
20
+ }>>;
12
21
  doc: TestDocument | null;
13
22
  preflight: PreflightResult | null;
14
23
  preflightError: string | null;
@@ -7,6 +7,7 @@ import { changedPatch, editablePatch, payloadFieldErrors } from './testDocument.
7
7
  // Saves outlive their mounted reader. Reopening the same document must wait for its queue.
8
8
  const drainingSaves = new Map();
9
9
  const initialView = ()=>({
10
+ chosenPages: {},
10
11
  doc: null,
11
12
  preflight: null,
12
13
  preflightError: null,
@@ -55,6 +56,7 @@ function stage(session, partial) {
55
56
  if (!session.saved || session.view.stale || session.reloading || session.view.loadState !== 'ready' || session.controller.signal.aborted) return;
56
57
  const patch = editablePatch(session.saved.status, partial);
57
58
  if (!Object.keys(patch).length) return;
59
+ if (patch.pageCollection !== undefined && patch.pageCollection !== optimistic(session)?.pageCollection) session.view.chosenPages = {};
58
60
  session.pending = {
59
61
  ...session.pending,
60
62
  ...patch
@@ -63,7 +65,7 @@ function stage(session, partial) {
63
65
  if (!session.queue.length) session.view.saving = 'idle';
64
66
  session.publish();
65
67
  }
66
- /** Section data only: staging never saves. Call commit on blur or a section action. */ export function useTestDocument(id, { apiRoute }) {
68
+ /** Section data only: staging never saves. Call commit on blur or a section action. */ export function useTestDocument(id, { apiRoute, refetchPreflightOnFocus = false }) {
67
69
  const base = apiRoute.replace(/\/$/, '');
68
70
  const documentId = String(id);
69
71
  const key = JSON.stringify([
@@ -90,11 +92,12 @@ function stage(session, partial) {
90
92
  controller,
91
93
  reloading: false,
92
94
  preflightReads: 0,
95
+ preflightLoading: false,
93
96
  publish () {
94
97
  if (controller.signal.aborted) return;
95
98
  session.view.doc = session.view.loadState === 'ready' ? optimistic(session) : null;
96
99
  session.view.hasUnsaved = session.queue.length > 0 || Object.keys(session.pending).length > 0;
97
- session.view.preflightPending = session.preflightReads > 0;
100
+ session.view.preflightPending = session.preflightReads > 0 || session.preflightLoading;
98
101
  setState({
99
102
  key,
100
103
  view: {
@@ -109,7 +112,7 @@ function stage(session, partial) {
109
112
  async function refreshPreflight(initial = false) {
110
113
  if (controller.signal.aborted) return;
111
114
  const revision = ++preflightRevision;
112
- session.preflightReads++;
115
+ session.preflightLoading = true;
113
116
  session.publish();
114
117
  // createApi has no signal option; this read must abort without cancelling submitted PATCHes.
115
118
  let data;
@@ -134,12 +137,9 @@ function stage(session, partial) {
134
137
  } catch {
135
138
  message = API_MESSAGES.network;
136
139
  }
137
- session.preflightReads--;
138
140
  if (controller.signal.aborted) return;
139
- if (revision !== preflightRevision) {
140
- session.publish();
141
- return;
142
- }
141
+ if (revision !== preflightRevision) return;
142
+ session.preflightLoading = false;
143
143
  if (data) {
144
144
  session.view.preflight = data;
145
145
  session.view.preflightError = null;
@@ -257,12 +257,9 @@ function stage(session, partial) {
257
257
  session.publish();
258
258
  }
259
259
  if (saved) {
260
- try {
261
- await refreshPreflight();
262
- } finally{
263
- session.preflightReads--;
264
- session.publish();
265
- }
260
+ // Recovery is complete; readiness now belongs to the latest preflight read.
261
+ session.preflightReads--;
262
+ await refreshPreflight();
266
263
  }
267
264
  };
268
265
  // Recover both the preceding tail and this step: unexpected throws must not strand later saves.
@@ -367,11 +364,52 @@ function stage(session, partial) {
367
364
  }, [
368
365
  key
369
366
  ]);
367
+ useEffect(()=>{
368
+ if (!refetchPreflightOnFocus) return;
369
+ let lastReturn = -Infinity;
370
+ const focus = ()=>{
371
+ if (document.visibilityState !== 'visible' || Date.now() - lastReturn < 100) return;
372
+ lastReturn = Date.now();
373
+ void refreshPreflight();
374
+ };
375
+ const visible = ()=>{
376
+ if (document.visibilityState === 'visible') focus();
377
+ };
378
+ window.addEventListener('focus', focus);
379
+ document.addEventListener('visibilitychange', visible);
380
+ return ()=>{
381
+ window.removeEventListener('focus', focus);
382
+ document.removeEventListener('visibilitychange', visible);
383
+ };
384
+ }, [
385
+ refetchPreflightOnFocus,
386
+ refreshPreflight
387
+ ]);
388
+ const choosePage = useCallback((arm, page)=>{
389
+ const session = current.current;
390
+ const doc = session?.key === key ? optimistic(session) : null;
391
+ if (!session || !doc) return Promise.resolve();
392
+ session.view.chosenPages = {
393
+ ...session.view.chosenPages,
394
+ [arm]: page
395
+ };
396
+ stage(session, {
397
+ arms: doc.arms.map((item)=>item.key === arm ? {
398
+ ...item,
399
+ pageTarget: page.path
400
+ } : item)
401
+ });
402
+ return commit();
403
+ }, [
404
+ key,
405
+ commit
406
+ ]);
370
407
  return {
371
408
  ...state.key === key ? state.view : initialView(),
372
409
  stage: stagePartial,
373
410
  commit,
374
411
  reload,
375
- refreshPreflight
412
+ refreshPreflight,
413
+ choosePage
376
414
  };
377
415
  }
@@ -1,4 +1,12 @@
1
- export declare function useApi(): {
1
+ export declare function useApi(apiRoute: string): {
2
+ wrapBlock: (body: import("../types/endpoints.js").WrapBlockBody) => Promise<import("./api.js").ApiResult<Record<string, unknown>>>;
3
+ pages: (query: {
4
+ collection: string;
5
+ q?: string;
6
+ path?: string;
7
+ page?: number;
8
+ }, signal?: AbortSignal) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").PagesResult>>;
9
+ placementStatus: (body: import("../types/endpoints.js").PlacementStatusBody) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").PlacementStatusResult>>;
2
10
  start: (id: import("../types/contracts.js").DocId) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").TransitionSuccess>>;
3
11
  allocate: (id: import("../types/contracts.js").DocId, body: import("../types/endpoints.js").AllocateBody, options?: {
4
12
  notFound?: string;
@@ -9,7 +17,7 @@ export declare function useApi(): {
9
17
  notFound?: string;
10
18
  codes?: Readonly<Record<string, string>>;
11
19
  fallback?: string;
12
- }) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").TransitionSuccess>>;
20
+ }) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").AddMetricResult>>;
13
21
  stop: (id: import("../types/contracts.js").DocId) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").TransitionSuccess>>;
14
22
  decide: (id: import("../types/contracts.js").DocId, body: import("../types/endpoints.js").DecideBody) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").TransitionSuccess>>;
15
23
  releaseHoldout: (id: import("../types/contracts.js").DocId) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").TransitionSuccess>>;
@@ -18,6 +26,7 @@ export declare function useApi(): {
18
26
  dismissContentEntry: (id: import("../types/contracts.js").DocId, body: import("../types/endpoints.js").DismissContentEntryBody) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").DismissContentEntryResult>>;
19
27
  restoreContentEntry: (id: import("../types/contracts.js").DocId, body: import("../types/endpoints.js").RestoreContentEntryBody) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").RestoreContentEntryResult>>;
20
28
  archive: (id: import("../types/contracts.js").DocId, body: import("../types/endpoints.js").ArchiveBody) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").TransitionSuccess>>;
29
+ clickKeys: (id: import("../types/contracts.js").DocId) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").ClickKeysResult>>;
21
30
  preflight: (id: import("../types/contracts.js").DocId) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").PreflightResult>>;
22
31
  previewToken: (id: import("../types/contracts.js").DocId, arm: string) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").PreviewTokenResult>>;
23
32
  results: (id: import("../types/contracts.js").DocId, options?: {
@@ -1,10 +1,8 @@
1
1
  'use client';
2
2
  import { useMemo } from 'react';
3
- import { useConfig } from '@payloadcms/ui';
4
3
  import { createApi } from './api.js';
5
- export function useApi() {
6
- const { config } = useConfig();
7
- return useMemo(()=>createApi(config.routes.api), [
8
- config.routes.api
4
+ export function useApi(apiRoute) {
5
+ return useMemo(()=>createApi(apiRoute), [
6
+ apiRoute
9
7
  ]);
10
8
  }
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { accentStyles } from '../../ui/accent.js';
3
- import { formatAdminURL } from 'payload/shared';
3
+ import { flattenTopLevelFields, formatAdminURL } from 'payload/shared';
4
4
  import { notFound, redirect } from 'next/navigation.js';
5
5
  import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent';
6
6
  import { serializeAttributeKeys, serializeFlagPoints, serializeMetricSources } from '../../collections/serialize.js';
7
- import { puckCollections } from '../../options.js';
7
+ import { serializePageCollections } from '../pageCollections.js';
8
+ import { collectionLabel as resolveLabel } from '../../collections/labels.js';
9
+ import { documentEditor, puckCollections } from '../../options.js';
8
10
  import { xpOptionsFromReq } from '../../options/fromReq.js';
9
11
  import { XP_TESTS_SURFACE } from '../paths.js';
10
12
  import { SURFACE_COPY } from '../copy/surface.js';
@@ -26,6 +28,9 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
26
28
  } else if (tail.length === 1) {
27
29
  route = 'test';
28
30
  id = tail[0];
31
+ } else if (tail.length === 2 && tail[0] === 'by-slug') {
32
+ route = 'test';
33
+ id = tail[1];
29
34
  } else if (tail.length === 2 && tail[1] === 'results') {
30
35
  route = 'results';
31
36
  id = tail[0];
@@ -71,6 +76,22 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
71
76
  children: SURFACE_COPY.noAccess
72
77
  });
73
78
  }
79
+ if (canManage && id && tail[0] === 'by-slug') {
80
+ const result = await req.payload.find({
81
+ collection: 'xp-experiments',
82
+ req,
83
+ overrideAccess: false,
84
+ depth: 0,
85
+ limit: 1,
86
+ where: {
87
+ slug: {
88
+ equals: id
89
+ }
90
+ }
91
+ });
92
+ if (!result.docs[0]) return notFound();
93
+ id = String(result.docs[0].id);
94
+ }
74
95
  const flagPoints = serializeFlagPoints(options);
75
96
  // xp-state is manager-readable only. Results-only readers do not edit settings.
76
97
  if (canManage && flagPoints.length) {
@@ -83,20 +104,22 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
83
104
  const defaults = state.flagDefaults;
84
105
  for (const point of flagPoints)point.effectiveDefaults = defaults?.[point.id]?.params ?? point.defaultParams;
85
106
  }
86
- const registrations = puckCollections(options);
87
- const pages = route === 'new' && canManage && options.puck?.enabled && registrations.length ? (await Promise.all(registrations.map(async ({ slug: collection })=>{
107
+ const registrations = [
108
+ ...new Set([
109
+ ...options.puck?.enabled ? puckCollections(options) : [],
110
+ ...options.blocks?.collections ?? []
111
+ ].map((entry)=>entry.slug))
112
+ ];
113
+ const pages = route === 'new' && canManage && registrations.length ? (await Promise.all(registrations.map(async (collection)=>{
88
114
  try {
89
115
  const config = req.payload.config.collections.find((item)=>item.slug === collection);
90
116
  const useAsTitle = config?.admin.useAsTitle ?? 'id';
91
- const label = config?.labels.plural;
92
- // Keep /rsc dependency-safe: translations is not a declared runtime dependency,
93
- // so a per-language label picks the request language, then English, then any string.
94
- const byLanguage = label && typeof label === 'object' ? label : {};
95
- const collectionLabel = typeof label === 'string' ? label : [
96
- byLanguage[req.i18n?.language ?? 'en'],
97
- byLanguage.en,
98
- ...Object.values(byLanguage)
99
- ].find((value)=>typeof value === 'string') ?? collection;
117
+ const collectionLabel = resolveLabel(config?.labels.plural, collection, req);
118
+ // appliesTo may inspect any stored field; ordinary Puck lists keep their title-only projection.
119
+ const predicateFields = options.blocks?.collections.some((entry)=>entry.slug === collection) ? Object.fromEntries(flattenTopLevelFields(config?.fields ?? []).map((field)=>[
120
+ field.name,
121
+ true
122
+ ])) : {};
100
123
  const result = await req.payload.find({
101
124
  collection,
102
125
  req,
@@ -106,28 +129,35 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
106
129
  sort: '-updatedAt',
107
130
  draft: true,
108
131
  select: {
109
- [useAsTitle]: true
132
+ [useAsTitle]: true,
133
+ ...predicateFields
110
134
  }
111
135
  });
136
+ // The new-test list uses the latest draft's editing intent, as the "Test a block" field condition does (§4.1).
112
137
  return result.docs.map((doc)=>{
138
+ const editor = documentEditor(options, collection, doc);
139
+ if (editor !== 'puck' && editor !== 'blocks') return null;
113
140
  const id = doc.id;
114
141
  const customUrl = options.admin?.editorUrl?.({
115
142
  collection,
116
143
  id,
117
- draft: true
144
+ draft: true,
145
+ editor,
146
+ doc
118
147
  });
119
148
  const title = doc[useAsTitle];
120
149
  return {
121
150
  collection,
122
151
  collectionLabel,
123
152
  id,
153
+ editor,
124
154
  title: typeof title === 'string' || typeof title === 'number' ? String(title) : String(id),
125
155
  editorUrl: typeof customUrl === 'string' ? customUrl : formatAdminURL({
126
156
  adminRoute,
127
157
  path: `/collections/${collection}/${encodeURIComponent(String(id))}`
128
158
  })
129
159
  };
130
- });
160
+ }).filter((page)=>page !== null);
131
161
  } catch {
132
162
  return [];
133
163
  }
@@ -151,6 +181,18 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
151
181
  ...pages === undefined ? {} : {
152
182
  pages
153
183
  },
184
+ pageCollections: serializePageCollections(options.pages?.collections ?? [], req),
185
+ collectionTitles: Object.fromEntries(req.payload.config.collections.flatMap((collection)=>{
186
+ const title = collection.admin.useAsTitle;
187
+ return title && title !== 'id' ? [
188
+ [
189
+ collection.slug,
190
+ title
191
+ ]
192
+ ] : [];
193
+ })),
194
+ userId: req.user.id,
195
+ userCollection: req.payload.config.admin.user,
154
196
  attributeKeys: serializeAttributeKeys(options),
155
197
  metricSources: serializeMetricSources(options),
156
198
  ...descriptions ? {
@@ -0,0 +1,3 @@
1
+ import type { ResolvedOptions } from '../types/contracts.js';
2
+ /** Same document-kind rule as the scan; Payload blocks are top level, Puck is recursive. */
3
+ export declare function placementSlugs(options: Pick<ResolvedOptions, 'blocks' | 'puck'>, collection: string, doc: Record<string, unknown> | undefined): string[];