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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +122 -27
  2. package/dist/admin/MetricSourceSelect.js +3 -3
  3. package/dist/admin/PlacementStatus.d.ts +3 -0
  4. package/dist/admin/PlacementStatus.js +54 -0
  5. package/dist/admin/PreviewLinks.d.ts +2 -1
  6. package/dist/admin/PreviewLinks.js +2 -2
  7. package/dist/admin/TestsDashboardCards.d.ts +1 -0
  8. package/dist/admin/TestsDashboardCards.js +4 -8
  9. package/dist/admin/TestsNavLink.d.ts +1 -0
  10. package/dist/admin/TestsNavLink.js +4 -10
  11. package/dist/admin/actions/ConfirmDialog.d.ts +2 -1
  12. package/dist/admin/actions/ConfirmDialog.js +2 -2
  13. package/dist/admin/actions/DecideDialog.d.ts +2 -1
  14. package/dist/admin/actions/DecideDialog.js +2 -2
  15. package/dist/admin/api.d.ts +18 -2
  16. package/dist/admin/api.js +47 -2
  17. package/dist/admin/blocks/BlockPanel.d.ts +4 -0
  18. package/dist/admin/blocks/BlockPanel.js +199 -0
  19. package/dist/admin/blocks/TestBlockField.d.ts +5 -0
  20. package/dist/admin/blocks/TestBlockField.js +163 -0
  21. package/dist/admin/blocks/formApi.d.ts +46 -0
  22. package/dist/admin/blocks/formApi.js +159 -0
  23. package/dist/admin/blocks/inject.d.ts +7 -0
  24. package/dist/admin/blocks/inject.js +41 -0
  25. package/dist/admin/blocks/model.d.ts +17 -0
  26. package/dist/admin/blocks/model.js +45 -0
  27. package/dist/admin/blocks/statusCache.d.ts +9 -0
  28. package/dist/admin/blocks/statusCache.js +92 -0
  29. package/dist/admin/blocks/useBlockForm.d.ts +17 -0
  30. package/dist/admin/blocks/useBlockForm.js +46 -0
  31. package/dist/admin/copy/actions.d.ts +2 -0
  32. package/dist/admin/copy/actions.js +9 -0
  33. package/dist/admin/copy/blocks.d.ts +43 -0
  34. package/dist/admin/copy/blocks.js +64 -0
  35. package/dist/admin/copy/common.d.ts +1 -0
  36. package/dist/admin/copy/common.js +1 -1
  37. package/dist/admin/copy/goals.d.ts +2 -0
  38. package/dist/admin/copy/goals.js +4 -1
  39. package/dist/admin/copy/newTest.d.ts +10 -0
  40. package/dist/admin/copy/newTest.js +10 -0
  41. package/dist/admin/copy/pages.d.ts +24 -0
  42. package/dist/admin/copy/pages.js +24 -0
  43. package/dist/admin/copy/readiness.d.ts +23 -3
  44. package/dist/admin/copy/readiness.js +24 -3
  45. package/dist/admin/copy/results.d.ts +1 -0
  46. package/dist/admin/copy/results.js +1 -0
  47. package/dist/admin/copy/testPage.d.ts +4 -0
  48. package/dist/admin/copy/testPage.js +4 -0
  49. package/dist/admin/goals/XpGoalField.d.ts +4 -0
  50. package/dist/admin/goals/XpGoalField.js +168 -0
  51. package/dist/admin/goals/xpGoalField.css +48 -0
  52. package/dist/admin/metricSource.model.d.ts +2 -0
  53. package/dist/admin/metricSource.model.js +4 -0
  54. package/dist/admin/pageCollections.d.ts +10 -0
  55. package/dist/admin/pageCollections.js +20 -0
  56. package/dist/admin/paths.d.ts +9 -0
  57. package/dist/admin/paths.js +9 -0
  58. package/dist/admin/placementStatus.model.d.ts +2 -0
  59. package/dist/admin/placementStatus.model.js +19 -0
  60. package/dist/admin/placements/placementLabels.d.ts +11 -0
  61. package/dist/admin/placements/placementLabels.js +21 -0
  62. package/dist/admin/placements/placementLabels.model.d.ts +13 -0
  63. package/dist/admin/placements/placementLabels.model.js +12 -0
  64. package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
  65. package/dist/admin/placements/usePlacementLabels.js +102 -0
  66. package/dist/admin/results/DataQualityPanel.js +4 -1
  67. package/dist/admin/results/results.model.d.ts +3 -0
  68. package/dist/admin/results/results.model.js +8 -4
  69. package/dist/admin/surface/TestsHome.js +39 -10
  70. package/dist/admin/surface/TestsSurface.js +7 -6
  71. package/dist/admin/surface/contracts.d.ts +6 -5
  72. package/dist/admin/surface/goals/GoalForm.js +71 -44
  73. package/dist/admin/surface/goals/GoalsList.js +5 -4
  74. package/dist/admin/surface/goals/data.js +4 -1
  75. package/dist/admin/surface/goals/goals.model.d.ts +1 -2
  76. package/dist/admin/surface/goals/goals.model.js +1 -3
  77. package/dist/admin/surface/home.model.d.ts +3 -3
  78. package/dist/admin/surface/home.model.js +8 -4
  79. package/dist/admin/surface/newTest/NewTest.js +56 -17
  80. package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
  81. package/dist/admin/surface/newTest/PageWizard.js +214 -0
  82. package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
  83. package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
  84. package/dist/admin/surface/pagePicker/PageCombobox.d.ts +10 -0
  85. package/dist/admin/surface/pagePicker/PageCombobox.js +395 -0
  86. package/dist/admin/surface/pagePicker/pagePicker.css +10 -0
  87. package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +27 -0
  88. package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
  89. package/dist/admin/surface/readiness/ActionDialogs.js +99 -64
  90. package/dist/admin/surface/readiness/Banner.js +30 -6
  91. package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
  92. package/dist/admin/surface/readiness/readiness.model.js +92 -9
  93. package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
  94. package/dist/admin/surface/results/ReleasePages.js +3 -7
  95. package/dist/admin/surface/results/ResultsBody.js +21 -2
  96. package/dist/admin/surface/results/ResultsHeader.js +5 -13
  97. package/dist/admin/surface/results/useResults.d.ts +20 -1
  98. package/dist/admin/surface/test/AdvancedSection.js +111 -104
  99. package/dist/admin/surface/test/GoalSection.js +94 -14
  100. package/dist/admin/surface/test/LengthSection.js +1 -1
  101. package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
  102. package/dist/admin/surface/test/PageHoldout.js +67 -0
  103. package/dist/admin/surface/test/TestPage.js +5 -9
  104. package/dist/admin/surface/test/VersionsSection.js +12 -5
  105. package/dist/admin/surface/test/fields.d.ts +13 -4
  106. package/dist/admin/surface/test/fields.js +46 -14
  107. package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
  108. package/dist/admin/surface/test/jumpToSection.js +4 -3
  109. package/dist/admin/surface/test/pageParts.d.ts +4 -1
  110. package/dist/admin/surface/test/pageParts.js +49 -1
  111. package/dist/admin/surface/test/testPage.css +3 -1
  112. package/dist/admin/surface/testDocument.model.d.ts +1 -0
  113. package/dist/admin/surface/testDocument.model.js +2 -1
  114. package/dist/admin/surface/useTestDocument.d.ts +15 -2
  115. package/dist/admin/surface/useTestDocument.js +53 -15
  116. package/dist/admin/surfaceUrls.d.ts +7 -0
  117. package/dist/admin/surfaceUrls.js +19 -0
  118. package/dist/admin/theme.css +2 -0
  119. package/dist/admin/useApi.d.ts +21 -2
  120. package/dist/admin/useApi.js +3 -5
  121. package/dist/admin/views/CollectionListRedirect.d.ts +1 -0
  122. package/dist/admin/views/CollectionListRedirect.js +1 -1
  123. package/dist/admin/views/ExperimentsRootView.js +28 -133
  124. package/dist/admin/views/GoalEditRedirect.d.ts +4 -0
  125. package/dist/admin/views/GoalEditRedirect.js +14 -0
  126. package/dist/admin/views/GoalsCollectionListView.d.ts +2 -0
  127. package/dist/admin/views/GoalsCollectionListView.js +4 -0
  128. package/dist/admin/views/TestsCollectionListView.d.ts +2 -0
  129. package/dist/admin/views/TestsCollectionListView.js +4 -0
  130. package/dist/admin/views/TestsNavLinkServer.d.ts +1 -0
  131. package/dist/admin/views/TestsNavLinkServer.js +1 -1
  132. package/dist/admin/views/buildSurfaceProps.d.ts +8 -0
  133. package/dist/admin/views/buildSurfaceProps.js +143 -0
  134. package/dist/admin/views/renderCollectionList.d.ts +2 -0
  135. package/dist/admin/views/renderCollectionList.js +34 -0
  136. package/dist/admin/views/renderTestsSurface.d.ts +3 -0
  137. package/dist/admin/views/renderTestsSurface.js +34 -0
  138. package/dist/blocks/bindings.d.ts +3 -0
  139. package/dist/blocks/bindings.js +34 -0
  140. package/dist/blocks/createExperimentBlock.d.ts +6 -0
  141. package/dist/blocks/createExperimentBlock.js +77 -0
  142. package/dist/blocks/formState.d.ts +31 -0
  143. package/dist/blocks/formState.js +146 -0
  144. package/dist/blocks/model.d.ts +41 -0
  145. package/dist/blocks/model.js +34 -0
  146. package/dist/client/ExperimentGroup.d.ts +6 -0
  147. package/dist/client/ExperimentGroup.js +9 -0
  148. package/dist/client/ExperimentsProvider.js +20 -4
  149. package/dist/client/clickGoals.d.ts +4 -0
  150. package/dist/client/clickGoals.js +43 -0
  151. package/dist/collections/experiments/config.js +21 -7
  152. package/dist/collections/experiments/deleteGuard.d.ts +2 -1
  153. package/dist/collections/experiments/deleteGuard.js +12 -1
  154. package/dist/collections/experiments/editability.js +4 -2
  155. package/dist/collections/experiments/pickSlug.d.ts +2 -0
  156. package/dist/collections/experiments/pickSlug.js +8 -0
  157. package/dist/collections/labels.d.ts +3 -0
  158. package/dist/collections/labels.js +20 -0
  159. package/dist/collections/metrics.js +28 -6
  160. package/dist/collections/phases.js +1 -0
  161. package/dist/collections/serialize.js +7 -0
  162. package/dist/db/clickGoals.d.ts +9 -0
  163. package/dist/db/clickGoals.js +22 -0
  164. package/dist/db/delete.d.ts +1 -0
  165. package/dist/db/delete.js +1 -0
  166. package/dist/db/locks.d.ts +8 -0
  167. package/dist/db/locks.js +27 -4
  168. package/dist/db/names.d.ts +4 -0
  169. package/dist/db/names.js +4 -0
  170. package/dist/db/pageBindings.d.ts +4 -0
  171. package/dist/db/pageBindings.js +17 -0
  172. package/dist/db/pageBusy.d.ts +2 -0
  173. package/dist/db/pageBusy.js +10 -0
  174. package/dist/db/pageContent.d.ts +10 -0
  175. package/dist/db/pageContent.js +79 -0
  176. package/dist/db/placements.d.ts +33 -0
  177. package/dist/db/placements.js +54 -0
  178. package/dist/db/slugs.d.ts +8 -0
  179. package/dist/db/slugs.js +40 -0
  180. package/dist/db/snapshots.d.ts +6 -0
  181. package/dist/db/snapshots.js +8 -5
  182. package/dist/endpoints/clickKeys.d.ts +5 -0
  183. package/dist/endpoints/clickKeys.js +17 -0
  184. package/dist/endpoints/events.js +4 -1
  185. package/dist/endpoints/index.js +28 -0
  186. package/dist/endpoints/pages.d.ts +5 -0
  187. package/dist/endpoints/pages.js +120 -0
  188. package/dist/endpoints/placementStatus.d.ts +6 -0
  189. package/dist/endpoints/placementStatus.js +238 -0
  190. package/dist/endpoints/readBody.d.ts +2 -0
  191. package/dist/endpoints/readBody.js +24 -0
  192. package/dist/endpoints/results.js +5 -1
  193. package/dist/endpoints/summary.js +11 -4
  194. package/dist/endpoints/wrapBlock.d.ts +6 -0
  195. package/dist/endpoints/wrapBlock.js +76 -0
  196. package/dist/errors.d.ts +3 -3
  197. package/dist/errors.js +4 -4
  198. package/dist/events/rows.d.ts +1 -1
  199. package/dist/events/rows.js +5 -3
  200. package/dist/exports/adminClient.d.ts +5 -0
  201. package/dist/exports/adminClient.js +5 -0
  202. package/dist/exports/adminRsc.d.ts +3 -0
  203. package/dist/exports/adminRsc.js +3 -0
  204. package/dist/exports/blocks.d.ts +2 -0
  205. package/dist/exports/blocks.js +1 -0
  206. package/dist/exports/client.d.ts +1 -0
  207. package/dist/exports/client.js +1 -0
  208. package/dist/exports/index.d.ts +6 -1
  209. package/dist/exports/index.js +3 -0
  210. package/dist/exports/puck.d.ts +3 -1
  211. package/dist/exports/puck.js +2 -0
  212. package/dist/exports/puckEditor.d.ts +3 -0
  213. package/dist/exports/puckEditor.js +2 -0
  214. package/dist/exports/rsc.d.ts +3 -0
  215. package/dist/exports/rsc.js +2 -0
  216. package/dist/fields/xpGoal.d.ts +3 -0
  217. package/dist/fields/xpGoal.js +16 -0
  218. package/dist/globals/state.js +10 -0
  219. package/dist/goals/cache.d.ts +3 -0
  220. package/dist/goals/cache.js +35 -0
  221. package/dist/goals/fairness.d.ts +40 -0
  222. package/dist/goals/fairness.js +200 -0
  223. package/dist/goals/key.d.ts +4 -0
  224. package/dist/goals/key.js +3 -0
  225. package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
  226. package/dist/goals/picker/clickGoalPicker.model.js +45 -0
  227. package/dist/goals/picker/copy.d.ts +16 -0
  228. package/dist/goals/picker/copy.js +15 -0
  229. package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
  230. package/dist/goals/picker/useClickGoalPicker.js +220 -0
  231. package/dist/goals/validate.d.ts +3 -0
  232. package/dist/goals/validate.js +14 -0
  233. package/dist/hooks/pageGuard.d.ts +5 -1
  234. package/dist/hooks/pageGuard.js +157 -95
  235. package/dist/hooks/puckGoals.d.ts +3 -0
  236. package/dist/hooks/puckGoals.js +38 -0
  237. package/dist/onInit.js +29 -0
  238. package/dist/options.d.ts +2 -0
  239. package/dist/options.js +41 -1
  240. package/dist/plugin.js +50 -43
  241. package/dist/puck/copy.d.ts +1 -0
  242. package/dist/puck/copy.js +1 -0
  243. package/dist/puck/editor/editor.chrome.css +14 -0
  244. package/dist/puck/editor/editor.css +106 -0
  245. package/dist/puck/editor/editor.model.d.ts +4 -2
  246. package/dist/puck/editor/editor.model.js +6 -3
  247. package/dist/puck/editor/integration.d.ts +10 -0
  248. package/dist/puck/editor/integration.js +30 -0
  249. package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
  250. package/dist/puck/editor/xpGoalPuckEditorField.js +142 -0
  251. package/dist/puck/integration.d.ts +6 -0
  252. package/dist/puck/integration.js +21 -0
  253. package/dist/puck/xpGoalField.d.ts +3 -0
  254. package/dist/puck/xpGoalField.js +6 -0
  255. package/dist/results/health.d.ts +4 -0
  256. package/dist/results/health.js +27 -0
  257. package/dist/results/snapshot.js +15 -2
  258. package/dist/sanitizeReadinessDetail.js +43 -0
  259. package/dist/server/pageKindSeoContract.d.ts +27 -0
  260. package/dist/server/pageKindSeoContract.js +80 -0
  261. package/dist/server/recordContentChange.js +58 -8
  262. package/dist/server/resolveExperimentBlocks.d.ts +5 -0
  263. package/dist/server/resolveExperimentBlocks.js +54 -0
  264. package/dist/server/runResults.js +56 -15
  265. package/dist/server/toClientAssignments.js +2 -1
  266. package/dist/transitions/addMetric.d.ts +2 -1
  267. package/dist/transitions/addMetric.js +28 -1
  268. package/dist/transitions/checkStart.d.ts +14 -1
  269. package/dist/transitions/checkStart.js +92 -8
  270. package/dist/transitions/decide.js +25 -0
  271. package/dist/transitions/deleteExperiment.d.ts +3 -1
  272. package/dist/transitions/deleteExperiment.js +27 -1
  273. package/dist/transitions/kernel.js +4 -0
  274. package/dist/transitions/makeContent.d.ts +5 -0
  275. package/dist/transitions/makeContent.js +39 -11
  276. package/dist/transitions/pageCollection.d.ts +10 -0
  277. package/dist/transitions/pageCollection.js +20 -0
  278. package/dist/transitions/pageVersions.d.ts +10 -0
  279. package/dist/transitions/pageVersions.js +44 -0
  280. package/dist/transitions/phaseRecord.d.ts +1 -0
  281. package/dist/transitions/placementInteractions.d.ts +40 -0
  282. package/dist/transitions/placementInteractions.js +94 -0
  283. package/dist/transitions/placements.d.ts +8 -0
  284. package/dist/transitions/placements.js +148 -1
  285. package/dist/transitions/preflightData.d.ts +2 -2
  286. package/dist/transitions/preflightData.js +137 -5
  287. package/dist/transitions/resolveCaps.d.ts +1 -0
  288. package/dist/transitions/resolveCaps.js +6 -2
  289. package/dist/transitions/start.js +46 -1
  290. package/dist/types/admin.d.ts +1 -1
  291. package/dist/types/assignments.d.ts +1 -0
  292. package/dist/types/contracts.d.ts +15 -0
  293. package/dist/types/endpoints.d.ts +92 -5
  294. package/dist/types/index.d.ts +3 -0
  295. package/dist/types/puck.d.ts +7 -1
  296. package/dist/types/snapshot.d.ts +17 -4
  297. package/dist/types/surface.d.ts +13 -0
  298. package/dist/ui/ui.css +4 -0
  299. package/package.json +8 -1
@@ -0,0 +1,199 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Banner, Button, ConfirmationModal, toast, useModal, useFormInitializing, useRowLabel } from '@payloadcms/ui';
4
+ import { formatAdminURL } from 'payload/shared';
5
+ import { useEffect, useId, useRef, useState } from 'react';
6
+ import { copyOriginalIntoVersion, syncExperimentSlots, unwrapExperimentBlock } from '../../blocks/formState.js';
7
+ import { BLOCK_COPY, blockSyncNotice, blockUnwrapNotice, blockDeleteConfirmation, blockUnwrapConfirmation, blockRunningUnwrap, blockDecidedUnwrap, blockFinishedCopy } from '../copy/blocks.js';
8
+ import { API_MESSAGES } from '../copy/common.js';
9
+ import { isOrphan, liveStatus, slotLabel, statusSentence } from './model.js';
10
+ import { createApi } from '../api.js';
11
+ import { useBlockStatus, setBlockStatus } from './statusCache.js';
12
+ import { useBlockForm } from './useBlockForm.js';
13
+ import { BlockChangedError, RefusalError } from './formApi.js';
14
+ function errorMessage(error) {
15
+ return error instanceof BlockChangedError || error instanceof RefusalError ? error.message : API_MESSAGES.failed;
16
+ }
17
+ export function BlockStatusField({ path }) {
18
+ const context = useBlockForm(path), { block, field, manager, config, collection, replace } = context;
19
+ const initializing = useFormInitializing();
20
+ const status = useBlockStatus(config.routes.api, block?.experiment, true);
21
+ const synced = useRef(false), syncing = useRef(false);
22
+ const [notice, setNotice] = useState(false), [error, setError] = useState(), [busy, setBusy] = useState(false);
23
+ const [syncError, setSyncError] = useState();
24
+ const { openModal } = useModal();
25
+ const modalSlug = `xp-unwrap-${useId()}`;
26
+ const canSync = !initializing && manager && block && field && liveStatus(status) && [
27
+ 'draft',
28
+ 'stopped'
29
+ ].includes(status.status);
30
+ async function sync() {
31
+ if (!canSync || syncing.current) return;
32
+ syncing.current = true;
33
+ setBusy(true);
34
+ setSyncError(undefined);
35
+ try {
36
+ const next = syncExperimentSlots(block, status.arms, field);
37
+ if (JSON.stringify(next.slots) !== JSON.stringify(block.slots)) {
38
+ await replace([
39
+ next
40
+ ]);
41
+ setNotice(true);
42
+ }
43
+ } catch (error) {
44
+ setSyncError(errorMessage(error));
45
+ } finally{
46
+ syncing.current = false;
47
+ setBusy(false);
48
+ }
49
+ }
50
+ useEffect(()=>{
51
+ if (synced.current || !canSync) return;
52
+ synced.current = true;
53
+ void sync();
54
+ });
55
+ if (!status || !manager) return null;
56
+ const draft = liveStatus(status) && status.status === 'draft';
57
+ const stopped = liveStatus(status) && status.status === 'stopped';
58
+ async function unwrap() {
59
+ if (!block || !field || busy) return;
60
+ setBusy(true);
61
+ setError(undefined);
62
+ let deleted = false;
63
+ try {
64
+ if (draft) {
65
+ const api = createApi(config.routes.api);
66
+ const found = await api.findTestIdBySlug(block.experiment);
67
+ if (!found.ok) throw new RefusalError(found.message);
68
+ const removed = await api.deleteTest(found.data);
69
+ if (!removed.ok) throw new RefusalError(removed.message);
70
+ deleted = true;
71
+ setBlockStatus(config.routes.api, block.experiment, {
72
+ deleted: true
73
+ });
74
+ }
75
+ await replace(unwrapExperimentBlock(block, field));
76
+ toast.success(blockUnwrapNotice(Boolean(collection?.versions?.drafts), deleted));
77
+ } catch (error) {
78
+ setError(deleted ? BLOCK_COPY.finishUnwrap : errorMessage(error));
79
+ } finally{
80
+ setBusy(false);
81
+ }
82
+ }
83
+ const sentence = statusSentence(status);
84
+ const linkedCopy = liveStatus(status) ? status.status === 'running' ? blockRunningUnwrap : status.status === 'decided' ? blockDecidedUnwrap : status.status === 'archived' && status.winnerArm ? blockFinishedCopy(slotLabel(status.winnerArm, status)) : undefined : undefined;
85
+ const href = formatAdminURL({
86
+ adminRoute: config.routes.admin,
87
+ path: `/experiments/by-slug/${encodeURIComponent(block?.experiment ?? '')}`
88
+ });
89
+ return /*#__PURE__*/ _jsxs("div", {
90
+ children: [
91
+ liveStatus(status) && status.status === 'running' ? /*#__PURE__*/ _jsx(Banner, {
92
+ children: sentence
93
+ }) : linkedCopy && liveStatus(status) && status.status === 'archived' ? null : /*#__PURE__*/ _jsx("p", {
94
+ children: sentence
95
+ }),
96
+ linkedCopy ? /*#__PURE__*/ _jsxs("p", {
97
+ children: [
98
+ linkedCopy.before,
99
+ /*#__PURE__*/ _jsx("a", {
100
+ href: href,
101
+ children: linkedCopy.link
102
+ }),
103
+ linkedCopy.after
104
+ ]
105
+ }) : null,
106
+ notice ? /*#__PURE__*/ _jsx("p", {
107
+ role: "status",
108
+ children: blockSyncNotice(Boolean(collection?.versions?.drafts))
109
+ }) : null,
110
+ isOrphan(status) || draft || stopped ? /*#__PURE__*/ _jsx(Button, {
111
+ disabled: busy,
112
+ onClick: ()=>draft || stopped ? openModal(modalSlug) : void unwrap(),
113
+ children: draft ? BLOCK_COPY.removeAndUnwrap : BLOCK_COPY.unwrap
114
+ }) : null,
115
+ draft || stopped ? /*#__PURE__*/ _jsx(ConfirmationModal, {
116
+ modalSlug: modalSlug,
117
+ heading: draft ? BLOCK_COPY.removeAndUnwrap : BLOCK_COPY.unwrap,
118
+ body: draft ? blockDeleteConfirmation(status.name) : blockUnwrapConfirmation(status.name),
119
+ confirmLabel: draft ? BLOCK_COPY.deleteAndUnwrap : BLOCK_COPY.unwrap,
120
+ cancelLabel: BLOCK_COPY.cancel,
121
+ onConfirm: unwrap
122
+ }) : null,
123
+ error ? /*#__PURE__*/ _jsx("p", {
124
+ role: "alert",
125
+ children: error
126
+ }) : null,
127
+ syncError ? /*#__PURE__*/ _jsx("p", {
128
+ role: "alert",
129
+ children: syncError
130
+ }) : null,
131
+ syncError && canSync ? /*#__PURE__*/ _jsx(Button, {
132
+ disabled: busy,
133
+ onClick: ()=>void sync(),
134
+ children: BLOCK_COPY.retry
135
+ }) : null
136
+ ]
137
+ });
138
+ }
139
+ export function BlockSlotRowLabel() {
140
+ const { data, path } = useRowLabel();
141
+ const { config, block } = useBlockForm(path);
142
+ const status = useBlockStatus(config.routes.api, block?.experiment);
143
+ return /*#__PURE__*/ _jsx("span", {
144
+ children: slotLabel(data?.slot ?? '', status)
145
+ });
146
+ }
147
+ export function CopyOriginalField({ path }) {
148
+ const { block, field, form, manager, config, replace } = useBlockForm(path);
149
+ const status = useBlockStatus(config.routes.api, block?.experiment);
150
+ const rowPath = path.slice(0, path.lastIndexOf('.'));
151
+ const slot = form.getDataByPath(`${rowPath}.slot`);
152
+ const [busy, setBusy] = useState(false), [error, setError] = useState();
153
+ const { openModal } = useModal();
154
+ const modalSlug = `xp-copy-original-${useId()}`;
155
+ if (!manager || !field || !block || !slot || slot === 'control' || !liveStatus(status) || ![
156
+ 'draft',
157
+ 'stopped',
158
+ 'running'
159
+ ].includes(status.status)) return null;
160
+ async function copy() {
161
+ if (!block || !field || busy || !slot) return;
162
+ setBusy(true);
163
+ setError(undefined);
164
+ try {
165
+ await replace([
166
+ copyOriginalIntoVersion(block, slot, field)
167
+ ]);
168
+ } catch (error) {
169
+ setError(errorMessage(error));
170
+ } finally{
171
+ setBusy(false);
172
+ }
173
+ }
174
+ function requestCopy() {
175
+ if (block?.slots.find((row)=>row.slot === slot)?.blocks?.length) openModal(modalSlug);
176
+ else void copy();
177
+ }
178
+ return /*#__PURE__*/ _jsxs("div", {
179
+ children: [
180
+ /*#__PURE__*/ _jsx(Button, {
181
+ disabled: busy,
182
+ onClick: requestCopy,
183
+ children: BLOCK_COPY.copy
184
+ }),
185
+ /*#__PURE__*/ _jsx(ConfirmationModal, {
186
+ modalSlug: modalSlug,
187
+ heading: BLOCK_COPY.copy,
188
+ body: BLOCK_COPY.confirmCopy,
189
+ confirmLabel: BLOCK_COPY.replace,
190
+ cancelLabel: BLOCK_COPY.cancel,
191
+ onConfirm: copy
192
+ }),
193
+ error ? /*#__PURE__*/ _jsx("p", {
194
+ role: "alert",
195
+ children: error
196
+ }) : null
197
+ ]
198
+ });
199
+ }
@@ -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;
@@ -0,0 +1,163 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Button, SelectInput, useAuth, useConfig, useDocumentInfo, useForm, useFormFields, useFormModified, useServerFunctions } from '@payloadcms/ui';
4
+ import { formatAdminURL } from 'payload/shared';
5
+ import { useMemo, useRef, useState } from 'react';
6
+ import { hostBlocks } from '../../blocks/model.js';
7
+ import { wrapExperimentBlock } from '../../blocks/formState.js';
8
+ import { CreateRefused, createTest } from '../../puck/editor/createTest.js';
9
+ import { createApi } from '../api.js';
10
+ import { BLOCK_COPY, blockSaveNotice, blockTestName } from '../copy/blocks.js';
11
+ import { API_MESSAGES } from '../copy/common.js';
12
+ import { adaptClientBlocksField, BlockChangedError, dataAt, experimentBlockSlug, findClientBlocksField, replaceBlockRow, wrapMode } from './formApi.js';
13
+ import { blockChoices, labelText } from './model.js';
14
+ export function TestBlockField(props) {
15
+ const { permissions } = useAuth();
16
+ const testPermissions = permissions?.collections?.['xp-experiments'];
17
+ return testPermissions?.update === true ? /*#__PURE__*/ _jsx(ManagedTestBlockField, {
18
+ ...props
19
+ }) : null;
20
+ }
21
+ function ManagedTestBlockField({ blocksField, blockSlug: suppliedSlug }) {
22
+ const form = useForm(), modified = useFormModified(), document = useDocumentInfo();
23
+ const { config, getEntityConfig } = useConfig(), { getFormState } = useServerFunctions();
24
+ useFormFields(([fields])=>fields);
25
+ const collection = document.collectionSlug ? getEntityConfig({
26
+ collectionSlug: document.collectionSlug
27
+ }) : undefined;
28
+ const clientField = collection ? findClientBlocksField(collection.fields, blocksField) : undefined;
29
+ const field = useMemo(()=>clientField ? adaptClientBlocksField(clientField, config) : undefined, [
30
+ clientField,
31
+ config
32
+ ]);
33
+ const [choice, setChoice] = useState(), [busy, setBusy] = useState(false), [error, setError] = useState();
34
+ const wrapping = useRef(false);
35
+ if (!field || !collection || !document.collectionSlug) return null;
36
+ const blockSlug = suppliedSlug ?? experimentBlockSlug(field);
37
+ const rows = hostBlocks(form.getDataByPath(blocksField)), choices = blockChoices(rows, field, blockSlug);
38
+ const wrappers = rows.filter((block)=>block.blockType === blockSlug && typeof block.experiment === 'string');
39
+ async function wrap() {
40
+ if (!field || wrapping.current || !document.collectionSlug || !collection) return;
41
+ setError(undefined);
42
+ if (wrapMode === 'fallback' && modified) {
43
+ setError(BLOCK_COPY.saveFirst);
44
+ return;
45
+ }
46
+ const selected = blockChoices(form.getDataByPath(blocksField), field, blockSlug).find((item)=>String(item.index) === choice);
47
+ if (!selected) return;
48
+ const title = collection.admin?.useAsTitle ? form.getDataByPath(collection.admin.useAsTitle) : undefined;
49
+ const name = blockTestName(typeof title === 'string' && title.trim() ? title : labelText(collection.labels.singular) ?? collection.slug, selected.label);
50
+ const beforeCreate = JSON.stringify(form.getData());
51
+ wrapping.current = true;
52
+ setBusy(true);
53
+ try {
54
+ const test = await createTest(config.routes.api, {
55
+ kind: 'block',
56
+ name,
57
+ unitType: 'visitor'
58
+ });
59
+ if (wrapMode === 'fallback') {
60
+ if (JSON.stringify(form.getData()) !== beforeCreate) {
61
+ setError(BLOCK_COPY.saveFirst);
62
+ return;
63
+ }
64
+ if (document.id == null || !selected.block.id) {
65
+ setError(BLOCK_COPY.wrapRefusals['wrap-target-missing']);
66
+ return;
67
+ }
68
+ const result = await createApi(config.routes.api).wrapBlock({
69
+ collection: document.collectionSlug,
70
+ id: document.id,
71
+ field: blocksField,
72
+ blockId: selected.block.id,
73
+ experiment: test.slug
74
+ });
75
+ if (!result.ok) {
76
+ setError(result.code && Object.hasOwn(BLOCK_COPY.wrapRefusals, result.code) ? BLOCK_COPY.wrapRefusals[result.code] : API_MESSAGES.failed);
77
+ return;
78
+ }
79
+ window.location.reload();
80
+ } else {
81
+ await replaceBlockRow({
82
+ form,
83
+ getFormState,
84
+ collectionSlug: document.collectionSlug,
85
+ id: document.id,
86
+ field: blocksField,
87
+ index: selected.index,
88
+ expectedBlock: selected.block,
89
+ rows: [
90
+ wrapExperimentBlock(selected.block, test.slug, field, {}, blockSlug)
91
+ ],
92
+ docPermissions: document.docPermissions,
93
+ docPreferences: await document.getDocPreferences?.()
94
+ });
95
+ }
96
+ setChoice(undefined);
97
+ } catch (cause) {
98
+ setError(cause instanceof CreateRefused || cause instanceof BlockChangedError ? cause.message : API_MESSAGES.failed);
99
+ } finally{
100
+ wrapping.current = false;
101
+ setBusy(false);
102
+ }
103
+ }
104
+ return /*#__PURE__*/ _jsxs("div", {
105
+ children: [
106
+ /*#__PURE__*/ _jsx("h3", {
107
+ children: BLOCK_COPY.testBlock
108
+ }),
109
+ choices.length ? /*#__PURE__*/ _jsxs(_Fragment, {
110
+ children: [
111
+ /*#__PURE__*/ _jsx(SelectInput, {
112
+ name: "xpBlockChoice",
113
+ path: "xpBlockChoice",
114
+ label: BLOCK_COPY.pick,
115
+ options: choices.map((item)=>({
116
+ label: item.option,
117
+ value: String(item.index)
118
+ })),
119
+ value: choice,
120
+ readOnly: busy,
121
+ onChange: (option)=>{
122
+ if (!Array.isArray(option)) setChoice(option?.value);
123
+ }
124
+ }),
125
+ /*#__PURE__*/ _jsx(Button, {
126
+ disabled: busy || choice === undefined,
127
+ onClick: ()=>void wrap(),
128
+ children: BLOCK_COPY.testBlock
129
+ })
130
+ ]
131
+ }) : /*#__PURE__*/ _jsx("p", {
132
+ children: BLOCK_COPY.empty
133
+ }),
134
+ error ? /*#__PURE__*/ _jsx("p", {
135
+ role: "alert",
136
+ children: error
137
+ }) : null,
138
+ wrappers.map((block)=>{
139
+ const slug = block.experiment;
140
+ const inSavedDocument = hostBlocks(dataAt(document.data, blocksField)).some((row)=>row.blockType === blockSlug && row.experiment === slug);
141
+ const saved = !modified && inSavedDocument;
142
+ return /*#__PURE__*/ _jsxs("p", {
143
+ children: [
144
+ !inSavedDocument ? /*#__PURE__*/ _jsxs(_Fragment, {
145
+ children: [
146
+ blockSaveNotice(Boolean(collection.versions?.drafts)),
147
+ ' '
148
+ ]
149
+ }) : null,
150
+ /*#__PURE__*/ _jsx("a", {
151
+ "aria-disabled": !saved,
152
+ href: saved ? formatAdminURL({
153
+ adminRoute: config.routes.admin,
154
+ path: `/experiments/by-slug/${encodeURIComponent(slug)}`
155
+ }) : undefined,
156
+ children: BLOCK_COPY.setup
157
+ })
158
+ ]
159
+ }, block.id ?? slug);
160
+ })
161
+ ]
162
+ });
163
+ }
@@ -0,0 +1,46 @@
1
+ import type { ClientConfig, ClientField, Field } from 'payload';
2
+ import type { useForm, useServerFunctions } from '@payloadcms/ui';
3
+ import { type HostBlock } from '../../blocks/model.js';
4
+ /** The only form-state failure with block-specific editor copy. */
5
+ export declare class BlockChangedError extends Error {
6
+ constructor();
7
+ }
8
+ /** An API refusal already translated into editor-facing copy. */
9
+ export declare class RefusalError extends Error {
10
+ }
11
+ export type RowsField = Extract<Field, {
12
+ type: 'blocks';
13
+ }>;
14
+ export declare const wrapMode: 'form-api' | 'fallback';
15
+ /** Client twin: unnamed layout containers do not contribute to a data path. */
16
+ export declare function findClientBlocksField(fields: readonly ClientField[], name: string): Extract<ClientField, {
17
+ type: 'blocks';
18
+ }> | undefined;
19
+ /** Only schema structure is needed by the pure helpers, never server validators.
20
+ * Resolve references at every depth, including inside the Experiment slots field.
21
+ */
22
+ export declare function adaptClientBlocksField(field: Extract<ClientField, {
23
+ type: 'blocks';
24
+ }>, config: Pick<ClientConfig, 'blocks'>): RowsField;
25
+ export declare function experimentBlockSlug(field: RowsField): string;
26
+ export declare function rowLocation(path: string): {
27
+ field: string;
28
+ index: number;
29
+ } | undefined;
30
+ export declare function dataAt(data: unknown, path: string): unknown;
31
+ /** Payload 3.90.2 and 4 beta.6 use the same row APIs. Sync replaces the whole
32
+ * wrapper so add/drop/reorder and the server-rendered component slots stay together.
33
+ * N=0 needs UPDATE: removeFieldRow alone serializes the empty field as number 0.
34
+ */
35
+ export declare function replaceBlockRow(args: {
36
+ form: ReturnType<typeof useForm>;
37
+ getFormState: ReturnType<typeof useServerFunctions>['getFormState'];
38
+ collectionSlug: string;
39
+ id?: string | number;
40
+ field: string;
41
+ index: number;
42
+ rows: HostBlock[];
43
+ expectedBlock?: HostBlock;
44
+ docPermissions?: Parameters<ReturnType<typeof useServerFunctions>['getFormState']>[0]['docPermissions'];
45
+ docPreferences?: Parameters<ReturnType<typeof useServerFunctions>['getFormState']>[0]['docPreferences'];
46
+ }): Promise<void>;
@@ -0,0 +1,159 @@
1
+ import { EXPERIMENT_BLOCK_SLUG, hostBlocks } from '../../blocks/model.js';
2
+ import { BLOCK_COPY } from '../copy/blocks.js';
3
+ /** The only form-state failure with block-specific editor copy. */ export class BlockChangedError extends Error {
4
+ constructor(){
5
+ super(BLOCK_COPY.blockChanged);
6
+ }
7
+ }
8
+ /** An API refusal already translated into editor-facing copy. */ export class RefusalError extends Error {
9
+ }
10
+ export const wrapMode = 'form-api';
11
+ /** Client twin: unnamed layout containers do not contribute to a data path. */ export function findClientBlocksField(fields, name) {
12
+ for (const field of fields){
13
+ if (field.type === 'blocks' && field.name === name) return field;
14
+ if (field.type === 'row' || field.type === 'collapsible') {
15
+ const found = findClientBlocksField(field.fields, name);
16
+ if (found) return found;
17
+ }
18
+ if (field.type === 'tabs') for (const tab of field.tabs){
19
+ if ('name' in tab && tab.name) continue;
20
+ const found = findClientBlocksField(tab.fields, name);
21
+ if (found) return found;
22
+ }
23
+ }
24
+ }
25
+ /** Only schema structure is needed by the pure helpers, never server validators.
26
+ * Resolve references at every depth, including inside the Experiment slots field.
27
+ */ export function adaptClientBlocksField(field, config) {
28
+ function block(block) {
29
+ return {
30
+ ...block,
31
+ fields: block.fields.map(adapt)
32
+ };
33
+ }
34
+ function adapt(field) {
35
+ if (field.type === 'blocks') return {
36
+ ...field,
37
+ blocks: [
38
+ ...field.blocks.map(block),
39
+ ...(field.blockReferences ?? []).flatMap((reference)=>{
40
+ const resolved = typeof reference === 'string' ? config.blocks?.find((item)=>item.slug === reference) : reference;
41
+ return resolved ? [
42
+ block(resolved)
43
+ ] : [];
44
+ })
45
+ ],
46
+ blockReferences: []
47
+ };
48
+ if ('fields' in field) return {
49
+ ...field,
50
+ fields: field.fields.map(adapt)
51
+ };
52
+ if (field.type === 'tabs') return {
53
+ ...field,
54
+ tabs: field.tabs.map((tab)=>({
55
+ ...tab,
56
+ fields: tab.fields.map(adapt)
57
+ }))
58
+ };
59
+ return field;
60
+ }
61
+ return adapt(field);
62
+ }
63
+ export function experimentBlockSlug(field) {
64
+ return field.blocks.find((block)=>block.fields.some((field)=>'name' in field && field.name === 'xpBlockStatus'))?.slug ?? EXPERIMENT_BLOCK_SLUG;
65
+ }
66
+ export function rowLocation(path) {
67
+ const match = /^([^.]+)\.(\d+)(?:\.|$)/.exec(path);
68
+ return match ? {
69
+ field: match[1],
70
+ index: Number(match[2])
71
+ } : undefined;
72
+ }
73
+ export function dataAt(data, path) {
74
+ return path.split('.').reduce((value, key)=>value && typeof value === 'object' ? value[key] : undefined, data);
75
+ }
76
+ /** Stable object keys; array order and scalar values remain significant. */ function stringify(value) {
77
+ return JSON.stringify(value, (_, item)=>item && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b])=>a < b ? -1 : a > b ? 1 : 0)) : item);
78
+ }
79
+ /** Payload 3.90.2 and 4 beta.6 use the same row APIs. Sync replaces the whole
80
+ * wrapper so add/drop/reorder and the server-rendered component slots stay together.
81
+ * N=0 needs UPDATE: removeFieldRow alone serializes the empty field as number 0.
82
+ */ export async function replaceBlockRow(args) {
83
+ const { form, getFormState, collectionSlug, id, field, index, rows } = args;
84
+ const data = form.getData(), before = hostBlocks(data[field]);
85
+ if (args.expectedBlock && stringify(form.getDataByPath(`${field}.${index}`)) !== stringify(args.expectedBlock)) throw new BlockChangedError();
86
+ // Compare the same reader on both sides: getData() keeps empty row counts as 0.
87
+ const original = stringify(form.getDataByPath(field));
88
+ const next = [
89
+ ...before.slice(0, index),
90
+ ...rows,
91
+ ...before.slice(index + 1)
92
+ ];
93
+ const result = await getFormState({
94
+ collectionSlug,
95
+ id,
96
+ schemaPath: collectionSlug,
97
+ data: {
98
+ ...data,
99
+ [field]: next
100
+ },
101
+ docPermissions: args.docPermissions,
102
+ docPreferences: args.docPreferences ?? {
103
+ fields: {}
104
+ },
105
+ operation: id == null ? 'create' : 'update',
106
+ renderAllFields: true,
107
+ skipValidation: true
108
+ });
109
+ if (stringify(form.getDataByPath(field)) !== original) throw new BlockChangedError();
110
+ if (!result.state) throw new Error('Missing server form state');
111
+ const state = result.state;
112
+ function subFieldState(rowIndex) {
113
+ const prefix = `${field}.${rowIndex}.`;
114
+ return Object.fromEntries(Object.entries(state).filter(([path])=>path.startsWith(prefix)).map(([path, value])=>[
115
+ path.slice(prefix.length),
116
+ value
117
+ ]));
118
+ }
119
+ if (!rows.length) {
120
+ form.removeFieldRow({
121
+ path: field,
122
+ rowIndex: index
123
+ });
124
+ if (next.length === 0) form.dispatchFields({
125
+ type: 'UPDATE',
126
+ path: field,
127
+ value: [],
128
+ disableFormData: false
129
+ });
130
+ return;
131
+ }
132
+ form.replaceFieldRow({
133
+ path: field,
134
+ rowIndex: index,
135
+ schemaPath: `${collectionSlug}.${field}`,
136
+ blockType: rows[0].blockType,
137
+ subFieldState: subFieldState(index)
138
+ });
139
+ for(let offset = 1; offset < rows.length; offset++)form.addFieldRow({
140
+ path: field,
141
+ rowIndex: index + offset,
142
+ schemaPath: `${collectionSlug}.${field}`,
143
+ blockType: rows[offset].blockType,
144
+ subFieldState: subFieldState(index + offset)
145
+ });
146
+ // REPLACE_ROW invents a metadata id even when the data id is unchanged.
147
+ // Keep server row metadata (including custom labels) and the wrapper React key:
148
+ // otherwise a sync remounts the status field and loses its save notice.
149
+ const oldRows = form.getFields()[field]?.rows ?? [];
150
+ const serverRows = state[field]?.rows;
151
+ if (serverRows) form.dispatchFields({
152
+ type: 'UPDATE',
153
+ path: field,
154
+ rows: serverRows.map((row)=>({
155
+ ...oldRows.find((old)=>old.id === row.id),
156
+ ...row
157
+ }))
158
+ });
159
+ }
@@ -0,0 +1,7 @@
1
+ import type { Field } from 'payload';
2
+ /** Copy the coordinates supplied by CORE-L2, including every containing array. */
3
+ export declare function injectTestBlock(fields: Field[], registration: {
4
+ field: string;
5
+ blockSlug?: string;
6
+ appliesTo?: (doc: Record<string, unknown>) => boolean;
7
+ }): Field[];
@@ -0,0 +1,41 @@
1
+ import { findRegisteredBlocksField } from '../../blocks/formState.js';
2
+ import { XP_TEST_BLOCK } from '../paths.js';
3
+ /** Copy the coordinates supplied by CORE-L2, including every containing array. */ export function injectTestBlock(fields, registration) {
4
+ const found = findRegisteredBlocksField(fields, registration.field);
5
+ if (!found) return fields;
6
+ const ui = {
7
+ name: 'xpTestBlock',
8
+ type: 'ui',
9
+ admin: {
10
+ condition: (data)=>{
11
+ try {
12
+ return registration.appliesTo ? registration.appliesTo(data) === true : true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ },
17
+ components: {
18
+ Field: {
19
+ path: XP_TEST_BLOCK,
20
+ clientProps: {
21
+ blocksField: registration.field,
22
+ blockSlug: registration.blockSlug
23
+ }
24
+ }
25
+ }
26
+ }
27
+ };
28
+ function insert(value, path) {
29
+ const [key, ...rest] = path;
30
+ if (key === undefined) return value;
31
+ const copy = Array.isArray(value) ? [
32
+ ...value
33
+ ] : {
34
+ ...value
35
+ };
36
+ if (!rest.length) copy.splice(Number(key) + 1, 0, ui);
37
+ else copy[key] = insert(value[key], rest);
38
+ return copy;
39
+ }
40
+ return insert(fields, found.path);
41
+ }
@@ -0,0 +1,17 @@
1
+ import type { PlacementStatusResult } from '../../types/endpoints.js';
2
+ import type { RowsField } from './formApi.js';
3
+ export type BlockStatus = PlacementStatusResult['slugs'][string];
4
+ export type LiveBlockStatus = Extract<BlockStatus, {
5
+ status: string;
6
+ }>;
7
+ export declare function liveStatus(status: BlockStatus | null): status is LiveBlockStatus;
8
+ export declare function slotLabel(slot: string, status: BlockStatus | null): string;
9
+ export declare function statusSentence(status: BlockStatus): string;
10
+ export declare function isOrphan(status: BlockStatus): boolean;
11
+ export declare function labelText(value: unknown): string | undefined;
12
+ export declare function blockChoices(value: unknown, field: RowsField, blockSlug: string): {
13
+ block: import("../../blocks/model.js").HostBlock;
14
+ index: number;
15
+ label: string;
16
+ option: string;
17
+ }[];