@delmaredigital/payload-experiments 0.2.0-rc.7 → 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 (267) 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/audience.d.ts +2 -1
  19. package/dist/admin/copy/audience.js +3 -2
  20. package/dist/admin/copy/common.d.ts +13 -6
  21. package/dist/admin/copy/common.js +14 -8
  22. package/dist/admin/copy/goals.d.ts +21 -6
  23. package/dist/admin/copy/goals.js +27 -6
  24. package/dist/admin/copy/readiness.d.ts +19 -0
  25. package/dist/admin/copy/readiness.js +19 -0
  26. package/dist/admin/copy/recommendation.d.ts +0 -3
  27. package/dist/admin/copy/recommendation.js +0 -3
  28. package/dist/admin/copy/results.d.ts +41 -2
  29. package/dist/admin/copy/results.js +42 -3
  30. package/dist/admin/copy/review.d.ts +1 -1
  31. package/dist/admin/copy/review.js +1 -1
  32. package/dist/admin/copy/setting.d.ts +2 -0
  33. package/dist/admin/copy/setting.js +2 -0
  34. package/dist/admin/copy/surface.d.ts +7 -4
  35. package/dist/admin/copy/surface.js +9 -4
  36. package/dist/admin/copy/testPage.d.ts +10 -0
  37. package/dist/admin/copy/testPage.js +10 -0
  38. package/dist/admin/format/date.d.ts +2 -0
  39. package/dist/admin/format/date.js +10 -0
  40. package/dist/admin/previewLinks.model.d.ts +2 -2
  41. package/dist/admin/previewLinks.model.js +5 -7
  42. package/dist/admin/results/CiChart.js +11 -21
  43. package/dist/admin/results/DataQualityPanel.d.ts +4 -2
  44. package/dist/admin/results/DataQualityPanel.js +30 -17
  45. package/dist/admin/results/DataTable.d.ts +16 -0
  46. package/dist/admin/results/DataTable.js +152 -0
  47. package/dist/admin/results/StatTile.d.ts +7 -0
  48. package/dist/admin/results/StatTile.js +22 -0
  49. package/dist/admin/results/VerdictHero.d.ts +13 -0
  50. package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
  51. package/dist/admin/results/releasePages.model.d.ts +17 -0
  52. package/dist/admin/results/releasePages.model.js +10 -0
  53. package/dist/admin/results/results.css +64 -0
  54. package/dist/admin/results/results.model.d.ts +5 -1
  55. package/dist/admin/results/results.model.js +10 -2
  56. package/dist/admin/surface/ResultsRoute.d.ts +1 -1
  57. package/dist/admin/surface/ResultsRoute.js +59 -34
  58. package/dist/admin/surface/SurfaceShell.d.ts +11 -2
  59. package/dist/admin/surface/SurfaceShell.js +49 -14
  60. package/dist/admin/surface/TestsHome.d.ts +6 -1
  61. package/dist/admin/surface/TestsHome.js +183 -112
  62. package/dist/admin/surface/TestsSurface.js +56 -16
  63. package/dist/admin/surface/goals/GoalForm.js +341 -308
  64. package/dist/admin/surface/goals/GoalsList.js +123 -89
  65. package/dist/admin/surface/goals/data.d.ts +8 -2
  66. package/dist/admin/surface/goals/data.js +11 -3
  67. package/dist/admin/surface/goals/goals.css +10 -13
  68. package/dist/admin/surface/home.model.d.ts +7 -8
  69. package/dist/admin/surface/home.model.js +23 -12
  70. package/dist/admin/surface/newTest/NewTest.js +5 -5
  71. package/dist/admin/surface/pageBlock/parts.js +31 -26
  72. package/dist/admin/surface/readiness/ActionDialogs.js +43 -14
  73. package/dist/admin/surface/readiness/Banner.d.ts +3 -0
  74. package/dist/admin/surface/readiness/Banner.js +58 -33
  75. package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
  76. package/dist/admin/surface/readiness/SaveStatus.js +6 -3
  77. package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
  78. package/dist/admin/surface/readiness/readiness.model.js +18 -9
  79. package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
  80. package/dist/admin/surface/readiness/useTransition.js +2 -1
  81. package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
  82. package/dist/admin/surface/results/ReleasePages.js +116 -0
  83. package/dist/admin/surface/results/ResultsBody.d.ts +11 -1
  84. package/dist/admin/surface/results/ResultsBody.js +276 -105
  85. package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
  86. package/dist/admin/surface/results/ResultsHeader.js +177 -0
  87. package/dist/admin/surface/results/recommendation.model.js +2 -6
  88. package/dist/admin/surface/results/useResults.d.ts +21 -3
  89. package/dist/admin/surface/results/useResults.js +2 -1
  90. package/dist/admin/surface/setting/parts.js +75 -88
  91. package/dist/admin/surface/setting/setting.css +3 -15
  92. package/dist/admin/surface/surface.css +62 -47
  93. package/dist/admin/surface/test/AdvancedSection.js +55 -48
  94. package/dist/admin/surface/test/GoalSection.js +39 -32
  95. package/dist/admin/surface/test/LengthSection.js +46 -34
  96. package/dist/admin/surface/test/Rail.d.ts +4 -0
  97. package/dist/admin/surface/test/Rail.js +83 -0
  98. package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
  99. package/dist/admin/surface/test/ReadinessBar.js +33 -0
  100. package/dist/admin/surface/test/Section.d.ts +1 -1
  101. package/dist/admin/surface/test/Section.js +36 -16
  102. package/dist/admin/surface/test/TargetingSection.js +168 -142
  103. package/dist/admin/surface/test/TestPage.d.ts +2 -1
  104. package/dist/admin/surface/test/TestPage.js +146 -68
  105. package/dist/admin/surface/test/VersionsSection.js +151 -120
  106. package/dist/admin/surface/test/fields.d.ts +11 -4
  107. package/dist/admin/surface/test/fields.js +84 -85
  108. package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
  109. package/dist/admin/surface/test/jumpToSection.js +10 -0
  110. package/dist/admin/surface/test/pageParts.js +9 -3
  111. package/dist/admin/surface/test/testPage.css +81 -14
  112. package/dist/admin/surface/useTestDocument.js +10 -4
  113. package/dist/admin/targeting/targeting.model.d.ts +1 -3
  114. package/dist/admin/targeting/targeting.model.js +3 -8
  115. package/dist/admin/theme.css +115 -36
  116. package/dist/admin/theme.d.ts +38 -0
  117. package/dist/admin/theme.js +38 -1
  118. package/dist/admin/useApi.d.ts +17 -2
  119. package/dist/admin/views/ExperimentsRootView.js +12 -2
  120. package/dist/collections/experiments/config.js +25 -0
  121. package/dist/collections/experiments/lockHook.js +3 -1
  122. package/dist/collections/identities.js +6 -1
  123. package/dist/collections/metrics.js +7 -0
  124. package/dist/collections/metricsGuards.d.ts +9 -0
  125. package/dist/collections/metricsGuards.js +33 -0
  126. package/dist/contentDone.d.ts +4 -0
  127. package/dist/contentDone.js +5 -0
  128. package/dist/db/identities.d.ts +8 -7
  129. package/dist/db/identities.js +9 -6
  130. package/dist/db/metricUsage.d.ts +19 -0
  131. package/dist/db/metricUsage.js +54 -0
  132. package/dist/db/names.d.ts +5 -0
  133. package/dist/db/names.js +5 -0
  134. package/dist/db/publishedContent.d.ts +10 -0
  135. package/dist/db/publishedContent.js +43 -0
  136. package/dist/db/results.d.ts +2 -0
  137. package/dist/db/results.js +6 -2
  138. package/dist/db/snapshots.d.ts +18 -6
  139. package/dist/db/snapshots.js +23 -7
  140. package/dist/db/sweep.js +11 -6
  141. package/dist/endpoints/index.js +19 -1
  142. package/dist/endpoints/results.js +5 -0
  143. package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
  144. package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
  145. package/dist/endpoints/summary.js +20 -2
  146. package/dist/endpoints/transitions.d.ts +15 -0
  147. package/dist/endpoints/transitions.js +36 -0
  148. package/dist/errors.d.ts +8 -0
  149. package/dist/errors.js +28 -0
  150. package/dist/exports/index.d.ts +2 -1
  151. package/dist/exports/index.js +1 -0
  152. package/dist/exports/puckEditor.d.ts +1 -0
  153. package/dist/exports/puckEditor.js +1 -0
  154. package/dist/exports/rsc.d.ts +5 -1
  155. package/dist/exports/rsc.js +2 -0
  156. package/dist/hooks/pageGuard.d.ts +5 -0
  157. package/dist/hooks/pageGuard.js +121 -0
  158. package/dist/log.js +6 -3
  159. package/dist/manifest/compile.d.ts +5 -0
  160. package/dist/manifest/compile.js +5 -1
  161. package/dist/options.js +23 -0
  162. package/dist/plugin.js +26 -2
  163. package/dist/puck/copy.d.ts +1 -1
  164. package/dist/puck/copy.js +1 -1
  165. package/dist/puck/editor/editor.chrome.css +235 -0
  166. package/dist/puck/editor/editor.css +241 -43
  167. package/dist/results/aggregate.js +4 -3
  168. package/dist/results/analyse.d.ts +4 -1
  169. package/dist/results/analyse.js +27 -11
  170. package/dist/results/cuped.js +3 -2
  171. package/dist/results/identityCredit.d.ts +15 -7
  172. package/dist/results/identityCredit.js +32 -33
  173. package/dist/results/rolloutUnits.js +2 -1
  174. package/dist/results/snapshot.js +8 -2
  175. package/dist/results/srm.js +2 -1
  176. package/dist/sanitizeReadinessDetail.d.ts +7 -0
  177. package/dist/sanitizeReadinessDetail.js +54 -0
  178. package/dist/server/linkIdentity.d.ts +10 -1
  179. package/dist/server/linkIdentity.js +17 -15
  180. package/dist/server/metadata.d.ts +5 -0
  181. package/dist/server/metadata.js +9 -1
  182. package/dist/server/runResults.d.ts +3 -2
  183. package/dist/server/runResults.js +121 -31
  184. package/dist/targeting/values.d.ts +4 -0
  185. package/dist/targeting/values.js +10 -0
  186. package/dist/transitions/addMetric.js +8 -0
  187. package/dist/transitions/archive.js +2 -3
  188. package/dist/transitions/checkStart.d.ts +6 -3
  189. package/dist/transitions/checkStart.js +15 -0
  190. package/dist/transitions/contentFields.d.ts +7 -0
  191. package/dist/transitions/contentFields.js +20 -0
  192. package/dist/transitions/decide.js +6 -1
  193. package/dist/transitions/dismissContentEntry.d.ts +8 -0
  194. package/dist/transitions/dismissContentEntry.js +45 -0
  195. package/dist/transitions/makeContent.d.ts +5 -2
  196. package/dist/transitions/makeContent.js +217 -118
  197. package/dist/transitions/phaseRecord.d.ts +8 -2
  198. package/dist/transitions/placements.d.ts +1 -0
  199. package/dist/transitions/placements.js +4 -0
  200. package/dist/transitions/preflightData.d.ts +1 -1
  201. package/dist/transitions/preflightData.js +42 -4
  202. package/dist/transitions/releasePages.d.ts +8 -0
  203. package/dist/transitions/releasePages.js +26 -0
  204. package/dist/transitions/restoreContentEntry.d.ts +8 -0
  205. package/dist/transitions/restoreContentEntry.js +47 -0
  206. package/dist/transitions/start.js +30 -5
  207. package/dist/types/contracts.d.ts +14 -0
  208. package/dist/types/endpoints.d.ts +34 -3
  209. package/dist/types/index.d.ts +1 -0
  210. package/dist/types/index.js +1 -0
  211. package/dist/types/reasons.d.ts +3 -0
  212. package/dist/types/reasons.js +1 -0
  213. package/dist/types/snapshot.d.ts +6 -1
  214. package/dist/ui/Banner.d.ts +2 -0
  215. package/dist/ui/Banner.js +2 -0
  216. package/dist/ui/Breadcrumb.d.ts +7 -0
  217. package/dist/ui/Breadcrumb.js +25 -0
  218. package/dist/ui/Button.d.ts +15 -0
  219. package/dist/ui/Button.js +32 -0
  220. package/dist/ui/Card.d.ts +12 -0
  221. package/dist/ui/Card.js +40 -0
  222. package/dist/ui/Checkbox.d.ts +2 -0
  223. package/dist/ui/Checkbox.js +2 -0
  224. package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
  225. package/dist/{admin/components → ui}/ChipsInput.js +20 -12
  226. package/dist/ui/Dialog.d.ts +13 -0
  227. package/dist/ui/Dialog.js +85 -0
  228. package/dist/ui/EmptyState.d.ts +2 -0
  229. package/dist/ui/EmptyState.js +2 -0
  230. package/dist/ui/Feedback.d.ts +24 -0
  231. package/dist/ui/Feedback.js +40 -0
  232. package/dist/ui/Inputs.d.ts +34 -0
  233. package/dist/ui/Inputs.js +225 -0
  234. package/dist/ui/NumberInput.d.ts +2 -0
  235. package/dist/ui/NumberInput.js +2 -0
  236. package/dist/ui/Range.d.ts +2 -0
  237. package/dist/ui/Range.js +2 -0
  238. package/dist/ui/SearchInput.d.ts +2 -0
  239. package/dist/ui/SearchInput.js +2 -0
  240. package/dist/ui/SectionCard.d.ts +2 -0
  241. package/dist/ui/SectionCard.js +2 -0
  242. package/dist/ui/Select.d.ts +2 -0
  243. package/dist/ui/Select.js +2 -0
  244. package/dist/ui/StatusChip.d.ts +2 -0
  245. package/dist/ui/StatusChip.js +2 -0
  246. package/dist/ui/Tabs.d.ts +14 -0
  247. package/dist/ui/Tabs.js +54 -0
  248. package/dist/ui/TextInput.d.ts +2 -0
  249. package/dist/ui/TextInput.js +2 -0
  250. package/dist/ui/Textarea.d.ts +2 -0
  251. package/dist/ui/Textarea.js +2 -0
  252. package/dist/ui/Toggle.d.ts +2 -0
  253. package/dist/ui/Toggle.js +2 -0
  254. package/dist/ui/accent.d.ts +21 -0
  255. package/dist/ui/accent.js +81 -0
  256. package/dist/ui/index.d.ts +9 -0
  257. package/dist/ui/index.js +9 -0
  258. package/dist/ui/ui.css +92 -0
  259. package/package.json +1 -1
  260. package/dist/admin/components/chipsInput.css +0 -11
  261. package/dist/admin/results/ArmTable.d.ts +0 -8
  262. package/dist/admin/results/ArmTable.js +0 -113
  263. package/dist/admin/results/VerdictCard.d.ts +0 -9
  264. package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
  265. package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
  266. package/dist/admin/surface/test/TestHeader.d.ts +0 -4
  267. package/dist/admin/surface/test/TestHeader.js +0 -70
@@ -1,49 +1,148 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useRef, useState } from 'react';
4
- import { Button } from '@payloadcms/ui';
5
- import { ACTIONS_COPY as C } from '../copy/actions.js';
6
- export function MakeContentResult({ result, onRetry, busy = false }) {
3
+ import { useLayoutEffect, useRef, useState } from 'react';
4
+ import { contentDone } from '../../contentDone.js';
5
+ import { Button } from '../../ui/Button.js';
6
+ import { Banner } from '../../ui/Banner.js';
7
+ import { API_MESSAGES } from '../copy/common.js';
8
+ import { ACTIONS_COPY as C, contentReportCopy } from '../copy/actions.js';
9
+ export function MakeContentResult({ result, onRetry, busy = false, id, api, canManage = false, isDecided = false, onFinish, onBusyChange, onActionStart }) {
7
10
  const inFlight = useRef(false);
11
+ const container = useRef(null);
12
+ const focusTarget = useRef(null);
8
13
  const [pending, setPending] = useState(false);
9
- async function retry() {
14
+ const [error, setError] = useState(null);
15
+ const [report, setReport] = useState({
16
+ source: result,
17
+ docs: result.docs
18
+ });
19
+ // A Retry supplies a new report. Entry actions use the authoritative returned doc.
20
+ if (report.source !== result) setReport({
21
+ source: result,
22
+ docs: result.docs
23
+ });
24
+ const docs = report.source === result ? report.docs : result.docs;
25
+ const editable = canManage && isDecided;
26
+ useLayoutEffect(()=>{
27
+ if (busy || pending || !focusTarget.current) return;
28
+ const target = focusTarget.current === 'finish' ? container.current?.querySelector('[data-finish-archiving]') : focusTarget.current;
29
+ target?.focus();
30
+ focusTarget.current = null;
31
+ }, [
32
+ docs,
33
+ busy,
34
+ pending
35
+ ]);
36
+ async function run(action) {
10
37
  if (inFlight.current || busy) return;
11
38
  inFlight.current = true;
12
39
  setPending(true);
40
+ setError(null);
41
+ onActionStart?.();
42
+ onBusyChange?.(true);
13
43
  try {
14
- await onRetry();
44
+ await action();
45
+ } catch {
46
+ setError(API_MESSAGES.network);
15
47
  } finally{
16
48
  inFlight.current = false;
17
49
  setPending(false);
50
+ onBusyChange?.(false);
18
51
  }
19
52
  }
20
- const failed = result.docs.some((doc)=>!doc.ok);
53
+ async function changeEntry(entry, button) {
54
+ if (!editable || !api || id === undefined) return;
55
+ const response = await (entry.dismissedAt ? api.restoreContentEntry : api.dismissContentEntry)(id, {
56
+ collection: entry.collection,
57
+ id: entry.id
58
+ });
59
+ if (!response.ok) {
60
+ setError(response.message);
61
+ return;
62
+ }
63
+ const decision = response.data?.doc.decision;
64
+ const updated = decision?.contentReport?.docs?.find((doc)=>doc.collection === entry.collection && String(doc.id) === String(entry.id));
65
+ if (!updated) {
66
+ setError(API_MESSAGES.unreadable);
67
+ return;
68
+ }
69
+ const updatedDocs = docs.map((doc)=>doc.collection === entry.collection && String(doc.id) === String(entry.id) ? updated : doc);
70
+ focusTarget.current = !entry.dismissedAt && !contentDone(docs) && contentDone(updatedDocs) && onFinish ? 'finish' : button;
71
+ setReport((current)=>({
72
+ ...current,
73
+ docs: updatedDocs
74
+ }));
75
+ }
76
+ const retryable = docs.some((doc)=>!doc.ok && !doc.dismissedAt);
21
77
  return /*#__PURE__*/ _jsxs("div", {
78
+ ref: container,
22
79
  "aria-live": "polite",
23
80
  children: [
81
+ error ? /*#__PURE__*/ _jsx(Banner, {
82
+ tone: "error",
83
+ children: error
84
+ }) : null,
85
+ result.doc?.status === 'archived' ? /*#__PURE__*/ _jsx("p", {
86
+ children: C.contentArchived
87
+ }) : null,
24
88
  /*#__PURE__*/ _jsx("ul", {
25
- children: result.docs.map((doc, index)=>/*#__PURE__*/ _jsxs("li", {
89
+ children: docs.map((doc, index)=>{
90
+ const label = 'title' in doc && typeof doc.title === 'string' && doc.title || `${doc.collection}/${doc.id}`;
91
+ return /*#__PURE__*/ _jsxs("li", {
26
92
  children: [
27
- 'title' in doc && typeof doc.title === 'string' && doc.title || `${doc.collection}/${doc.id}`,
28
- ": ",
29
- doc.ok ? C.updated : C.failed,
30
- doc.error ? ` — ${doc.error}` : ''
93
+ /*#__PURE__*/ _jsxs("span", {
94
+ children: [
95
+ label,
96
+ ": ",
97
+ doc.ok ? C.updated : doc.dismissedAt ? C.dismissed : C.failed,
98
+ !doc.dismissedAt && (doc.detail !== undefined || doc.error !== undefined || !doc.ok) ? ` — ${contentReportCopy(doc.detail ?? doc.error, doc.ok)}` : ''
99
+ ]
100
+ }),
101
+ !doc.ok && editable && api && id !== undefined ? /*#__PURE__*/ _jsxs(_Fragment, {
102
+ children: [
103
+ doc.dismissedAt ? ' · ' : ' ',
104
+ /*#__PURE__*/ _jsx(Button, {
105
+ variant: "quiet",
106
+ size: "sm",
107
+ "aria-disabled": busy || pending || undefined,
108
+ "aria-label": doc.dismissedAt ? C.undoLabel(label) : C.dismissLabel(label),
109
+ onClick: (event)=>{
110
+ const button = event.currentTarget;
111
+ void run(()=>changeEntry(doc, button));
112
+ },
113
+ children: doc.dismissedAt ? C.undo : C.dismiss
114
+ })
115
+ ]
116
+ }) : null
31
117
  ]
32
- }, `${doc.collection}/${doc.id}/${index}`))
118
+ }, `${doc.collection}/${doc.id}/${index}`);
119
+ })
33
120
  }),
34
- failed ? /*#__PURE__*/ _jsxs(_Fragment, {
121
+ editable && retryable ? /*#__PURE__*/ _jsxs(_Fragment, {
35
122
  children: [
123
+ /*#__PURE__*/ _jsx("p", {
124
+ children: C.dismissHelp
125
+ }),
36
126
  /*#__PURE__*/ _jsx("p", {
37
127
  children: C.partial
38
128
  }),
39
129
  /*#__PURE__*/ _jsx(Button, {
40
- disabled: busy || pending,
130
+ "aria-disabled": busy || pending || undefined,
41
131
  onClick: ()=>{
42
- void retry();
132
+ void run(onRetry);
43
133
  },
44
134
  children: C.retry
45
135
  })
46
136
  ]
137
+ }) : null,
138
+ editable && contentDone(docs) && onFinish ? /*#__PURE__*/ _jsx(Button, {
139
+ "data-finish-archiving": true,
140
+ variant: "primary",
141
+ disabled: busy || pending,
142
+ onClick: ()=>{
143
+ void run(onFinish);
144
+ },
145
+ children: C.finish
47
146
  }) : null
48
147
  ]
49
148
  });
@@ -1,13 +1,11 @@
1
1
  import type { ExperimentKind, Status } from '../../types/contracts.js';
2
- import type { ArchiveBody, MakeContentResult } from '../../types/endpoints.js';
2
+ import type { ArchiveBody, ContentReport } from '../../types/endpoints.js';
3
3
  export type ActionGroup = {
4
4
  key: string;
5
5
  label?: string;
6
6
  kind?: string;
7
7
  };
8
- export type ContentReport = {
9
- docs: MakeContentResult['docs'];
10
- } | MakeContentResult['docs'];
8
+ export type { ContentReport } from '../../types/endpoints.js';
11
9
  export type ActionDecision = {
12
10
  winnerArm?: string | null;
13
11
  holdoutReleasedAt?: string | null;
@@ -1,3 +1,4 @@
1
+ import { contentDone } from '../../contentDone.js';
1
2
  import { armLabel } from '../labels.js';
2
3
  import { ACTIONS_COPY as C, hardCodedLabel, makeContentCopy, releaseCopy } from '../copy/actions.js';
3
4
  import { REVIEW_COPY } from '../copy/review.js';
@@ -45,8 +46,6 @@ export function availableActions({ status, kind, arms, decision, canManage }) {
45
46
  const winner = arms.find((group)=>group.key === decision?.winnerArm);
46
47
  const label = actionGroupLabel(decision?.winnerArm ?? 'control', winner?.label);
47
48
  const rolloutOpen = arms.some((group)=>group.key === 'holdout') && Boolean(decision?.winnerArm) && decision?.winnerArm !== 'control' && !decision?.holdoutReleasedAt;
48
- const report = decision?.contentReport;
49
- const docs = Array.isArray(report) ? report : report?.docs;
50
49
  return [
51
50
  ...rolloutOpen ? [
52
51
  action('release-holdout', C.release, releaseCopy(label))
@@ -54,7 +53,7 @@ export function availableActions({ status, kind, arms, decision, canManage }) {
54
53
  ...kind !== 'code' ? [
55
54
  action('make-content', C.makeContent, makeContentCopy(label, kind), 'primary')
56
55
  ] : [],
57
- ...docs?.length && docs.every((doc)=>doc.ok) ? [
56
+ ...contentDone(decision?.contentReport) ? [
58
57
  action('after-content', C.finish, C.finishBody, 'secondary', {
59
58
  mode: 'after-content'
60
59
  })
@@ -1,6 +1,5 @@
1
1
  import { armLabel } from './labels.js';
2
- import { LOCKED_MESSAGES } from './copy/common.js';
3
- import { eachVersion, groupShare, holdoutClause, layerSentence, participation, peopleLabel, uncounted } from './copy/audience.js';
2
+ import { eachVersion, groupShare, holdoutClause, holdoutRemainder, layerSentence, participation, peopleLabel, uncounted } from './copy/audience.js';
4
3
  const percent = (value)=>String(Math.round(value * 10) / 10);
5
4
  export function allocationSentence({ allocation, unitType, arms, layer, status }) {
6
5
  const groups = arms.filter((group)=>group.kind !== 'holdout');
@@ -8,10 +7,9 @@ export function allocationSentence({ allocation, unitType, arms, layer, status }
8
7
  const equal = groups.every((group)=>group.weight === groups[0].weight);
9
8
  const shares = equal ? eachVersion(percent(allocation * groups[0].weight / 100)) : groups.map((group)=>groupShare(percent(allocation * group.weight / 100), armLabel(group.key, group.label))).join(', ');
10
9
  const holdout = arms.find((group)=>group.kind === 'holdout');
11
- let sentence = participation(percent(allocation), peopleLabel[unitType], shares) + (holdout ? holdoutClause(percent(holdout.weight)) : uncounted);
12
- if ((layer || status === 'running') && !sentence.endsWith('.')) sentence += '.';
10
+ let sentence = participation(percent(allocation), peopleLabel[unitType], shares) + (holdout ? holdoutClause(percent(holdout.weight)) : '') + (allocation < 100 ? holdout ? holdoutRemainder : uncounted : '');
11
+ if ((holdout || layer || status === 'running') && !sentence.endsWith('.')) sentence += '.';
13
12
  if (layer) sentence += layerSentence(layer.name, layer.used, layer.free);
14
- if (status === 'running') sentence += ` ${LOCKED_MESSAGES.runningAllocation}`;
15
13
  return sentence;
16
14
  }
17
15
  // Payload arrays are flattened into row fields; their parent value is a row count.
@@ -1,5 +1,6 @@
1
1
  import type { DocId } from '../types/contracts.js';
2
- import type { AddMetricBody, AllocateBody, ArchiveBody, DecideBody, MakeContentResult, PreflightResult, PreviewTokenResult, RefusalCode, ResultsResponse, SummaryResponse, TransitionSuccess } from '../types/endpoints.js';
2
+ import type { RestoreContentEntryBody, RestoreContentEntryResult, DismissContentEntryBody, DismissContentEntryResult, AddMetricBody, AllocateBody, ArchiveBody, DecideBody, MakeContentResult, PreflightResult, PreviewTokenResult, RefusalCode, ResultsResponse, SummaryResponse, TransitionSuccess } from '../types/endpoints.js';
3
+ import { refusalMessage } from './copy/common.js';
3
4
  export type ApiResult<T> = {
4
5
  ok: true;
5
6
  data: T | undefined;
@@ -10,23 +11,28 @@ export type ApiResult<T> = {
10
11
  message: string;
11
12
  retryable: boolean;
12
13
  };
14
+ type RequestOptions = NonNullable<Parameters<typeof refusalMessage>[3]>;
13
15
  export declare function createApi(apiBase: string): {
14
16
  start: (id: DocId) => Promise<ApiResult<TransitionSuccess>>;
15
- allocate: (id: DocId, body: AllocateBody) => Promise<ApiResult<TransitionSuccess>>;
16
- addMetric: (id: DocId, body: AddMetricBody) => Promise<ApiResult<TransitionSuccess>>;
17
+ allocate: (id: DocId, body: AllocateBody, options?: RequestOptions) => Promise<ApiResult<TransitionSuccess>>;
18
+ addMetric: (id: DocId, body: AddMetricBody, options?: RequestOptions) => Promise<ApiResult<TransitionSuccess>>;
17
19
  stop: (id: DocId) => Promise<ApiResult<TransitionSuccess>>;
18
20
  decide: (id: DocId, body: DecideBody) => Promise<ApiResult<TransitionSuccess>>;
19
21
  releaseHoldout: (id: DocId) => Promise<ApiResult<TransitionSuccess>>;
22
+ releasePages: (id: DocId) => Promise<ApiResult<TransitionSuccess>>;
20
23
  makeContent: (id: DocId) => Promise<ApiResult<MakeContentResult>>;
24
+ dismissContentEntry: (id: DocId, body: DismissContentEntryBody) => Promise<ApiResult<DismissContentEntryResult>>;
25
+ restoreContentEntry: (id: DocId, body: RestoreContentEntryBody) => Promise<ApiResult<RestoreContentEntryResult>>;
21
26
  archive: (id: DocId, body: ArchiveBody) => Promise<ApiResult<TransitionSuccess>>;
22
27
  preflight: (id: DocId) => Promise<ApiResult<PreflightResult>>;
23
28
  previewToken: (id: DocId, arm: string) => Promise<ApiResult<PreviewTokenResult>>;
24
29
  results: (id: DocId, options?: {
25
30
  phase?: number;
26
31
  series?: boolean;
27
- }) => Promise<ApiResult<ResultsResponse>>;
32
+ } & RequestOptions) => Promise<ApiResult<ResultsResponse>>;
28
33
  recompute: (id: DocId) => Promise<ApiResult<{
29
34
  snapshotId: string;
30
35
  }>>;
31
36
  summary: (ids: readonly DocId[]) => Promise<ApiResult<SummaryResponse>>;
32
37
  };
38
+ export {};
package/dist/admin/api.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { API_MESSAGES, refusalMessage } from './copy/common.js';
2
+ import { CONTENT_ENTRY_REFUSALS } from './copy/actions.js';
2
3
  export function createApi(apiBase) {
3
4
  const base = `${apiBase.replace(/\/$/, '')}/xp/experiments`;
4
5
  const route = (id, action)=>`${base}/${encodeURIComponent(id)}/${action}`;
5
- async function request(path, method, body) {
6
+ async function request(path, method, body, options) {
6
7
  try {
7
8
  const response = await fetch(path(), {
8
9
  method,
@@ -28,7 +29,7 @@ export function createApi(apiBase) {
28
29
  ...response.status === 501 ? {
29
30
  code: 'not-implemented'
30
31
  } : {},
31
- message: refusalMessage(undefined, response.status),
32
+ message: refusalMessage(undefined, response.status, undefined, options),
32
33
  retryable: response.status >= 500 && response.status !== 501
33
34
  };
34
35
  }
@@ -46,7 +47,7 @@ export function createApi(apiBase) {
46
47
  ...code ? {
47
48
  code
48
49
  } : {},
49
- message: refusalMessage(code, response.status, typeof error?.message === 'string' ? error.message : undefined),
50
+ message: refusalMessage(code, response.status, typeof error?.message === 'string' ? error.message : undefined, options),
50
51
  retryable: code === 'commit-unverified' || response.status >= 500 && response.status !== 501
51
52
  };
52
53
  } catch {
@@ -58,15 +59,18 @@ export function createApi(apiBase) {
58
59
  };
59
60
  }
60
61
  }
61
- const post = (id, action, body)=>request(()=>route(id, action), 'POST', body);
62
+ const post = (id, action, body, options)=>request(()=>route(id, action), 'POST', body, options);
62
63
  return {
63
64
  start: (id)=>post(id, 'start'),
64
- allocate: (id, body)=>post(id, 'allocate', body),
65
- addMetric: (id, body)=>post(id, 'add-metric', body),
65
+ allocate: (id, body, options)=>post(id, 'allocate', body, options),
66
+ addMetric: (id, body, options)=>post(id, 'add-metric', body, options),
66
67
  stop: (id)=>post(id, 'stop'),
67
68
  decide: (id, body)=>post(id, 'decide', body),
68
69
  releaseHoldout: (id)=>post(id, 'release-holdout'),
70
+ releasePages: (id)=>post(id, 'release-pages'),
69
71
  makeContent: (id)=>post(id, 'make-content'),
72
+ dismissContentEntry: (id, body)=>post(id, 'content-report/dismiss', body, CONTENT_ENTRY_REFUSALS),
73
+ restoreContentEntry: (id, body)=>post(id, 'content-report/restore', body, CONTENT_ENTRY_REFUSALS),
70
74
  archive: (id, body)=>post(id, 'archive', body),
71
75
  preflight: (id)=>request(()=>route(id, 'preflight'), 'GET'),
72
76
  previewToken: (id, arm)=>request(()=>`${route(id, 'preview-token')}?arm=${encodeURIComponent(arm)}`, 'GET'),
@@ -76,7 +80,7 @@ export function createApi(apiBase) {
76
80
  if (options.series) query.set('series', '1');
77
81
  const qs = query.toString();
78
82
  return `${route(id, 'results')}${qs ? `?${qs}` : ''}`;
79
- }, 'GET'),
83
+ }, 'GET', undefined, options),
80
84
  recompute: (id)=>post(id, 'recompute'),
81
85
  summary: async (ids)=>{
82
86
  const data = {};
@@ -8,13 +8,13 @@ export declare const ACTIONS_COPY: {
8
8
  readonly finish: "Finish archiving";
9
9
  readonly archive: "Archive";
10
10
  readonly discard: "Discard winner";
11
- readonly startBody: "Start this experiment?";
11
+ readonly startBody: "Start this test?";
12
12
  readonly restartBody: "Starting again begins a new round; results restart.";
13
13
  readonly stopBody: "Everyone will see the Original. Starting again begins a new round; results restart.";
14
14
  readonly original: "Everyone will see the Original.";
15
15
  readonly discardBody: "Everyone goes back to the Original. The decision note is kept.";
16
- readonly archiveBody: "Archive this experiment?";
17
- readonly finishBody: "The content has been updated. Finish archiving this experiment.";
16
+ readonly archiveBody: "Archive this test?";
17
+ readonly finishBody: "The content has been updated. Finish archiving this test.";
18
18
  readonly noResults: "There are no results yet.";
19
19
  readonly noRecommendation: "The results don't recommend a group yet.";
20
20
  readonly different: "You chose a different group than the results recommend.";
@@ -25,12 +25,48 @@ export declare const ACTIONS_COPY: {
25
25
  readonly cancel: "Cancel";
26
26
  readonly retry: "Retry";
27
27
  readonly done: "Done";
28
+ readonly dismiss: "Dismiss";
29
+ readonly dismissed: "Dismissed";
30
+ readonly undo: "Undo";
31
+ readonly contentArchived: "The winner is now the content and the test is archived.";
32
+ readonly dismissLabel: (page: string) => string;
33
+ readonly undoLabel: (page: string) => string;
34
+ readonly dismissHelp: "Dismiss a page you'll update by hand, then finish archiving.";
35
+ readonly contentEntryNotDecided: "This test isn't decided, so its pages can't be changed.";
36
+ readonly contentEntryMissing: "This page is no longer in the list. Reload to see the current list.";
37
+ readonly contentEntryDismissed: "This page was already dismissed.";
38
+ readonly contentEntryNotDismissed: "This page isn't dismissed. Reload to see its current state.";
39
+ readonly contentEntryUpdated: "This page is already updated.";
40
+ readonly contentEntryNotFound: "This test no longer exists.";
41
+ readonly contentEntryFallback: "This page can't be changed right now. Reload to see its current state.";
28
42
  readonly loading: "Loading results…";
29
43
  readonly updated: "Updated";
30
44
  readonly failed: "Failed";
45
+ readonly contentReasons: {
46
+ localized: string;
47
+ locked: string;
48
+ forbidden: string;
49
+ 'not-found': string;
50
+ validation: string;
51
+ 'update-failed': string;
52
+ };
53
+ readonly missingContent: "This page no longer exists; nothing to change.";
31
54
  readonly partial: "Some pages couldn't be updated. Fix them and retry.";
32
55
  };
33
56
  export declare const decideCopy: (winner: string, holdout: boolean, original: boolean) => string;
34
57
  export declare const releaseCopy: (winner: string) => string;
35
58
  export declare const makeContentCopy: (winner: string, kind: "block" | "page" | "flag") => string;
36
59
  export declare const hardCodedLabel: (winner: string) => string;
60
+ /** Old reports can contain host error text: never interpolate an unknown string. */
61
+ export declare const contentReportCopy: (reason: string | undefined, ok?: boolean) => string;
62
+ export declare const CONTENT_ENTRY_REFUSALS: {
63
+ readonly notFound: "This test no longer exists.";
64
+ readonly codes: {
65
+ readonly 'not-decided': "This test isn't decided, so its pages can't be changed.";
66
+ readonly 'entry-not-found': "This page is no longer in the list. Reload to see the current list.";
67
+ readonly 'entry-ok': "This page is already updated.";
68
+ readonly 'entry-dismissed': "This page was already dismissed.";
69
+ readonly 'entry-not-dismissed': "This page isn't dismissed. Reload to see its current state.";
70
+ };
71
+ readonly fallback: "This page can't be changed right now. Reload to see its current state.";
72
+ };
@@ -8,13 +8,13 @@ export const ACTIONS_COPY = {
8
8
  finish: 'Finish archiving',
9
9
  archive: 'Archive',
10
10
  discard: 'Discard winner',
11
- startBody: 'Start this experiment?',
11
+ startBody: 'Start this test?',
12
12
  restartBody: 'Starting again begins a new round; results restart.',
13
13
  stopBody: 'Everyone will see the Original. Starting again begins a new round; results restart.',
14
14
  original: 'Everyone will see the Original.',
15
15
  discardBody: 'Everyone goes back to the Original. The decision note is kept.',
16
- archiveBody: 'Archive this experiment?',
17
- finishBody: 'The content has been updated. Finish archiving this experiment.',
16
+ archiveBody: 'Archive this test?',
17
+ finishBody: 'The content has been updated. Finish archiving this test.',
18
18
  noResults: 'There are no results yet.',
19
19
  noRecommendation: "The results don't recommend a group yet.",
20
20
  different: 'You chose a different group than the results recommend.',
@@ -25,12 +25,48 @@ export const ACTIONS_COPY = {
25
25
  cancel: 'Cancel',
26
26
  retry: 'Retry',
27
27
  done: 'Done',
28
+ dismiss: 'Dismiss',
29
+ dismissed: 'Dismissed',
30
+ undo: 'Undo',
31
+ contentArchived: 'The winner is now the content and the test is archived.',
32
+ dismissLabel: (page)=>`Dismiss ${page}`,
33
+ undoLabel: (page)=>`Undo dismiss ${page}`,
34
+ dismissHelp: "Dismiss a page you'll update by hand, then finish archiving.",
35
+ contentEntryNotDecided: "This test isn't decided, so its pages can't be changed.",
36
+ contentEntryMissing: 'This page is no longer in the list. Reload to see the current list.',
37
+ contentEntryDismissed: 'This page was already dismissed.',
38
+ contentEntryNotDismissed: "This page isn't dismissed. Reload to see its current state.",
39
+ contentEntryUpdated: 'This page is already updated.',
40
+ contentEntryNotFound: 'This test no longer exists.',
41
+ contentEntryFallback: "This page can't be changed right now. Reload to see its current state.",
28
42
  loading: 'Loading results…',
29
43
  updated: 'Updated',
30
44
  failed: 'Failed',
45
+ contentReasons: {
46
+ localized: "This page has more than one language; Make it the content can't publish it safely yet. Copy the winning version by hand.",
47
+ locked: 'Someone is editing this page. Wait for them to finish, then retry.',
48
+ forbidden: "You can't read or update this page. Ask someone with access to update it by hand. If it was deleted or moved to trash, discard the winner to archive this test.",
49
+ 'not-found': 'This page could not be found. Check whether it was deleted.',
50
+ validation: "This page couldn't be saved automatically. Update it by hand, then retry.",
51
+ 'update-failed': "This page couldn't be updated this time. Retry; if it keeps happening, ask a developer."
52
+ },
53
+ missingContent: 'This page no longer exists; nothing to change.',
31
54
  partial: "Some pages couldn't be updated. Fix them and retry."
32
55
  };
33
56
  export const decideCopy = (winner, holdout, original)=>original ? ACTIONS_COPY.original : holdout ? `Everyone except the holdout will see ${winner}. The holdout keeps seeing the Original so we can measure the shipped change.` : `Everyone will see ${winner}.`;
34
57
  export const releaseCopy = (winner)=>`The holdout will see ${winner} too. Measuring since shipping ends.`;
35
- export const makeContentCopy = (winner, kind)=>kind === 'flag' ? `Saves ${winner}'s settings as the new defaults so the experiment can be archived.` : `Copies ${winner} into the page content so the experiment can be archived.`;
58
+ export const makeContentCopy = (winner, kind)=>kind === 'flag' ? `Saves ${winner}'s settings as the new defaults so the test can be archived.` : `Copies ${winner} into the page content so the test can be archived.`;
36
59
  export const hardCodedLabel = (winner)=>`The developer has hard-coded ${winner}`;
60
+ /** Old reports can contain host error text: never interpolate an unknown string. */ export const contentReportCopy = (reason, ok = false)=>reason === 'not-found' && ok ? ACTIONS_COPY.missingContent : ACTIONS_COPY.contentReasons[reason !== undefined && Object.hasOwn(ACTIONS_COPY.contentReasons, reason) ? reason : 'update-failed'];
61
+ // Each dismiss/restore refusal code has its own sentence; server text is never shown.
62
+ export const CONTENT_ENTRY_REFUSALS = {
63
+ notFound: ACTIONS_COPY.contentEntryNotFound,
64
+ codes: {
65
+ 'not-decided': ACTIONS_COPY.contentEntryNotDecided,
66
+ 'entry-not-found': ACTIONS_COPY.contentEntryMissing,
67
+ 'entry-ok': ACTIONS_COPY.contentEntryUpdated,
68
+ 'entry-dismissed': ACTIONS_COPY.contentEntryDismissed,
69
+ 'entry-not-dismissed': ACTIONS_COPY.contentEntryNotDismissed
70
+ },
71
+ fallback: ACTIONS_COPY.contentEntryFallback
72
+ };
@@ -1,11 +1,11 @@
1
1
  export const ARM_SELECT_COPY = {
2
- experiment: 'Experiment',
2
+ experiment: 'Test',
3
3
  group: 'Group',
4
4
  clear: 'Clear',
5
- forbidden: "You can't see experiments. Ask an admin.",
6
- unavailable: "Experiments couldn't be loaded. Reload and try again.",
7
- loading: 'Loading experiments…',
5
+ forbidden: "You can't see tests. Ask an admin.",
6
+ unavailable: "Tests couldn't be loaded. Reload and try again.",
7
+ loading: 'Loading tests…',
8
8
  running: 'Changing this content mid-test mixes two versions in one result',
9
9
  noFlagPoint: 'No setting is configured for this field.'
10
10
  };
11
- export const staleExperiment = (name)=>`This was set for an earlier experiment${name ? ` (${name})` : ''} and won't be used now.`;
11
+ export const staleExperiment = (name)=>`This was set for an earlier test${name ? ` (${name})` : ''} and won't be used now.`;
@@ -1,6 +1,6 @@
1
1
  export declare const AUDIENCE_COPY: {
2
2
  readonly listPlaceholder: "Type a value and press Enter";
3
- readonly listDescription: "Add one value per chip, for example each page path.";
3
+ readonly listDescription: "Add one value per chip.";
4
4
  readonly removeValue: (value: string) => string;
5
5
  readonly removeSymbol: "×";
6
6
  readonly emptyListNote: "Add at least one value — an empty list can't be saved.";
@@ -25,6 +25,7 @@ export declare const eachVersion: (share: string) => string;
25
25
  export declare const groupShare: (share: string, label: string) => string;
26
26
  export declare const uncounted = "; the rest see the Original and are not counted";
27
27
  export declare const holdoutClause: (weight: string) => string;
28
+ export declare const holdoutRemainder = "; everyone else sees the Original and is not counted";
28
29
  export declare const layerSentence: (name: string, used: number, free: number) => string;
29
30
  export declare const peopleLabel: {
30
31
  readonly visitor: "visitors";
@@ -1,6 +1,6 @@
1
1
  export const AUDIENCE_COPY = {
2
2
  listPlaceholder: 'Type a value and press Enter',
3
- listDescription: 'Add one value per chip, for example each page path.',
3
+ listDescription: 'Add one value per chip.',
4
4
  removeValue: (value)=>`Remove ${value}`,
5
5
  removeSymbol: '×',
6
6
  emptyListNote: "Add at least one value — an empty list can't be saved."
@@ -24,7 +24,8 @@ export const participation = (allocation, people, groups)=>`${allocation}% of ${
24
24
  export const eachVersion = (share)=>`${share}% see each version`;
25
25
  export const groupShare = (share, label)=>`${share}% see ${label === 'Original' ? 'the Original' : label}`;
26
26
  export const uncounted = '; the rest see the Original and are not counted';
27
- export const holdoutClause = (weight)=>`; the holdout (${weight}% of them) sees the Original and is measured; everyone else sees the Original and is not counted.`;
27
+ export const holdoutClause = (weight)=>`; the holdout (${weight}% of them) sees the Original and is measured`;
28
+ export const holdoutRemainder = '; everyone else sees the Original and is not counted';
28
29
  export const layerSentence = (name, used, free)=>` Uses ${used} of the 100 slices in layer ${name}; ${free} are free.`;
29
30
  export const peopleLabel = {
30
31
  visitor: 'visitors',
@@ -1,5 +1,8 @@
1
1
  export declare const API_MESSAGES: {
2
- readonly notFound: "This experiment no longer exists.";
2
+ readonly notFound: "This test no longer exists.";
3
+ readonly goalNotFound: "That goal no longer exists. Choose another goal.";
4
+ readonly roundChanged: "This round has changed. Reload the page.";
5
+ readonly roundNotFound: "That round no longer exists. Choose another round.";
3
6
  readonly commitUnverified: "The change may not have been saved. Reload and try again.";
4
7
  readonly notImplemented: "This isn't available yet.";
5
8
  readonly unauthenticated: "Your session has expired. Sign in again.";
@@ -9,12 +12,16 @@ export declare const API_MESSAGES: {
9
12
  readonly unreadable: "Couldn't read the server's response. Reload and try again.";
10
13
  };
11
14
  export declare const LOCKED_MESSAGES: {
12
- readonly creation: "Set when the experiment was created.";
15
+ readonly creation: "Set when the test was created.";
13
16
  readonly running: "Locked while the test is running so everyone in this round sees the same thing. Stop the test to change it.";
14
- readonly runningAllocation: "Raise it from the actions panel; lowering takes effect in the next round.";
15
- readonly runningMetrics: "Add supporting metrics or guardrails from the actions panel; the main metric is fixed for this round.";
17
+ readonly runningAllocation: "To include more people, use \"Let more people in\" at the bottom of the page. To include fewer, stop the test; the change applies when you start it again.";
18
+ readonly runningMetrics: "Add a guardrail or something to watch with \"Add a goal\" at the bottom of the page; the main goal is fixed for this round.";
16
19
  readonly decided: "Locked after the decision.";
17
- readonly archived: "Archived experiments can't be changed.";
20
+ readonly archived: "Archived tests can't be changed.";
18
21
  readonly other: "Can't be changed in this state.";
19
22
  };
20
- export declare function refusalMessage(code: string | undefined, status: number, message?: string): string;
23
+ export declare function refusalMessage(code: string | undefined, status: number, message?: string, options?: {
24
+ notFound?: string;
25
+ codes?: Readonly<Record<string, string>>;
26
+ fallback?: string;
27
+ }): string;
@@ -1,5 +1,8 @@
1
1
  export const API_MESSAGES = {
2
- notFound: 'This experiment no longer exists.',
2
+ notFound: 'This test no longer exists.',
3
+ goalNotFound: 'That goal no longer exists. Choose another goal.',
4
+ roundChanged: 'This round has changed. Reload the page.',
5
+ roundNotFound: 'That round no longer exists. Choose another round.',
3
6
  commitUnverified: 'The change may not have been saved. Reload and try again.',
4
7
  notImplemented: "This isn't available yet.",
5
8
  unauthenticated: 'Your session has expired. Sign in again.',
@@ -9,19 +12,22 @@ export const API_MESSAGES = {
9
12
  unreadable: "Couldn't read the server's response. Reload and try again."
10
13
  };
11
14
  export const LOCKED_MESSAGES = {
12
- creation: 'Set when the experiment was created.',
15
+ creation: 'Set when the test was created.',
13
16
  running: 'Locked while the test is running so everyone in this round sees the same thing. Stop the test to change it.',
14
- runningAllocation: 'Raise it from the actions panel; lowering takes effect in the next round.',
15
- runningMetrics: 'Add supporting metrics or guardrails from the actions panel; the main metric is fixed for this round.',
17
+ runningAllocation: 'To include more people, use "Let more people in" at the bottom of the page. To include fewer, stop the test; the change applies when you start it again.',
18
+ runningMetrics: 'Add a guardrail or something to watch with "Add a goal" at the bottom of the page; the main goal is fixed for this round.',
16
19
  decided: 'Locked after the decision.',
17
- archived: "Archived experiments can't be changed.",
20
+ archived: "Archived tests can't be changed.",
18
21
  other: "Can't be changed in this state."
19
22
  };
20
- export function refusalMessage(code, status, message) {
23
+ export function refusalMessage(code, status, message, options) {
24
+ // A per-call code wins over the status branches: entry-not-found is a 404 that is not "this test no longer exists".
25
+ const mapped = code && options?.codes && Object.hasOwn(options.codes, code) ? options.codes[code] : undefined;
26
+ if (mapped !== undefined) return mapped;
21
27
  if (code === 'unauthenticated' || status === 401) return API_MESSAGES.unauthenticated;
22
- if (code === 'not-found' || status === 404) return API_MESSAGES.notFound;
28
+ if (code === 'not-found' || status === 404) return options?.notFound ?? API_MESSAGES.notFound;
23
29
  if (code === 'commit-unverified') return API_MESSAGES.commitUnverified;
24
30
  if (code === 'not-implemented' || status === 501) return API_MESSAGES.notImplemented;
25
31
  if (code === 'forbidden' || status === 403) return API_MESSAGES.forbidden;
26
- return message || API_MESSAGES.failed;
32
+ return options?.fallback ?? (message || API_MESSAGES.failed);
27
33
  }