@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
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @delmaredigital/payload-experiments
2
2
 
3
+ > ⚠️ Upgrading to 0.4? Run `payload migrate:create` for four nullable plugin columns (`xp_experiments.page_collection` and its `_xp_experiments_v` counterpart, `xp_phases.placement_docs`, `xp_state.retired_slugs`), plus the Experiment block and `xpGoal` columns if you adopt them, then `payload generate:importmap`. Saves on pages, blocks and Puck collections can now be refused while a test runs on them (`page-busy`, `interacting-tests`); Puck tests whose slot is only on a draft can't start until the page is published; host `metricSources` ids may not start with `click:`; `admin.editorUrl` now receives a required `editor` argument. See the CHANGELOG's BREAKING section.
4
+
3
5
  > ⚠️ Upgrading to 0.3? For 0.3.0-rc.1, every `linkIdentity` call must supply `verified`; it now returns an outcome. Run `payload migrate:create` for the nullable `xp_identities.verified_at` column and `user_id` index, plus the release-pages columns listed in the CHANGELOG (`decision_pages_released_at` / `decision_pages_released_by` on `xp_experiments` and their `version_` counterparts on `_xp_experiments_v`). Existing identity links become unverified; see the CHANGELOG for optional backfill.
4
6
 
5
7
  > ⚠️ Upgrading to 0.2? After upgrading, run `payload generate:importmap`: the Tests surface is registered through admin.dependencies, and without it the Tests routes show a 'not installed' message instead of the Tests screen. In 0.2.0-rc.2, the Tests server views moved from `/rsc` to `/admin/rsc`, so the import map must be regenerated when upgrading from rc.1 too. The old experiments edit-form components are gone from `/admin/client`, and the `xp-experiments` admin list and edit views now redirect to the Tests surface.
@@ -426,6 +428,7 @@ schema. Optional callbacks integrate host-owned content:
426
428
 
427
429
  A `MetricSource`, registered in `metricSources`, has `id`, `label`,
428
430
  `kind: 'binomial' | 'count' | 'revenue'`, and `keyedBy: 'visitor' | 'user'`.
431
+ Source ids starting with `click:` are reserved for click goals and refused at boot.
429
432
  `clientWritable` optionally permits client conversions (revenue is always
430
433
  server-only). `query({ from, to, unitIds? })` returns a promise of
431
434
  `[{ unitId, value, ts }]`, **one row per event**, with `ts` a `Date`; use the
@@ -456,6 +459,21 @@ are recorded with `trackConversion`. Beacon conversions for a non-writable name
456
459
  are dropped silently. Only listed names become `events:<name>` sources. They
457
460
  support visitor and user units and do not represent counts or revenue in v1.
458
461
 
462
+ ### Testing page SEO in the host
463
+
464
+ `pageKindSeoContract({ secret, original, versions, state, winner? })` from `/rsc`
465
+ returns `{ manifest, rows }`. States are `running`, `decided` and `archived`;
466
+ provide a winner path for the latter two. Build the host's test runtime with
467
+ `loadManifest: async () => manifest` and the same secret. For each row, set
468
+ `next/headers` to its signed `headers`, render metadata for `servedPath`, and
469
+ assert `expect.robots`, `expect.canonical` and `expect.inSitemap` against the
470
+ host's metadata and sitemap functions. This includes the Original served as
471
+ each version, including the homepage.
472
+
473
+ A host-level test mocks four boundaries: `next/headers`, the page loader,
474
+ the runtime and the site URL. Set `metadataBase` so relative canonical paths
475
+ resolve to your site URL.
476
+
459
477
  ### Whole-page options
460
478
 
461
479
  | Option | Purpose |
@@ -489,7 +507,18 @@ pages: {
489
507
  Adapt both callbacks for the site's homepage path. These checks run before Start;
490
508
  they do not protect pages from later deletion, unpublishing or renaming. Keep an
491
509
  archived winner published at the same address: archived test URLs still point to it.
492
- No migration is needed.
510
+ Whole-page registrations also install the sidebar test status and content-change hooks.
511
+ For 0.4.0, run `payload migrate:create` for `xp_state.retired_slugs`,
512
+ `xp_experiments.page_collection` and its version column, then
513
+ `payload generate:importmap`.
514
+
515
+ `contentOf(doc)` optionally selects the content your site renders (for example,
516
+ `doc.layout`). Published copies are compared with nested `id` and `blockName`
517
+ keys removed. Without it, content checks and page content-change comparisons
518
+ are skipped. `copyHint` (up to 200 characters), `copyUrl` (a root-relative path)
519
+ and `copyLabel` (up to 40 characters, requires `copyUrl`) customize the guidance
520
+ for making version pages. The default copy link opens the registered collection's
521
+ list under the configured admin route.
493
522
 
494
523
  ### `admin` options
495
524
 
@@ -504,7 +533,7 @@ For example, pass:
504
533
 
505
534
  ```ts
506
535
  admin: {
507
- editorUrl: ({ collection, id, draft }) =>
536
+ editorUrl: ({ collection, id, draft, editor, doc }) =>
508
537
  `/admin/puck-editor/${encodeURIComponent(collection)}/${encodeURIComponent(String(id))}`,
509
538
  previewUrl: async ({ collection, slug }) => {
510
539
  if (collection !== 'pages' || slug === undefined) return null
@@ -570,7 +599,10 @@ separately. Only manual, viewport and pageview exposure policies are accepted.
570
599
  Conversions require a client-writable source, never revenue; client values are
571
600
  stored as `1`, attributes are allow-listed, and the host's `events.rateLimit`
572
601
  adapter runs when supplied. Accepted and dropped events both return `202 {}`;
573
- malformed requests can return errors.
602
+ malformed requests can return errors. `click:<key>` conversions are also accepted
603
+ for defined click-goal keys matching `[a-z0-9][a-z0-9_-]{0,63}`; undefined or
604
+ invalid keys are dropped with `202`. Clicks carry no attributes, and defined
605
+ keys may be cached for up to 30 seconds.
574
606
 
575
607
  **The real bound on slow or method-override bodies is the host firewall/platform
576
608
  timeout.** A stream under the byte limit can remain open, and Payload can read
@@ -588,13 +620,15 @@ for these patterns.
588
620
 
589
621
  ## Admin and client
590
622
 
591
- The sidebar includes a **Tests** group with **All tests** and **Goals**. The
592
- dashboard also includes **Tests** and **Goals** cards above its widgets. Goals
593
- appears only for users who can manage tests. Both additions follow the host's
594
- admin route and keep host-provided navigation and dashboard entries first.
595
- After upgrading, run `payload generate:importmap` to register the dashboard card
596
- component and the server nav wrapper.
597
- A host nav group also labelled "Tests" shares Payload's NavGroup DOM id and open-state preference with the plugin's group, so pick another label.
623
+ **Tests** is a regular navigation group for managers only, with **All tests**
624
+ and **Goals**. It is the first named group, after Payload's default
625
+ Collections/Globals groups when the host has ungrouped entities. Payload's own
626
+ dashboard shows the Tests cards. A host collection with `group: 'Tests'` joins
627
+ the plugin's group.
628
+
629
+ After upgrading, run `payload generate:importmap` to register the two list views
630
+ and the Goals edit redirect. Without regeneration, both list URLs and the Goal
631
+ create/edit URLs show Payload's raw views.
598
632
 
599
633
  ### Client assignments and exposure
600
634
 
@@ -647,32 +681,61 @@ configured to allow client writes; record revenue on the server.
647
681
 
648
682
  ### Puck render and editor configs
649
683
 
650
- Register the factories from `/puck` and `/puck/editor`, using the same
651
- `ExperimentSlot` component name in both configs. Pass the host's Payload routes:
684
+ Works with: payload-puck ≥0.10.3 (experiments ≥0.4.0).
685
+
686
+ For payload-puck hosts, add one integration object on each side. In the render
687
+ config, pass `integrations` to `extendConfig`:
652
688
 
653
689
  ```ts
654
- import { createExperimentSlot } from '@delmaredigital/payload-experiments/puck'
655
- import { createExperimentSlotEditor, experimentOutlinePlugin } from '@delmaredigital/payload-experiments/puck/editor'
690
+ import { extendConfig } from '@delmaredigital/payload-puck/config'
691
+ import { experimentsPuck } from '@delmaredigital/payload-experiments/puck'
692
+ import { baseConfig } from './baseConfig'
656
693
 
657
- const routes = { admin: '/manage', api: '/content-api' } // Same as Payload config.routes
658
- const accent = '#1f3a68' // Same as the plugin's theme.accent
694
+ export const renderConfig = extendConfig({
695
+ base: baseConfig,
696
+ integrations: [experimentsPuck()],
697
+ })
698
+ ```
659
699
 
660
- // Render config
661
- components: { ExperimentSlot: createExperimentSlot({ routes }) }
700
+ Call `experimentsPuckEditor()` once at module scope in a `'use client'` module,
701
+ then pass that array to `PuckConfigProvider`. A new object per render rebuilds
702
+ Puck's config and plugin list.
662
703
 
663
- // Editor config
664
- components: { ExperimentSlot: createExperimentSlotEditor({ routes, accent }) }
665
- ```
704
+ ```tsx
705
+ 'use client'
666
706
 
667
- Add `experimentOutlinePlugin()` to Puck's `plugins` alongside this block config
668
- (with payload-puck, use its `plugins` option). It composes with the host's outline
669
- override and hides empty version slots that the block's test doesn't use. Content
670
- in an unused slot stays visible in the outline and gets a warning in the block.
707
+ import { PuckConfigProvider } from '@delmaredigital/payload-puck/client'
708
+ import { experimentsPuckEditor } from '@delmaredigital/payload-experiments/puck/editor'
709
+ import { editorConfig } from './editorConfig'
671
710
 
672
- ```ts
673
- plugins: [experimentOutlinePlugin()]
711
+ const integrations = [experimentsPuckEditor()]
712
+
713
+ export function PuckProvider({ children }: { children: React.ReactNode }) {
714
+ return <PuckConfigProvider config={editorConfig} integrations={integrations}>
715
+ {children}
716
+ </PuckConfigProvider>
717
+ }
674
718
  ```
675
719
 
720
+ Each object includes the test block and its **A/B tests** sidebar category. The
721
+ editor object also includes the outline plugin: it hides empty version slots
722
+ that the block's test doesn't use, while content in unused slots stays visible
723
+ with a warning. Both helpers accept `{ routes }`; the editor also accepts
724
+ `accent`. Use the host's Payload routes and the plugin's `theme.accent`.
725
+
726
+ #### Wiring Puck by hand
727
+
728
+ The separate pieces stay exported:
729
+
730
+ - `/puck`: `ExperimentSlot`, `createExperimentSlot({ routes })`,
731
+ `EXPERIMENT_SLOT_TYPE` and `createExperimentSlotField({ routes })`.
732
+ - `/puck/editor`: `ExperimentSlotEditor`,
733
+ `createExperimentSlotEditor({ routes, accent })` and `experimentOutlinePlugin()`.
734
+
735
+ Register the render and editor blocks under the same `EXPERIMENT_SLOT_TYPE` key (import it from `/puck`),
736
+ add a sidebar category listing it, and add `experimentOutlinePlugin()` to Puck's
737
+ `plugins`. It composes with the host's outline override.
738
+
676
739
  Routes default to Payload's `/admin` and `/api` when omitted. The existing
677
740
  `ExperimentSlot` and `ExperimentSlotEditor` exports remain available with these
678
741
  defaults. Hosts on non-default routes switch to the factories. The standalone
@@ -815,3 +878,35 @@ The WCAG 2.x gate requires at least 4.5:1 **foreground text on the accent fill**
815
878
  in each theme (the accent is used as a fill, not as page text). A failing
816
879
  colour is refused with its ratio. Status chips retain Payload semantic colours.
817
880
  `deriveAccent` is also exported from `/puck/editor` for editor integrations.
881
+
882
+ ## Click goals
883
+
884
+ Click goals use a goal's short name as their key (`click:<short-name>`). Add
885
+ `xpGoalField()` from the root entry to a host block or array row. Its manager
886
+ picker includes inline creation with "+ New click goal"; run
887
+ `payload generate:importmap` to register the picker. For Puck's `xpGoal` prop,
888
+ use `xpGoalPuckEditorField()` from `/puck/editor` in your editor config and
889
+ `xpGoalPuckField()` from `/puck` in your render config. Run the
890
+ host's `payload migrate:create`: each table using the Payload field (and its
891
+ version table) gains a nullable text column. Render the value on the actual
892
+ interactive element as `data-xp-goal={xpGoal}`; code can also set
893
+ `data-xp-goal="join"` directly. Define a click goal with that short name first.
894
+ For managers, an undefined key blocks the save through the Payload field's
895
+ server validation, refreshing a cached miss immediately.
896
+ Draft saves are also refused when the collection validates drafts
897
+ (`versions.drafts.validate`). Editors without goal-management access see a
898
+ plain text input. Goals in
899
+ related documents and keys placed only in code are invisible to the content
900
+ fairness check.
901
+
902
+ `ExperimentsProvider` records native clicks when its server assignments arm
903
+ click goals. Nested providers share one document listener; the deepest armed
904
+ provider supplies the route and pageview id. Enter on links/buttons and Space
905
+ on buttons count through the browser's native click. A focusable `div` needs
906
+ `role="button"` and the host's own keyboard handler. Beacons use `sendBeacon`
907
+ (with a keepalive fallback) so navigation/unload does not discard the click.
908
+ Click and exposure beacons share `events.rateLimit`; size the adapter for both.
909
+ New click goals are accepted within 30 seconds. A click goal may be primary;
910
+ primary and guardrail goals must appear in every tested version or outside all
911
+ of them. Secondary asymmetry warns, and results health checks published rows
912
+ again on every run.
@@ -1,14 +1,14 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { SelectInput, useField } from '@payloadcms/ui';
4
- import { metricSourceHint, metricSourceOptions } from './metricSource.model.js';
4
+ import { metricSourceHint, metricSourceOptions, sourceFor } from './metricSource.model.js';
5
5
  import { METRICS_COPY } from './copy/metrics.js';
6
6
  export function MetricSourceSelect({ path, field, readOnly }) {
7
7
  const { value, setValue } = useField({
8
8
  path
9
9
  });
10
10
  const sources = field.admin?.custom?.sources ?? [];
11
- const hint = metricSourceHint(sources.find((source)=>source.id === value));
11
+ const hint = metricSourceHint(sourceFor(value, sources));
12
12
  const locked = Boolean(readOnly || field.admin?.readOnly);
13
13
  return /*#__PURE__*/ _jsxs("div", {
14
14
  children: [
@@ -19,7 +19,7 @@ export function MetricSourceSelect({ path, field, readOnly }) {
19
19
  description: field.admin?.description,
20
20
  required: field.required,
21
21
  options: metricSourceOptions(sources),
22
- value: value ?? '',
22
+ value: sourceFor(value, sources)?.id ?? value ?? '',
23
23
  readOnly: locked,
24
24
  onChange: (option)=>{
25
25
  if (!locked) setValue(option && !Array.isArray(option) ? option.value : null);
@@ -0,0 +1,3 @@
1
+ import type { UIFieldClientProps } from 'payload';
2
+ /** Manage-only endpoint: access-filtered editors see no status or error text. */
3
+ export declare function PlacementStatus(_props: UIFieldClientProps): import("react").JSX.Element;
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useConfig, useDocumentInfo } from '@payloadcms/ui';
4
+ import { useEffect, useMemo, useState } from 'react';
5
+ import { formatAdminURL } from 'payload/shared';
6
+ import { createApi } from './api.js';
7
+ import { pageStatusCopy } from './placementStatus.model.js';
8
+ /** Manage-only endpoint: access-filtered editors see no status or error text. */ export function PlacementStatus(_props) {
9
+ const { id, collectionSlug } = useDocumentInfo();
10
+ const { config } = useConfig();
11
+ const api = useMemo(()=>createApi(config.routes.api), [
12
+ config.routes.api
13
+ ]);
14
+ const [tests, setTests] = useState([]);
15
+ useEffect(()=>{
16
+ let active = true;
17
+ setTests([]);
18
+ if (id != null && collectionSlug) void api.placementStatus({
19
+ docs: [
20
+ {
21
+ collection: collectionSlug,
22
+ id
23
+ }
24
+ ]
25
+ }).then((result)=>{
26
+ if (active && result.ok) setTests(result.data?.docs[`${collectionSlug}:${id}`]?.tests ?? []);
27
+ });
28
+ return ()=>{
29
+ active = false;
30
+ };
31
+ }, [
32
+ api,
33
+ collectionSlug,
34
+ id
35
+ ]);
36
+ return /*#__PURE__*/ _jsx(_Fragment, {
37
+ children: tests.map((test)=>{
38
+ const copy = pageStatusCopy(test);
39
+ return copy ? /*#__PURE__*/ _jsxs("p", {
40
+ children: [
41
+ copy,
42
+ ' ',
43
+ /*#__PURE__*/ _jsx("a", {
44
+ href: formatAdminURL({
45
+ adminRoute: config.routes.admin,
46
+ path: `/experiments/by-slug/${encodeURIComponent(test.slug)}`
47
+ }),
48
+ children: "Back to the test"
49
+ })
50
+ ]
51
+ }, test.slug) : null;
52
+ })
53
+ });
54
+ }
@@ -1,6 +1,7 @@
1
1
  import type { DocId } from '../types/contracts.js';
2
2
  import type { PreflightResult } from '../types/endpoints.js';
3
- export declare function PreviewLinks({ experimentId, placements, arms, flagPoint }: {
3
+ export declare function PreviewLinks({ experimentId, placements, arms, flagPoint, apiRoute }: {
4
+ apiRoute: string;
4
5
  flagPoint?: string;
5
6
  experimentId: DocId;
6
7
  placements: PreflightResult['placements'];
@@ -8,8 +8,8 @@ import { REVIEW_COPY as C } from './copy/review.js';
8
8
  import { TEST_PAGE } from './copy/testPage.js';
9
9
  import { API_MESSAGES, refusalMessage } from './copy/common.js';
10
10
  import { actionablePreviewPages } from './previewLinks.model.js';
11
- export function PreviewLinks({ experimentId, placements, arms, flagPoint }) {
12
- const api = useApi();
11
+ export function PreviewLinks({ experimentId, placements, arms, flagPoint, apiRoute }) {
12
+ const api = useApi(apiRoute);
13
13
  const inFlight = useRef(false);
14
14
  const [busy, setBusy] = useState(false);
15
15
  const [error, setError] = useState(null);
@@ -1,3 +1,4 @@
1
1
  import './theme.css';
2
2
  import './dashboard.css';
3
+ /** @deprecated Retained for existing import maps until 0.5.0. */
3
4
  export declare function TestsDashboardCards(): import("react").JSX.Element | null;
@@ -1,20 +1,16 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { surfaceUrls } from './surfaceUrls.js';
3
4
  import './theme.css';
4
5
  import './dashboard.css';
5
6
  import { Card, useAuth, useConfig } from '@payloadcms/ui';
6
- import { formatAdminURL } from 'payload/shared';
7
7
  import { SURFACE_COPY } from './copy/surface.js';
8
- export function TestsDashboardCards() {
8
+ /** @deprecated Retained for existing import maps until 0.5.0. */ export function TestsDashboardCards() {
9
9
  const { config } = useConfig();
10
10
  const { permissions } = useAuth();
11
11
  const testPermissions = permissions?.collections?.['xp-experiments'];
12
12
  const canManage = testPermissions?.update === true;
13
13
  if (!canManage) return null;
14
- const href = (path)=>formatAdminURL({
15
- adminRoute: config.routes.admin,
16
- path
17
- });
18
14
  return /*#__PURE__*/ _jsxs("section", {
19
15
  className: "xp-dash-group",
20
16
  "aria-labelledby": "xp-dash-title",
@@ -32,7 +28,7 @@ export function TestsDashboardCards() {
32
28
  title: SURFACE_COPY.navAllTests,
33
29
  titleAs: "h3",
34
30
  buttonAriaLabel: SURFACE_COPY.dashShowTests,
35
- href: href('/experiments'),
31
+ href: surfaceUrls.home(config.routes.admin),
36
32
  id: "card-xp-tests"
37
33
  })
38
34
  }),
@@ -41,7 +37,7 @@ export function TestsDashboardCards() {
41
37
  title: SURFACE_COPY.navGoals,
42
38
  titleAs: "h3",
43
39
  buttonAriaLabel: SURFACE_COPY.dashShowGoals,
44
- href: href('/experiments/goals'),
40
+ href: surfaceUrls.goals(config.routes.admin),
45
41
  id: "card-xp-goals"
46
42
  })
47
43
  })
@@ -1,4 +1,5 @@
1
1
  import './theme.css';
2
+ /** @deprecated Retained for existing import maps until 0.5.0. */
2
3
  declare function TestsNav({ isOpen }: {
3
4
  isOpen?: boolean;
4
5
  }): import("react").JSX.Element | null;
@@ -1,22 +1,16 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { surfaceUrls } from './surfaceUrls.js';
3
4
  import './theme.css';
4
5
  import { Link, NavGroup, useAuth, useConfig } from '@payloadcms/ui';
5
- import { formatAdminURL } from 'payload/shared';
6
6
  import { usePathname } from 'next/navigation.js';
7
7
  import { SURFACE_COPY } from './copy/surface.js';
8
- function TestsNav({ isOpen = true }) {
8
+ /** @deprecated Retained for existing import maps until 0.5.0. */ function TestsNav({ isOpen = true }) {
9
9
  const { config } = useConfig();
10
10
  const { permissions } = useAuth();
11
11
  const pathname = usePathname();
12
- const href = formatAdminURL({
13
- adminRoute: config.routes.admin,
14
- path: '/experiments'
15
- });
16
- const goalsHref = formatAdminURL({
17
- adminRoute: config.routes.admin,
18
- path: '/experiments/goals'
19
- });
12
+ const href = surfaceUrls.home(config.routes.admin);
13
+ const goalsHref = surfaceUrls.goals(config.routes.admin);
20
14
  const base = href.replace(/\/$/, '');
21
15
  const goalsBase = goalsHref.replace(/\/$/, '');
22
16
  const goalsActive = pathname === goalsBase || Boolean(pathname?.startsWith(`${goalsBase}/`));
@@ -8,7 +8,8 @@ export declare function DialogFrame({ title, onClose, children, initialFocus, di
8
8
  onClose: () => void;
9
9
  children: ReactNode;
10
10
  }): import("react").JSX.Element;
11
- export declare function ConfirmDialog({ id, action, winnerLabel, onClose, canManage, isDecided }: {
11
+ export declare function ConfirmDialog({ id, action, winnerLabel, onClose, canManage, isDecided, apiRoute }: {
12
+ apiRoute: string;
12
13
  canManage?: boolean;
13
14
  isDecided?: boolean;
14
15
  id: DocId;
@@ -20,8 +20,8 @@ export function DialogFrame({ title, onClose, children, initialFocus, dismissibl
20
20
  children: children
21
21
  });
22
22
  }
23
- export function ConfirmDialog({ id, action, winnerLabel, onClose, canManage = false, isDecided = false }) {
24
- const api = useApi();
23
+ export function ConfirmDialog({ id, action, winnerLabel, onClose, canManage = false, isDecided = false, apiRoute }) {
24
+ const api = useApi(apiRoute);
25
25
  const [typed, setTyped] = useState('');
26
26
  const [busy, setBusy] = useState(false);
27
27
  const [reportBusy, setReportBusy] = useState(false);
@@ -1,7 +1,8 @@
1
1
  import type { DocId } from '../../types/contracts.js';
2
2
  import type { PreflightResult } from '../../types/endpoints.js';
3
3
  import type { ActionGroup } from './actions.model.js';
4
- export declare function DecideDialog({ id, arms, checks, onClose }: {
4
+ export declare function DecideDialog({ id, arms, checks, onClose, apiRoute }: {
5
+ apiRoute: string;
5
6
  id: DocId;
6
7
  arms: readonly ActionGroup[];
7
8
  checks?: PreflightResult['checks'];
@@ -10,8 +10,8 @@ import { RESULTS_COPY } from '../copy/results.js';
10
10
  import { ACTIONS_COPY as C, decideCopy } from '../copy/actions.js';
11
11
  import { decideState } from './decide.model.js';
12
12
  import { DialogFrame } from './ConfirmDialog.js';
13
- export function DecideDialog({ id, arms, checks = [], onClose }) {
14
- const api = useApi();
13
+ export function DecideDialog({ id, arms, checks = [], onClose, apiRoute }) {
14
+ const api = useApi(apiRoute);
15
15
  const [snapshot, setSnapshot] = useState(null);
16
16
  const [chosenArm, setChosenArm] = useState(null);
17
17
  const [note, setNote] = useState('');
@@ -1,5 +1,5 @@
1
1
  import type { DocId } from '../types/contracts.js';
2
- import type { RestoreContentEntryBody, RestoreContentEntryResult, DismissContentEntryBody, DismissContentEntryResult, AddMetricBody, AllocateBody, ArchiveBody, DecideBody, MakeContentResult, PreflightResult, PreviewTokenResult, RefusalCode, ResultsResponse, SummaryResponse, TransitionSuccess } from '../types/endpoints.js';
2
+ import type { WrapBlockBody, PagesResult, PlacementStatusBody, PlacementStatusResult, RestoreContentEntryBody, RestoreContentEntryResult, DismissContentEntryBody, DismissContentEntryResult, AddMetricBody, AllocateBody, ArchiveBody, DecideBody, MakeContentResult, AddMetricResult, ClickKeysResult, PreflightResult, PreviewTokenResult, RefusalCode, ResultsResponse, SummaryResponse, TransitionSuccess } from '../types/endpoints.js';
3
3
  import { refusalMessage } from './copy/common.js';
4
4
  export type ApiResult<T> = {
5
5
  ok: true;
@@ -13,9 +13,24 @@ export type ApiResult<T> = {
13
13
  };
14
14
  type RequestOptions = NonNullable<Parameters<typeof refusalMessage>[3]>;
15
15
  export declare function createApi(apiBase: string): {
16
+ findTestIdBySlug: (slug: string) => Promise<Extract<ApiResult<DocId>, {
17
+ ok: false;
18
+ }> | {
19
+ ok: true;
20
+ data: DocId;
21
+ }>;
22
+ deleteTest: (id: DocId) => Promise<ApiResult<unknown>>;
23
+ wrapBlock: (body: WrapBlockBody) => Promise<ApiResult<Record<string, unknown>>>;
24
+ pages: (query: {
25
+ collection: string;
26
+ q?: string;
27
+ path?: string;
28
+ page?: number;
29
+ }, signal?: AbortSignal) => Promise<ApiResult<PagesResult>>;
30
+ placementStatus: (body: PlacementStatusBody) => Promise<ApiResult<PlacementStatusResult>>;
16
31
  start: (id: DocId) => Promise<ApiResult<TransitionSuccess>>;
17
32
  allocate: (id: DocId, body: AllocateBody, options?: RequestOptions) => Promise<ApiResult<TransitionSuccess>>;
18
- addMetric: (id: DocId, body: AddMetricBody, options?: RequestOptions) => Promise<ApiResult<TransitionSuccess>>;
33
+ addMetric: (id: DocId, body: AddMetricBody, options?: RequestOptions) => Promise<ApiResult<AddMetricResult>>;
19
34
  stop: (id: DocId) => Promise<ApiResult<TransitionSuccess>>;
20
35
  decide: (id: DocId, body: DecideBody) => Promise<ApiResult<TransitionSuccess>>;
21
36
  releaseHoldout: (id: DocId) => Promise<ApiResult<TransitionSuccess>>;
@@ -24,6 +39,7 @@ export declare function createApi(apiBase: string): {
24
39
  dismissContentEntry: (id: DocId, body: DismissContentEntryBody) => Promise<ApiResult<DismissContentEntryResult>>;
25
40
  restoreContentEntry: (id: DocId, body: RestoreContentEntryBody) => Promise<ApiResult<RestoreContentEntryResult>>;
26
41
  archive: (id: DocId, body: ArchiveBody) => Promise<ApiResult<TransitionSuccess>>;
42
+ clickKeys: (id: DocId) => Promise<ApiResult<ClickKeysResult>>;
27
43
  preflight: (id: DocId) => Promise<ApiResult<PreflightResult>>;
28
44
  previewToken: (id: DocId, arm: string) => Promise<ApiResult<PreviewTokenResult>>;
29
45
  results: (id: DocId, options?: {
package/dist/admin/api.js CHANGED
@@ -1,13 +1,18 @@
1
1
  import { API_MESSAGES, refusalMessage } from './copy/common.js';
2
+ import { BLOCK_COPY } from './copy/blocks.js';
2
3
  import { CONTENT_ENTRY_REFUSALS } from './copy/actions.js';
3
4
  export function createApi(apiBase) {
5
+ const collectionBase = `${apiBase.replace(/\/$/, '')}/xp-experiments`;
4
6
  const base = `${apiBase.replace(/\/$/, '')}/xp/experiments`;
5
7
  const route = (id, action)=>`${base}/${encodeURIComponent(id)}/${action}`;
6
- async function request(path, method, body, options) {
8
+ async function request(path, method, body, options, signal) {
7
9
  try {
8
10
  const response = await fetch(path(), {
9
11
  method,
10
12
  credentials: 'include',
13
+ ...signal ? {
14
+ signal
15
+ } : {},
11
16
  ...body === undefined ? {} : {
12
17
  headers: {
13
18
  'Content-Type': 'application/json'
@@ -48,7 +53,7 @@ export function createApi(apiBase) {
48
53
  code
49
54
  } : {},
50
55
  message: refusalMessage(code, response.status, typeof error?.message === 'string' ? error.message : undefined, options),
51
- retryable: code === 'commit-unverified' || response.status >= 500 && response.status !== 501
56
+ retryable: code === 'page-busy' || code === 'commit-unverified' || response.status >= 500 && response.status !== 501
52
57
  };
53
58
  } catch {
54
59
  return {
@@ -61,6 +66,45 @@ export function createApi(apiBase) {
61
66
  }
62
67
  const post = (id, action, body, options)=>request(()=>route(id, action), 'POST', body, options);
63
68
  return {
69
+ findTestIdBySlug: async (slug)=>{
70
+ const result = await request(()=>`${collectionBase}?where[slug][equals]=${encodeURIComponent(slug)}&depth=0&limit=1`, 'GET');
71
+ if (!result.ok) return result;
72
+ const id = result.data?.docs?.[0]?.id;
73
+ if (id === undefined) return {
74
+ ok: false,
75
+ status: 404,
76
+ code: 'not-found',
77
+ message: refusalMessage('not-found', 404, undefined, {
78
+ notFound: BLOCK_COPY.alreadyDeleted
79
+ }),
80
+ retryable: false
81
+ };
82
+ return {
83
+ ok: true,
84
+ data: id
85
+ };
86
+ },
87
+ deleteTest: (id)=>request(()=>`${collectionBase}/${encodeURIComponent(id)}`, 'DELETE', undefined, {
88
+ notFound: BLOCK_COPY.noLongerDraft,
89
+ forbidden: BLOCK_COPY.noLongerDraft,
90
+ codes: {
91
+ 'invalid-status': BLOCK_COPY.noLongerDraft
92
+ }
93
+ }),
94
+ wrapBlock: (body)=>request(()=>`${apiBase.replace(/\/$/, '')}/xp/blocks/wrap`, 'POST', body),
95
+ pages: (query, signal)=>request(()=>`${apiBase.replace(/\/$/, '')}/xp/pages?${new URLSearchParams({
96
+ collection: query.collection,
97
+ ...query.q ? {
98
+ q: query.q
99
+ } : {},
100
+ ...query.path !== undefined ? {
101
+ path: query.path
102
+ } : {},
103
+ ...query.page ? {
104
+ page: String(query.page)
105
+ } : {}
106
+ })}`, 'GET', undefined, undefined, signal),
107
+ placementStatus: (body)=>request(()=>`${apiBase.replace(/\/$/, '')}/xp/placements/status`, 'POST', body),
64
108
  start: (id)=>post(id, 'start'),
65
109
  allocate: (id, body, options)=>post(id, 'allocate', body, options),
66
110
  addMetric: (id, body, options)=>post(id, 'add-metric', body, options),
@@ -72,6 +116,7 @@ export function createApi(apiBase) {
72
116
  dismissContentEntry: (id, body)=>post(id, 'content-report/dismiss', body, CONTENT_ENTRY_REFUSALS),
73
117
  restoreContentEntry: (id, body)=>post(id, 'content-report/restore', body, CONTENT_ENTRY_REFUSALS),
74
118
  archive: (id, body)=>post(id, 'archive', body),
119
+ clickKeys: (id)=>request(()=>route(id, 'click-keys'), 'GET'),
75
120
  preflight: (id)=>request(()=>route(id, 'preflight'), 'GET'),
76
121
  previewToken: (id, arm)=>request(()=>`${route(id, 'preview-token')}?arm=${encodeURIComponent(arm)}`, 'GET'),
77
122
  results: (id, options = {})=>request(()=>{
@@ -0,0 +1,4 @@
1
+ import type { UIFieldClientProps } from 'payload';
2
+ export declare function BlockStatusField({ path }: UIFieldClientProps): import("react").JSX.Element | null;
3
+ export declare function BlockSlotRowLabel(): import("react").JSX.Element;
4
+ export declare function CopyOriginalField({ path }: UIFieldClientProps): import("react").JSX.Element | null;