@delmaredigital/payload-experiments 0.3.0-rc.4 → 0.4.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 (265) hide show
  1. package/README.md +113 -20
  2. package/dist/admin/MetricSourceSelect.js +3 -3
  3. package/dist/admin/PlacementStatus.d.ts +3 -0
  4. package/dist/admin/PlacementStatus.js +54 -0
  5. package/dist/admin/PreviewLinks.d.ts +2 -1
  6. package/dist/admin/PreviewLinks.js +2 -2
  7. package/dist/admin/actions/ConfirmDialog.d.ts +2 -1
  8. package/dist/admin/actions/ConfirmDialog.js +2 -2
  9. package/dist/admin/actions/DecideDialog.d.ts +2 -1
  10. package/dist/admin/actions/DecideDialog.js +2 -2
  11. package/dist/admin/api.d.ts +11 -2
  12. package/dist/admin/api.js +20 -2
  13. package/dist/admin/blocks/BlockPanel.d.ts +4 -0
  14. package/dist/admin/blocks/BlockPanel.js +153 -0
  15. package/dist/admin/blocks/TestBlockField.d.ts +5 -0
  16. package/dist/admin/blocks/TestBlockField.js +163 -0
  17. package/dist/admin/blocks/formApi.d.ts +43 -0
  18. package/dist/admin/blocks/formApi.js +153 -0
  19. package/dist/admin/blocks/inject.d.ts +7 -0
  20. package/dist/admin/blocks/inject.js +41 -0
  21. package/dist/admin/blocks/model.d.ts +17 -0
  22. package/dist/admin/blocks/model.js +45 -0
  23. package/dist/admin/blocks/statusCache.d.ts +7 -0
  24. package/dist/admin/blocks/statusCache.js +83 -0
  25. package/dist/admin/blocks/useBlockForm.d.ts +17 -0
  26. package/dist/admin/blocks/useBlockForm.js +46 -0
  27. package/dist/admin/copy/actions.d.ts +2 -0
  28. package/dist/admin/copy/actions.js +9 -0
  29. package/dist/admin/copy/blocks.d.ts +27 -0
  30. package/dist/admin/copy/blocks.js +38 -0
  31. package/dist/admin/copy/goals.d.ts +2 -0
  32. package/dist/admin/copy/goals.js +4 -1
  33. package/dist/admin/copy/newTest.d.ts +10 -0
  34. package/dist/admin/copy/newTest.js +10 -0
  35. package/dist/admin/copy/pages.d.ts +26 -0
  36. package/dist/admin/copy/pages.js +26 -0
  37. package/dist/admin/copy/readiness.d.ts +22 -3
  38. package/dist/admin/copy/readiness.js +23 -3
  39. package/dist/admin/copy/results.d.ts +1 -0
  40. package/dist/admin/copy/results.js +1 -0
  41. package/dist/admin/copy/testPage.d.ts +4 -0
  42. package/dist/admin/copy/testPage.js +4 -0
  43. package/dist/admin/goals/XpGoalField.d.ts +2 -0
  44. package/dist/admin/goals/XpGoalField.js +152 -0
  45. package/dist/admin/metricSource.model.d.ts +2 -0
  46. package/dist/admin/metricSource.model.js +4 -0
  47. package/dist/admin/pageCollections.d.ts +10 -0
  48. package/dist/admin/pageCollections.js +20 -0
  49. package/dist/admin/paths.d.ts +6 -0
  50. package/dist/admin/paths.js +6 -0
  51. package/dist/admin/placementStatus.model.d.ts +2 -0
  52. package/dist/admin/placementStatus.model.js +19 -0
  53. package/dist/admin/placements/placementLabels.d.ts +11 -0
  54. package/dist/admin/placements/placementLabels.js +21 -0
  55. package/dist/admin/placements/placementLabels.model.d.ts +13 -0
  56. package/dist/admin/placements/placementLabels.model.js +12 -0
  57. package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
  58. package/dist/admin/placements/usePlacementLabels.js +102 -0
  59. package/dist/admin/results/DataQualityPanel.js +4 -1
  60. package/dist/admin/results/results.model.d.ts +3 -0
  61. package/dist/admin/results/results.model.js +8 -4
  62. package/dist/admin/surface/TestsHome.js +37 -6
  63. package/dist/admin/surface/TestsSurface.js +5 -1
  64. package/dist/admin/surface/contracts.d.ts +6 -5
  65. package/dist/admin/surface/goals/GoalForm.js +67 -34
  66. package/dist/admin/surface/goals/GoalsList.js +3 -3
  67. package/dist/admin/surface/goals/data.js +4 -1
  68. package/dist/admin/surface/goals/goals.model.d.ts +1 -2
  69. package/dist/admin/surface/goals/goals.model.js +1 -3
  70. package/dist/admin/surface/home.model.d.ts +1 -1
  71. package/dist/admin/surface/home.model.js +8 -4
  72. package/dist/admin/surface/newTest/NewTest.js +54 -13
  73. package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
  74. package/dist/admin/surface/newTest/PageWizard.js +209 -0
  75. package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
  76. package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
  77. package/dist/admin/surface/pagePicker/PagePicker.d.ts +10 -0
  78. package/dist/admin/surface/pagePicker/PagePicker.js +172 -0
  79. package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +25 -0
  80. package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
  81. package/dist/admin/surface/readiness/ActionDialogs.js +99 -64
  82. package/dist/admin/surface/readiness/Banner.js +30 -6
  83. package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
  84. package/dist/admin/surface/readiness/readiness.model.js +91 -9
  85. package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
  86. package/dist/admin/surface/results/ReleasePages.js +3 -7
  87. package/dist/admin/surface/results/ResultsBody.js +21 -2
  88. package/dist/admin/surface/results/ResultsHeader.js +2 -5
  89. package/dist/admin/surface/results/useResults.d.ts +10 -1
  90. package/dist/admin/surface/test/AdvancedSection.js +111 -104
  91. package/dist/admin/surface/test/GoalSection.js +90 -7
  92. package/dist/admin/surface/test/LengthSection.js +1 -1
  93. package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
  94. package/dist/admin/surface/test/PageHoldout.js +67 -0
  95. package/dist/admin/surface/test/TestPage.js +2 -1
  96. package/dist/admin/surface/test/VersionsSection.js +12 -5
  97. package/dist/admin/surface/test/fields.d.ts +13 -4
  98. package/dist/admin/surface/test/fields.js +46 -14
  99. package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
  100. package/dist/admin/surface/test/jumpToSection.js +4 -3
  101. package/dist/admin/surface/test/pageParts.d.ts +4 -1
  102. package/dist/admin/surface/test/pageParts.js +79 -2
  103. package/dist/admin/surface/test/testPage.css +8 -1
  104. package/dist/admin/surface/testDocument.model.d.ts +1 -0
  105. package/dist/admin/surface/testDocument.model.js +2 -1
  106. package/dist/admin/surface/useTestDocument.d.ts +11 -2
  107. package/dist/admin/surface/useTestDocument.js +53 -15
  108. package/dist/admin/useApi.d.ts +11 -2
  109. package/dist/admin/useApi.js +3 -5
  110. package/dist/admin/views/ExperimentsRootView.js +58 -16
  111. package/dist/blocks/bindings.d.ts +3 -0
  112. package/dist/blocks/bindings.js +34 -0
  113. package/dist/blocks/createExperimentBlock.d.ts +6 -0
  114. package/dist/blocks/createExperimentBlock.js +77 -0
  115. package/dist/blocks/formState.d.ts +31 -0
  116. package/dist/blocks/formState.js +146 -0
  117. package/dist/blocks/model.d.ts +41 -0
  118. package/dist/blocks/model.js +34 -0
  119. package/dist/client/ExperimentGroup.d.ts +6 -0
  120. package/dist/client/ExperimentGroup.js +9 -0
  121. package/dist/client/ExperimentsProvider.js +20 -4
  122. package/dist/client/clickGoals.d.ts +4 -0
  123. package/dist/client/clickGoals.js +43 -0
  124. package/dist/collections/experiments/config.js +12 -2
  125. package/dist/collections/experiments/deleteGuard.d.ts +2 -1
  126. package/dist/collections/experiments/deleteGuard.js +12 -1
  127. package/dist/collections/experiments/editability.js +4 -2
  128. package/dist/collections/experiments/pickSlug.d.ts +2 -0
  129. package/dist/collections/experiments/pickSlug.js +8 -0
  130. package/dist/collections/labels.d.ts +3 -0
  131. package/dist/collections/labels.js +20 -0
  132. package/dist/collections/metrics.js +12 -2
  133. package/dist/collections/phases.js +1 -0
  134. package/dist/collections/serialize.js +7 -0
  135. package/dist/db/clickGoals.d.ts +9 -0
  136. package/dist/db/clickGoals.js +22 -0
  137. package/dist/db/delete.d.ts +1 -0
  138. package/dist/db/delete.js +1 -0
  139. package/dist/db/locks.d.ts +8 -0
  140. package/dist/db/locks.js +27 -4
  141. package/dist/db/names.d.ts +4 -0
  142. package/dist/db/names.js +4 -0
  143. package/dist/db/pageBindings.d.ts +4 -0
  144. package/dist/db/pageBindings.js +17 -0
  145. package/dist/db/pageBusy.d.ts +2 -0
  146. package/dist/db/pageBusy.js +10 -0
  147. package/dist/db/pageContent.d.ts +10 -0
  148. package/dist/db/pageContent.js +79 -0
  149. package/dist/db/placements.d.ts +33 -0
  150. package/dist/db/placements.js +54 -0
  151. package/dist/db/slugs.d.ts +8 -0
  152. package/dist/db/slugs.js +40 -0
  153. package/dist/db/snapshots.d.ts +6 -0
  154. package/dist/db/snapshots.js +8 -5
  155. package/dist/endpoints/clickKeys.d.ts +5 -0
  156. package/dist/endpoints/clickKeys.js +17 -0
  157. package/dist/endpoints/events.js +4 -1
  158. package/dist/endpoints/index.js +28 -0
  159. package/dist/endpoints/pages.d.ts +5 -0
  160. package/dist/endpoints/pages.js +120 -0
  161. package/dist/endpoints/placementStatus.d.ts +6 -0
  162. package/dist/endpoints/placementStatus.js +238 -0
  163. package/dist/endpoints/readBody.d.ts +2 -0
  164. package/dist/endpoints/readBody.js +24 -0
  165. package/dist/endpoints/results.js +5 -1
  166. package/dist/endpoints/summary.js +11 -4
  167. package/dist/endpoints/wrapBlock.d.ts +6 -0
  168. package/dist/endpoints/wrapBlock.js +76 -0
  169. package/dist/errors.d.ts +3 -3
  170. package/dist/errors.js +4 -4
  171. package/dist/events/rows.d.ts +1 -1
  172. package/dist/events/rows.js +5 -3
  173. package/dist/exports/adminClient.d.ts +5 -0
  174. package/dist/exports/adminClient.js +5 -0
  175. package/dist/exports/blocks.d.ts +2 -0
  176. package/dist/exports/blocks.js +1 -0
  177. package/dist/exports/client.d.ts +1 -0
  178. package/dist/exports/client.js +1 -0
  179. package/dist/exports/index.d.ts +6 -1
  180. package/dist/exports/index.js +3 -0
  181. package/dist/exports/puck.d.ts +3 -1
  182. package/dist/exports/puck.js +2 -0
  183. package/dist/exports/puckEditor.d.ts +3 -0
  184. package/dist/exports/puckEditor.js +2 -0
  185. package/dist/exports/rsc.d.ts +3 -0
  186. package/dist/exports/rsc.js +2 -0
  187. package/dist/fields/xpGoal.d.ts +3 -0
  188. package/dist/fields/xpGoal.js +28 -0
  189. package/dist/globals/state.js +10 -0
  190. package/dist/goals/cache.d.ts +3 -0
  191. package/dist/goals/cache.js +35 -0
  192. package/dist/goals/fairness.d.ts +40 -0
  193. package/dist/goals/fairness.js +200 -0
  194. package/dist/goals/key.d.ts +4 -0
  195. package/dist/goals/key.js +3 -0
  196. package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
  197. package/dist/goals/picker/clickGoalPicker.model.js +45 -0
  198. package/dist/goals/picker/copy.d.ts +16 -0
  199. package/dist/goals/picker/copy.js +15 -0
  200. package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
  201. package/dist/goals/picker/useClickGoalPicker.js +220 -0
  202. package/dist/hooks/pageGuard.d.ts +5 -1
  203. package/dist/hooks/pageGuard.js +157 -95
  204. package/dist/onInit.js +29 -0
  205. package/dist/options.d.ts +2 -0
  206. package/dist/options.js +41 -1
  207. package/dist/plugin.js +41 -28
  208. package/dist/puck/copy.d.ts +1 -0
  209. package/dist/puck/copy.js +1 -0
  210. package/dist/puck/editor/editor.chrome.css +1 -0
  211. package/dist/puck/editor/editor.css +1 -0
  212. package/dist/puck/editor/editor.model.d.ts +4 -2
  213. package/dist/puck/editor/editor.model.js +6 -3
  214. package/dist/puck/editor/integration.d.ts +10 -0
  215. package/dist/puck/editor/integration.js +30 -0
  216. package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
  217. package/dist/puck/editor/xpGoalPuckEditorField.js +134 -0
  218. package/dist/puck/integration.d.ts +6 -0
  219. package/dist/puck/integration.js +21 -0
  220. package/dist/puck/xpGoalField.d.ts +3 -0
  221. package/dist/puck/xpGoalField.js +6 -0
  222. package/dist/results/health.d.ts +4 -0
  223. package/dist/results/health.js +27 -0
  224. package/dist/results/snapshot.js +15 -2
  225. package/dist/sanitizeReadinessDetail.js +41 -0
  226. package/dist/server/pageKindSeoContract.d.ts +27 -0
  227. package/dist/server/pageKindSeoContract.js +80 -0
  228. package/dist/server/recordContentChange.js +58 -8
  229. package/dist/server/resolveExperimentBlocks.d.ts +5 -0
  230. package/dist/server/resolveExperimentBlocks.js +54 -0
  231. package/dist/server/runResults.js +56 -15
  232. package/dist/server/toClientAssignments.js +2 -1
  233. package/dist/transitions/addMetric.d.ts +2 -1
  234. package/dist/transitions/addMetric.js +28 -1
  235. package/dist/transitions/checkStart.d.ts +13 -1
  236. package/dist/transitions/checkStart.js +89 -7
  237. package/dist/transitions/decide.js +24 -0
  238. package/dist/transitions/deleteExperiment.d.ts +3 -1
  239. package/dist/transitions/deleteExperiment.js +27 -1
  240. package/dist/transitions/kernel.js +4 -0
  241. package/dist/transitions/makeContent.d.ts +5 -0
  242. package/dist/transitions/makeContent.js +39 -11
  243. package/dist/transitions/pageCollection.d.ts +10 -0
  244. package/dist/transitions/pageCollection.js +20 -0
  245. package/dist/transitions/pageVersions.d.ts +10 -0
  246. package/dist/transitions/pageVersions.js +44 -0
  247. package/dist/transitions/phaseRecord.d.ts +1 -0
  248. package/dist/transitions/placementInteractions.d.ts +40 -0
  249. package/dist/transitions/placementInteractions.js +94 -0
  250. package/dist/transitions/placements.d.ts +8 -0
  251. package/dist/transitions/placements.js +148 -1
  252. package/dist/transitions/preflightData.d.ts +2 -2
  253. package/dist/transitions/preflightData.js +137 -5
  254. package/dist/transitions/resolveCaps.d.ts +1 -0
  255. package/dist/transitions/resolveCaps.js +6 -2
  256. package/dist/transitions/start.js +45 -1
  257. package/dist/types/admin.d.ts +1 -1
  258. package/dist/types/assignments.d.ts +1 -0
  259. package/dist/types/contracts.d.ts +15 -0
  260. package/dist/types/endpoints.d.ts +92 -5
  261. package/dist/types/index.d.ts +3 -0
  262. package/dist/types/puck.d.ts +7 -1
  263. package/dist/types/snapshot.d.ts +17 -4
  264. package/dist/types/surface.d.ts +13 -0
  265. package/package.json +8 -1
@@ -0,0 +1,12 @@
1
+ export const placementDetail = (detail)=>detail && typeof detail === 'object' && !Array.isArray(detail) ? detail : {};
2
+ export function placementDetailKey(value) {
3
+ const detail = placementDetail(value);
4
+ return JSON.stringify([
5
+ detail.collection,
6
+ detail.id == null ? null : String(detail.id),
7
+ detail.path
8
+ ]);
9
+ }
10
+ export const fallbackPlacementLabel = (value)=>({
11
+ title: placementDetail(value).path || 'This page'
12
+ });
@@ -0,0 +1,12 @@
1
+ import type { PreflightResult } from '../../types/endpoints.js';
2
+ import type { PlacementLabel } from './placementLabels.js';
3
+ export { placementDetailKey } from './placementLabels.js';
4
+ /** The view owns a cache, including misses, so repeated warnings never repeat reads. */
5
+ export declare function usePlacementLabels({ details, preflight, apiRoute, adminRoute, canManage, collectionTitles }: {
6
+ details: unknown[];
7
+ preflight?: PreflightResult | null;
8
+ apiRoute: string;
9
+ adminRoute: string;
10
+ canManage: boolean;
11
+ collectionTitles?: Record<string, string>;
12
+ }): Map<string, PlacementLabel>;
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { createApi } from '../api.js';
4
+ import { placementDetail, placementDetailKey, preflightPlacementLabel, resolvedPlacementLabel } from './placementLabels.js';
5
+ export { placementDetailKey } from './placementLabels.js';
6
+ /** The view owns a cache, including misses, so repeated warnings never repeat reads. */ export function usePlacementLabels({ details, preflight, apiRoute, adminRoute, canManage, collectionTitles }) {
7
+ const titlesSignature = JSON.stringify(collectionTitles);
8
+ const cache = useMemo(()=>new Map(), [
9
+ apiRoute,
10
+ adminRoute,
11
+ titlesSignature
12
+ ]);
13
+ const mounted = useRef(true);
14
+ useEffect(()=>{
15
+ mounted.current = true;
16
+ return ()=>{
17
+ mounted.current = false;
18
+ };
19
+ }, []);
20
+ const [, refresh] = useState(0);
21
+ const signature = JSON.stringify(details.map(placementDetail));
22
+ useEffect(()=>{
23
+ if (!canManage || preflight) return;
24
+ const pending = [
25
+ ...new Map(JSON.parse(signature).map((detail)=>[
26
+ placementDetailKey(detail),
27
+ detail
28
+ ])).values()
29
+ ].filter((detail)=>!cache.has(placementDetailKey(detail)));
30
+ const blocks = new Map();
31
+ const jobs = [];
32
+ const save = (value, label)=>{
33
+ cache.set(placementDetailKey(value), label);
34
+ };
35
+ for (const value of pending){
36
+ const detail = placementDetail(value);
37
+ // Reserve immediately: repeated renders do not issue duplicate requests.
38
+ save(value, resolvedPlacementLabel(value, undefined, adminRoute));
39
+ if (!detail.collection) continue;
40
+ if (detail.id != null) {
41
+ blocks.set(detail.collection, [
42
+ ...blocks.get(detail.collection) ?? [],
43
+ value
44
+ ]);
45
+ continue;
46
+ }
47
+ if (!detail.path) continue;
48
+ jobs.push(createApi(apiRoute).pages({
49
+ collection: detail.collection,
50
+ path: detail.path
51
+ }).then((result)=>{
52
+ if (result.ok) save(value, resolvedPlacementLabel(value, result.data?.docs[0], adminRoute));
53
+ }));
54
+ }
55
+ for (const [collection, values] of blocks)jobs.push((async ()=>{
56
+ try {
57
+ const ids = [
58
+ ...new Set(values.map((value)=>String(placementDetail(value).id)))
59
+ ];
60
+ const query = new URLSearchParams({
61
+ 'where[id][in]': ids.join(','),
62
+ depth: '0',
63
+ limit: String(ids.length)
64
+ });
65
+ const response = await fetch(`${apiRoute.replace(/\/$/, '')}/${encodeURIComponent(collection)}?${query}`, {
66
+ credentials: 'include'
67
+ });
68
+ if (!response.ok) return;
69
+ const data = await response.json();
70
+ const titleField = collectionTitles?.[collection];
71
+ for (const value of values){
72
+ const doc = data.docs?.find((doc)=>String(doc.id) === String(placementDetail(value).id));
73
+ if (doc) save(value, resolvedPlacementLabel(value, {
74
+ id: placementDetail(value).id,
75
+ title: titleField && typeof doc[titleField] === 'string' ? doc[titleField] : undefined
76
+ }, adminRoute));
77
+ }
78
+ } catch {}
79
+ })());
80
+ void Promise.all(jobs).then(()=>{
81
+ if (mounted.current && jobs.length) refresh((value)=>value + 1);
82
+ });
83
+ }, [
84
+ signature,
85
+ preflight,
86
+ apiRoute,
87
+ adminRoute,
88
+ canManage,
89
+ collectionTitles,
90
+ cache
91
+ ]);
92
+ return new Map(details.map((detail)=>{
93
+ const label = preflight ? preflightPlacementLabel(detail, preflight, adminRoute) : cache.get(placementDetailKey(detail)) ?? resolvedPlacementLabel(detail, undefined, adminRoute);
94
+ // Results-only readers may not be able to open the collection, so they get the name without a link.
95
+ return [
96
+ placementDetailKey(detail),
97
+ canManage ? label : {
98
+ title: label.title
99
+ }
100
+ ];
101
+ }));
102
+ }
@@ -41,7 +41,10 @@ export function DataQualityPanel({ items, problem = false }) {
41
41
  }) : null,
42
42
  /*#__PURE__*/ _jsx("p", {
43
43
  className: "xp-quality-next",
44
- children: item.nextStep
44
+ children: item.href ? /*#__PURE__*/ _jsx("a", {
45
+ href: item.href,
46
+ children: item.nextStep
47
+ }) : item.nextStep
45
48
  })
46
49
  ]
47
50
  }, item.id)) : /*#__PURE__*/ _jsx("p", {
@@ -2,6 +2,7 @@ import type { DocId, Status } from '../../types/contracts.js';
2
2
  import type { PhaseSummary, ResultsResponse } from '../../types/endpoints.js';
3
3
  import type { Snapshot } from '../../types/snapshot.js';
4
4
  import { type DisplaySnapshot } from './snapshot.js';
5
+ import type { PlacementLabel } from '../placements/placementLabels.model.js';
5
6
  type Metric = Snapshot['metrics'][number];
6
7
  type Arm = Metric['perArm'][number];
7
8
  type Decision = {
@@ -88,6 +89,7 @@ interface RoundLineInput {
88
89
  }
89
90
  export declare function powerLine({ snapshot, phaseKind, status }: RoundLineInput): string | null;
90
91
  export interface ResultsContext {
92
+ placementLabels?: Map<string, PlacementLabel>;
91
93
  status?: string;
92
94
  phase?: PhaseSummary;
93
95
  minExposuresPerArm?: number;
@@ -102,6 +104,7 @@ export interface DataQualityItem {
102
104
  message: string;
103
105
  detail?: string;
104
106
  nextStep: string;
107
+ href?: string;
105
108
  shares?: string[];
106
109
  }
107
110
  export declare function dataQuality(snapshot: DisplaySnapshot, ctx?: ResultsContext): DataQualityItem[];
@@ -1,9 +1,10 @@
1
- import { READINESS_COPY, versionPageUnavailableCopy, versionPageUncheckedCopy } from '../copy/readiness.js';
1
+ import { READINESS_COPY, placementUncheckedCopy, versionPageUnavailableCopy, versionPageUncheckedCopy } from '../copy/readiness.js';
2
2
  import { armLabel, phaseKindLabel, verdictCodeLabel } from '../labels.js';
3
3
  import { RESULTS_COPY, cappedCount, collectingCopy, conflictCopy, decidedTitle, exclusionCopy, ignoredCopy, metricUnavailable, moreDays, roundLabel, shareCopy, shippingTitle, srmP, uncappedTotal } from '../copy/results.js';
4
4
  import { formatCI, formatCount, formatMetric, formatPercent, formatProbability } from './format.js';
5
5
  import { legacyMinimum, legacyRolloutLine, legacyTooFew } from './legacy.js';
6
6
  import { isLegacySnapshot } from './snapshot.js';
7
+ import { placementDetailKey } from '../placements/placementLabels.model.js';
7
8
  export function defaultRound({ phases, decision, status }) {
8
9
  if ((status === 'decided' || status === 'archived') && decision) return decision.phase;
9
10
  return phases.reduce((latest, phase)=>latest === null || phase.phase > latest ? phase.phase : latest, null);
@@ -132,9 +133,12 @@ function smallest(values) {
132
133
  }
133
134
  export function dataQuality(snapshot, ctx = {}) {
134
135
  const items = (snapshot.health ?? []).map((item)=>({
135
- id: `${item.code}:${item.path}`,
136
- message: READINESS_COPY.needsAttention,
137
- nextStep: item.code === 'version-page-unavailable' ? versionPageUnavailableCopy(item.path, ctx.status) : versionPageUncheckedCopy(item.path, ctx.status)
136
+ id: item.code === 'placement-unchecked' ? `${item.code}:${JSON.stringify(item.detail)}` : `${item.code}:${'path' in item ? item.path : item.metricId}`,
137
+ message: item.code === 'placement-unchecked' ? placementUncheckedCopy(ctx.placementLabels?.get(placementDetailKey(item.detail))?.title ?? item.detail.path ?? 'This page') : item.code === 'goal-in-some-versions' ? `${item.name} now appears in some versions but not all, so this result is no longer a fair comparison. Add it to every version, or remove it.` : READINESS_COPY.needsAttention,
138
+ ...item.code === 'placement-unchecked' ? {
139
+ href: ctx.placementLabels?.get(placementDetailKey(item.detail))?.href
140
+ } : {},
141
+ nextStep: item.code === 'placement-unchecked' ? `Open ${ctx.placementLabels?.get(placementDetailKey(item.detail))?.title ?? item.detail.path ?? 'the page'}` : item.code === 'goal-in-some-versions' ? 'Edit the versions' : item.code === 'version-page-unavailable' ? versionPageUnavailableCopy(item.path, ctx.status) : versionPageUncheckedCopy(item.path, ctx.status)
138
142
  }));
139
143
  // Test snapshots store null by design; only rollout snapshots record the shipped winner.
140
144
  if (!isLegacySnapshot(snapshot) && snapshot.phaseKind === 'rollout' && snapshot.winnerArm === null) items.push({
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useState } from 'react';
4
- import { Link } from '@payloadcms/ui';
5
4
  import { formatAdminURL } from 'payload/shared';
6
5
  import { fetchAllDocs } from '../../lib/fetchAllDocs.js';
7
6
  import { createApi } from '../api.js';
@@ -10,6 +9,7 @@ import { Banner, EmptyState, StatusChip } from '../../ui/Feedback.js';
10
9
  import { Button } from '../../ui/Button.js';
11
10
  import { SearchInput } from '../../ui/Inputs.js';
12
11
  import { SurfaceShell } from './SurfaceShell.js';
12
+ import { sanitizeReadinessDetail } from '../../sanitizeReadinessDetail.js';
13
13
  import { groupTests, homeCard } from './home.model.js';
14
14
  const HOME_FIELDS = [
15
15
  'name',
@@ -52,11 +52,40 @@ export function TestsHome({ adminRoute, apiRoute, canManage = false, crumbs, fla
52
52
  const docs = response.docs.sort((a, b)=>(Date.parse(b.updatedAt ?? '') || 0) - (Date.parse(a.updatedAt ?? '') || 0));
53
53
  if (!active) return;
54
54
  const summary = await api.summary(docs.map((doc)=>doc.id));
55
+ const summaries = summary.ok ? summary.data ?? {} : {};
56
+ const ids = [
57
+ ...new Set(docs.flatMap((doc)=>{
58
+ const item = summaries[doc.id];
59
+ if (!canManage || ![
60
+ 'draft',
61
+ 'stopped'
62
+ ].includes(doc.status) || item?.ready === true || item?.next?.code !== 'goal-in-some-versions') return [];
63
+ const detail = sanitizeReadinessDetail(item.next.code, item.next.detail);
64
+ return detail?.map((row)=>String(row.metricId)) ?? [];
65
+ }))
66
+ ];
67
+ const goalNames = {};
68
+ if (ids.length) {
69
+ try {
70
+ const query = new URLSearchParams({
71
+ 'where[id][in]': ids.join(','),
72
+ depth: '0',
73
+ limit: String(ids.length)
74
+ });
75
+ const response = await fetch(`${apiRoute.replace(/\/$/, '')}/xp-metrics?${query}`, {
76
+ credentials: 'include',
77
+ signal: controller.signal
78
+ });
79
+ const body = response.ok ? await response.json() : undefined;
80
+ for (const goal of body?.docs ?? [])if (typeof goal.name === 'string' && goal.name.trim() && ids.includes(String(goal.id))) goalNames[String(goal.id)] = goal.name;
81
+ } catch {}
82
+ }
55
83
  if (active) setState({
56
84
  loading: false,
57
85
  error: false,
58
86
  tests: docs,
59
- summaries: summary.ok ? summary.data ?? {} : {},
87
+ goalNames,
88
+ summaries,
60
89
  summaryError: !summary.ok
61
90
  });
62
91
  } catch {
@@ -74,7 +103,8 @@ export function TestsHome({ adminRoute, apiRoute, canManage = false, crumbs, fla
74
103
  }, [
75
104
  api,
76
105
  apiRoute,
77
- attempt
106
+ attempt,
107
+ canManage
78
108
  ]);
79
109
  const retry = /*#__PURE__*/ _jsx(Button, {
80
110
  size: "sm",
@@ -153,6 +183,7 @@ export function TestsHome({ adminRoute, apiRoute, canManage = false, crumbs, fla
153
183
  }) : /*#__PURE__*/ _jsx(TestGroups, {
154
184
  tests: state.tests,
155
185
  summaries: state.summaries,
186
+ goalNames: state.goalNames,
156
187
  adminRoute: adminRoute,
157
188
  flagPoints: flagPoints,
158
189
  search: search,
@@ -162,7 +193,7 @@ export function TestsHome({ adminRoute, apiRoute, canManage = false, crumbs, fla
162
193
  })
163
194
  });
164
195
  }
165
- function TestGroups({ tests, summaries, adminRoute, flagPoints, search, clear }) {
196
+ function TestGroups({ tests, summaries, goalNames, adminRoute, flagPoints, search, clear }) {
166
197
  const groups = groupTests(tests, summaries, search);
167
198
  if (!groups.some((group)=>group.tests.length)) return /*#__PURE__*/ _jsxs("div", {
168
199
  className: "xp-tests-no-matches",
@@ -184,7 +215,7 @@ function TestGroups({ tests, summaries, adminRoute, flagPoints, search, clear })
184
215
  const rows = /*#__PURE__*/ _jsx("div", {
185
216
  className: "xp-tests-list",
186
217
  children: group.tests.map((test)=>{
187
- const card = homeCard(test, summaries[test.id], adminRoute, flagPoints);
218
+ const card = homeCard(test, summaries[test.id], adminRoute, flagPoints, goalNames);
188
219
  return /*#__PURE__*/ _jsxs("article", {
189
220
  className: "xp-tests-row",
190
221
  children: [
@@ -194,7 +225,7 @@ function TestGroups({ tests, summaries, adminRoute, flagPoints, search, clear })
194
225
  /*#__PURE__*/ _jsxs("div", {
195
226
  className: "xp-tests-row-name",
196
227
  children: [
197
- /*#__PURE__*/ _jsx(Link, {
228
+ /*#__PURE__*/ _jsx("a", {
198
229
  href: card.href,
199
230
  children: test.name
200
231
  }),
@@ -28,7 +28,11 @@ function SurfacePage(props) {
28
28
  flagPoints: props.flagPoints,
29
29
  attributeKeys: props.attributeKeys,
30
30
  metricSources: props.metricSources,
31
- descriptions: props.admin?.descriptions
31
+ admin: props.admin,
32
+ pageCollections: props.pageCollections,
33
+ collectionTitles: props.collectionTitles,
34
+ userId: props.userId,
35
+ userCollection: props.userCollection
32
36
  };
33
37
  const crumbs = [
34
38
  {
@@ -1,4 +1,5 @@
1
1
  import type { ComponentType } from 'react';
2
+ import type { TestsSurfaceProps } from '../../types/surface.js';
2
3
  import type { SerializedAttributeKeys, SerializedFlagPoint, SerializedMetricSource } from '../../types/admin.js';
3
4
  import type { useTestDocument } from './useTestDocument.js';
4
5
  /** Sections of the test page, in readiness order (surface spec §6.2, §8). */
@@ -10,17 +11,17 @@ export type TestDocApi = ReturnType<typeof useTestDocument>;
10
11
  export interface SurfaceProps {
11
12
  adminRoute: string;
12
13
  apiRoute: string;
14
+ collectionTitles?: Record<string, string>;
15
+ userId?: string | number;
16
+ userCollection?: string;
13
17
  canManage: boolean;
14
18
  canReadResults: boolean;
15
19
  isAdmin: boolean;
16
20
  flagPoints: SerializedFlagPoint[];
17
21
  attributeKeys: SerializedAttributeKeys;
18
22
  metricSources: SerializedMetricSource[];
19
- descriptions?: {
20
- block?: string;
21
- document?: string;
22
- setting?: string;
23
- };
23
+ pageCollections?: TestsSurfaceProps['pageCollections'];
24
+ admin?: TestsSurfaceProps['admin'];
24
25
  }
25
26
  export interface SectionProps {
26
27
  test: TestDocApi;
@@ -1,11 +1,13 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState } from 'react';
4
4
  import { useRouter } from 'next/navigation.js';
5
5
  import { formatAdminURL } from 'payload/shared';
6
6
  import { GOALS_COPY as C, goalDeleteLocked, goalInUse } from '../../copy/goals.js';
7
- import { metricSourceHint, metricSourceOptions } from '../../metricSource.model.js';
7
+ import { metricSourceHint, metricSourceOptions, sourceFor } from '../../metricSource.model.js';
8
8
  import { validateCappingType, validateCappingValue } from '../../../collections/metricValidation.js';
9
+ import { CLICK_KEY } from '../../../goals/key.js';
10
+ import { clickGoalSlug } from '../../../goals/picker/clickGoalPicker.model.js';
9
11
  import { goalSlug, goalUsage } from './goals.model.js';
10
12
  import { GoalResponseError, goalPages, goalResponse } from './data.js';
11
13
  import { SURFACE_COPY } from '../../copy/surface.js';
@@ -55,7 +57,7 @@ export function GoalForm({ surface, id, returnToTest }) {
55
57
  const advanced = useRef(null);
56
58
  const [confirmDelete, setConfirmDelete] = useState(false);
57
59
  useEffect(()=>{
58
- if (advanced.current && (fieldErrors.capType || fieldErrors.capValue || fieldErrors.warningPercent)) advanced.current.open = true;
60
+ if (advanced.current && (fieldErrors.slug || fieldErrors.capType || fieldErrors.capValue || fieldErrors.warningPercent)) advanced.current.open = true;
59
61
  }, [
60
62
  fieldErrors
61
63
  ]);
@@ -84,7 +86,7 @@ export function GoalForm({ surface, id, returnToTest }) {
84
86
  ]);
85
87
  if (loaded.id == null) throw new Error(C.missing);
86
88
  if (controller.signal.aborted) return;
87
- const defaultCap = surface.metricSources.some((source)=>source.id === loaded.source && source.kind === 'revenue') && loaded.capping?.type == null && loaded.capping?.value == null;
89
+ const defaultCap = sourceFor(loaded.source, surface.metricSources)?.kind === 'revenue' && loaded.capping?.type == null && loaded.capping?.value == null;
88
90
  setGoal(loaded);
89
91
  setUsedBy(goalUsage(tests).get(loaded.id) ?? 0);
90
92
  setDraft({
@@ -107,7 +109,8 @@ export function GoalForm({ surface, id, returnToTest }) {
107
109
  id,
108
110
  attempt
109
111
  ]);
110
- const source = surface.metricSources.find((source)=>source.id === draft.source);
112
+ const source = sourceFor(draft.source, surface.metricSources);
113
+ const click = draft.source.startsWith('click:');
111
114
  const hint = metricSourceHint(source);
112
115
  const options = metricSourceOptions(surface.metricSources);
113
116
  const set = (key, value)=>setDraft((current)=>({
@@ -116,11 +119,14 @@ export function GoalForm({ surface, id, returnToTest }) {
116
119
  }));
117
120
  async function submit() {
118
121
  if (inFlight.current || !ready || !surface.canManage) return;
119
- const capValue = draft.capValue.trim() === '' ? null : Number(draft.capValue);
120
- const typeError = validateCappingType(draft.capType || null, capValue, source?.kind);
121
- const valueError = validateCappingValue(capValue, draft.capType || null);
122
+ const capValue = click || draft.capValue.trim() === '' ? null : Number(draft.capValue);
123
+ const typeError = validateCappingType(click ? null : draft.capType || null, capValue, source?.kind);
124
+ const valueError = validateCappingValue(capValue, click ? null : draft.capType || null);
122
125
  const warningPercent = draft.warningPercent.trim() === '' ? 5 : Number(draft.warningPercent);
123
126
  const errors = {
127
+ ...click && !CLICK_KEY.test(draft.slug) ? {
128
+ slug: C.slugRule
129
+ } : {},
124
130
  ...typeError !== true ? {
125
131
  capType: typeError
126
132
  } : {},
@@ -141,12 +147,12 @@ export function GoalForm({ surface, id, returnToTest }) {
141
147
  const body = {
142
148
  name: draft.name,
143
149
  slug: usedBy && goal ? goal.slug : draft.slug,
144
- source: usedBy && goal ? goal.source : draft.source,
145
- direction: draft.direction,
150
+ source: usedBy && goal ? goal.source : click ? `click:${draft.slug}` : draft.source,
151
+ direction: click ? 'increase' : draft.direction,
146
152
  attributionWindowHours: draft.attributionWindowHours,
147
153
  minSampleSize: draft.minSampleSize,
148
154
  capping: {
149
- type: draft.capType || null,
155
+ type: click ? null : draft.capType || null,
150
156
  value: capValue
151
157
  },
152
158
  guardrailThreshold: warningPercent / 100
@@ -170,11 +176,11 @@ export function GoalForm({ surface, id, returnToTest }) {
170
176
  })}`
171
177
  }) : listUrl);
172
178
  } catch (cause) {
173
- setError(cause instanceof Error ? cause.message : C.saveError);
179
+ setError(cause instanceof GoalResponseError ? cause.message : C.saveError);
174
180
  if (cause instanceof GoalResponseError) setFieldErrors(Object.fromEntries(Object.entries(cause.fields).flatMap(([path, message])=>fieldNames[path] ? [
175
181
  [
176
182
  fieldNames[path],
177
- message
183
+ path === 'slug' ? C.duplicateSlug : message
178
184
  ]
179
185
  ] : [])));
180
186
  inFlight.current = false;
@@ -272,41 +278,54 @@ export function GoalForm({ surface, id, returnToTest }) {
272
278
  ...current,
273
279
  name,
274
280
  ...!slugEdited && !usedBy ? {
275
- slug: goalSlug(name)
281
+ slug: click ? clickGoalSlug(name) : goalSlug(name)
276
282
  } : {}
277
283
  }));
284
+ if (click && !slugEdited && !usedBy) setFieldErrors((current)=>({
285
+ ...current,
286
+ slug: clickGoalSlug(name) ? undefined : C.slugRule
287
+ }));
278
288
  }
279
289
  }),
280
- /*#__PURE__*/ _jsx(TextInput, {
281
- id: "xp-goal-slug",
282
- label: C.slug,
283
- required: true,
284
- value: draft.slug,
285
- readOnly: usedBy > 0,
286
- help: usedBy ? C.slugLocked : C.slugHelp,
287
- error: fieldErrors.slug,
288
- onChange: (event)=>{
289
- setSlugEdited(true);
290
- set('slug', event.currentTarget.value);
291
- }
292
- }),
290
+ !click ? /*#__PURE__*/ _jsx(_Fragment, {
291
+ children: /*#__PURE__*/ _jsx(TextInput, {
292
+ id: "xp-goal-slug",
293
+ label: C.slug,
294
+ required: true,
295
+ value: draft.slug,
296
+ readOnly: usedBy > 0,
297
+ help: usedBy ? C.slugLocked : C.slugHelp,
298
+ error: fieldErrors.slug,
299
+ onChange: (event)=>{
300
+ setSlugEdited(true);
301
+ set('slug', event.currentTarget.value);
302
+ }
303
+ })
304
+ }) : null,
293
305
  /*#__PURE__*/ _jsxs(Select, {
294
306
  id: "xp-goal-source",
295
307
  label: C.source,
296
308
  required: true,
297
- value: draft.source,
309
+ value: click ? 'click:' : draft.source,
298
310
  disabled: usedBy > 0,
299
311
  help: usedBy ? C.sourceLocked : hint || undefined,
300
312
  error: fieldErrors.source,
301
313
  onChange: (event)=>{
302
314
  const sourceId = event.currentTarget.value;
303
- const revenue = surface.metricSources.find((source)=>source.id === sourceId)?.kind === 'revenue';
315
+ const revenue = sourceFor(sourceId, surface.metricSources)?.kind === 'revenue';
304
316
  setDraft((current)=>({
305
317
  ...current,
306
318
  source: sourceId,
319
+ ...!slugEdited && !usedBy ? {
320
+ slug: sourceId === 'click:' ? clickGoalSlug(current.name) : goalSlug(current.name)
321
+ } : {},
307
322
  capType: revenue ? 'percentile' : '',
308
323
  capValue: revenue ? '0.99' : ''
309
324
  }));
325
+ if (sourceId === 'click:' && !slugEdited && !usedBy) setFieldErrors((current)=>({
326
+ ...current,
327
+ slug: clickGoalSlug(draft.name) ? undefined : C.slugRule
328
+ }));
310
329
  },
311
330
  children: [
312
331
  /*#__PURE__*/ _jsx("option", {
@@ -315,15 +334,15 @@ export function GoalForm({ surface, id, returnToTest }) {
315
334
  }),
316
335
  options.map((option)=>/*#__PURE__*/ _jsx("option", {
317
336
  value: option.value,
318
- children: option.label
337
+ children: option.value === 'click:' ? sourceFor(option.value, surface.metricSources)?.label : option.label
319
338
  }, option.value)),
320
- draft.source && !options.some((option)=>option.value === draft.source) ? /*#__PURE__*/ _jsx("option", {
339
+ !click && draft.source && !options.some((option)=>option.value === draft.source) ? /*#__PURE__*/ _jsx("option", {
321
340
  value: draft.source,
322
341
  children: draft.source
323
342
  }) : null
324
343
  ]
325
344
  }),
326
- /*#__PURE__*/ _jsxs(Select, {
345
+ !click ? /*#__PURE__*/ _jsxs(Select, {
327
346
  id: "xp-goal-direction",
328
347
  label: C.direction,
329
348
  value: draft.direction,
@@ -339,7 +358,7 @@ export function GoalForm({ surface, id, returnToTest }) {
339
358
  children: C.decrease
340
359
  })
341
360
  ]
342
- }),
361
+ }) : null,
343
362
  /*#__PURE__*/ _jsx(NumberInput, {
344
363
  id: "xp-goal-window",
345
364
  label: C.window,
@@ -370,7 +389,21 @@ export function GoalForm({ surface, id, returnToTest }) {
370
389
  /*#__PURE__*/ _jsxs("div", {
371
390
  className: "xp-goal-advanced-fields",
372
391
  children: [
373
- /*#__PURE__*/ _jsxs("fieldset", {
392
+ click ? /*#__PURE__*/ _jsx(_Fragment, {
393
+ children: /*#__PURE__*/ _jsx(TextInput, {
394
+ id: "xp-goal-slug",
395
+ label: C.slug,
396
+ required: true,
397
+ value: draft.slug,
398
+ readOnly: usedBy > 0,
399
+ help: usedBy ? C.slugLocked : C.slugHelp,
400
+ error: fieldErrors.slug,
401
+ onChange: (event)=>{
402
+ setSlugEdited(true);
403
+ set('slug', event.currentTarget.value);
404
+ }
405
+ })
406
+ }) : /*#__PURE__*/ _jsxs("fieldset", {
374
407
  children: [
375
408
  /*#__PURE__*/ _jsx("legend", {
376
409
  children: C.capping
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { sourceFor } from '../../metricSource.model.js';
3
4
  import { useEffect, useState } from 'react';
4
- import { Link } from '@payloadcms/ui';
5
5
  import { formatAdminURL } from 'payload/shared';
6
6
  import { Banner, EmptyState } from '../../../ui/Feedback.js';
7
7
  import { Button } from '../../../ui/Button.js';
@@ -111,7 +111,7 @@ export function GoalsList({ surface, returnToTest }) {
111
111
  className: "xp-tests-list",
112
112
  children: goals.map((goal)=>{
113
113
  const count = usage.get(goal.id) ?? 0;
114
- const kind = surface.metricSources.find((source)=>source.id === goal.source)?.kind;
114
+ const kind = sourceFor(goal.source, surface.metricSources)?.kind;
115
115
  const label = kind && Object.hasOwn(METRIC_KIND_LABELS, kind) ? METRIC_KIND_LABELS[kind] : undefined;
116
116
  const href = url(`/experiments/goals/${encodeURIComponent(String(goal.id))}${returnQuery}`);
117
117
  return /*#__PURE__*/ _jsxs("article", {
@@ -123,7 +123,7 @@ export function GoalsList({ surface, returnToTest }) {
123
123
  /*#__PURE__*/ _jsxs("div", {
124
124
  className: "xp-tests-row-name",
125
125
  children: [
126
- /*#__PURE__*/ _jsx(Link, {
126
+ /*#__PURE__*/ _jsx("a", {
127
127
  href: href,
128
128
  children: goal.name
129
129
  }),
@@ -17,7 +17,10 @@ export class GoalResponseError extends Error {
17
17
  function goalRefusal(body, status, fallback, options) {
18
18
  const fields = payloadFieldErrors(body, '_section', fallback);
19
19
  const first = body?.errors?.[0];
20
- throw new GoalResponseError(refusalMessage(first?.code, status, Object.values(fields).join(' ') || fallback, options), fields);
20
+ throw new GoalResponseError(refusalMessage(first?.code, status, undefined, {
21
+ ...options,
22
+ fallback
23
+ }), fields);
21
24
  }
22
25
  export async function goalPages(base, collection, signal) {
23
26
  const result = await fetchAllDocs(`${base}/${collection}?depth=0`, {
@@ -26,5 +26,4 @@ export interface GoalUsageTest {
26
26
  }
27
27
  /** Count each experiment once per goal, retaining the API's id types. */
28
28
  export declare function goalUsage(tests: readonly GoalUsageTest[]): Map<DocId, number>;
29
- export declare function goalSlug(name: string): string;
30
- export {};
29
+ export { goalSlug } from '../../../goals/picker/clickGoalPicker.model.js';
@@ -21,6 +21,4 @@
21
21
  tests.size
22
22
  ]));
23
23
  }
24
- export function goalSlug(name) {
25
- return name.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
26
- }
24
+ export { goalSlug } from '../../../goals/picker/clickGoalPicker.model.js';
@@ -26,7 +26,7 @@ export declare function groupTests(tests: readonly HomeTest[], summaries: Summar
26
26
  label: "Needs attention" | "Running" | "Decided" | "Draft" | "Archived" | "Ready to start";
27
27
  tests: HomeTest[];
28
28
  }[];
29
- export declare function homeCard(test: HomeTest, summary: Summary, adminRoute: string, flagPoints?: ReadinessContext['flagPoints']): {
29
+ export declare function homeCard(test: HomeTest, summary: Summary, adminRoute: string, flagPoints?: ReadinessContext['flagPoints'], goalNames?: Readonly<Record<string, string>>): {
30
30
  kind: "Whole page" | "Page block" | "Setting";
31
31
  pill: {
32
32
  label: string;