@delmaredigital/payload-experiments 0.1.0-rc.7 → 0.2.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 (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/index.d.ts +1 -1
  135. package/dist/exports/rsc.d.ts +3 -0
  136. package/dist/exports/rsc.js +3 -0
  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 +1 -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,175 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { formatAdminURL } from 'payload/shared';
3
+ import { notFound, redirect } from 'next/navigation.js';
4
+ import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent';
5
+ import { serializeAttributeKeys, serializeFlagPoints, serializeMetricSources } from '../../collections/serialize.js';
6
+ import { puckCollections } from '../../options.js';
7
+ import { xpOptionsFromReq } from '../../options/fromReq.js';
8
+ import { XP_TESTS_SURFACE } from '../paths.js';
9
+ import { SURFACE_COPY } from '../copy/surface.js';
10
+ import { routeAccess } from './access.model.js';
11
+ export async function ExperimentsRootView({ initPageResult, params, searchParams }) {
12
+ const { req } = initPageResult;
13
+ const adminRoute = req.payload.config.routes.admin;
14
+ const segments = (await params)?.segments;
15
+ const path = Array.isArray(segments) ? segments : [];
16
+ const tail = path[0] === 'experiments' ? path.slice(1) : path;
17
+ let route;
18
+ let id;
19
+ if (tail.length === 0) route = 'home';
20
+ else if (tail.length === 1 && tail[0] === 'new') route = 'new';
21
+ else if (tail.length === 1) {
22
+ route = 'test';
23
+ id = tail[0];
24
+ } else if (tail.length === 2 && tail[1] === 'results') {
25
+ route = 'results';
26
+ id = tail[0];
27
+ } else return notFound();
28
+ if (!req.user) {
29
+ const { login } = req.payload.config.admin.routes;
30
+ const query = new URLSearchParams();
31
+ for (const [key, value] of Object.entries(await searchParams ?? {})){
32
+ if (Array.isArray(value)) value.forEach((item)=>query.append(key, item));
33
+ else if (value !== undefined) query.set(key, String(value));
34
+ }
35
+ const currentPath = formatAdminURL({
36
+ adminRoute,
37
+ path: `/experiments${tail.length ? `/${tail.map(encodeURIComponent).join('/')}` : ''}${query.size ? `?${query}` : ''}`
38
+ });
39
+ return redirect(formatAdminURL({
40
+ adminRoute,
41
+ path: `${login}?redirect=${encodeURIComponent(currentPath)}`
42
+ }));
43
+ }
44
+ const options = xpOptionsFromReq(req);
45
+ async function permits(access) {
46
+ try {
47
+ return await access({
48
+ req
49
+ }) === true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+ const canManage = await permits(options.access.manage);
55
+ const isAdmin = options.access.admin ? await permits(options.access.admin) : false;
56
+ const canReadResults = options.access.readResults ? await permits(options.access.readResults) : canManage;
57
+ if (routeAccess({
58
+ route,
59
+ hasUser: true,
60
+ canManage,
61
+ canReadResults
62
+ }) !== 'ok') {
63
+ return /*#__PURE__*/ _jsx("div", {
64
+ style: {
65
+ padding: 'var(--base)',
66
+ color: 'var(--theme-text)',
67
+ background: 'var(--theme-bg)'
68
+ },
69
+ children: SURFACE_COPY.noAccess
70
+ });
71
+ }
72
+ const flagPoints = serializeFlagPoints(options);
73
+ // xp-state is manager-readable only. Results-only readers do not edit settings.
74
+ if (canManage && flagPoints.length) {
75
+ const state = await req.payload.findGlobal({
76
+ slug: 'xp-state',
77
+ req,
78
+ overrideAccess: false,
79
+ depth: 0
80
+ });
81
+ const defaults = state.flagDefaults;
82
+ for (const point of flagPoints)point.effectiveDefaults = defaults?.[point.id]?.params ?? point.defaultParams;
83
+ }
84
+ const registrations = puckCollections(options);
85
+ const pages = route === 'new' && canManage && options.puck?.enabled && registrations.length ? (await Promise.all(registrations.map(async ({ slug: collection })=>{
86
+ try {
87
+ const config = req.payload.config.collections.find((item)=>item.slug === collection);
88
+ const useAsTitle = config?.admin.useAsTitle ?? 'id';
89
+ const label = config?.labels.plural;
90
+ // Keep /rsc dependency-safe: translations is not a declared runtime dependency,
91
+ // so a per-language label picks the request language, then English, then any string.
92
+ const byLanguage = label && typeof label === 'object' ? label : {};
93
+ const collectionLabel = typeof label === 'string' ? label : [
94
+ byLanguage[req.i18n?.language ?? 'en'],
95
+ byLanguage.en,
96
+ ...Object.values(byLanguage)
97
+ ].find((value)=>typeof value === 'string') ?? collection;
98
+ const result = await req.payload.find({
99
+ collection,
100
+ req,
101
+ overrideAccess: false,
102
+ depth: 0,
103
+ limit: 50,
104
+ sort: '-updatedAt',
105
+ draft: true,
106
+ select: {
107
+ [useAsTitle]: true
108
+ }
109
+ });
110
+ return result.docs.map((doc)=>{
111
+ const id = doc.id;
112
+ const customUrl = options.admin?.editorUrl?.({
113
+ collection,
114
+ id,
115
+ draft: true
116
+ });
117
+ const title = doc[useAsTitle];
118
+ return {
119
+ collection,
120
+ collectionLabel,
121
+ id,
122
+ title: typeof title === 'string' || typeof title === 'number' ? String(title) : String(id),
123
+ editorUrl: typeof customUrl === 'string' ? customUrl : formatAdminURL({
124
+ adminRoute,
125
+ path: `/collections/${collection}/${encodeURIComponent(String(id))}`
126
+ })
127
+ };
128
+ });
129
+ } catch {
130
+ return [];
131
+ }
132
+ }))).flat() : undefined;
133
+ const descriptions = options.admin?.descriptions;
134
+ const clientProps = {
135
+ route,
136
+ ...id === undefined ? {} : {
137
+ id
138
+ },
139
+ adminRoute,
140
+ apiRoute: req.payload.config.routes.api,
141
+ canManage,
142
+ canReadResults,
143
+ isAdmin,
144
+ flagPoints,
145
+ ...pages === undefined ? {} : {
146
+ pages
147
+ },
148
+ attributeKeys: serializeAttributeKeys(options),
149
+ metricSources: serializeMetricSources(options),
150
+ ...descriptions ? {
151
+ admin: {
152
+ descriptions: {
153
+ ...descriptions
154
+ }
155
+ }
156
+ } : {}
157
+ };
158
+ return RenderServerComponent({
159
+ Component: XP_TESTS_SURFACE,
160
+ Fallback: TestsSurfaceFallback,
161
+ importMap: req.payload.importMap,
162
+ clientProps
163
+ });
164
+ }
165
+ function TestsSurfaceFallback() {
166
+ return /*#__PURE__*/ _jsx("div", {
167
+ role: "alert",
168
+ style: {
169
+ padding: 'var(--base)',
170
+ color: 'var(--theme-text)',
171
+ background: 'var(--theme-bg)'
172
+ },
173
+ children: SURFACE_COPY.notInstalled
174
+ });
175
+ }
@@ -0,0 +1,7 @@
1
+ import type { TestsRoute } from '../../types/surface.js';
2
+ export declare function routeAccess({ route, hasUser, canManage, canReadResults }: {
3
+ route: TestsRoute;
4
+ hasUser: boolean;
5
+ canManage: unknown;
6
+ canReadResults?: unknown;
7
+ }): 'login' | 'unauthorized' | 'ok';
@@ -0,0 +1,5 @@
1
+ export function routeAccess({ route, hasUser, canManage, canReadResults }) {
2
+ if (!hasUser) return 'login';
3
+ const allowed = route === 'results' ? canReadResults ?? canManage : canManage;
4
+ return allowed === true ? 'ok' : 'unauthorized';
5
+ }
@@ -1,5 +1,4 @@
1
1
  import * as paths from '../../admin/paths.js';
2
- import { TAB_DESCRIPTIONS } from '../../admin/copy/tabs.js';
3
2
  import { managedAccess } from '../access.js';
4
3
  import { serializeAttributeKeys, serializeFlagPoints } from '../serialize.js';
5
4
  import { TARGETING_OPS, isOpValid } from '../../targeting/ops.js';
@@ -9,17 +8,6 @@ import { lockHook } from './lockHook.js';
9
8
  import { deleteGuard, deleteGuardAfter } from './deleteGuard.js';
10
9
  import { checkArms } from './armInvariants.js';
11
10
  import { XpNotImplemented } from '../../errors.js';
12
- const locked = (field, admin = {})=>({
13
- ...admin,
14
- custom: {
15
- ...admin.custom,
16
- field
17
- },
18
- components: {
19
- ...admin.components,
20
- Description: paths.XP_LOCKED_NOTICE
21
- }
22
- });
23
11
  const positiveInteger = (value)=>value != null && Number.isInteger(value) && value > 0 || 'Enter a positive whole number.';
24
12
  export function experimentsCollection(options) {
25
13
  const flagPoints = serializeFlagPoints(options);
@@ -43,7 +31,8 @@ export function experimentsCollection(options) {
43
31
  graphQL: false,
44
32
  disableDuplicate: true,
45
33
  versions: {
46
- drafts: false
34
+ drafts: false,
35
+ maxPerDoc: 50
47
36
  },
48
37
  access: {
49
38
  ...managedAccess(options),
@@ -59,19 +48,26 @@ export function experimentsCollection(options) {
59
48
  } : false
60
49
  },
61
50
  admin: {
51
+ // group:false hides navigation while keeping custom view routes reachable (hidden causes 404s).
52
+ group: false,
62
53
  useAsTitle: 'name',
63
54
  defaultColumns: [
64
55
  'name',
65
56
  'status',
66
57
  'kind',
67
- 'flagPoint',
68
- 'listSummary'
58
+ 'flagPoint'
69
59
  ],
70
60
  components: {
71
61
  views: {
62
+ list: {
63
+ Component: paths.XP_TESTS_LIST_REDIRECT
64
+ },
72
65
  edit: {
66
+ default: {
67
+ Component: paths.XP_TEST_EDIT_REDIRECT
68
+ },
73
69
  results: {
74
- Component: paths.XP_RESULTS_VIEW,
70
+ Component: paths.XP_TEST_EDIT_REDIRECT,
75
71
  path: '/results',
76
72
  tab: {
77
73
  label: 'Results',
@@ -98,16 +94,6 @@ export function experimentsCollection(options) {
98
94
  ]
99
95
  },
100
96
  fields: [
101
- {
102
- name: 'actions',
103
- type: 'ui',
104
- admin: {
105
- position: 'sidebar',
106
- components: {
107
- Field: paths.XP_ACTIONS
108
- }
109
- }
110
- },
111
97
  {
112
98
  name: 'status',
113
99
  type: 'select',
@@ -122,30 +108,7 @@ export function experimentsCollection(options) {
122
108
  ],
123
109
  admin: {
124
110
  position: 'sidebar',
125
- readOnly: true,
126
- components: {
127
- Cell: paths.XP_STATUS_CELL
128
- }
129
- }
130
- },
131
- {
132
- name: 'listSummary',
133
- type: 'ui',
134
- admin: {
135
- condition: ()=>false,
136
- components: {
137
- Cell: paths.XP_SUMMARY_CELL
138
- }
139
- }
140
- },
141
- {
142
- name: 'phaseHistory',
143
- type: 'ui',
144
- admin: {
145
- position: 'sidebar',
146
- components: {
147
- Field: paths.XP_PHASE_HISTORY
148
- }
111
+ readOnly: true
149
112
  }
150
113
  },
151
114
  {
@@ -153,13 +116,11 @@ export function experimentsCollection(options) {
153
116
  tabs: [
154
117
  {
155
118
  label: 'What',
156
- description: TAB_DESCRIPTIONS.What,
157
119
  fields: [
158
120
  {
159
121
  name: 'name',
160
122
  type: 'text',
161
- required: true,
162
- admin: locked('name')
123
+ required: true
163
124
  },
164
125
  {
165
126
  name: 'slug',
@@ -169,8 +130,7 @@ export function experimentsCollection(options) {
169
130
  },
170
131
  {
171
132
  name: 'hypothesis',
172
- type: 'textarea',
173
- admin: locked('hypothesis')
133
+ type: 'textarea'
174
134
  },
175
135
  {
176
136
  name: 'kind',
@@ -189,14 +149,6 @@ export function experimentsCollection(options) {
189
149
  {
190
150
  name: 'flagPoint',
191
151
  type: 'text',
192
- admin: locked('flagPoint', {
193
- components: {
194
- Field: paths.XP_FLAG_POINT_SELECT
195
- },
196
- custom: {
197
- flagPoints
198
- }
199
- }),
200
152
  validate: (value, { data })=>!value && data?.kind !== 'flag' || flagPoints.some((point)=>point.id === value) || 'Choose a registered flag point.'
201
153
  },
202
154
  {
@@ -209,7 +161,6 @@ export function experimentsCollection(options) {
209
161
  'pageview',
210
162
  'manual'
211
163
  ],
212
- admin: locked('exposurePolicy'),
213
164
  validate: (value, { data })=>{
214
165
  const allowed = data?.kind === 'block' ? [
215
166
  'render',
@@ -226,7 +177,6 @@ export function experimentsCollection(options) {
226
177
  },
227
178
  {
228
179
  label: 'Who',
229
- description: TAB_DESCRIPTIONS.Who,
230
180
  fields: [
231
181
  {
232
182
  name: 'unitType',
@@ -246,12 +196,7 @@ export function experimentsCollection(options) {
246
196
  defaultValue: 100,
247
197
  min: 1,
248
198
  max: 100,
249
- validate: (value)=>positiveInteger(value) === true && value <= 100 || 'Enter a whole percentage from 1 to 100.',
250
- admin: locked('allocation', {
251
- components: {
252
- Field: paths.XP_ALLOCATION_FIELD
253
- }
254
- })
199
+ validate: (value)=>positiveInteger(value) === true && value <= 100 || 'Enter a whole percentage from 1 to 100.'
255
200
  },
256
201
  {
257
202
  name: 'layer',
@@ -261,21 +206,12 @@ export function experimentsCollection(options) {
261
206
  {
262
207
  name: 'targeting',
263
208
  type: 'array',
264
- admin: locked('targeting'),
265
209
  fields: [
266
210
  {
267
211
  name: 'attribute',
268
212
  type: 'text',
269
213
  required: true,
270
- validate: (value)=>typeof value === 'string' && Object.hasOwn(attributeKeys, value) || 'Choose a registered attribute.',
271
- admin: {
272
- components: {
273
- Field: paths.XP_TARGETING_ATTRIBUTE
274
- },
275
- custom: {
276
- attributeKeys
277
- }
278
- }
214
+ validate: (value)=>typeof value === 'string' && Object.hasOwn(attributeKeys, value) || 'Choose a registered attribute.'
279
215
  },
280
216
  {
281
217
  name: 'op',
@@ -284,15 +220,7 @@ export function experimentsCollection(options) {
284
220
  options: [
285
221
  ...new Set(Object.values(TARGETING_OPS).flat())
286
222
  ],
287
- validate: (value, { siblingData })=>isOpValid(attributeKeys[siblingData?.attribute ?? '']?.type ?? '', value) || 'Choose an operator supported by this attribute.',
288
- admin: {
289
- components: {
290
- Field: paths.XP_TARGETING_OP
291
- },
292
- custom: {
293
- attributeKeys
294
- }
295
- }
223
+ validate: (value, { siblingData })=>isOpValid(attributeKeys[siblingData?.attribute ?? '']?.type ?? '', value) || 'Choose an operator supported by this attribute.'
296
224
  },
297
225
  {
298
226
  name: 'value',
@@ -305,14 +233,6 @@ export function experimentsCollection(options) {
305
233
  const valid = type === 'string' && (op === 'in' || op === 'nin') ? Array.isArray(value) && value.length > 0 && value.every((item)=>typeof item === 'string') : typeof value === type && (type !== 'number' || Number.isFinite(value));
306
234
  if (!valid && type === 'string' && (op === 'in' || op === 'nin') && Array.isArray(value) && value.length === 0) return 'Add at least one value; an empty list matches nobody.';
307
235
  return valid || 'Enter a value of the type required by this attribute and operator.';
308
- },
309
- admin: {
310
- components: {
311
- Field: paths.XP_TARGETING_VALUE
312
- },
313
- custom: {
314
- attributeKeys
315
- }
316
236
  }
317
237
  }
318
238
  ]
@@ -321,7 +241,6 @@ export function experimentsCollection(options) {
321
241
  },
322
242
  {
323
243
  label: 'Groups',
324
- description: TAB_DESCRIPTIONS.Groups,
325
244
  fields: [
326
245
  {
327
246
  name: 'arms',
@@ -337,14 +256,6 @@ export function experimentsCollection(options) {
337
256
  throw error;
338
257
  }
339
258
  },
340
- admin: locked('arms', {
341
- components: {
342
- Field: paths.XP_ARMS_FIELD
343
- },
344
- custom: {
345
- flagPoints
346
- }
347
- }),
348
259
  fields: [
349
260
  {
350
261
  name: 'key',
@@ -405,12 +316,10 @@ export function experimentsCollection(options) {
405
316
  },
406
317
  {
407
318
  label: 'Metrics',
408
- description: TAB_DESCRIPTIONS.Metrics,
409
319
  fields: [
410
320
  {
411
321
  name: 'metrics',
412
322
  type: 'group',
413
- admin: locked('metrics'),
414
323
  fields: [
415
324
  metric('primary'),
416
325
  metric('secondary', true),
@@ -420,7 +329,6 @@ export function experimentsCollection(options) {
420
329
  {
421
330
  name: 'analysis',
422
331
  type: 'group',
423
- admin: locked('analysis'),
424
332
  fields: [
425
333
  {
426
334
  name: 'method',
@@ -475,21 +383,6 @@ export function experimentsCollection(options) {
475
383
  ]
476
384
  }
477
385
  ]
478
- },
479
- {
480
- label: 'Review',
481
- description: TAB_DESCRIPTIONS.Review,
482
- fields: [
483
- {
484
- name: 'review',
485
- type: 'ui',
486
- admin: {
487
- components: {
488
- Field: paths.XP_REVIEW_PANEL
489
- }
490
- }
491
- }
492
- ]
493
386
  }
494
387
  ]
495
388
  },
@@ -3,5 +3,11 @@ export const createDefaults = ({ data, operation })=>{
3
3
  if (operation === 'create' && data && (!data.arms || Array.isArray(data.arms) && data.arms.length === 0)) {
4
4
  data.arms = defaultArms(data.kind);
5
5
  }
6
+ if (operation === 'create' && data && data.analysis?.plannedDurationDays == null) {
7
+ data.analysis = {
8
+ ...data.analysis,
9
+ plannedDurationDays: 28
10
+ };
11
+ }
6
12
  return data;
7
13
  };
@@ -2,57 +2,88 @@ import { XpRefusal } from '../errors.js';
2
2
  import { XP_LAYER_USAGE } from '../admin/paths.js';
3
3
  import { managedAccess } from './access.js';
4
4
  export function layersCollection(options) {
5
+ const usageAdmin = {
6
+ description: 'See which tests use this group and how much room is left.',
7
+ components: {
8
+ Field: XP_LAYER_USAGE
9
+ }
10
+ };
5
11
  return {
6
12
  slug: 'xp-layers',
7
13
  graphQL: false,
8
14
  disableDuplicate: true,
15
+ // group:false hides navigation while keeping collection routes reachable (hidden causes 404s).
16
+ labels: {
17
+ singular: 'Test group',
18
+ plural: 'Test groups (advanced)'
19
+ },
9
20
  admin: {
21
+ group: false,
10
22
  useAsTitle: 'name'
11
23
  },
12
24
  access: managedAccess(options),
13
25
  fields: [
14
26
  {
15
27
  name: 'name',
28
+ label: 'Name',
16
29
  type: 'text',
17
- required: true
30
+ required: true,
31
+ admin: {
32
+ description: 'Give this test group a name people will recognize.'
33
+ }
18
34
  },
19
35
  {
20
36
  name: 'slug',
37
+ label: 'Short name (used in code)',
21
38
  type: 'text',
22
39
  unique: true,
23
40
  required: true,
41
+ admin: {
42
+ description: 'Use a unique short name for code that refers to this group.'
43
+ },
24
44
  validate: (value, { operation, previousValue })=>{
25
45
  if (operation === 'update' && value === undefined) return true;
26
- if (typeof value !== 'string' || !value.length) return 'Enter a slug.';
27
- return operation !== 'update' || value === previousValue || 'slug is set when the layer is created.';
46
+ if (typeof value !== 'string' || !value.length) return 'Enter a short name.';
47
+ return operation !== 'update' || value === previousValue || "The short name can't change after the group is created.";
28
48
  }
29
49
  },
30
50
  {
31
51
  name: 'unitType',
52
+ label: 'Counts',
32
53
  type: 'select',
33
54
  required: true,
34
55
  options: [
35
- 'visitor',
36
- 'user'
56
+ {
57
+ label: 'Visitors',
58
+ value: 'visitor'
59
+ },
60
+ {
61
+ label: 'Signed-in users',
62
+ value: 'user'
63
+ }
37
64
  ],
65
+ admin: {
66
+ description: 'Choose whether tests in this group count visitors or signed-in users.'
67
+ },
38
68
  validate: (value, { operation, previousValue })=>{
39
69
  if (operation === 'update' && value === undefined) return true;
40
70
  if (value !== 'visitor' && value !== 'user') return 'Choose visitors or signed-in users.';
41
- return operation !== 'update' || value === previousValue || 'unitType is set when the layer is created.';
71
+ return operation !== 'update' || value === previousValue || "Who it counts can't change after the group is created.";
42
72
  }
43
73
  },
44
74
  {
45
75
  name: 'description',
46
- type: 'textarea'
76
+ label: 'Notes',
77
+ type: 'textarea',
78
+ admin: {
79
+ description: 'Explain when people should use this test group.'
80
+ }
47
81
  },
48
82
  {
49
83
  name: 'usage',
84
+ label: 'Tests in this group',
50
85
  type: 'ui',
51
- admin: {
52
- components: {
53
- Field: XP_LAYER_USAGE
54
- }
55
- }
86
+ admin: usageAdmin
56
87
  }
57
88
  ],
58
89
  hooks: {