@delmaredigital/payload-experiments 0.2.0-rc.6 → 0.3.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 (291) hide show
  1. package/README.md +129 -11
  2. package/dist/admin/PreviewLinks.d.ts +2 -1
  3. package/dist/admin/PreviewLinks.js +24 -16
  4. package/dist/admin/actions/ConfirmDialog.d.ts +7 -4
  5. package/dist/admin/actions/ConfirmDialog.js +37 -82
  6. package/dist/admin/actions/DecideDialog.d.ts +3 -1
  7. package/dist/admin/actions/DecideDialog.js +30 -13
  8. package/dist/admin/actions/MakeContentResult.d.ts +13 -2
  9. package/dist/admin/actions/MakeContentResult.js +115 -16
  10. package/dist/admin/actions/actions.model.d.ts +2 -4
  11. package/dist/admin/actions/actions.model.js +2 -3
  12. package/dist/admin/allocation.model.js +3 -5
  13. package/dist/admin/api.d.ts +10 -4
  14. package/dist/admin/api.js +11 -7
  15. package/dist/admin/copy/actions.d.ts +39 -3
  16. package/dist/admin/copy/actions.js +40 -4
  17. package/dist/admin/copy/armSelect.js +5 -5
  18. package/dist/admin/copy/arms.d.ts +1 -7
  19. package/dist/admin/copy/arms.js +1 -7
  20. package/dist/admin/copy/audience.d.ts +2 -1
  21. package/dist/admin/copy/audience.js +3 -2
  22. package/dist/admin/copy/common.d.ts +13 -6
  23. package/dist/admin/copy/common.js +14 -8
  24. package/dist/admin/copy/goals.d.ts +21 -6
  25. package/dist/admin/copy/goals.js +27 -6
  26. package/dist/admin/copy/readiness.d.ts +19 -0
  27. package/dist/admin/copy/readiness.js +19 -0
  28. package/dist/admin/copy/recommendation.d.ts +0 -3
  29. package/dist/admin/copy/recommendation.js +0 -3
  30. package/dist/admin/copy/results.d.ts +41 -2
  31. package/dist/admin/copy/results.js +42 -3
  32. package/dist/admin/copy/review.d.ts +1 -1
  33. package/dist/admin/copy/review.js +1 -1
  34. package/dist/admin/copy/setting.d.ts +2 -0
  35. package/dist/admin/copy/setting.js +2 -0
  36. package/dist/admin/copy/surface.d.ts +7 -4
  37. package/dist/admin/copy/surface.js +9 -4
  38. package/dist/admin/copy/testPage.d.ts +12 -0
  39. package/dist/admin/copy/testPage.js +12 -0
  40. package/dist/admin/format/date.d.ts +2 -0
  41. package/dist/admin/format/date.js +10 -0
  42. package/dist/admin/labels.d.ts +2 -7
  43. package/dist/admin/labels.js +2 -7
  44. package/dist/admin/previewLinks.model.d.ts +2 -2
  45. package/dist/admin/previewLinks.model.js +5 -7
  46. package/dist/admin/results/CiChart.js +11 -21
  47. package/dist/admin/results/DataQualityPanel.d.ts +4 -2
  48. package/dist/admin/results/DataQualityPanel.js +30 -17
  49. package/dist/admin/results/DataTable.d.ts +16 -0
  50. package/dist/admin/results/DataTable.js +152 -0
  51. package/dist/admin/results/StatTile.d.ts +7 -0
  52. package/dist/admin/results/StatTile.js +22 -0
  53. package/dist/admin/results/VerdictHero.d.ts +13 -0
  54. package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
  55. package/dist/admin/results/releasePages.model.d.ts +17 -0
  56. package/dist/admin/results/releasePages.model.js +10 -0
  57. package/dist/admin/results/results.css +64 -0
  58. package/dist/admin/results/results.model.d.ts +6 -2
  59. package/dist/admin/results/results.model.js +12 -5
  60. package/dist/admin/surface/ResultsRoute.d.ts +1 -1
  61. package/dist/admin/surface/ResultsRoute.js +60 -31
  62. package/dist/admin/surface/SurfaceShell.d.ts +11 -2
  63. package/dist/admin/surface/SurfaceShell.js +49 -14
  64. package/dist/admin/surface/TestsHome.d.ts +6 -1
  65. package/dist/admin/surface/TestsHome.js +183 -112
  66. package/dist/admin/surface/TestsSurface.js +56 -16
  67. package/dist/admin/surface/contracts.d.ts +1 -0
  68. package/dist/admin/surface/goals/GoalForm.js +341 -308
  69. package/dist/admin/surface/goals/GoalsList.js +123 -89
  70. package/dist/admin/surface/goals/data.d.ts +8 -2
  71. package/dist/admin/surface/goals/data.js +11 -3
  72. package/dist/admin/surface/goals/goals.css +10 -13
  73. package/dist/admin/surface/home.model.d.ts +7 -8
  74. package/dist/admin/surface/home.model.js +23 -12
  75. package/dist/admin/surface/newTest/NewTest.js +5 -5
  76. package/dist/admin/surface/pageBlock/parts.js +31 -26
  77. package/dist/admin/surface/readiness/ActionDialogs.js +43 -14
  78. package/dist/admin/surface/readiness/Banner.d.ts +3 -0
  79. package/dist/admin/surface/readiness/Banner.js +63 -36
  80. package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
  81. package/dist/admin/surface/readiness/SaveStatus.js +6 -3
  82. package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
  83. package/dist/admin/surface/readiness/readiness.model.js +18 -9
  84. package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
  85. package/dist/admin/surface/readiness/useTransition.js +2 -1
  86. package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
  87. package/dist/admin/surface/results/ReleasePages.js +116 -0
  88. package/dist/admin/surface/results/ResultsBody.d.ts +15 -2
  89. package/dist/admin/surface/results/ResultsBody.js +281 -107
  90. package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
  91. package/dist/admin/surface/results/ResultsHeader.js +177 -0
  92. package/dist/admin/surface/results/recommendation.model.js +2 -6
  93. package/dist/admin/surface/results/useResults.d.ts +21 -3
  94. package/dist/admin/surface/results/useResults.js +2 -1
  95. package/dist/admin/surface/setting/parts.js +75 -88
  96. package/dist/admin/surface/setting/setting.css +3 -15
  97. package/dist/admin/surface/surface.css +62 -47
  98. package/dist/admin/surface/test/AdvancedSection.js +55 -48
  99. package/dist/admin/surface/test/GoalSection.js +39 -32
  100. package/dist/admin/surface/test/LengthSection.js +46 -34
  101. package/dist/admin/surface/test/Rail.d.ts +4 -0
  102. package/dist/admin/surface/test/Rail.js +83 -0
  103. package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
  104. package/dist/admin/surface/test/ReadinessBar.js +33 -0
  105. package/dist/admin/surface/test/Section.d.ts +1 -1
  106. package/dist/admin/surface/test/Section.js +36 -16
  107. package/dist/admin/surface/test/TargetingSection.js +168 -142
  108. package/dist/admin/surface/test/TestPage.d.ts +2 -1
  109. package/dist/admin/surface/test/TestPage.js +146 -59
  110. package/dist/admin/surface/test/VersionsSection.js +151 -120
  111. package/dist/admin/surface/test/fields.d.ts +11 -4
  112. package/dist/admin/surface/test/fields.js +84 -85
  113. package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
  114. package/dist/admin/surface/test/jumpToSection.js +10 -0
  115. package/dist/admin/surface/test/pageParts.js +9 -3
  116. package/dist/admin/surface/test/testPage.css +81 -14
  117. package/dist/admin/surface/useTestDocument.d.ts +1 -0
  118. package/dist/admin/surface/useTestDocument.js +40 -9
  119. package/dist/admin/targeting/targeting.model.d.ts +1 -3
  120. package/dist/admin/targeting/targeting.model.js +3 -8
  121. package/dist/admin/theme.css +115 -36
  122. package/dist/admin/theme.d.ts +38 -0
  123. package/dist/admin/theme.js +38 -1
  124. package/dist/admin/useApi.d.ts +17 -2
  125. package/dist/admin/views/ExperimentsRootView.js +12 -2
  126. package/dist/collections/experiments/config.js +44 -1
  127. package/dist/collections/experiments/lockHook.js +16 -2
  128. package/dist/collections/identities.js +6 -1
  129. package/dist/collections/metrics.js +7 -0
  130. package/dist/collections/metricsGuards.d.ts +9 -0
  131. package/dist/collections/metricsGuards.js +33 -0
  132. package/dist/constants.d.ts +1 -0
  133. package/dist/constants.js +1 -0
  134. package/dist/contentDone.d.ts +4 -0
  135. package/dist/contentDone.js +5 -0
  136. package/dist/db/identities.d.ts +8 -7
  137. package/dist/db/identities.js +9 -6
  138. package/dist/db/metricUsage.d.ts +19 -0
  139. package/dist/db/metricUsage.js +54 -0
  140. package/dist/db/names.d.ts +5 -0
  141. package/dist/db/names.js +5 -0
  142. package/dist/db/publishedContent.d.ts +10 -0
  143. package/dist/db/publishedContent.js +43 -0
  144. package/dist/db/results.d.ts +2 -0
  145. package/dist/db/results.js +6 -2
  146. package/dist/db/snapshots.d.ts +18 -6
  147. package/dist/db/snapshots.js +23 -7
  148. package/dist/db/staleEdit.d.ts +6 -0
  149. package/dist/db/staleEdit.js +10 -0
  150. package/dist/db/sweep.js +11 -6
  151. package/dist/endpoints/events.js +3 -3
  152. package/dist/endpoints/index.js +19 -1
  153. package/dist/endpoints/recompute.js +2 -1
  154. package/dist/endpoints/results.js +5 -0
  155. package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
  156. package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
  157. package/dist/endpoints/summary.js +20 -2
  158. package/dist/endpoints/transitions.d.ts +15 -0
  159. package/dist/endpoints/transitions.js +36 -0
  160. package/dist/errors.d.ts +8 -0
  161. package/dist/errors.js +28 -0
  162. package/dist/exports/adminClient.d.ts +1 -0
  163. package/dist/exports/adminClient.js +1 -0
  164. package/dist/exports/index.d.ts +2 -1
  165. package/dist/exports/index.js +1 -0
  166. package/dist/exports/puckEditor.d.ts +1 -0
  167. package/dist/exports/puckEditor.js +1 -0
  168. package/dist/exports/rsc.d.ts +5 -1
  169. package/dist/exports/rsc.js +2 -0
  170. package/dist/hooks/hostHook.js +3 -2
  171. package/dist/hooks/pageGuard.d.ts +5 -0
  172. package/dist/hooks/pageGuard.js +121 -0
  173. package/dist/labels/arms.d.ts +7 -0
  174. package/dist/labels/arms.js +7 -0
  175. package/dist/log.d.ts +7 -0
  176. package/dist/log.js +38 -0
  177. package/dist/manifest/compile.d.ts +5 -0
  178. package/dist/manifest/compile.js +5 -1
  179. package/dist/options.js +23 -0
  180. package/dist/plugin.js +26 -2
  181. package/dist/puck/copy.d.ts +1 -1
  182. package/dist/puck/copy.js +6 -6
  183. package/dist/puck/editor/editor.chrome.css +235 -0
  184. package/dist/puck/editor/editor.css +241 -43
  185. package/dist/results/aggregate.js +4 -3
  186. package/dist/results/analyse.d.ts +4 -1
  187. package/dist/results/analyse.js +27 -11
  188. package/dist/results/cuped.js +3 -2
  189. package/dist/results/identityCredit.d.ts +15 -7
  190. package/dist/results/identityCredit.js +32 -33
  191. package/dist/results/rolloutUnits.js +2 -1
  192. package/dist/results/snapshot.js +8 -2
  193. package/dist/results/srm.js +2 -1
  194. package/dist/results/verdict.d.ts +2 -0
  195. package/dist/results/verdict.js +18 -1
  196. package/dist/sanitizeReadinessDetail.d.ts +7 -0
  197. package/dist/sanitizeReadinessDetail.js +54 -0
  198. package/dist/server/linkIdentity.d.ts +10 -1
  199. package/dist/server/linkIdentity.js +20 -18
  200. package/dist/server/metadata.d.ts +5 -0
  201. package/dist/server/metadata.js +9 -1
  202. package/dist/server/recordContentChange.js +2 -1
  203. package/dist/server/recordExposures.js +3 -3
  204. package/dist/server/runResults.d.ts +3 -2
  205. package/dist/server/runResults.js +125 -34
  206. package/dist/server/trackConversion.js +5 -6
  207. package/dist/targeting/values.d.ts +4 -0
  208. package/dist/targeting/values.js +10 -0
  209. package/dist/transitions/addMetric.js +8 -0
  210. package/dist/transitions/archive.js +2 -3
  211. package/dist/transitions/checkStart.d.ts +6 -3
  212. package/dist/transitions/checkStart.js +15 -0
  213. package/dist/transitions/contentFields.d.ts +7 -0
  214. package/dist/transitions/contentFields.js +20 -0
  215. package/dist/transitions/decide.js +6 -1
  216. package/dist/transitions/deleteExperiment.js +2 -1
  217. package/dist/transitions/dismissContentEntry.d.ts +8 -0
  218. package/dist/transitions/dismissContentEntry.js +45 -0
  219. package/dist/transitions/makeContent.d.ts +5 -2
  220. package/dist/transitions/makeContent.js +217 -118
  221. package/dist/transitions/phaseRecord.d.ts +8 -2
  222. package/dist/transitions/placements.d.ts +1 -0
  223. package/dist/transitions/placements.js +4 -0
  224. package/dist/transitions/preflightData.d.ts +1 -1
  225. package/dist/transitions/preflightData.js +42 -4
  226. package/dist/transitions/releasePages.d.ts +8 -0
  227. package/dist/transitions/releasePages.js +26 -0
  228. package/dist/transitions/restoreContentEntry.d.ts +8 -0
  229. package/dist/transitions/restoreContentEntry.js +47 -0
  230. package/dist/transitions/start.js +30 -5
  231. package/dist/types/contracts.d.ts +14 -0
  232. package/dist/types/endpoints.d.ts +34 -3
  233. package/dist/types/index.d.ts +1 -0
  234. package/dist/types/index.js +1 -0
  235. package/dist/types/reasons.d.ts +3 -0
  236. package/dist/types/reasons.js +1 -0
  237. package/dist/types/snapshot.d.ts +6 -1
  238. package/dist/ui/Banner.d.ts +2 -0
  239. package/dist/ui/Banner.js +2 -0
  240. package/dist/ui/Breadcrumb.d.ts +7 -0
  241. package/dist/ui/Breadcrumb.js +25 -0
  242. package/dist/ui/Button.d.ts +15 -0
  243. package/dist/ui/Button.js +32 -0
  244. package/dist/ui/Card.d.ts +12 -0
  245. package/dist/ui/Card.js +40 -0
  246. package/dist/ui/Checkbox.d.ts +2 -0
  247. package/dist/ui/Checkbox.js +2 -0
  248. package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
  249. package/dist/{admin/components → ui}/ChipsInput.js +20 -12
  250. package/dist/ui/Dialog.d.ts +13 -0
  251. package/dist/ui/Dialog.js +85 -0
  252. package/dist/ui/EmptyState.d.ts +2 -0
  253. package/dist/ui/EmptyState.js +2 -0
  254. package/dist/ui/Feedback.d.ts +24 -0
  255. package/dist/ui/Feedback.js +40 -0
  256. package/dist/ui/Inputs.d.ts +34 -0
  257. package/dist/ui/Inputs.js +225 -0
  258. package/dist/ui/NumberInput.d.ts +2 -0
  259. package/dist/ui/NumberInput.js +2 -0
  260. package/dist/ui/Range.d.ts +2 -0
  261. package/dist/ui/Range.js +2 -0
  262. package/dist/ui/SearchInput.d.ts +2 -0
  263. package/dist/ui/SearchInput.js +2 -0
  264. package/dist/ui/SectionCard.d.ts +2 -0
  265. package/dist/ui/SectionCard.js +2 -0
  266. package/dist/ui/Select.d.ts +2 -0
  267. package/dist/ui/Select.js +2 -0
  268. package/dist/ui/StatusChip.d.ts +2 -0
  269. package/dist/ui/StatusChip.js +2 -0
  270. package/dist/ui/Tabs.d.ts +14 -0
  271. package/dist/ui/Tabs.js +54 -0
  272. package/dist/ui/TextInput.d.ts +2 -0
  273. package/dist/ui/TextInput.js +2 -0
  274. package/dist/ui/Textarea.d.ts +2 -0
  275. package/dist/ui/Textarea.js +2 -0
  276. package/dist/ui/Toggle.d.ts +2 -0
  277. package/dist/ui/Toggle.js +2 -0
  278. package/dist/ui/accent.d.ts +21 -0
  279. package/dist/ui/accent.js +81 -0
  280. package/dist/ui/index.d.ts +9 -0
  281. package/dist/ui/index.js +9 -0
  282. package/dist/ui/ui.css +92 -0
  283. package/package.json +1 -1
  284. package/dist/admin/components/chipsInput.css +0 -11
  285. package/dist/admin/results/ArmTable.d.ts +0 -8
  286. package/dist/admin/results/ArmTable.js +0 -113
  287. package/dist/admin/results/VerdictCard.d.ts +0 -9
  288. package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
  289. package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
  290. package/dist/admin/surface/test/TestHeader.d.ts +0 -4
  291. package/dist/admin/surface/test/TestHeader.js +0 -69
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from 'react';
4
- import { Button, TextInput } from '@payloadcms/ui';
5
- import { DialogFrame } from '../../actions/ConfirmDialog.js';
4
+ import { Button } from '../../../ui/Button.js';
5
+ import { TextInput } from '../../../ui/Inputs.js';
6
+ import { Dialog } from '../../../ui/Dialog.js';
6
7
  import { MakeContentResult } from '../../actions/MakeContentResult.js';
7
8
  import { expectedConfirm, matchesConfirm } from '../../../transitions/confirmPhrases.js';
8
9
  import { ACTIONS_COPY as A } from '../../copy/actions.js';
@@ -15,14 +16,18 @@ import { useTransition } from './useTransition.js';
15
16
  const api = createApi(surface.apiRoute);
16
17
  const [typed, setTyped] = useState('');
17
18
  const [content, setContent] = useState(null);
19
+ const [reportBusy, setReportBusy] = useState(false);
18
20
  const transition = useTransition(test, onClose);
19
21
  const phrase = action.confirm.phrase ? expectedConfirm(action.confirm.phrase.action, winnerLabel) : null;
20
22
  const ready = action.id !== 'start' || Boolean(test.preflight && !test.preflightPending && !test.preflightError && test.preflight.checks.every((check)=>check.ok));
21
23
  const enabled = ready && !transition.busy && !test.hasUnsaved && test.saving !== 'saving' && (!phrase || matchesConfirm(phrase, typed));
22
- async function submit() {
24
+ async function submit(finish = false) {
23
25
  if (!enabled || !test.doc) return;
24
26
  const id = test.doc.id;
25
27
  await transition.run(async ()=>{
28
+ if (finish) return api.archive(id, {
29
+ mode: 'after-content'
30
+ });
26
31
  if (action.archive) return api.archive(id, {
27
32
  ...action.archive,
28
33
  ...phrase ? {
@@ -44,22 +49,32 @@ import { useTransition } from './useTransition.js';
44
49
  message: API_MESSAGES.unreadable
45
50
  } : result;
46
51
  }, {
47
- reloadOnSuccess: action.id !== 'make-content'
52
+ reloadOnSuccess: finish || action.id !== 'make-content'
48
53
  });
49
54
  }
50
55
  function close() {
51
- if (transition.busy) return;
56
+ if (transition.busy || reportBusy) return;
52
57
  if (content) void test.reload();
53
58
  onClose();
54
59
  }
55
- return /*#__PURE__*/ _jsxs(DialogFrame, {
60
+ return /*#__PURE__*/ _jsxs(Dialog, {
61
+ open: true,
62
+ initialFocus: action.id === 'start' ? undefined : 'cancel',
63
+ dismissible: !transition.busy && !reportBusy,
56
64
  title: action.confirm.title,
57
65
  onClose: close,
58
66
  children: [
59
67
  content ? /*#__PURE__*/ _jsx(MakeContentResult, {
60
68
  result: content,
61
- onRetry: submit,
62
- busy: transition.busy
69
+ id: test.doc?.id,
70
+ api: api,
71
+ canManage: surface.canManage,
72
+ isDecided: (content.doc?.status ?? test.doc?.status) === 'decided',
73
+ onRetry: ()=>submit(),
74
+ onFinish: ()=>submit(true),
75
+ busy: transition.busy,
76
+ onBusyChange: setReportBusy,
77
+ onActionStart: transition.clearError
63
78
  }) : /*#__PURE__*/ _jsxs(_Fragment, {
64
79
  children: [
65
80
  /*#__PURE__*/ _jsx("p", {
@@ -77,7 +92,7 @@ import { useTransition } from './useTransition.js';
77
92
  ]
78
93
  }),
79
94
  /*#__PURE__*/ _jsx(TextInput, {
80
- path: "xp-confirmation",
95
+ id: "xp-confirmation",
81
96
  label: A.confirmation,
82
97
  value: typed,
83
98
  readOnly: transition.busy,
@@ -99,7 +114,8 @@ import { useTransition } from './useTransition.js';
99
114
  children: action.label
100
115
  }) : null,
101
116
  /*#__PURE__*/ _jsx(Button, {
102
- disabled: transition.busy,
117
+ "data-dialog-cancel": true,
118
+ disabled: transition.busy || reportBusy,
103
119
  onClick: close,
104
120
  children: content ? A.done : A.cancel
105
121
  })
@@ -113,7 +129,9 @@ import { useTransition } from './useTransition.js';
113
129
  const allocation = Number(value);
114
130
  const valid = value.trim() !== '' && Number.isInteger(allocation) && allocation > current && allocation <= 100;
115
131
  const disabled = transition.busy || test.hasUnsaved || test.saving === 'saving';
116
- return /*#__PURE__*/ _jsxs(DialogFrame, {
132
+ return /*#__PURE__*/ _jsxs(Dialog, {
133
+ open: true,
134
+ dismissible: !transition.busy,
117
135
  title: C.allocate,
118
136
  onClose: ()=>{
119
137
  if (!transition.busy) onClose();
@@ -146,8 +164,11 @@ import { useTransition } from './useTransition.js';
146
164
  /*#__PURE__*/ _jsx(Button, {
147
165
  disabled: disabled || !valid,
148
166
  onClick: ()=>{
167
+ // Not-found can also mean the experiment is missing; use round copy for the missing phase record.
149
168
  if (valid && test.doc) void transition.run(()=>createApi(surface.apiRoute).allocate(test.doc.id, {
150
169
  allocation
170
+ }, {
171
+ notFound: API_MESSAGES.roundChanged
151
172
  }));
152
173
  },
153
174
  children: C.allocate
@@ -197,12 +218,17 @@ import { useTransition } from './useTransition.js';
197
218
  const available = metrics.filter((item)=>!attached.has(String(item.id)));
198
219
  const selected = available.find((item)=>String(item.id) === metric);
199
220
  const disabled = transition.busy || test.hasUnsaved || test.saving === 'saving';
200
- return /*#__PURE__*/ _jsxs(DialogFrame, {
201
- title: C.addMetric,
221
+ return /*#__PURE__*/ _jsxs(Dialog, {
222
+ open: true,
223
+ dismissible: !transition.busy,
224
+ title: C.addGoal,
202
225
  onClose: ()=>{
203
226
  if (!transition.busy) onClose();
204
227
  },
205
228
  children: [
229
+ /*#__PURE__*/ _jsx("p", {
230
+ children: C.addMetric
231
+ }),
206
232
  loading ? /*#__PURE__*/ _jsx("p", {
207
233
  role: "status",
208
234
  children: R.loadingMetrics
@@ -257,12 +283,15 @@ import { useTransition } from './useTransition.js';
257
283
  /*#__PURE__*/ _jsx(Button, {
258
284
  disabled: disabled || loading || !!loadError || !selected,
259
285
  onClick: ()=>{
286
+ // Goal and phase lookups share not-found; prefer the more likely missing goal without reading server text.
260
287
  if (selected && test.doc) void transition.run(()=>createApi(surface.apiRoute).addMetric(test.doc.id, {
261
288
  metric: selected.id,
262
289
  role
290
+ }, {
291
+ notFound: API_MESSAGES.goalNotFound
263
292
  }));
264
293
  },
265
- children: C.addMetric
294
+ children: C.addGoal
266
295
  }),
267
296
  /*#__PURE__*/ _jsx(Button, {
268
297
  disabled: transition.busy,
@@ -1,2 +1,5 @@
1
1
  import type { SectionProps } from '../contracts.js';
2
2
  export declare function Banner(props: SectionProps): import("react").JSX.Element | null;
3
+ export declare function ReadinessContent({ test, surface, resultsVersion, bar }: SectionProps & {
4
+ bar?: boolean;
5
+ }): import("react").JSX.Element;
@@ -1,12 +1,13 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { theme } from '../../theme.js';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
4
3
  import { useEffect, useState } from 'react';
5
- import { Button } from '@payloadcms/ui';
4
+ import { Button } from '../../../ui/Button.js';
5
+ import { SaveStatus, saveStatusMessage } from './SaveStatus.js';
6
+ import { jumpToSection } from '../test/jumpToSection.js';
6
7
  import { actionGroupLabel, availableActions } from '../../actions/actions.model.js';
7
8
  import { createApi } from '../../api.js';
8
9
  import { READINESS_COPY as C, decidedCopy, nextCopy, runningCopy, stoppedCopy } from '../../copy/readiness.js';
9
- import { daysRemaining, displayDate, nextStep } from './readiness.model.js';
10
+ import { READINESS, daysRemaining, displayDate, nextStep } from './readiness.model.js';
10
11
  import { AddMetricAction, AllocateAction, ConfirmAction } from './ActionDialogs.js';
11
12
  import { useTransition } from './useTransition.js';
12
13
  const RUNNING_ACTIONS = [
@@ -15,25 +16,26 @@ const RUNNING_ACTIONS = [
15
16
  'allocate'
16
17
  ];
17
18
  export function Banner(props) {
18
- return props.test.doc ? /*#__PURE__*/ _jsx(BannerContent, {
19
+ return props.test.doc ? /*#__PURE__*/ _jsx(ReadinessContent, {
19
20
  ...props
20
21
  }, `${props.test.doc.id}/${props.test.doc.status}`) : null;
21
22
  }
22
- function BannerContent({ test, surface }) {
23
+ export function ReadinessContent({ test, surface, resultsVersion, bar = false }) {
23
24
  const [selected, setSelected] = useState(null);
24
25
  const [people, setPeople] = useState(null);
25
26
  const transition = useTransition(test);
26
27
  const doc = test.doc;
27
28
  useEffect(()=>{
28
29
  let active = true;
29
- setPeople(null);
30
+ // The id/status key resets the count; refreshes retain it while loading.
30
31
  if (surface.canReadResults && doc.status === 'running') {
31
- void createApi(surface.apiRoute).summary([
32
- doc.id
33
- ]).then((result)=>{
34
- const count = result.ok ? result.data?.[doc.id]?.exposures : undefined;
32
+ void Promise.resolve().then(()=>active ? createApi(surface.apiRoute).summary([
33
+ doc.id
34
+ ]) : undefined).then((result)=>{
35
+ if (!result?.ok) return;
36
+ const count = result.data?.[doc.id]?.exposures;
35
37
  if (active) setPeople(typeof count === 'number' && Number.isFinite(count) ? count : null);
36
- });
38
+ }).catch(()=>{});
37
39
  }
38
40
  return ()=>{
39
41
  active = false;
@@ -42,7 +44,8 @@ function BannerContent({ test, surface }) {
42
44
  doc.id,
43
45
  doc.status,
44
46
  surface.apiRoute,
45
- surface.canReadResults
47
+ surface.canReadResults,
48
+ resultsVersion
46
49
  ]);
47
50
  const decision = doc.decision;
48
51
  const winner = actionGroupLabel(decision?.winnerArm ?? 'control', doc.arms.find((item)=>item.key === decision?.winnerArm)?.label);
@@ -62,48 +65,72 @@ function BannerContent({ test, surface }) {
62
65
  canManage: surface.canManage
63
66
  }).filter((action)=>setup ? action.id === 'start' : doc.status === 'running' ? RUNNING_ACTIONS.includes(action.id) : true).sort((a, b)=>doc.status === 'running' ? RUNNING_ACTIONS.indexOf(a.id) - RUNNING_ACTIONS.indexOf(b.id) : 0).map((action)=>action.id === 'add-metric' ? {
64
67
  ...action,
65
- label: C.addMetric
68
+ label: C.addGoal
66
69
  } : action.id === 'allocate' ? {
67
70
  ...action,
68
71
  label: C.allocate
69
72
  } : action);
70
73
  const action = actions.find((item)=>item.id === selected);
71
- const sentence = setup ? ready ? C.ready : next ? nextCopy(next.sentence) : test.preflightError ? C.checkError : C.checking : doc.status === 'running' ? runningCopy(displayDate(doc.startedAt), people, daysRemaining(doc, Date.now())) : doc.status === 'decided' ? decidedCopy(displayDate(doc.decidedAt), winner) : C.archived;
74
+ const sentence = setup ? bar && test.preflightError ? null : ready ? C.ready : next ? nextCopy(next.sentence) : test.preflightError ? C.checkError : C.checking : doc.status === 'running' ? runningCopy(displayDate(doc.startedAt), people, daysRemaining(doc, Date.now())) : doc.status === 'decided' ? decidedCopy(displayDate(doc.decidedAt), winner) : C.archived;
72
75
  return /*#__PURE__*/ _jsxs("div", {
73
- className: `xp-readiness xp-readiness--${setup && ready ? 'ready' : 'neutral'}`,
74
- style: {
75
- padding: '1rem',
76
- border: `1px solid ${theme.color.borderStrong}`,
77
- borderRadius: theme.radius.small,
78
- background: setup && ready ? theme.color.successSurface : theme.color.surface,
79
- color: theme.color.text
80
- },
76
+ className: `xp-readiness xp-readiness--${setup && ready ? 'ready' : 'neutral'}${bar ? ' xp-readiness--bar xp-tests-container' : ''}`,
81
77
  children: [
82
- /*#__PURE__*/ _jsx("p", {
83
- role: "status",
84
- children: setup && next && !ready && next.section !== 'header' ? /*#__PURE__*/ _jsx("a", {
85
- href: `#section-${next.section}`,
86
- children: sentence
87
- }) : sentence
78
+ /*#__PURE__*/ _jsxs("div", {
79
+ className: "xp-readiness__state",
80
+ children: [
81
+ bar ? /*#__PURE__*/ _jsxs(_Fragment, {
82
+ children: [
83
+ /*#__PURE__*/ _jsx(SaveStatus, {
84
+ test: test
85
+ }),
86
+ sentence && (saveStatusMessage(test) || test.preflightError) ? /*#__PURE__*/ _jsx("span", {
87
+ className: "xp-readiness__separator",
88
+ "aria-hidden": "true",
89
+ children: "·"
90
+ }) : null
91
+ ]
92
+ }) : null,
93
+ sentence ? /*#__PURE__*/ _jsx("p", {
94
+ role: "status",
95
+ children: setup && next && !ready && next.section !== 'header' ? /*#__PURE__*/ _jsxs("a", {
96
+ href: `#section-${next.section}`,
97
+ onClick: (event)=>{
98
+ event.preventDefault();
99
+ if (next.section !== 'header') jumpToSection(next.section);
100
+ },
101
+ children: [
102
+ sentence,
103
+ next.nextStep ? /*#__PURE__*/ _jsxs(_Fragment, {
104
+ children: [
105
+ /*#__PURE__*/ _jsx("br", {}),
106
+ /*#__PURE__*/ _jsx("span", {
107
+ children: next.nextStep
108
+ })
109
+ ]
110
+ }) : null
111
+ ]
112
+ }) : sentence
113
+ }) : null
114
+ ]
88
115
  }),
116
+ setup && test.preflight?.checks.some((check)=>check.code === 'page-targets-unchecked' && check.warning) ? /*#__PURE__*/ _jsx("p", {
117
+ children: C.uncheckedPages
118
+ }) : null,
119
+ test.preflight?.checks.filter((check)=>check.code === 'make-content-unsupported' && check.warning).map((check)=>/*#__PURE__*/ _jsx("p", {
120
+ children: READINESS[check.code].next(check.detail, doc)
121
+ }, check.code)),
89
122
  doc.status === 'stopped' ? /*#__PURE__*/ _jsx("p", {
90
123
  children: stoppedCopy(displayDate(doc.stoppedAt))
91
124
  }) : null,
92
125
  /*#__PURE__*/ _jsx("div", {
93
126
  className: "xp-readiness__actions",
94
- style: {
95
- display: 'flex',
96
- gap: '0.75rem',
97
- flexWrap: 'wrap',
98
- alignItems: 'center'
99
- },
100
127
  children: actions.map((item)=>{
101
128
  const reason = item.id === 'start' ? startReason : unsaved ? C.saveFirst : item.id === 'allocate' && doc.allocation >= 100 ? C.everyone : undefined;
102
129
  const disabled = transition.busy || !!selected || !!reason;
103
130
  return /*#__PURE__*/ _jsx("span", {
104
131
  title: reason,
105
132
  children: /*#__PURE__*/ _jsx(Button, {
106
- buttonStyle: item.tone === 'primary' ? 'primary' : 'secondary',
133
+ variant: item.tone === 'primary' || bar && item.id === 'stop' ? 'primary' : 'secondary',
107
134
  disabled: disabled,
108
135
  onClick: ()=>{
109
136
  if (disabled) return;
@@ -1,4 +1,5 @@
1
1
  import type { TestDocApi } from '../contracts.js';
2
+ export declare function saveStatusMessage(test: TestDocApi): string | null;
2
3
  export declare function SaveStatus({ test }: {
3
4
  test: TestDocApi;
4
5
  }): import("react").JSX.Element;
@@ -1,8 +1,11 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useRef, useState } from 'react';
4
- import { Button } from '@payloadcms/ui';
4
+ import { Button } from '../../../ui/Button.js';
5
5
  import { READINESS_COPY as C } from '../../copy/readiness.js';
6
+ export function saveStatusMessage(test) {
7
+ return test.saving === 'saving' ? C.saving : test.saving === 'saved' || test.saving === 'idle' ? C.saved : test.saving === 'error' && !test.stale ? C.saveError : null;
8
+ }
6
9
  export function SaveStatus({ test }) {
7
10
  const inFlight = useRef(false);
8
11
  const [retrying, setRetrying] = useState(false);
@@ -17,7 +20,7 @@ export function SaveStatus({ test }) {
17
20
  setRetrying(false);
18
21
  }
19
22
  }
20
- const message = test.saving === 'saving' ? C.saving : test.saving === 'saved' ? C.saved : test.saving === 'error' ? C.saveError : null;
23
+ const message = saveStatusMessage(test);
21
24
  return /*#__PURE__*/ _jsxs("div", {
22
25
  className: "xp-save-status",
23
26
  "aria-live": "polite",
@@ -30,7 +33,7 @@ export function SaveStatus({ test }) {
30
33
  children: [
31
34
  /*#__PURE__*/ _jsx("span", {
32
35
  role: "alert",
33
- children: C.checkError
36
+ children: test.preflightError
34
37
  }),
35
38
  ' ',
36
39
  /*#__PURE__*/ _jsx("span", {
@@ -9,8 +9,10 @@ export type ReadinessContext = Pick<TestDocument, 'arms' | 'unitType' | 'flagPoi
9
9
  export declare const READINESS: Record<ReadinessCode, {
10
10
  section: SectionId | 'header';
11
11
  next(detail: unknown, ctx: ReadinessContext): string | null;
12
+ nextStep?: string;
12
13
  }>;
13
14
  export declare function nextStep(preflight: PreflightResult | null, ctx: ReadinessContext): {
15
+ nextStep?: string | undefined;
14
16
  section: "header" | SectionId;
15
17
  sentence: string;
16
18
  code: ReadinessCode;
@@ -1,5 +1,6 @@
1
+ import { formatDate } from '../../format/date.js';
1
2
  import { actionGroupLabel } from '../../actions/actions.model.js';
2
- import { READINESS_COPY as C, attributeNext, contentNext, goalPeopleNext, parameterNext, placementNext, ruleNext, versionsNext } from '../../copy/readiness.js';
3
+ import { unsupportedContentCopy, versionPageUnavailableCopy, versionPageUncheckedCopy, placementFailureCopy, unpublishedPageCopy, READINESS_COPY as C, attributeNext, contentNext, goalPeopleNext, parameterNext, placementNext, ruleNext, versionsNext } from '../../copy/readiness.js';
3
4
  import { SECTION_ORDER } from '../contracts.js';
4
5
  const object = (value)=>value && typeof value === 'object' && !Array.isArray(value) ? value : {};
5
6
  const first = (value)=>object(Array.isArray(value) ? value[0] : value);
@@ -15,8 +16,9 @@ const row = (section, next)=>({
15
16
  });
16
17
  const versions = row('versions', (detail)=>versionsNext(detailText(detail, C.versionFallback)));
17
18
  /** Exhaustive mapping of the checkStart ReadinessCode union, including all ten invariant checks. */ export const READINESS = {
19
+ 'make-content-unsupported': row('header', (detail)=>unsupportedContentCopy(text(detail, 'This page'))),
18
20
  'invalid-status': row('header', ()=>null),
19
- 'no-placements': row('what', ()=>C.noPlacements),
21
+ 'no-placements': row('what', (detail)=>detail === undefined ? C.noPlacements : placementFailureCopy(detail)),
20
22
  'empty-slot': row('versions', (detail, ctx)=>{
21
23
  const placements = Array.isArray(detail) ? detail : [];
22
24
  const missing = ctx.arms.find((item)=>item.kind !== 'holdout' && placements.some((placement)=>{
@@ -34,6 +36,7 @@ const versions = row('versions', (detail)=>versionsNext(detailText(detail, C.ver
34
36
  }),
35
37
  'flag-point-busy': row('what', ()=>C.busy),
36
38
  'flag-placement-missing': row('versions', (detail, ctx)=>{
39
+ if (typeof detail === 'string') return placementFailureCopy(detail);
37
40
  const value = object(detail);
38
41
  const placement = first(value.placements);
39
42
  const missing = Array.isArray(value.missing) ? value.missing[0] : undefined;
@@ -47,7 +50,15 @@ const versions = row('versions', (detail)=>versionsNext(detailText(detail, C.ver
47
50
  'layer-full': row('versions', ()=>C.full),
48
51
  'layer-unit-mismatch': row('header', ()=>C.differentPeople),
49
52
  'page-no-targets': row('versions', ()=>C.noTargets),
53
+ 'page-target-unpublished': row('versions', (detail)=>unpublishedPageCopy(text(detail, 'This page'))),
54
+ 'page-targets-unchecked': row('versions', ()=>C.uncheckedPages),
55
+ 'version-page-unavailable': row('versions', (detail)=>versionPageUnavailableCopy(text(detail, 'This page'))),
56
+ 'version-page-unchecked': row('versions', (detail)=>versionPageUncheckedCopy(text(detail, 'This page'))),
50
57
  'page-user-unit': row('header', ()=>C.pageVisitors),
58
+ 'block-path-rule': {
59
+ ...row('who', ()=>C.blockPathRule),
60
+ nextStep: C.removeRule
61
+ },
51
62
  'invalid-targeting-rule': row('who', (detail)=>{
52
63
  const attributes = object(detail).attributes;
53
64
  return ruleNext(detailText(detail, Array.isArray(attributes) && attributes.length ? attributes.map(String).join(', ') : C.ruleFallback));
@@ -79,7 +90,10 @@ export function nextStep(preflight, ctx) {
79
90
  if (sentence) return {
80
91
  section,
81
92
  sentence,
82
- code: check.code
93
+ code: check.code,
94
+ ...mapping.nextStep ? {
95
+ nextStep: mapping.nextStep
96
+ } : {}
83
97
  };
84
98
  }
85
99
  }
@@ -93,12 +107,7 @@ export function sectionDone(test) {
93
107
  }
94
108
  export function displayDate(value) {
95
109
  if (typeof value !== 'string' || !Number.isFinite(Date.parse(value))) return null;
96
- return new Intl.DateTimeFormat('en-US', {
97
- year: 'numeric',
98
- month: 'short',
99
- day: 'numeric',
100
- timeZone: 'UTC'
101
- }).format(new Date(value));
110
+ return formatDate(value);
102
111
  }
103
112
  export function daysRemaining(doc, now) {
104
113
  const days = doc.analysis?.plannedDurationDays;
@@ -7,4 +7,5 @@ export declare function useTransition(test: TestDocApi, onSuccess?: () => void):
7
7
  run: (request: () => Promise<ApiResult<unknown>>, { reloadOnSuccess }?: {
8
8
  reloadOnSuccess?: boolean | undefined;
9
9
  }) => Promise<void>;
10
+ clearError: () => void;
10
11
  };
@@ -29,6 +29,7 @@ import { API_MESSAGES } from '../../copy/common.js';
29
29
  return {
30
30
  busy,
31
31
  error,
32
- run
32
+ run,
33
+ clearError: ()=>setError(null)
33
34
  };
34
35
  }
@@ -0,0 +1,14 @@
1
+ import type { DocId } from '../../../types/contracts.js';
2
+ import { type PageReleaseDecision } from '../../results/releasePages.model.js';
3
+ export declare function ReleasePagesDialog({ id, apiRoute, onClose, onSuccess, onRefusal }: {
4
+ id: DocId;
5
+ apiRoute: string;
6
+ onClose(): void;
7
+ onSuccess(): void;
8
+ onRefusal(message: string): void;
9
+ }): import("react").JSX.Element;
10
+ /** An inaccessible or deleted author never hides the recorded release date. */
11
+ export declare function PagesReleased({ decision, apiRoute }: {
12
+ decision: PageReleaseDecision;
13
+ apiRoute: string;
14
+ }): import("react").JSX.Element;
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { useAuth, useConfig } from '@payloadcms/ui';
5
+ import { Button } from '../../../ui/Button.js';
6
+ import { Dialog } from '../../../ui/Dialog.js';
7
+ import { createApi } from '../../api.js';
8
+ import { ACTIONS_COPY } from '../../copy/actions.js';
9
+ import { RESULTS_COPY as C, pagesReleasedCopy } from '../../copy/results.js';
10
+ import { formatDate } from '../../format/date.js';
11
+ import { pagesReleasedBy } from '../../results/releasePages.model.js';
12
+ export function ReleasePagesDialog({ id, apiRoute, onClose, onSuccess, onRefusal }) {
13
+ const [busy, setBusy] = useState(false);
14
+ const inFlight = useRef(false);
15
+ const mounted = useRef(true);
16
+ useEffect(()=>{
17
+ mounted.current = true;
18
+ return ()=>{
19
+ mounted.current = false;
20
+ };
21
+ }, []);
22
+ async function submit() {
23
+ if (inFlight.current) return;
24
+ inFlight.current = true;
25
+ setBusy(true);
26
+ const result = await createApi(apiRoute).releasePages(id);
27
+ if (!mounted.current) return;
28
+ onClose();
29
+ if (result.ok) onSuccess();
30
+ else onRefusal(result.message);
31
+ }
32
+ return /*#__PURE__*/ _jsxs(Dialog, {
33
+ open: true,
34
+ title: C.releasePages,
35
+ initialFocus: "cancel",
36
+ dismissible: !busy,
37
+ onClose: ()=>{
38
+ if (!inFlight.current) onClose();
39
+ },
40
+ children: [
41
+ /*#__PURE__*/ _jsx("p", {
42
+ children: C.releasePagesBody
43
+ }),
44
+ /*#__PURE__*/ _jsxs("div", {
45
+ className: "xp-ui-button-group",
46
+ children: [
47
+ /*#__PURE__*/ _jsx(Button, {
48
+ "data-dialog-cancel": true,
49
+ disabled: busy,
50
+ onClick: onClose,
51
+ children: ACTIONS_COPY.cancel
52
+ }),
53
+ /*#__PURE__*/ _jsx(Button, {
54
+ variant: "primary",
55
+ busy: busy,
56
+ onClick: ()=>{
57
+ void submit();
58
+ },
59
+ children: C.releasePages
60
+ })
61
+ ]
62
+ })
63
+ ]
64
+ });
65
+ }
66
+ /** An inaccessible or deleted author never hides the recorded release date. */ export function PagesReleased({ decision, apiRoute }) {
67
+ const { user } = useAuth();
68
+ const { config } = useConfig();
69
+ const collection = config.admin.user;
70
+ const by = decision.pagesReleasedBy;
71
+ const [resolved, setResolved] = useState(null);
72
+ const key = JSON.stringify([
73
+ apiRoute,
74
+ collection,
75
+ by
76
+ ]);
77
+ const own = pagesReleasedBy(by, user?.id);
78
+ useEffect(()=>{
79
+ if (!by || own || !collection) return;
80
+ const controller = new AbortController();
81
+ const query = new URLSearchParams({
82
+ depth: '0',
83
+ 'select[name]': 'true',
84
+ 'select[email]': 'true'
85
+ });
86
+ void (async ()=>{
87
+ try {
88
+ const response = await fetch(`${apiRoute.replace(/\/$/, '')}/${encodeURIComponent(collection)}/${encodeURIComponent(by)}?${query}`, {
89
+ method: 'GET',
90
+ credentials: 'include',
91
+ signal: controller.signal
92
+ });
93
+ if (!response.ok) return;
94
+ const author = await response.json();
95
+ if (!controller.signal.aborted && author) setResolved({
96
+ key,
97
+ author: {
98
+ name: typeof author.name === 'string' ? author.name : null,
99
+ email: typeof author.email === 'string' ? author.email : null
100
+ }
101
+ });
102
+ } catch {}
103
+ })();
104
+ return ()=>controller.abort();
105
+ }, [
106
+ apiRoute,
107
+ by,
108
+ collection,
109
+ key,
110
+ own
111
+ ]);
112
+ const name = own ?? pagesReleasedBy(by, user?.id, resolved?.key === key ? resolved.author : null);
113
+ return /*#__PURE__*/ _jsx("p", {
114
+ children: pagesReleasedCopy(formatDate(decision.pagesReleasedAt), name)
115
+ });
116
+ }
@@ -1,7 +1,20 @@
1
1
  import '../../styles.js';
2
+ import type { ReactNode } from 'react';
2
3
  import type { DocId } from '../../../types/contracts.js';
4
+ import type { ResultsResponse } from '../../../types/endpoints.js';
3
5
  import type { SurfaceProps } from '../contracts.js';
4
- export declare function ResultsBody({ id, surface }: {
6
+ export type ResultsPageParts = {
7
+ children: ReactNode;
8
+ actions: ReactNode;
9
+ released: ReactNode;
10
+ data?: ResultsResponse;
11
+ };
12
+ type ResultsBodyProps = {
13
+ renderPage?: (parts: ResultsPageParts) => ReactNode;
5
14
  id: DocId;
6
15
  surface: SurfaceProps;
7
- }): import("react").JSX.Element;
16
+ onRecomputed?: () => void;
17
+ onPagesReleased?: () => void;
18
+ };
19
+ export declare function ResultsBody({ id, surface, onRecomputed, onPagesReleased, renderPage }: ResultsBodyProps): import("react").JSX.Element;
20
+ export {};