@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
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:`. 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
@@ -647,32 +679,61 @@ configured to allow client writes; record revenue on the server.
647
679
 
648
680
  ### Puck render and editor configs
649
681
 
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:
682
+ Works with: payload-puck ≥0.10.3 (experiments ≥0.4.0).
683
+
684
+ For payload-puck hosts, add one integration object on each side. In the render
685
+ config, pass `integrations` to `extendConfig`:
652
686
 
653
687
  ```ts
654
- import { createExperimentSlot } from '@delmaredigital/payload-experiments/puck'
655
- import { createExperimentSlotEditor, experimentOutlinePlugin } from '@delmaredigital/payload-experiments/puck/editor'
688
+ import { extendConfig } from '@delmaredigital/payload-puck/config'
689
+ import { experimentsPuck } from '@delmaredigital/payload-experiments/puck'
690
+ import { baseConfig } from './baseConfig'
656
691
 
657
- const routes = { admin: '/manage', api: '/content-api' } // Same as Payload config.routes
658
- const accent = '#1f3a68' // Same as the plugin's theme.accent
692
+ export const renderConfig = extendConfig({
693
+ base: baseConfig,
694
+ integrations: [experimentsPuck()],
695
+ })
696
+ ```
659
697
 
660
- // Render config
661
- components: { ExperimentSlot: createExperimentSlot({ routes }) }
698
+ Call `experimentsPuckEditor()` once at module scope in a `'use client'` module,
699
+ then pass that array to `PuckConfigProvider`. A new object per render rebuilds
700
+ Puck's config and plugin list.
662
701
 
663
- // Editor config
664
- components: { ExperimentSlot: createExperimentSlotEditor({ routes, accent }) }
665
- ```
702
+ ```tsx
703
+ 'use client'
666
704
 
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.
705
+ import { PuckConfigProvider } from '@delmaredigital/payload-puck/client'
706
+ import { experimentsPuckEditor } from '@delmaredigital/payload-experiments/puck/editor'
707
+ import { editorConfig } from './editorConfig'
671
708
 
672
- ```ts
673
- plugins: [experimentOutlinePlugin()]
709
+ const integrations = [experimentsPuckEditor()]
710
+
711
+ export function PuckProvider({ children }: { children: React.ReactNode }) {
712
+ return <PuckConfigProvider config={editorConfig} integrations={integrations}>
713
+ {children}
714
+ </PuckConfigProvider>
715
+ }
674
716
  ```
675
717
 
718
+ Each object includes the test block and its **A/B tests** sidebar category. The
719
+ editor object also includes the outline plugin: it hides empty version slots
720
+ that the block's test doesn't use, while content in unused slots stays visible
721
+ with a warning. Both helpers accept `{ routes }`; the editor also accepts
722
+ `accent`. Use the host's Payload routes and the plugin's `theme.accent`.
723
+
724
+ #### Wiring Puck by hand
725
+
726
+ The separate pieces stay exported:
727
+
728
+ - `/puck`: `ExperimentSlot`, `createExperimentSlot({ routes })`,
729
+ `EXPERIMENT_SLOT_TYPE` and `createExperimentSlotField({ routes })`.
730
+ - `/puck/editor`: `ExperimentSlotEditor`,
731
+ `createExperimentSlotEditor({ routes, accent })` and `experimentOutlinePlugin()`.
732
+
733
+ Register the render and editor blocks under the same `EXPERIMENT_SLOT_TYPE` key (import it from `/puck`),
734
+ add a sidebar category listing it, and add `experimentOutlinePlugin()` to Puck's
735
+ `plugins`. It composes with the host's outline override.
736
+
676
737
  Routes default to Payload's `/admin` and `/api` when omitted. The existing
677
738
  `ExperimentSlot` and `ExperimentSlotEditor` exports remain available with these
678
739
  defaults. Hosts on non-default routes switch to the factories. The standalone
@@ -815,3 +876,35 @@ The WCAG 2.x gate requires at least 4.5:1 **foreground text on the accent fill**
815
876
  in each theme (the accent is used as a fill, not as page text). A failing
816
877
  colour is refused with its ratio. Status chips retain Payload semantic colours.
817
878
  `deriveAccent` is also exported from `/puck/editor` for editor integrations.
879
+
880
+ ## Click goals
881
+
882
+ Click goals use a goal's short name as their key (`click:<short-name>`). Add
883
+ `xpGoalField()` from the root entry to a host block or array row. Its manager
884
+ picker includes inline creation with "+ New click goal"; run
885
+ `payload generate:importmap` to register the picker. For Puck's `xpGoal` prop,
886
+ use `xpGoalPuckEditorField()` from `/puck/editor` in your editor config and
887
+ `xpGoalPuckField()` from `/puck` in your render config. Run the
888
+ host's `payload migrate:create`: each table using the Payload field (and its
889
+ version table) gains a nullable text column. Render the value on the actual
890
+ interactive element as `data-xp-goal={xpGoal}`; code can also set
891
+ `data-xp-goal="join"` directly. Define a click goal with that short name first.
892
+ For managers, an undefined key blocks the save through the Payload field's
893
+ server validation, refreshing a cached miss immediately.
894
+ Draft saves are also refused when the collection validates drafts
895
+ (`versions.drafts.validate`). Editors without goal-management access see a
896
+ plain text input. Goals in
897
+ related documents and keys placed only in code are invisible to the content
898
+ fairness check.
899
+
900
+ `ExperimentsProvider` records native clicks when its server assignments arm
901
+ click goals. Nested providers share one document listener; the deepest armed
902
+ provider supplies the route and pageview id. Enter on links/buttons and Space
903
+ on buttons count through the browser's native click. A focusable `div` needs
904
+ `role="button"` and the host's own keyboard handler. Beacons use `sendBeacon`
905
+ (with a keepalive fallback) so navigation/unload does not discard the click.
906
+ Click and exposure beacons share `events.rateLimit`; size the adapter for both.
907
+ New click goals are accepted within 30 seconds. A click goal may be primary;
908
+ primary and guardrail goals must appear in every tested version or outside all
909
+ of them. Secondary asymmetry warns, and results health checks published rows
910
+ 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);
@@ -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,17 @@ export type ApiResult<T> = {
13
13
  };
14
14
  type RequestOptions = NonNullable<Parameters<typeof refusalMessage>[3]>;
15
15
  export declare function createApi(apiBase: string): {
16
+ wrapBlock: (body: WrapBlockBody) => Promise<ApiResult<Record<string, unknown>>>;
17
+ pages: (query: {
18
+ collection: string;
19
+ q?: string;
20
+ path?: string;
21
+ page?: number;
22
+ }, signal?: AbortSignal) => Promise<ApiResult<PagesResult>>;
23
+ placementStatus: (body: PlacementStatusBody) => Promise<ApiResult<PlacementStatusResult>>;
16
24
  start: (id: DocId) => Promise<ApiResult<TransitionSuccess>>;
17
25
  allocate: (id: DocId, body: AllocateBody, options?: RequestOptions) => Promise<ApiResult<TransitionSuccess>>;
18
- addMetric: (id: DocId, body: AddMetricBody, options?: RequestOptions) => Promise<ApiResult<TransitionSuccess>>;
26
+ addMetric: (id: DocId, body: AddMetricBody, options?: RequestOptions) => Promise<ApiResult<AddMetricResult>>;
19
27
  stop: (id: DocId) => Promise<ApiResult<TransitionSuccess>>;
20
28
  decide: (id: DocId, body: DecideBody) => Promise<ApiResult<TransitionSuccess>>;
21
29
  releaseHoldout: (id: DocId) => Promise<ApiResult<TransitionSuccess>>;
@@ -24,6 +32,7 @@ export declare function createApi(apiBase: string): {
24
32
  dismissContentEntry: (id: DocId, body: DismissContentEntryBody) => Promise<ApiResult<DismissContentEntryResult>>;
25
33
  restoreContentEntry: (id: DocId, body: RestoreContentEntryBody) => Promise<ApiResult<RestoreContentEntryResult>>;
26
34
  archive: (id: DocId, body: ArchiveBody) => Promise<ApiResult<TransitionSuccess>>;
35
+ clickKeys: (id: DocId) => Promise<ApiResult<ClickKeysResult>>;
27
36
  preflight: (id: DocId) => Promise<ApiResult<PreflightResult>>;
28
37
  previewToken: (id: DocId, arm: string) => Promise<ApiResult<PreviewTokenResult>>;
29
38
  results: (id: DocId, options?: {
package/dist/admin/api.js CHANGED
@@ -3,11 +3,14 @@ import { CONTENT_ENTRY_REFUSALS } from './copy/actions.js';
3
3
  export function createApi(apiBase) {
4
4
  const base = `${apiBase.replace(/\/$/, '')}/xp/experiments`;
5
5
  const route = (id, action)=>`${base}/${encodeURIComponent(id)}/${action}`;
6
- async function request(path, method, body, options) {
6
+ async function request(path, method, body, options, signal) {
7
7
  try {
8
8
  const response = await fetch(path(), {
9
9
  method,
10
10
  credentials: 'include',
11
+ ...signal ? {
12
+ signal
13
+ } : {},
11
14
  ...body === undefined ? {} : {
12
15
  headers: {
13
16
  'Content-Type': 'application/json'
@@ -48,7 +51,7 @@ export function createApi(apiBase) {
48
51
  code
49
52
  } : {},
50
53
  message: refusalMessage(code, response.status, typeof error?.message === 'string' ? error.message : undefined, options),
51
- retryable: code === 'commit-unverified' || response.status >= 500 && response.status !== 501
54
+ retryable: code === 'page-busy' || code === 'commit-unverified' || response.status >= 500 && response.status !== 501
52
55
  };
53
56
  } catch {
54
57
  return {
@@ -61,6 +64,20 @@ export function createApi(apiBase) {
61
64
  }
62
65
  const post = (id, action, body, options)=>request(()=>route(id, action), 'POST', body, options);
63
66
  return {
67
+ wrapBlock: (body)=>request(()=>`${apiBase.replace(/\/$/, '')}/xp/blocks/wrap`, 'POST', body),
68
+ pages: (query, signal)=>request(()=>`${apiBase.replace(/\/$/, '')}/xp/pages?${new URLSearchParams({
69
+ collection: query.collection,
70
+ ...query.q ? {
71
+ q: query.q
72
+ } : {},
73
+ ...query.path !== undefined ? {
74
+ path: query.path
75
+ } : {},
76
+ ...query.page ? {
77
+ page: String(query.page)
78
+ } : {}
79
+ })}`, 'GET', undefined, undefined, signal),
80
+ placementStatus: (body)=>request(()=>`${apiBase.replace(/\/$/, '')}/xp/placements/status`, 'POST', body),
64
81
  start: (id)=>post(id, 'start'),
65
82
  allocate: (id, body, options)=>post(id, 'allocate', body, options),
66
83
  addMetric: (id, body, options)=>post(id, 'add-metric', body, options),
@@ -72,6 +89,7 @@ export function createApi(apiBase) {
72
89
  dismissContentEntry: (id, body)=>post(id, 'content-report/dismiss', body, CONTENT_ENTRY_REFUSALS),
73
90
  restoreContentEntry: (id, body)=>post(id, 'content-report/restore', body, CONTENT_ENTRY_REFUSALS),
74
91
  archive: (id, body)=>post(id, 'archive', body),
92
+ clickKeys: (id)=>request(()=>route(id, 'click-keys'), 'GET'),
75
93
  preflight: (id)=>request(()=>route(id, 'preflight'), 'GET'),
76
94
  previewToken: (id, arm)=>request(()=>`${route(id, 'preview-token')}?arm=${encodeURIComponent(arm)}`, 'GET'),
77
95
  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;
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Banner, Button, ConfirmationModal, useModal, useFormInitializing, useRowLabel } from '@payloadcms/ui';
4
+ import { useEffect, useId, useRef, useState } from 'react';
5
+ import { copyOriginalIntoVersion, syncExperimentSlots, unwrapExperimentBlock } from '../../blocks/formState.js';
6
+ import { BLOCK_COPY, blockSyncNotice } from '../copy/blocks.js';
7
+ import { API_MESSAGES } from '../copy/common.js';
8
+ import { isOrphan, liveStatus, slotLabel, statusSentence } from './model.js';
9
+ import { useBlockStatus } from './statusCache.js';
10
+ import { useBlockForm } from './useBlockForm.js';
11
+ export function BlockStatusField({ path }) {
12
+ const context = useBlockForm(path), { block, field, manager, config, collection, replace } = context;
13
+ const initializing = useFormInitializing();
14
+ const status = useBlockStatus(config.routes.api, block?.experiment, true);
15
+ const synced = useRef(false), syncing = useRef(false);
16
+ const [notice, setNotice] = useState(false), [error, setError] = useState(), [busy, setBusy] = useState(false);
17
+ const [syncFailed, setSyncFailed] = useState(false);
18
+ const canSync = !initializing && manager && block && field && liveStatus(status) && [
19
+ 'draft',
20
+ 'stopped'
21
+ ].includes(status.status);
22
+ async function sync() {
23
+ if (!canSync || syncing.current) return;
24
+ syncing.current = true;
25
+ setBusy(true);
26
+ setSyncFailed(false);
27
+ try {
28
+ const next = syncExperimentSlots(block, status.arms, field);
29
+ if (JSON.stringify(next.slots) !== JSON.stringify(block.slots)) {
30
+ await replace([
31
+ next
32
+ ]);
33
+ setNotice(true);
34
+ }
35
+ } catch {
36
+ setSyncFailed(true);
37
+ } finally{
38
+ syncing.current = false;
39
+ setBusy(false);
40
+ }
41
+ }
42
+ useEffect(()=>{
43
+ if (synced.current || !canSync) return;
44
+ synced.current = true;
45
+ void sync();
46
+ });
47
+ if (!status) return null;
48
+ async function unwrap() {
49
+ if (!block || !field || busy) return;
50
+ setBusy(true);
51
+ setError(undefined);
52
+ try {
53
+ await replace(unwrapExperimentBlock(block, field));
54
+ } catch {
55
+ setError(API_MESSAGES.failed);
56
+ } finally{
57
+ setBusy(false);
58
+ }
59
+ }
60
+ const sentence = statusSentence(status);
61
+ return /*#__PURE__*/ _jsxs("div", {
62
+ children: [
63
+ liveStatus(status) && status.status === 'running' ? /*#__PURE__*/ _jsx(Banner, {
64
+ children: sentence
65
+ }) : /*#__PURE__*/ _jsx("p", {
66
+ children: sentence
67
+ }),
68
+ notice ? /*#__PURE__*/ _jsx("p", {
69
+ role: "status",
70
+ children: blockSyncNotice(Boolean(collection?.versions?.drafts))
71
+ }) : null,
72
+ manager && isOrphan(status) ? /*#__PURE__*/ _jsx(Button, {
73
+ disabled: busy,
74
+ onClick: ()=>void unwrap(),
75
+ children: BLOCK_COPY.unwrap
76
+ }) : null,
77
+ error ? /*#__PURE__*/ _jsx("p", {
78
+ role: "alert",
79
+ children: error
80
+ }) : null,
81
+ syncFailed ? /*#__PURE__*/ _jsx("p", {
82
+ role: "alert",
83
+ children: API_MESSAGES.failed
84
+ }) : null,
85
+ syncFailed && canSync ? /*#__PURE__*/ _jsx(Button, {
86
+ disabled: busy,
87
+ onClick: ()=>void sync(),
88
+ children: BLOCK_COPY.retry
89
+ }) : null
90
+ ]
91
+ });
92
+ }
93
+ export function BlockSlotRowLabel() {
94
+ const { data, path } = useRowLabel();
95
+ const { config, block } = useBlockForm(path);
96
+ const status = useBlockStatus(config.routes.api, block?.experiment);
97
+ return /*#__PURE__*/ _jsx("span", {
98
+ children: slotLabel(data?.slot ?? '', status)
99
+ });
100
+ }
101
+ export function CopyOriginalField({ path }) {
102
+ const { block, field, form, manager, config, replace } = useBlockForm(path);
103
+ const status = useBlockStatus(config.routes.api, block?.experiment);
104
+ const rowPath = path.slice(0, path.lastIndexOf('.'));
105
+ const slot = form.getDataByPath(`${rowPath}.slot`);
106
+ const [busy, setBusy] = useState(false), [error, setError] = useState();
107
+ const { openModal } = useModal();
108
+ const modalSlug = `xp-copy-original-${useId()}`;
109
+ if (!manager || !field || !block || !slot || slot === 'control' || !liveStatus(status) || ![
110
+ 'draft',
111
+ 'stopped',
112
+ 'running'
113
+ ].includes(status.status)) return null;
114
+ async function copy() {
115
+ if (!block || !field || busy || !slot) return;
116
+ setBusy(true);
117
+ setError(undefined);
118
+ try {
119
+ await replace([
120
+ copyOriginalIntoVersion(block, slot, field)
121
+ ]);
122
+ } catch {
123
+ setError(API_MESSAGES.failed);
124
+ } finally{
125
+ setBusy(false);
126
+ }
127
+ }
128
+ function requestCopy() {
129
+ if (block?.slots.find((row)=>row.slot === slot)?.blocks?.length) openModal(modalSlug);
130
+ else void copy();
131
+ }
132
+ return /*#__PURE__*/ _jsxs("div", {
133
+ children: [
134
+ /*#__PURE__*/ _jsx(Button, {
135
+ disabled: busy,
136
+ onClick: requestCopy,
137
+ children: BLOCK_COPY.copy
138
+ }),
139
+ /*#__PURE__*/ _jsx(ConfirmationModal, {
140
+ modalSlug: modalSlug,
141
+ heading: BLOCK_COPY.copy,
142
+ body: BLOCK_COPY.confirmCopy,
143
+ confirmLabel: BLOCK_COPY.replace,
144
+ cancelLabel: BLOCK_COPY.cancel,
145
+ onConfirm: copy
146
+ }),
147
+ error ? /*#__PURE__*/ _jsx("p", {
148
+ role: "alert",
149
+ children: error
150
+ }) : null
151
+ ]
152
+ });
153
+ }
@@ -0,0 +1,5 @@
1
+ import type { UIFieldClientProps } from 'payload';
2
+ export declare function TestBlockField(props: UIFieldClientProps & {
3
+ blocksField: string;
4
+ blockSlug?: string;
5
+ }): import("react").JSX.Element | null;