@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
@@ -1,54 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { SelectInput, useField, useForm, useFormFields } from '@payloadcms/ui';
4
- import { EDITABLE_BY_STATUS, isEditable } from '../collections/experiments/editability.js';
5
- import { lockedReason } from './lockedNotice.model.js';
6
- import { ARMS_COPY } from './copy/arms.js';
7
- import { readArms } from './arms/arms.model.js';
8
- export function FlagPointSelect({ field, path, readOnly }) {
9
- const { value, setValue } = useField({
10
- path
11
- });
12
- const form = useForm();
13
- const status = useFormFields(([fields])=>fields.status?.value ?? 'draft');
14
- const rowKinds = useFormFields(([fields])=>readArms(fields).map((arm)=>arm.kind).join(','));
15
- const points = field.admin?.custom?.flagPoints ?? [];
16
- const editable = isEditable(status, 'flagPoint') && !readOnly && !form.disabled;
17
- const reason = lockedReason(status, 'flagPoint', EDITABLE_BY_STATUS);
18
- return /*#__PURE__*/ _jsxs("div", {
19
- className: "xp-flag-point",
20
- children: [
21
- /*#__PURE__*/ _jsx(SelectInput, {
22
- path: path,
23
- name: path,
24
- label: ARMS_COPY.flagPoint,
25
- options: points.map((point)=>({
26
- label: point.label,
27
- value: point.id
28
- })),
29
- value: value ?? '',
30
- readOnly: !editable,
31
- onChange: (option)=>{
32
- if (!editable || Array.isArray(option)) return;
33
- const point = points.find((candidate)=>candidate.id === option?.value);
34
- if ((point?.id ?? '') === (value ?? '')) return;
35
- setValue(point?.id ?? '');
36
- rowKinds.split(',').forEach((kind, index)=>{
37
- if (kind === 'control' || kind === 'variant') form.dispatchFields({
38
- type: 'UPDATE',
39
- path: `arms.${index}.params`,
40
- value: kind === 'control' && point ? {
41
- ...point.effectiveDefaults
42
- } : {}
43
- });
44
- });
45
- form.setModified(true);
46
- }
47
- }),
48
- reason ? /*#__PURE__*/ _jsx("p", {
49
- className: "xp-locked-notice",
50
- children: reason
51
- }) : null
52
- ]
53
- });
54
- }
@@ -1,2 +0,0 @@
1
- import type { FieldDescriptionClientProps } from 'payload';
2
- export declare function LockedFieldNotice(props: FieldDescriptionClientProps): import("react").JSX.Element;
@@ -1,26 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { FieldDescription, useDocumentInfo, useFormFields } from '@payloadcms/ui';
4
- import { EDITABLE_BY_STATUS } from '../collections/experiments/editability.js';
5
- import { lockedReason } from './lockedNotice.model.js';
6
- export function LockedFieldNotice(props) {
7
- const status = useFormFields(([fields])=>fields.status?.value);
8
- const { id } = useDocumentInfo();
9
- const admin = props.field.admin;
10
- const description = admin && 'description' in admin ? admin.description : undefined;
11
- const customField = admin?.custom?.field;
12
- const field = typeof customField === 'string' ? customField : props.path.split('.')[0];
13
- const reason = id != null && status ? lockedReason(status, field, EDITABLE_BY_STATUS) : null;
14
- return /*#__PURE__*/ _jsxs(_Fragment, {
15
- children: [
16
- /*#__PURE__*/ _jsx(FieldDescription, {
17
- description: description,
18
- path: props.path
19
- }),
20
- reason ? /*#__PURE__*/ _jsx("p", {
21
- className: "xp-locked-notice",
22
- children: reason
23
- }) : null
24
- ]
25
- });
26
- }
@@ -1,2 +0,0 @@
1
- import type { UIFieldClientProps } from 'payload';
2
- export declare function PhaseHistory(_props: UIFieldClientProps): import("react").JSX.Element | null;
@@ -1,76 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useConfig, useDocumentInfo, useFormFields } from '@payloadcms/ui';
4
- import { useEffect, useState } from 'react';
5
- import { phaseHistory } from './phaseHistory.model.js';
6
- import { HISTORY_COPY } from './copy/history.js';
7
- export function PhaseHistory(_props) {
8
- const { id, data } = useDocumentInfo();
9
- const { config } = useConfig();
10
- const api = config.routes.api;
11
- const decidedBy = useFormFields(([fields])=>{
12
- const decision = fields.decision?.value;
13
- return fields['decision.decidedBy']?.value ?? (decision && typeof decision === 'object' && 'decidedBy' in decision ? decision.decidedBy : undefined);
14
- }) ?? data?.decision?.decidedBy;
15
- const [state, setState] = useState(null);
16
- const key = `${api}:${id}`;
17
- useEffect(()=>{
18
- setState(null);
19
- if (id == null) return;
20
- const controller = new AbortController();
21
- async function load() {
22
- const docs = [];
23
- let page = 1;
24
- while(!controller.signal.aborted){
25
- const query = new URLSearchParams({
26
- depth: '0',
27
- sort: 'phase,startedAt',
28
- limit: '100',
29
- page: String(page),
30
- 'where[experiment][equals]': String(id)
31
- });
32
- const response = await fetch(`${api}/xp-phases?${query}`, {
33
- credentials: 'include',
34
- signal: controller.signal
35
- });
36
- if (!response.ok) {
37
- if (!controller.signal.aborted) setState(null);
38
- return;
39
- }
40
- const result = await response.json();
41
- docs.push(...result.docs);
42
- if (!result.hasNextPage) break;
43
- page++;
44
- }
45
- if (!controller.signal.aborted) setState({
46
- key,
47
- docs
48
- });
49
- }
50
- void load().catch(()=>{
51
- if (!controller.signal.aborted) setState(null);
52
- });
53
- return ()=>controller.abort();
54
- }, [
55
- api,
56
- id,
57
- key
58
- ]);
59
- if (id == null || state?.key !== key) return null;
60
- const history = phaseHistory(state.docs, {
61
- decidedBy
62
- });
63
- return /*#__PURE__*/ _jsxs("section", {
64
- "aria-label": HISTORY_COPY.rounds,
65
- children: [
66
- /*#__PURE__*/ _jsx("ul", {
67
- children: history.rows.map((row)=>/*#__PURE__*/ _jsx("li", {
68
- children: row.text
69
- }, `${row.phase}-${row.phaseKind}`))
70
- }),
71
- history.decision ? /*#__PURE__*/ _jsx("p", {
72
- children: history.decision
73
- }) : null
74
- ]
75
- });
76
- }
@@ -1,3 +0,0 @@
1
- import './styles.js';
2
- import type { DefaultCellComponentProps } from 'payload';
3
- export declare function StatusCell({ cellData, rowData }: DefaultCellComponentProps): import("react").JSX.Element;
@@ -1,19 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import './styles.js';
4
- import { Pill } from '@payloadcms/ui';
5
- import { statusPill } from './status.model.js';
6
- export function StatusCell({ cellData, rowData }) {
7
- const pill = statusPill({
8
- status: cellData,
9
- arms: rowData.arms,
10
- decision: rowData.decision
11
- });
12
- return /*#__PURE__*/ _jsx("span", {
13
- title: pill.tooltip,
14
- children: /*#__PURE__*/ _jsx(Pill, {
15
- className: `xp-status-pill xp-status-pill--${pill.tone}`,
16
- children: pill.label
17
- })
18
- });
19
- }
@@ -1,2 +0,0 @@
1
- import type { DefaultCellComponentProps } from 'payload';
2
- export declare function SummaryCell({ rowData }: DefaultCellComponentProps): import("react").JSX.Element;
@@ -1,32 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { useEffect, useState } from 'react';
4
- import { useApi } from './useApi.js';
5
- import { verdictCodeLabel } from './labels.js';
6
- import { summaryLine } from './copy/results.js';
7
- import { formatCount, formatNumber } from './results/format.js';
8
- import { requestSummary } from './summaryBatcher.js';
9
- export function SummaryCell({ rowData }) {
10
- const api = useApi();
11
- const id = rowData.id;
12
- const [state, setState] = useState(null);
13
- useEffect(()=>{
14
- let active = true;
15
- if (id !== undefined && id !== null) void requestSummary(api, id).then((entry)=>{
16
- if (active) setState({
17
- id,
18
- entry
19
- });
20
- });
21
- return ()=>{
22
- active = false;
23
- };
24
- }, [
25
- api,
26
- id
27
- ]);
28
- const entry = state?.id === id ? state?.entry : null;
29
- return /*#__PURE__*/ _jsx("span", {
30
- children: entry ? summaryLine(formatCount(entry.exposures), formatNumber(entry.days), entry.verdict ? verdictCodeLabel(entry.verdict.code) : '—') : '—'
31
- });
32
- }
@@ -1,6 +0,0 @@
1
- import type { DocId } from '../../types/contracts.js';
2
- export declare function AddMetricDialog({ id, attachedMetrics, onClose }: {
3
- id: DocId;
4
- attachedMetrics: readonly DocId[];
5
- onClose: () => void;
6
- }): import("react").JSX.Element;
@@ -1,138 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useRef, useState } from 'react';
4
- import { Button, useConfig } from '@payloadcms/ui';
5
- import { DialogFrame } from './ConfirmDialog.js';
6
- import { useApi } from '../useApi.js';
7
- import { reloadPage } from '../reload.js';
8
- import { REVIEW_COPY as C } from '../copy/review.js';
9
- import { ACTIONS_COPY } from '../copy/actions.js';
10
- import { API_MESSAGES, refusalMessage } from '../copy/common.js';
11
- export function AddMetricDialog({ id, attachedMetrics, onClose }) {
12
- const api = useApi();
13
- const { config } = useConfig();
14
- const [metrics, setMetrics] = useState([]);
15
- const [loading, setLoading] = useState(true);
16
- const [metric, setMetric] = useState('');
17
- const [role, setRole] = useState('secondary');
18
- const [busy, setBusy] = useState(false);
19
- const inFlight = useRef(false);
20
- const [error, setError] = useState(null);
21
- useEffect(()=>{
22
- const controller = new AbortController();
23
- async function load() {
24
- try {
25
- const response = await fetch(`${config.routes.api.replace(/\/$/, '')}/xp-metrics?depth=0&limit=100`, {
26
- credentials: 'include',
27
- signal: controller.signal
28
- });
29
- if (!response.ok) throw new Error(refusalMessage(undefined, response.status));
30
- const data = await response.json();
31
- if (!Array.isArray(data.docs)) throw new Error(API_MESSAGES.unreadable);
32
- if (!controller.signal.aborted) setMetrics(data.docs);
33
- } catch (cause) {
34
- if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : API_MESSAGES.network);
35
- } finally{
36
- if (!controller.signal.aborted) setLoading(false);
37
- }
38
- }
39
- void load();
40
- return ()=>controller.abort();
41
- }, [
42
- config.routes.api
43
- ]);
44
- const attached = new Set(attachedMetrics.map(String));
45
- const available = metrics.filter((item)=>!attached.has(String(item.id)));
46
- const selected = available.find((item)=>String(item.id) === metric);
47
- async function submit() {
48
- if (inFlight.current || loading || !selected) return;
49
- inFlight.current = true;
50
- setBusy(true);
51
- setError(null);
52
- const body = {
53
- metric: selected.id,
54
- role
55
- };
56
- const result = await api.addMetric(id, body);
57
- if (result.ok) {
58
- reloadPage();
59
- return;
60
- }
61
- setError(result.message);
62
- inFlight.current = false;
63
- setBusy(false);
64
- }
65
- return /*#__PURE__*/ _jsxs(DialogFrame, {
66
- title: C.addMetric,
67
- onClose: ()=>{
68
- if (!inFlight.current) onClose();
69
- },
70
- children: [
71
- loading ? /*#__PURE__*/ _jsx("p", {
72
- role: "status",
73
- children: C.loadingMetrics
74
- }) : null,
75
- /*#__PURE__*/ _jsxs("label", {
76
- children: [
77
- C.role,
78
- /*#__PURE__*/ _jsxs("select", {
79
- value: role,
80
- disabled: busy,
81
- onChange: (event)=>setRole(event.target.value),
82
- children: [
83
- /*#__PURE__*/ _jsx("option", {
84
- value: "secondary",
85
- children: C.supporting
86
- }),
87
- /*#__PURE__*/ _jsx("option", {
88
- value: "guardrail",
89
- children: C.guardrail
90
- })
91
- ]
92
- })
93
- ]
94
- }),
95
- /*#__PURE__*/ _jsxs("label", {
96
- children: [
97
- C.metric,
98
- /*#__PURE__*/ _jsxs("select", {
99
- value: metric,
100
- disabled: busy || loading,
101
- onChange: (event)=>setMetric(event.target.value),
102
- children: [
103
- /*#__PURE__*/ _jsx("option", {
104
- value: "",
105
- children: C.chooseMetric
106
- }),
107
- available.map((item)=>/*#__PURE__*/ _jsx("option", {
108
- value: String(item.id),
109
- children: item.name
110
- }, item.id))
111
- ]
112
- })
113
- ]
114
- }),
115
- !loading && !error && !available.length ? /*#__PURE__*/ _jsx("p", {
116
- children: C.noMetrics
117
- }) : null,
118
- error ? /*#__PURE__*/ _jsx("p", {
119
- role: "alert",
120
- children: error
121
- }) : null,
122
- /*#__PURE__*/ _jsx(Button, {
123
- disabled: busy || loading || !selected,
124
- onClick: ()=>{
125
- void submit();
126
- },
127
- children: C.addMetric
128
- }),
129
- /*#__PURE__*/ _jsx(Button, {
130
- disabled: busy,
131
- onClick: ()=>{
132
- if (!inFlight.current) onClose();
133
- },
134
- children: ACTIONS_COPY.cancel
135
- })
136
- ]
137
- });
138
- }
@@ -1,6 +0,0 @@
1
- import type { DocId } from '../../types/contracts.js';
2
- export declare function AllocateDialog({ id, allocation: current, onClose }: {
3
- id: DocId;
4
- allocation: number;
5
- onClose: () => void;
6
- }): import("react").JSX.Element;
@@ -1,81 +0,0 @@
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 { DialogFrame } from './ConfirmDialog.js';
6
- import { useApi } from '../useApi.js';
7
- import { reloadPage } from '../reload.js';
8
- import { REVIEW_COPY as C, REVIEW_HINTS } from '../copy/review.js';
9
- import { ACTIONS_COPY } from '../copy/actions.js';
10
- export function AllocateDialog({ id, allocation: current, onClose }) {
11
- const api = useApi();
12
- const [value, setValue] = useState(String(Math.min(100, current + 1)));
13
- const [busy, setBusy] = useState(false);
14
- const [error, setError] = useState(null);
15
- const inFlight = useRef(false);
16
- const allocation = Number(value);
17
- const valid = value.trim() !== '' && Number.isInteger(allocation) && allocation > current && allocation <= 100;
18
- async function submit() {
19
- if (inFlight.current || !valid) return;
20
- inFlight.current = true;
21
- setBusy(true);
22
- setError(null);
23
- const body = {
24
- allocation
25
- };
26
- const result = await api.allocate(id, body);
27
- if (result.ok) {
28
- reloadPage();
29
- return;
30
- }
31
- setError(result.code === 'allocation-lower' ? REVIEW_HINTS['allocation-lower'] : result.message);
32
- inFlight.current = false;
33
- setBusy(false);
34
- }
35
- return /*#__PURE__*/ _jsxs(DialogFrame, {
36
- title: C.raise,
37
- onClose: ()=>{
38
- if (!inFlight.current) onClose();
39
- },
40
- children: [
41
- /*#__PURE__*/ _jsx("p", {
42
- children: C.allocationBody
43
- }),
44
- /*#__PURE__*/ _jsxs("label", {
45
- children: [
46
- C.allocation,
47
- /*#__PURE__*/ _jsx("input", {
48
- type: "number",
49
- min: current + 1,
50
- max: 100,
51
- step: 1,
52
- value: value,
53
- disabled: busy,
54
- onChange: (event)=>setValue(event.target.value)
55
- })
56
- ]
57
- }),
58
- !valid ? /*#__PURE__*/ _jsx("p", {
59
- children: C.allocationInvalid
60
- }) : null,
61
- error ? /*#__PURE__*/ _jsx("p", {
62
- role: "alert",
63
- children: error
64
- }) : null,
65
- /*#__PURE__*/ _jsx(Button, {
66
- disabled: busy || !valid,
67
- onClick: ()=>{
68
- void submit();
69
- },
70
- children: C.raise
71
- }),
72
- /*#__PURE__*/ _jsx(Button, {
73
- disabled: busy,
74
- onClick: ()=>{
75
- if (!inFlight.current) onClose();
76
- },
77
- children: ACTIONS_COPY.cancel
78
- })
79
- ]
80
- });
81
- }
@@ -1,3 +0,0 @@
1
- import '../styles.js';
2
- import type { UIFieldClientProps } from 'payload';
3
- export declare function ExperimentActions(_props: UIFieldClientProps): import("react").JSX.Element | null;
@@ -1,110 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import '../styles.js';
4
- import { useState } from 'react';
5
- import { Button, useAuth, useDocumentInfo, useFormFields, useFormModified } from '@payloadcms/ui';
6
- import { canManage } from '../permissions.model.js';
7
- import { actionGroupLabel, availableActions } from './actions.model.js';
8
- import { ConfirmDialog } from './ConfirmDialog.js';
9
- import { DecideDialog } from './DecideDialog.js';
10
- import { ACTIONS_COPY } from '../copy/actions.js';
11
- import { AllocateDialog } from './AllocateDialog.js';
12
- import { AddMetricDialog } from './AddMetricDialog.js';
13
- const SAVE_FIRST = 'Save your changes first; these actions use the saved experiment.';
14
- export function ExperimentActions(_props) {
15
- const { id } = useDocumentInfo();
16
- const { permissions } = useAuth();
17
- const status = useFormFields(([fields])=>fields.status?.value ?? 'draft');
18
- const kind = useFormFields(([fields])=>fields.kind?.value ?? 'block');
19
- // A primitive slice keeps unrelated form edits from rerendering this panel.
20
- const groups = useFormFields(([fields])=>JSON.stringify(Object.keys(fields).filter((path)=>/^arms\.\d+\.(key|label|kind)$/.test(path)).sort().map((path)=>[
21
- path,
22
- fields[path]?.value
23
- ])));
24
- const armsByIndex = {};
25
- for (const [path, value] of JSON.parse(groups)){
26
- const [, index, field] = path.split('.');
27
- const group = armsByIndex[index] ??= {
28
- key: ''
29
- };
30
- group[field] = value;
31
- }
32
- const arms = Object.entries(armsByIndex).sort(([a], [b])=>Number(a) - Number(b)).map(([, group])=>group);
33
- const winnerArm = useFormFields(([fields])=>fields['decision.winnerArm']?.value);
34
- const holdoutReleasedAt = useFormFields(([fields])=>fields['decision.holdoutReleasedAt']?.value);
35
- const contentReport = useFormFields(([fields])=>fields['decision.contentReport']?.value);
36
- const allocation = useFormFields(([fields])=>fields.allocation?.value ?? 100);
37
- const attachedMetricsJson = useFormFields(([fields])=>{
38
- const relationId = (value)=>{
39
- if (Array.isArray(value)) return value.flatMap(relationId);
40
- if (typeof value === 'string' || typeof value === 'number') return [
41
- value
42
- ];
43
- if (value && typeof value === 'object' && 'id' in value) return relationId(value.id);
44
- return [];
45
- };
46
- return JSON.stringify([
47
- 'metrics.primary',
48
- 'metrics.secondary',
49
- 'metrics.guardrails'
50
- ].flatMap((path)=>relationId(fields[path]?.value)));
51
- });
52
- const attachedMetrics = JSON.parse(attachedMetricsJson);
53
- const [selected, setSelected] = useState(null);
54
- // Transitions act on the saved document (and the typed phrase uses the saved group label), so unsaved edits block them.
55
- const modified = useFormModified();
56
- const actions = availableActions({
57
- status,
58
- kind,
59
- arms,
60
- decision: {
61
- winnerArm,
62
- holdoutReleasedAt,
63
- contentReport
64
- },
65
- canManage: canManage(permissions)
66
- });
67
- const action = actions.find((candidate)=>candidate.id === selected);
68
- if (id == null) return null;
69
- const label = actionGroupLabel(winnerArm ?? 'control', arms.find((group)=>group.key === winnerArm)?.label);
70
- return /*#__PURE__*/ _jsxs("div", {
71
- className: "xp-actions",
72
- children: [
73
- modified && actions.length ? /*#__PURE__*/ _jsx("p", {
74
- className: "xp-actions__note",
75
- children: SAVE_FIRST
76
- }) : null,
77
- status === 'archived' || status === 'draft' ? /*#__PURE__*/ _jsx("p", {
78
- className: "xp-actions__note",
79
- children: status === 'archived' ? ACTIONS_COPY.deleteNoteArchived : ACTIONS_COPY.deleteNoteDraft
80
- }) : null,
81
- actions.map((item)=>/*#__PURE__*/ _jsx(Button, {
82
- buttonStyle: item.tone === 'primary' ? 'primary' : 'secondary',
83
- disabled: modified,
84
- className: `xp-action xp-action--${item.tone}`,
85
- onClick: ()=>{
86
- if (!modified) setSelected(item.id);
87
- },
88
- children: item.label
89
- }, item.id)),
90
- action?.id === 'allocate' ? /*#__PURE__*/ _jsx(AllocateDialog, {
91
- id: id,
92
- allocation: allocation,
93
- onClose: ()=>setSelected(null)
94
- }, `${id}/allocate`) : action?.id === 'add-metric' ? /*#__PURE__*/ _jsx(AddMetricDialog, {
95
- id: id,
96
- attachedMetrics: attachedMetrics,
97
- onClose: ()=>setSelected(null)
98
- }, `${id}/add-metric`) : action?.id === 'decide' ? /*#__PURE__*/ _jsx(DecideDialog, {
99
- id: id,
100
- arms: arms,
101
- onClose: ()=>setSelected(null)
102
- }, `${id}/decide`) : action ? /*#__PURE__*/ _jsx(ConfirmDialog, {
103
- id: id,
104
- action: action,
105
- winnerLabel: label,
106
- onClose: ()=>setSelected(null)
107
- }, `${id}/${action.id}`) : null
108
- ]
109
- });
110
- }
@@ -1,2 +0,0 @@
1
- import type { ArrayFieldClientProps } from 'payload';
2
- export declare function ArmsField({ field, path, readOnly }: ArrayFieldClientProps): import("react").JSX.Element;