@delmaredigital/payload-experiments 0.1.0-rc.7 → 0.2.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 (207) hide show
  1. package/README.md +28 -0
  2. package/dist/admin/LayerUsage.d.ts +1 -1
  3. package/dist/admin/LayerUsage.js +23 -11
  4. package/dist/admin/MetricSourceSelect.js +2 -1
  5. package/dist/admin/TestsNavLink.d.ts +1 -0
  6. package/dist/admin/TestsNavLink.js +30 -0
  7. package/dist/admin/allocation.model.js +1 -1
  8. package/dist/admin/copy/actions.d.ts +0 -2
  9. package/dist/admin/copy/actions.js +0 -2
  10. package/dist/admin/copy/advanced.d.ts +55 -0
  11. package/dist/admin/copy/advanced.js +55 -0
  12. package/dist/admin/copy/armSelect.js +1 -1
  13. package/dist/admin/copy/arms.d.ts +0 -13
  14. package/dist/admin/copy/arms.js +0 -13
  15. package/dist/admin/copy/{who.d.ts → audience.d.ts} +1 -9
  16. package/dist/admin/copy/{who.js → audience.js} +1 -9
  17. package/dist/admin/copy/history.d.ts +0 -4
  18. package/dist/admin/copy/history.js +3 -7
  19. package/dist/admin/copy/metrics.d.ts +2 -2
  20. package/dist/admin/copy/metrics.js +2 -2
  21. package/dist/admin/copy/newTest.d.ts +32 -0
  22. package/dist/admin/copy/newTest.js +32 -0
  23. package/dist/admin/copy/readiness.d.ts +45 -0
  24. package/dist/admin/copy/readiness.js +50 -0
  25. package/dist/admin/copy/recommendation.d.ts +26 -0
  26. package/dist/admin/copy/recommendation.js +25 -0
  27. package/dist/admin/copy/results.d.ts +0 -1
  28. package/dist/admin/copy/results.js +0 -1
  29. package/dist/admin/copy/review.d.ts +0 -17
  30. package/dist/admin/copy/review.js +1 -112
  31. package/dist/admin/copy/setting.d.ts +10 -0
  32. package/dist/admin/copy/setting.js +10 -0
  33. package/dist/admin/copy/surface.d.ts +36 -0
  34. package/dist/admin/copy/surface.js +47 -0
  35. package/dist/admin/copy/testPage.d.ts +88 -0
  36. package/dist/admin/copy/testPage.js +88 -0
  37. package/dist/admin/paths.d.ts +5 -13
  38. package/dist/admin/paths.js +5 -13
  39. package/dist/admin/review/review.model.d.ts +3 -26
  40. package/dist/admin/review/review.model.js +3 -30
  41. package/dist/admin/surface/ResultsRoute.d.ts +7 -0
  42. package/dist/admin/surface/ResultsRoute.js +58 -0
  43. package/dist/admin/surface/SurfaceShell.d.ts +6 -0
  44. package/dist/admin/surface/SurfaceShell.js +35 -0
  45. package/dist/admin/surface/TestsHome.d.ts +4 -0
  46. package/dist/admin/surface/TestsHome.js +212 -0
  47. package/dist/admin/surface/TestsSurface.d.ts +2 -0
  48. package/dist/admin/surface/TestsSurface.js +36 -0
  49. package/dist/admin/surface/contracts.d.ts +48 -0
  50. package/dist/admin/surface/contracts.js +8 -0
  51. package/dist/admin/surface/home.model.d.ts +41 -0
  52. package/dist/admin/surface/home.model.js +59 -0
  53. package/dist/admin/surface/newTest/NewTest.d.ts +7 -0
  54. package/dist/admin/surface/newTest/NewTest.js +355 -0
  55. package/dist/admin/surface/newTest/newTest.css +9 -0
  56. package/dist/admin/surface/pageBlock/pageBlock.model.d.ts +6 -0
  57. package/dist/admin/surface/pageBlock/pageBlock.model.js +8 -0
  58. package/dist/admin/surface/pageBlock/parts.d.ts +2 -0
  59. package/dist/admin/surface/pageBlock/parts.js +64 -0
  60. package/dist/admin/surface/readiness/ActionDialogs.d.ts +15 -0
  61. package/dist/admin/surface/readiness/ActionDialogs.js +274 -0
  62. package/dist/admin/surface/readiness/Banner.d.ts +2 -0
  63. package/dist/admin/surface/readiness/Banner.js +138 -0
  64. package/dist/admin/surface/readiness/SaveStatus.d.ts +4 -0
  65. package/dist/admin/surface/readiness/SaveStatus.js +50 -0
  66. package/dist/admin/surface/readiness/parts.d.ts +2 -0
  67. package/dist/admin/surface/readiness/parts.js +9 -0
  68. package/dist/admin/surface/readiness/readiness.model.d.ts +22 -0
  69. package/dist/admin/surface/readiness/readiness.model.js +107 -0
  70. package/dist/admin/surface/readiness/useTransition.d.ts +10 -0
  71. package/dist/admin/surface/readiness/useTransition.js +34 -0
  72. package/dist/admin/surface/results/ResultsBody.d.ts +7 -0
  73. package/dist/admin/surface/results/ResultsBody.js +262 -0
  74. package/dist/admin/surface/results/recommendation.model.d.ts +12 -0
  75. package/dist/admin/surface/results/recommendation.model.js +59 -0
  76. package/dist/admin/surface/results/useResults.d.ts +56 -0
  77. package/dist/admin/surface/results/useResults.js +183 -0
  78. package/dist/admin/surface/setting/parts.d.ts +3 -0
  79. package/dist/admin/surface/setting/parts.js +263 -0
  80. package/dist/admin/surface/setting/setting.css +29 -0
  81. package/dist/admin/surface/setting/setting.model.d.ts +8 -0
  82. package/dist/admin/surface/setting/setting.model.js +25 -0
  83. package/dist/admin/surface/surface.css +50 -0
  84. package/dist/admin/surface/test/AdvancedSection.d.ts +2 -0
  85. package/dist/admin/surface/test/AdvancedSection.js +153 -0
  86. package/dist/admin/surface/test/GoalSection.d.ts +2 -0
  87. package/dist/admin/surface/test/GoalSection.js +198 -0
  88. package/dist/admin/surface/test/LengthSection.d.ts +2 -0
  89. package/dist/admin/surface/test/LengthSection.js +87 -0
  90. package/dist/admin/surface/test/Section.d.ts +7 -0
  91. package/dist/admin/surface/test/Section.js +27 -0
  92. package/dist/admin/surface/test/TargetingSection.d.ts +2 -0
  93. package/dist/admin/surface/test/TargetingSection.js +164 -0
  94. package/dist/admin/surface/test/TestHeader.d.ts +4 -0
  95. package/dist/admin/surface/test/TestHeader.js +69 -0
  96. package/dist/admin/surface/test/TestPage.d.ts +7 -0
  97. package/dist/admin/surface/test/TestPage.js +84 -0
  98. package/dist/admin/surface/test/VersionsSection.d.ts +4 -0
  99. package/dist/admin/surface/test/VersionsSection.js +208 -0
  100. package/dist/admin/surface/test/fields.d.ts +55 -0
  101. package/dist/admin/surface/test/fields.js +185 -0
  102. package/dist/admin/surface/test/pageParts.d.ts +2 -0
  103. package/dist/admin/surface/test/pageParts.js +39 -0
  104. package/dist/admin/surface/test/testPage.css +23 -0
  105. package/dist/admin/surface/test/testPage.model.d.ts +17 -0
  106. package/dist/admin/surface/test/testPage.model.js +53 -0
  107. package/dist/admin/surface/testDocument.model.d.ts +39 -0
  108. package/dist/admin/surface/testDocument.model.js +61 -0
  109. package/dist/admin/surface/useTestDocument.d.ts +22 -0
  110. package/dist/admin/surface/useTestDocument.js +345 -0
  111. package/dist/admin/targeting/targeting.model.js +2 -2
  112. package/dist/admin/useLayerUsage.d.ts +0 -5
  113. package/dist/admin/useLayerUsage.js +1 -39
  114. package/dist/admin/views/CollectionEditRedirect.d.ts +4 -0
  115. package/dist/admin/views/CollectionEditRedirect.js +14 -0
  116. package/dist/admin/views/CollectionListRedirect.d.ts +2 -0
  117. package/dist/admin/views/CollectionListRedirect.js +8 -0
  118. package/dist/admin/views/ExperimentsRootView.d.ts +2 -0
  119. package/dist/admin/views/ExperimentsRootView.js +175 -0
  120. package/dist/admin/views/access.model.d.ts +7 -0
  121. package/dist/admin/views/access.model.js +5 -0
  122. package/dist/collections/experiments/config.js +18 -125
  123. package/dist/collections/experiments/createDefaults.js +6 -0
  124. package/dist/collections/layers.js +43 -12
  125. package/dist/collections/metrics.js +70 -14
  126. package/dist/collections/serialize.js +4 -1
  127. package/dist/db/snapshots.d.ts +2 -1
  128. package/dist/db/snapshots.js +2 -2
  129. package/dist/endpoints/results.js +7 -0
  130. package/dist/endpoints/summary.d.ts +1 -1
  131. package/dist/endpoints/summary.js +37 -1
  132. package/dist/exports/adminClient.d.ts +2 -13
  133. package/dist/exports/adminClient.js +2 -13
  134. package/dist/exports/adminRsc.d.ts +3 -0
  135. package/dist/exports/adminRsc.js +3 -0
  136. package/dist/exports/index.d.ts +1 -1
  137. package/dist/options.js +29 -0
  138. package/dist/plugin.js +25 -0
  139. package/dist/puck/editor/CreateExperimentDrawer.d.ts +2 -1
  140. package/dist/puck/editor/CreateExperimentDrawer.js +17 -57
  141. package/dist/puck/editor/ExperimentSlotEditor.js +6 -0
  142. package/dist/puck/editor/copy.d.ts +10 -9
  143. package/dist/puck/editor/copy.js +11 -10
  144. package/dist/puck/editor/createTest.d.ts +7 -0
  145. package/dist/puck/editor/createTest.js +50 -0
  146. package/dist/puck/editor/editor.model.d.ts +28 -1
  147. package/dist/puck/editor/editor.model.js +61 -0
  148. package/dist/results/analyse.js +4 -1
  149. package/dist/results/verdict.d.ts +1 -0
  150. package/dist/results/verdict.js +7 -4
  151. package/dist/transitions/checkStart.d.ts +1 -0
  152. package/dist/transitions/placements.d.ts +5 -1
  153. package/dist/transitions/placements.js +14 -7
  154. package/dist/transitions/preflightData.d.ts +7 -1
  155. package/dist/transitions/preflightData.js +199 -148
  156. package/dist/transitions/start.js +2 -2
  157. package/dist/types/admin.d.ts +1 -0
  158. package/dist/types/contracts.d.ts +17 -0
  159. package/dist/types/endpoints.d.ts +11 -1
  160. package/dist/types/snapshot.d.ts +1 -0
  161. package/dist/types/surface.d.ts +29 -0
  162. package/dist/types/surface.js +1 -0
  163. package/package.json +7 -1
  164. package/dist/access.d.ts +0 -1
  165. package/dist/access.js +0 -2
  166. package/dist/admin/AllocationField.d.ts +0 -2
  167. package/dist/admin/AllocationField.js +0 -80
  168. package/dist/admin/FlagPointSelect.d.ts +0 -2
  169. package/dist/admin/FlagPointSelect.js +0 -54
  170. package/dist/admin/LockedFieldNotice.d.ts +0 -2
  171. package/dist/admin/LockedFieldNotice.js +0 -26
  172. package/dist/admin/PhaseHistory.d.ts +0 -2
  173. package/dist/admin/PhaseHistory.js +0 -76
  174. package/dist/admin/StatusCell.d.ts +0 -3
  175. package/dist/admin/StatusCell.js +0 -19
  176. package/dist/admin/SummaryCell.d.ts +0 -2
  177. package/dist/admin/SummaryCell.js +0 -32
  178. package/dist/admin/actions/AddMetricDialog.d.ts +0 -6
  179. package/dist/admin/actions/AddMetricDialog.js +0 -138
  180. package/dist/admin/actions/AllocateDialog.d.ts +0 -6
  181. package/dist/admin/actions/AllocateDialog.js +0 -81
  182. package/dist/admin/actions/ExperimentActions.d.ts +0 -3
  183. package/dist/admin/actions/ExperimentActions.js +0 -110
  184. package/dist/admin/arms/ArmsField.d.ts +0 -2
  185. package/dist/admin/arms/ArmsField.js +0 -310
  186. package/dist/admin/arms/ParamsEditor.d.ts +0 -16
  187. package/dist/admin/arms/ParamsEditor.js +0 -79
  188. package/dist/admin/copy/tabs.d.ts +0 -7
  189. package/dist/admin/copy/tabs.js +0 -7
  190. package/dist/admin/permissions.model.d.ts +0 -10
  191. package/dist/admin/permissions.model.js +0 -4
  192. package/dist/admin/phaseHistory.model.d.ts +0 -19
  193. package/dist/admin/phaseHistory.model.js +0 -19
  194. package/dist/admin/results/ResultsView.d.ts +0 -3
  195. package/dist/admin/results/ResultsView.js +0 -312
  196. package/dist/admin/review/ReviewPanel.d.ts +0 -2
  197. package/dist/admin/review/ReviewPanel.js +0 -268
  198. package/dist/admin/summaryBatcher.d.ts +0 -8
  199. package/dist/admin/summaryBatcher.js +0 -33
  200. package/dist/admin/targeting/TargetingAttributeField.d.ts +0 -2
  201. package/dist/admin/targeting/TargetingAttributeField.js +0 -31
  202. package/dist/admin/targeting/TargetingOpField.d.ts +0 -2
  203. package/dist/admin/targeting/TargetingOpField.js +0 -45
  204. package/dist/admin/targeting/TargetingValueField.d.ts +0 -2
  205. package/dist/admin/targeting/TargetingValueField.js +0 -142
  206. package/dist/index.d.ts +0 -1
  207. package/dist/index.js +0 -4
@@ -0,0 +1,274 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { Button, TextInput } from '@payloadcms/ui';
5
+ import { DialogFrame } from '../../actions/ConfirmDialog.js';
6
+ import { MakeContentResult } from '../../actions/MakeContentResult.js';
7
+ import { expectedConfirm, matchesConfirm } from '../../../transitions/confirmPhrases.js';
8
+ import { ACTIONS_COPY as A } from '../../copy/actions.js';
9
+ import { REVIEW_COPY as R } from '../../copy/review.js';
10
+ import { READINESS_COPY as C } from '../../copy/readiness.js';
11
+ import { API_MESSAGES, refusalMessage } from '../../copy/common.js';
12
+ import { createApi } from '../../api.js';
13
+ import { useTransition } from './useTransition.js';
14
+ /** Prop-driven ConfirmDialog adapter: same model/copy, section reload instead of a page reload. */ export function ConfirmAction({ test, surface, action, winnerLabel, onClose }) {
15
+ const api = createApi(surface.apiRoute);
16
+ const [typed, setTyped] = useState('');
17
+ const [content, setContent] = useState(null);
18
+ const transition = useTransition(test, onClose);
19
+ const phrase = action.confirm.phrase ? expectedConfirm(action.confirm.phrase.action, winnerLabel) : null;
20
+ const ready = action.id !== 'start' || Boolean(test.preflight && !test.preflightPending && !test.preflightError && test.preflight.checks.every((check)=>check.ok));
21
+ const enabled = ready && !transition.busy && !test.hasUnsaved && test.saving !== 'saving' && (!phrase || matchesConfirm(phrase, typed));
22
+ async function submit() {
23
+ if (!enabled || !test.doc) return;
24
+ const id = test.doc.id;
25
+ await transition.run(async ()=>{
26
+ if (action.archive) return api.archive(id, {
27
+ ...action.archive,
28
+ ...phrase ? {
29
+ confirmation: typed.trim()
30
+ } : {}
31
+ });
32
+ if (action.id === 'start') return api.start(id);
33
+ if (action.id === 'stop') return api.stop(id);
34
+ if (action.id === 'release-holdout') return api.releaseHoldout(id);
35
+ const result = await api.makeContent(id);
36
+ if (result.ok && result.data) {
37
+ setContent(result.data);
38
+ return result;
39
+ }
40
+ return result.ok ? {
41
+ ok: false,
42
+ status: 0,
43
+ retryable: true,
44
+ message: API_MESSAGES.unreadable
45
+ } : result;
46
+ }, {
47
+ reloadOnSuccess: action.id !== 'make-content'
48
+ });
49
+ }
50
+ function close() {
51
+ if (transition.busy) return;
52
+ if (content) void test.reload();
53
+ onClose();
54
+ }
55
+ return /*#__PURE__*/ _jsxs(DialogFrame, {
56
+ title: action.confirm.title,
57
+ onClose: close,
58
+ children: [
59
+ content ? /*#__PURE__*/ _jsx(MakeContentResult, {
60
+ result: content,
61
+ onRetry: submit,
62
+ busy: transition.busy
63
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
64
+ children: [
65
+ /*#__PURE__*/ _jsx("p", {
66
+ children: action.confirm.body
67
+ }),
68
+ phrase ? /*#__PURE__*/ _jsxs(_Fragment, {
69
+ children: [
70
+ /*#__PURE__*/ _jsxs("p", {
71
+ children: [
72
+ A.typePhrase,
73
+ " ",
74
+ /*#__PURE__*/ _jsx("strong", {
75
+ children: phrase
76
+ })
77
+ ]
78
+ }),
79
+ /*#__PURE__*/ _jsx(TextInput, {
80
+ path: "xp-confirmation",
81
+ label: A.confirmation,
82
+ value: typed,
83
+ readOnly: transition.busy,
84
+ onChange: (event)=>setTyped(event.target.value)
85
+ })
86
+ ]
87
+ }) : null
88
+ ]
89
+ }),
90
+ transition.error ? /*#__PURE__*/ _jsx("p", {
91
+ role: "alert",
92
+ children: transition.error
93
+ }) : null,
94
+ !content ? /*#__PURE__*/ _jsx(Button, {
95
+ disabled: !enabled,
96
+ onClick: ()=>{
97
+ void submit();
98
+ },
99
+ children: action.label
100
+ }) : null,
101
+ /*#__PURE__*/ _jsx(Button, {
102
+ disabled: transition.busy,
103
+ onClick: close,
104
+ children: content ? A.done : A.cancel
105
+ })
106
+ ]
107
+ });
108
+ }
109
+ /** AllocateDialog's validation and copy, with data supplied by the test document. */ export function AllocateAction({ test, surface, onClose }) {
110
+ const current = test.doc?.allocation ?? 100;
111
+ const [value, setValue] = useState(String(Math.min(100, current + 1)));
112
+ const transition = useTransition(test, onClose);
113
+ const allocation = Number(value);
114
+ const valid = value.trim() !== '' && Number.isInteger(allocation) && allocation > current && allocation <= 100;
115
+ const disabled = transition.busy || test.hasUnsaved || test.saving === 'saving';
116
+ return /*#__PURE__*/ _jsxs(DialogFrame, {
117
+ title: C.allocate,
118
+ onClose: ()=>{
119
+ if (!transition.busy) onClose();
120
+ },
121
+ children: [
122
+ /*#__PURE__*/ _jsx("p", {
123
+ children: R.allocationBody
124
+ }),
125
+ /*#__PURE__*/ _jsxs("label", {
126
+ children: [
127
+ C.participation,
128
+ /*#__PURE__*/ _jsx("input", {
129
+ type: "number",
130
+ min: current + 1,
131
+ max: 100,
132
+ step: 1,
133
+ value: value,
134
+ disabled: disabled,
135
+ onChange: (event)=>setValue(event.target.value)
136
+ })
137
+ ]
138
+ }),
139
+ !valid ? /*#__PURE__*/ _jsx("p", {
140
+ children: R.allocationInvalid
141
+ }) : null,
142
+ transition.error ? /*#__PURE__*/ _jsx("p", {
143
+ role: "alert",
144
+ children: transition.error
145
+ }) : null,
146
+ /*#__PURE__*/ _jsx(Button, {
147
+ disabled: disabled || !valid,
148
+ onClick: ()=>{
149
+ if (valid && test.doc) void transition.run(()=>createApi(surface.apiRoute).allocate(test.doc.id, {
150
+ allocation
151
+ }));
152
+ },
153
+ children: C.allocate
154
+ }),
155
+ /*#__PURE__*/ _jsx(Button, {
156
+ disabled: transition.busy,
157
+ onClick: onClose,
158
+ children: A.cancel
159
+ })
160
+ ]
161
+ });
162
+ }
163
+ /** AddMetricDialog adapter; the primary goal is deliberately absent from the role picker. */ export function AddMetricAction({ test, surface, onClose }) {
164
+ const [metrics, setMetrics] = useState([]);
165
+ const [loading, setLoading] = useState(true);
166
+ const [loadError, setLoadError] = useState(null);
167
+ const [metric, setMetric] = useState('');
168
+ const [role, setRole] = useState('secondary');
169
+ const transition = useTransition(test, onClose);
170
+ useEffect(()=>{
171
+ const controller = new AbortController();
172
+ void (async ()=>{
173
+ try {
174
+ const response = await fetch(`${surface.apiRoute.replace(/\/$/, '')}/xp-metrics?depth=0&limit=100`, {
175
+ credentials: 'include',
176
+ signal: controller.signal
177
+ });
178
+ if (!response.ok) throw new Error(refusalMessage(undefined, response.status));
179
+ const data = await response.json();
180
+ if (!Array.isArray(data.docs)) throw new Error(API_MESSAGES.unreadable);
181
+ if (!controller.signal.aborted) setMetrics(data.docs);
182
+ } catch (cause) {
183
+ if (!controller.signal.aborted) setLoadError(cause instanceof Error ? cause.message : API_MESSAGES.network);
184
+ } finally{
185
+ if (!controller.signal.aborted) setLoading(false);
186
+ }
187
+ })();
188
+ return ()=>controller.abort();
189
+ }, [
190
+ surface.apiRoute
191
+ ]);
192
+ const attached = new Set([
193
+ test.doc?.metrics?.primary,
194
+ ...test.doc?.metrics?.secondary ?? [],
195
+ ...test.doc?.metrics?.guardrails ?? []
196
+ ].filter((id)=>id != null).map(String));
197
+ const available = metrics.filter((item)=>!attached.has(String(item.id)));
198
+ const selected = available.find((item)=>String(item.id) === metric);
199
+ const disabled = transition.busy || test.hasUnsaved || test.saving === 'saving';
200
+ return /*#__PURE__*/ _jsxs(DialogFrame, {
201
+ title: C.addMetric,
202
+ onClose: ()=>{
203
+ if (!transition.busy) onClose();
204
+ },
205
+ children: [
206
+ loading ? /*#__PURE__*/ _jsx("p", {
207
+ role: "status",
208
+ children: R.loadingMetrics
209
+ }) : null,
210
+ /*#__PURE__*/ _jsxs("label", {
211
+ children: [
212
+ R.role,
213
+ /*#__PURE__*/ _jsxs("select", {
214
+ value: role,
215
+ disabled: disabled,
216
+ onChange: (event)=>setRole(event.target.value),
217
+ children: [
218
+ /*#__PURE__*/ _jsx("option", {
219
+ value: "secondary",
220
+ children: R.supporting
221
+ }),
222
+ /*#__PURE__*/ _jsx("option", {
223
+ value: "guardrail",
224
+ children: R.guardrail
225
+ })
226
+ ]
227
+ })
228
+ ]
229
+ }),
230
+ /*#__PURE__*/ _jsxs("label", {
231
+ children: [
232
+ R.metric,
233
+ /*#__PURE__*/ _jsxs("select", {
234
+ value: metric,
235
+ disabled: disabled || loading,
236
+ onChange: (event)=>setMetric(event.target.value),
237
+ children: [
238
+ /*#__PURE__*/ _jsx("option", {
239
+ value: "",
240
+ children: R.chooseMetric
241
+ }),
242
+ available.map((item)=>/*#__PURE__*/ _jsx("option", {
243
+ value: String(item.id),
244
+ children: item.name
245
+ }, item.id))
246
+ ]
247
+ })
248
+ ]
249
+ }),
250
+ !loading && !loadError && !available.length ? /*#__PURE__*/ _jsx("p", {
251
+ children: R.noMetrics
252
+ }) : null,
253
+ loadError || transition.error ? /*#__PURE__*/ _jsx("p", {
254
+ role: "alert",
255
+ children: loadError ?? transition.error
256
+ }) : null,
257
+ /*#__PURE__*/ _jsx(Button, {
258
+ disabled: disabled || loading || !!loadError || !selected,
259
+ onClick: ()=>{
260
+ if (selected && test.doc) void transition.run(()=>createApi(surface.apiRoute).addMetric(test.doc.id, {
261
+ metric: selected.id,
262
+ role
263
+ }));
264
+ },
265
+ children: C.addMetric
266
+ }),
267
+ /*#__PURE__*/ _jsx(Button, {
268
+ disabled: transition.busy,
269
+ onClick: onClose,
270
+ children: A.cancel
271
+ })
272
+ ]
273
+ });
274
+ }
@@ -0,0 +1,2 @@
1
+ import type { SectionProps } from '../contracts.js';
2
+ export declare function Banner(props: SectionProps): import("react").JSX.Element | null;
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { Button } from '@payloadcms/ui';
5
+ import { actionGroupLabel, availableActions } from '../../actions/actions.model.js';
6
+ import { createApi } from '../../api.js';
7
+ import { READINESS_COPY as C, decidedCopy, nextCopy, runningCopy, stoppedCopy } from '../../copy/readiness.js';
8
+ import { daysRemaining, displayDate, nextStep } from './readiness.model.js';
9
+ import { AddMetricAction, AllocateAction, ConfirmAction } from './ActionDialogs.js';
10
+ import { useTransition } from './useTransition.js';
11
+ const RUNNING_ACTIONS = [
12
+ 'stop',
13
+ 'add-metric',
14
+ 'allocate'
15
+ ];
16
+ export function Banner(props) {
17
+ return props.test.doc ? /*#__PURE__*/ _jsx(BannerContent, {
18
+ ...props
19
+ }, `${props.test.doc.id}/${props.test.doc.status}`) : null;
20
+ }
21
+ function BannerContent({ test, surface }) {
22
+ const [selected, setSelected] = useState(null);
23
+ const [people, setPeople] = useState(null);
24
+ const transition = useTransition(test);
25
+ const doc = test.doc;
26
+ useEffect(()=>{
27
+ let active = true;
28
+ setPeople(null);
29
+ if (surface.canReadResults && doc.status === 'running') {
30
+ void createApi(surface.apiRoute).summary([
31
+ doc.id
32
+ ]).then((result)=>{
33
+ const count = result.ok ? result.data?.[doc.id]?.exposures : undefined;
34
+ if (active) setPeople(typeof count === 'number' && Number.isFinite(count) ? count : null);
35
+ });
36
+ }
37
+ return ()=>{
38
+ active = false;
39
+ };
40
+ }, [
41
+ doc.id,
42
+ doc.status,
43
+ surface.apiRoute,
44
+ surface.canReadResults
45
+ ]);
46
+ const decision = doc.decision;
47
+ const winner = actionGroupLabel(decision?.winnerArm ?? 'control', doc.arms.find((item)=>item.key === decision?.winnerArm)?.label);
48
+ const next = nextStep(test.preflight, {
49
+ ...doc,
50
+ flagPoints: surface.flagPoints
51
+ });
52
+ const setup = doc.status === 'draft' || doc.status === 'stopped';
53
+ const ready = Boolean(test.preflight && !test.preflightPending && !test.preflightError && test.preflight.checks.every((check)=>check.ok));
54
+ const unsaved = test.hasUnsaved || test.saving === 'saving';
55
+ const startReason = unsaved ? C.saveFirst : next?.sentence ?? (!ready ? test.preflightError ? C.checkError : C.checking : undefined);
56
+ const actions = availableActions({
57
+ status: doc.status,
58
+ kind: doc.kind,
59
+ arms: doc.arms,
60
+ decision,
61
+ canManage: surface.canManage
62
+ }).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' ? {
63
+ ...action,
64
+ label: C.addMetric
65
+ } : action.id === 'allocate' ? {
66
+ ...action,
67
+ label: C.allocate
68
+ } : action);
69
+ const action = actions.find((item)=>item.id === selected);
70
+ 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;
71
+ return /*#__PURE__*/ _jsxs("div", {
72
+ className: `xp-readiness xp-readiness--${setup && ready ? 'ready' : 'neutral'}`,
73
+ style: {
74
+ padding: '1rem',
75
+ border: '1px solid var(--theme-elevation-200)',
76
+ borderRadius: 'var(--style-radius-s)',
77
+ background: setup && ready ? 'var(--theme-success-100)' : 'var(--theme-elevation-50)',
78
+ color: 'var(--theme-text)'
79
+ },
80
+ children: [
81
+ /*#__PURE__*/ _jsx("p", {
82
+ role: "status",
83
+ children: setup && next && !ready && next.section !== 'header' ? /*#__PURE__*/ _jsx("a", {
84
+ href: `#section-${next.section}`,
85
+ children: sentence
86
+ }) : sentence
87
+ }),
88
+ doc.status === 'stopped' ? /*#__PURE__*/ _jsx("p", {
89
+ children: stoppedCopy(displayDate(doc.stoppedAt))
90
+ }) : null,
91
+ /*#__PURE__*/ _jsx("div", {
92
+ className: "xp-readiness__actions",
93
+ style: {
94
+ display: 'flex',
95
+ gap: '0.75rem',
96
+ flexWrap: 'wrap',
97
+ alignItems: 'center'
98
+ },
99
+ children: actions.map((item)=>{
100
+ const reason = item.id === 'start' ? startReason : unsaved ? C.saveFirst : item.id === 'allocate' && doc.allocation >= 100 ? C.everyone : undefined;
101
+ const disabled = transition.busy || !!selected || !!reason;
102
+ return /*#__PURE__*/ _jsx("span", {
103
+ title: reason,
104
+ children: /*#__PURE__*/ _jsx(Button, {
105
+ buttonStyle: item.tone === 'primary' ? 'primary' : 'secondary',
106
+ disabled: disabled,
107
+ onClick: ()=>{
108
+ if (disabled) return;
109
+ if (item.id === 'start' && doc.status === 'draft') void transition.run(()=>createApi(surface.apiRoute).start(doc.id));
110
+ else setSelected(item.id);
111
+ },
112
+ children: item.label
113
+ })
114
+ }, item.id);
115
+ })
116
+ }),
117
+ transition.error ? /*#__PURE__*/ _jsx("p", {
118
+ role: "alert",
119
+ children: transition.error
120
+ }) : null,
121
+ action?.id === 'allocate' ? /*#__PURE__*/ _jsx(AllocateAction, {
122
+ test: test,
123
+ surface: surface,
124
+ onClose: ()=>setSelected(null)
125
+ }) : action?.id === 'add-metric' ? /*#__PURE__*/ _jsx(AddMetricAction, {
126
+ test: test,
127
+ surface: surface,
128
+ onClose: ()=>setSelected(null)
129
+ }) : action ? /*#__PURE__*/ _jsx(ConfirmAction, {
130
+ test: test,
131
+ surface: surface,
132
+ action: action,
133
+ winnerLabel: winner,
134
+ onClose: ()=>setSelected(null)
135
+ }) : null
136
+ ]
137
+ });
138
+ }
@@ -0,0 +1,4 @@
1
+ import type { TestDocApi } from '../contracts.js';
2
+ export declare function SaveStatus({ test }: {
3
+ test: TestDocApi;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRef, useState } from 'react';
4
+ import { Button } from '@payloadcms/ui';
5
+ import { READINESS_COPY as C } from '../../copy/readiness.js';
6
+ export function SaveStatus({ test }) {
7
+ const inFlight = useRef(false);
8
+ const [retrying, setRetrying] = useState(false);
9
+ async function retry() {
10
+ if (inFlight.current || test.preflightPending || test.hasUnsaved || test.saving === 'saving') return;
11
+ inFlight.current = true;
12
+ setRetrying(true);
13
+ try {
14
+ await test.refreshPreflight();
15
+ } finally{
16
+ inFlight.current = false;
17
+ setRetrying(false);
18
+ }
19
+ }
20
+ const message = test.saving === 'saving' ? C.saving : test.saving === 'saved' ? C.saved : test.saving === 'error' ? C.saveError : null;
21
+ return /*#__PURE__*/ _jsxs("div", {
22
+ className: "xp-save-status",
23
+ "aria-live": "polite",
24
+ children: [
25
+ message ? /*#__PURE__*/ _jsx("span", {
26
+ role: test.saving === 'error' ? 'alert' : 'status',
27
+ children: message
28
+ }) : null,
29
+ test.preflightError ? /*#__PURE__*/ _jsxs("div", {
30
+ children: [
31
+ /*#__PURE__*/ _jsx("span", {
32
+ role: "alert",
33
+ children: C.checkError
34
+ }),
35
+ ' ',
36
+ /*#__PURE__*/ _jsx("span", {
37
+ title: test.hasUnsaved || test.saving === 'saving' ? C.saveFirst : undefined,
38
+ children: /*#__PURE__*/ _jsx(Button, {
39
+ disabled: retrying || test.preflightPending || test.hasUnsaved || test.saving === 'saving',
40
+ onClick: ()=>{
41
+ void retry();
42
+ },
43
+ children: C.retry
44
+ })
45
+ })
46
+ ]
47
+ }) : null
48
+ ]
49
+ });
50
+ }
@@ -0,0 +1,2 @@
1
+ import type { ReadinessParts } from '../contracts.js';
2
+ export declare const readinessParts: ReadinessParts;
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ import { Banner } from './Banner.js';
3
+ import { SaveStatus } from './SaveStatus.js';
4
+ import { sectionDone } from './readiness.model.js';
5
+ export const readinessParts = {
6
+ Banner,
7
+ SaveStatus,
8
+ sectionDone
9
+ };
@@ -0,0 +1,22 @@
1
+ import type { ReadinessCode, PreflightResult } from '../../../types/endpoints.js';
2
+ import type { SerializedFlagPoint } from '../../../types/admin.js';
3
+ import type { TestDocument } from '../testDocument.model.js';
4
+ import { type SectionId } from '../contracts.js';
5
+ export type ReadinessContext = Pick<TestDocument, 'arms' | 'unitType' | 'flagPoint'> & {
6
+ flagPoints?: readonly Pick<SerializedFlagPoint, 'id' | 'params'>[];
7
+ };
8
+ /** Exhaustive mapping of the checkStart ReadinessCode union, including all ten invariant checks. */
9
+ export declare const READINESS: Record<ReadinessCode, {
10
+ section: SectionId | 'header';
11
+ next(detail: unknown, ctx: ReadinessContext): string | null;
12
+ }>;
13
+ export declare function nextStep(preflight: PreflightResult | null, ctx: ReadinessContext): {
14
+ section: "header" | SectionId;
15
+ sentence: string;
16
+ code: ReadinessCode;
17
+ } | null;
18
+ export declare function sectionDone(test: {
19
+ preflight: PreflightResult | null;
20
+ }): Record<SectionId, boolean>;
21
+ export declare function displayDate(value: unknown): string | null;
22
+ export declare function daysRemaining(doc: TestDocument, now: number): number | null;
@@ -0,0 +1,107 @@
1
+ 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 { SECTION_ORDER } from '../contracts.js';
4
+ const object = (value)=>value && typeof value === 'object' && !Array.isArray(value) ? value : {};
5
+ const first = (value)=>object(Array.isArray(value) ? value[0] : value);
6
+ const text = (value, fallback)=>typeof value === 'string' && value.trim() ? value.trim() : fallback;
7
+ const detailText = (value, fallback)=>typeof value === 'string' ? text(value, fallback) : text(first(value).message ?? first(value).detail, fallback);
8
+ const group = (key, ctx)=>{
9
+ const found = ctx.arms.find((item)=>item.key === key);
10
+ return found ? actionGroupLabel(found.key, found.label) : C.groupFallback;
11
+ };
12
+ const row = (section, next)=>({
13
+ section,
14
+ next
15
+ });
16
+ const versions = row('versions', (detail)=>versionsNext(detailText(detail, C.versionFallback)));
17
+ /** Exhaustive mapping of the checkStart ReadinessCode union, including all ten invariant checks. */ export const READINESS = {
18
+ 'invalid-status': row('header', ()=>null),
19
+ 'no-placements': row('what', ()=>C.noPlacements),
20
+ 'empty-slot': row('versions', (detail, ctx)=>{
21
+ const placements = Array.isArray(detail) ? detail : [];
22
+ const missing = ctx.arms.find((item)=>item.kind !== 'holdout' && placements.some((placement)=>{
23
+ const slots = object(object(placement).slots);
24
+ return !item.slot || !(typeof slots[item.slot] === 'number' && Number(slots[item.slot]) > 0);
25
+ }));
26
+ return contentNext(group(missing?.key ?? first(detail).arm, ctx));
27
+ }),
28
+ 'flag-no-point': row('what', ()=>C.noPoint),
29
+ 'flag-params-invalid': row('versions', (detail, ctx)=>{
30
+ const issue = first(detail);
31
+ const point = ctx.flagPoints?.find((item)=>item.id === ctx.flagPoint);
32
+ const parameter = typeof issue.field === 'string' ? point?.params[issue.field]?.label ?? issue.field : C.parameterFallback;
33
+ return parameterNext(parameter, group(issue.arm, ctx));
34
+ }),
35
+ 'flag-point-busy': row('what', ()=>C.busy),
36
+ 'flag-placement-missing': row('versions', (detail, ctx)=>{
37
+ const value = object(detail);
38
+ const placement = first(value.placements);
39
+ const missing = Array.isArray(value.missing) ? value.missing[0] : undefined;
40
+ return placementNext(group(placement.arm ?? missing, ctx), detailText(typeof detail === 'string' ? detail : placement, ''));
41
+ }),
42
+ 'no-primary-metric': row('win', ()=>C.noGoal),
43
+ 'unknown-metric-source': row('win', ()=>C.unknownGoal),
44
+ 'unit-source-mismatch': row('win', (_, ctx)=>goalPeopleNext(ctx.unitType === 'user' ? 'signed-in users' : 'visitors')),
45
+ 'no-planned-duration': row('length', ()=>C.noDuration),
46
+ 'no-reach-per-day': row('length', ()=>C.noReach),
47
+ 'layer-full': row('versions', ()=>C.full),
48
+ 'layer-unit-mismatch': row('header', ()=>C.differentPeople),
49
+ 'page-no-targets': row('versions', ()=>C.noTargets),
50
+ 'page-user-unit': row('header', ()=>C.pageVisitors),
51
+ 'invalid-targeting-rule': row('who', (detail)=>{
52
+ const attributes = object(detail).attributes;
53
+ return ruleNext(detailText(detail, Array.isArray(attributes) && attributes.length ? attributes.map(String).join(', ') : C.ruleFallback));
54
+ }),
55
+ 'undeclared-targeting-key': row('who', (detail)=>{
56
+ const attributes = object(detail).attributes;
57
+ return attributeNext(text(Array.isArray(attributes) ? attributes[0] : object(detail).attribute, C.attributeFallback));
58
+ }),
59
+ 'weights-total': versions,
60
+ 'control-count': versions,
61
+ 'control-first': versions,
62
+ 'holdout-count': versions,
63
+ 'holdout-last': versions,
64
+ 'variant-count': versions,
65
+ 'invalid-key': versions,
66
+ 'duplicate-key': versions,
67
+ 'slot-required': versions,
68
+ 'duplicate-slot': versions
69
+ };
70
+ export function nextStep(preflight, ctx) {
71
+ for (const section of [
72
+ 'header',
73
+ ...SECTION_ORDER
74
+ ]){
75
+ for (const check of preflight?.checks ?? []){
76
+ const mapping = READINESS[check.code];
77
+ if (check.ok || mapping.section !== section) continue;
78
+ const sentence = mapping.next(check.detail, ctx);
79
+ if (sentence) return {
80
+ section,
81
+ sentence,
82
+ code: check.code
83
+ };
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ export function sectionDone(test) {
89
+ return Object.fromEntries(SECTION_ORDER.map((section)=>[
90
+ section,
91
+ test.preflight ? !test.preflight.checks.some((check)=>!check.ok && READINESS[check.code].section === section) : section === 'who' || section === 'advanced'
92
+ ]));
93
+ }
94
+ export function displayDate(value) {
95
+ 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));
102
+ }
103
+ export function daysRemaining(doc, now) {
104
+ const days = doc.analysis?.plannedDurationDays;
105
+ const start = typeof doc.startedAt === 'string' ? Date.parse(doc.startedAt) : NaN;
106
+ return typeof days === 'number' && days > 0 && Number.isFinite(start) ? Math.max(0, Math.ceil(days - (now - start) / 86_400_000)) : null;
107
+ }
@@ -0,0 +1,10 @@
1
+ import type { ApiResult } from '../../api.js';
2
+ import type { TestDocApi } from '../contracts.js';
3
+ /** The ref closes the gap before React renders the disabled button. */
4
+ export declare function useTransition(test: TestDocApi, onSuccess?: () => void): {
5
+ busy: boolean;
6
+ error: string | null;
7
+ run: (request: () => Promise<ApiResult<unknown>>, { reloadOnSuccess }?: {
8
+ reloadOnSuccess?: boolean | undefined;
9
+ }) => Promise<void>;
10
+ };
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+ import { useRef, useState } from 'react';
3
+ import { API_MESSAGES } from '../../copy/common.js';
4
+ /** The ref closes the gap before React renders the disabled button. */ export function useTransition(test, onSuccess) {
5
+ const inFlight = useRef(false);
6
+ const [busy, setBusy] = useState(false);
7
+ const [error, setError] = useState(null);
8
+ async function run(request, { reloadOnSuccess = true } = {}) {
9
+ if (inFlight.current || test.hasUnsaved || test.saving === 'saving') return;
10
+ inFlight.current = true;
11
+ setBusy(true);
12
+ setError(null);
13
+ try {
14
+ const result = await request();
15
+ if (result.ok) {
16
+ if (reloadOnSuccess) {
17
+ await test.reload();
18
+ onSuccess?.();
19
+ // Keep the successful action locked until the reloaded document replaces this view.
20
+ return;
21
+ }
22
+ } else setError(result.message);
23
+ } catch {
24
+ setError(API_MESSAGES.network);
25
+ }
26
+ inFlight.current = false;
27
+ setBusy(false);
28
+ }
29
+ return {
30
+ busy,
31
+ error,
32
+ run
33
+ };
34
+ }