@delmaredigital/payload-experiments 0.4.0-rc.1 → 0.4.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 (88) hide show
  1. package/README.md +10 -8
  2. package/dist/admin/TestsDashboardCards.d.ts +1 -0
  3. package/dist/admin/TestsDashboardCards.js +4 -8
  4. package/dist/admin/TestsNavLink.d.ts +1 -0
  5. package/dist/admin/TestsNavLink.js +4 -10
  6. package/dist/admin/api.d.ts +7 -0
  7. package/dist/admin/api.js +27 -0
  8. package/dist/admin/blocks/BlockPanel.js +65 -19
  9. package/dist/admin/blocks/formApi.d.ts +3 -0
  10. package/dist/admin/blocks/formApi.js +9 -3
  11. package/dist/admin/blocks/statusCache.d.ts +2 -0
  12. package/dist/admin/blocks/statusCache.js +11 -2
  13. package/dist/admin/copy/blocks.d.ts +16 -0
  14. package/dist/admin/copy/blocks.js +27 -1
  15. package/dist/admin/copy/common.d.ts +1 -0
  16. package/dist/admin/copy/common.js +1 -1
  17. package/dist/admin/copy/pages.d.ts +2 -4
  18. package/dist/admin/copy/pages.js +3 -5
  19. package/dist/admin/copy/readiness.d.ts +1 -0
  20. package/dist/admin/copy/readiness.js +1 -0
  21. package/dist/admin/goals/XpGoalField.d.ts +2 -0
  22. package/dist/admin/goals/XpGoalField.js +51 -35
  23. package/dist/admin/goals/xpGoalField.css +48 -0
  24. package/dist/admin/paths.d.ts +3 -0
  25. package/dist/admin/paths.js +3 -0
  26. package/dist/admin/surface/TestsHome.js +2 -4
  27. package/dist/admin/surface/TestsSurface.js +2 -5
  28. package/dist/admin/surface/goals/GoalForm.js +4 -10
  29. package/dist/admin/surface/goals/GoalsList.js +2 -1
  30. package/dist/admin/surface/home.model.d.ts +2 -2
  31. package/dist/admin/surface/newTest/NewTest.js +2 -4
  32. package/dist/admin/surface/newTest/PageWizard.js +7 -2
  33. package/dist/admin/surface/pagePicker/CopyRoute.d.ts +1 -1
  34. package/dist/admin/surface/pagePicker/{PagePicker.d.ts → PageCombobox.d.ts} +3 -3
  35. package/dist/admin/surface/pagePicker/PageCombobox.js +395 -0
  36. package/dist/admin/surface/pagePicker/pagePicker.css +10 -0
  37. package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +2 -0
  38. package/dist/admin/surface/readiness/readiness.model.js +1 -0
  39. package/dist/admin/surface/results/ResultsHeader.js +3 -8
  40. package/dist/admin/surface/results/useResults.d.ts +10 -0
  41. package/dist/admin/surface/test/GoalSection.js +4 -7
  42. package/dist/admin/surface/test/TestPage.js +3 -8
  43. package/dist/admin/surface/test/pageParts.js +10 -39
  44. package/dist/admin/surface/test/testPage.css +0 -5
  45. package/dist/admin/surface/useTestDocument.d.ts +4 -0
  46. package/dist/admin/surfaceUrls.d.ts +7 -0
  47. package/dist/admin/surfaceUrls.js +19 -0
  48. package/dist/admin/theme.css +2 -0
  49. package/dist/admin/useApi.d.ts +10 -0
  50. package/dist/admin/views/CollectionListRedirect.d.ts +1 -0
  51. package/dist/admin/views/CollectionListRedirect.js +1 -1
  52. package/dist/admin/views/ExperimentsRootView.js +18 -165
  53. package/dist/admin/views/GoalEditRedirect.d.ts +4 -0
  54. package/dist/admin/views/GoalEditRedirect.js +14 -0
  55. package/dist/admin/views/GoalsCollectionListView.d.ts +2 -0
  56. package/dist/admin/views/GoalsCollectionListView.js +4 -0
  57. package/dist/admin/views/TestsCollectionListView.d.ts +2 -0
  58. package/dist/admin/views/TestsCollectionListView.js +4 -0
  59. package/dist/admin/views/TestsNavLinkServer.d.ts +1 -0
  60. package/dist/admin/views/TestsNavLinkServer.js +1 -1
  61. package/dist/admin/views/buildSurfaceProps.d.ts +8 -0
  62. package/dist/admin/views/buildSurfaceProps.js +143 -0
  63. package/dist/admin/views/renderCollectionList.d.ts +2 -0
  64. package/dist/admin/views/renderCollectionList.js +34 -0
  65. package/dist/admin/views/renderTestsSurface.d.ts +3 -0
  66. package/dist/admin/views/renderTestsSurface.js +34 -0
  67. package/dist/collections/experiments/config.js +9 -5
  68. package/dist/collections/metrics.js +16 -4
  69. package/dist/exports/adminRsc.d.ts +3 -0
  70. package/dist/exports/adminRsc.js +3 -0
  71. package/dist/fields/xpGoal.js +2 -14
  72. package/dist/goals/validate.d.ts +3 -0
  73. package/dist/goals/validate.js +14 -0
  74. package/dist/hooks/puckGoals.d.ts +3 -0
  75. package/dist/hooks/puckGoals.js +38 -0
  76. package/dist/plugin.js +12 -18
  77. package/dist/puck/editor/editor.chrome.css +13 -0
  78. package/dist/puck/editor/editor.css +105 -0
  79. package/dist/puck/editor/xpGoalPuckEditorField.js +28 -20
  80. package/dist/sanitizeReadinessDetail.js +2 -0
  81. package/dist/transitions/checkStart.d.ts +2 -1
  82. package/dist/transitions/checkStart.js +3 -1
  83. package/dist/transitions/decide.js +1 -0
  84. package/dist/transitions/start.js +2 -1
  85. package/dist/types/endpoints.d.ts +1 -1
  86. package/dist/ui/ui.css +4 -0
  87. package/package.json +1 -1
  88. package/dist/admin/surface/pagePicker/PagePicker.js +0 -172
@@ -0,0 +1,7 @@
1
+ type SurfaceQuery = Record<string, string | string[] | undefined>;
2
+ export declare function surfaceQuery(query?: SurfaceQuery): string;
3
+ export declare const surfaceUrls: {
4
+ home: (adminRoute: string, query?: SurfaceQuery) => string;
5
+ goals: (adminRoute: string, query?: SurfaceQuery) => string;
6
+ };
7
+ export {};
@@ -0,0 +1,19 @@
1
+ import { formatAdminURL } from 'payload/shared';
2
+ export function surfaceQuery(query = {}) {
3
+ const params = new URLSearchParams();
4
+ for (const [key, value] of Object.entries(query)){
5
+ if (Array.isArray(value)) value.forEach((item)=>params.append(key, item));
6
+ else if (value !== undefined) params.set(key, value);
7
+ }
8
+ return params.size ? `?${params}` : '';
9
+ }
10
+ export const surfaceUrls = {
11
+ home: (adminRoute, query)=>formatAdminURL({
12
+ adminRoute,
13
+ path: `/collections/xp-experiments${surfaceQuery(query)}`
14
+ }),
15
+ goals: (adminRoute, query)=>formatAdminURL({
16
+ adminRoute,
17
+ path: `/collections/xp-metrics${surfaceQuery(query)}`
18
+ })
19
+ };
@@ -63,6 +63,8 @@
63
63
  --xp-space-3: 12px;
64
64
  --xp-space-4: 16px;
65
65
  --xp-space-5: 20px;
66
+ /* Payload's gap between form fields. */
67
+ --xp-field-gap: var(--base);
66
68
  --xp-space-6: 24px;
67
69
  --xp-space-7: 28px;
68
70
  --xp-space-8: 32px;
@@ -1,4 +1,11 @@
1
1
  export declare function useApi(apiRoute: string): {
2
+ findTestIdBySlug: (slug: string) => Promise<Extract<import("./api.js").ApiResult<import("../types/contracts.js").DocId>, {
3
+ ok: false;
4
+ }> | {
5
+ ok: true;
6
+ data: import("../types/contracts.js").DocId;
7
+ }>;
8
+ deleteTest: (id: import("../types/contracts.js").DocId) => Promise<import("./api.js").ApiResult<unknown>>;
2
9
  wrapBlock: (body: import("../types/endpoints.js").WrapBlockBody) => Promise<import("./api.js").ApiResult<Record<string, unknown>>>;
3
10
  pages: (query: {
4
11
  collection: string;
@@ -10,11 +17,13 @@ export declare function useApi(apiRoute: string): {
10
17
  start: (id: import("../types/contracts.js").DocId) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").TransitionSuccess>>;
11
18
  allocate: (id: import("../types/contracts.js").DocId, body: import("../types/endpoints.js").AllocateBody, options?: {
12
19
  notFound?: string;
20
+ forbidden?: string;
13
21
  codes?: Readonly<Record<string, string>>;
14
22
  fallback?: string;
15
23
  }) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").TransitionSuccess>>;
16
24
  addMetric: (id: import("../types/contracts.js").DocId, body: import("../types/endpoints.js").AddMetricBody, options?: {
17
25
  notFound?: string;
26
+ forbidden?: string;
18
27
  codes?: Readonly<Record<string, string>>;
19
28
  fallback?: string;
20
29
  }) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").AddMetricResult>>;
@@ -34,6 +43,7 @@ export declare function useApi(apiRoute: string): {
34
43
  series?: boolean;
35
44
  } & {
36
45
  notFound?: string;
46
+ forbidden?: string;
37
47
  codes?: Readonly<Record<string, string>>;
38
48
  fallback?: string;
39
49
  }) => Promise<import("./api.js").ApiResult<import("../types/endpoints.js").ResultsResponse>>;
@@ -1,2 +1,3 @@
1
1
  import type { ListViewServerProps } from 'payload';
2
+ /** @deprecated Retained for existing import maps until 0.5.0. */
2
3
  export declare function CollectionListRedirect({ payload }: Pick<ListViewServerProps, 'payload'>): void;
@@ -1,6 +1,6 @@
1
1
  import { formatAdminURL } from 'payload/shared';
2
2
  import { redirect } from 'next/navigation.js';
3
- export function CollectionListRedirect({ payload }) {
3
+ /** @deprecated Retained for existing import maps until 0.5.0. */ export function CollectionListRedirect({ payload }) {
4
4
  redirect(formatAdminURL({
5
5
  adminRoute: payload.config.routes.admin,
6
6
  path: '/experiments'
@@ -1,16 +1,9 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { accentStyles } from '../../ui/accent.js';
3
- import { flattenTopLevelFields, formatAdminURL } from 'payload/shared';
1
+ import { formatAdminURL } from 'payload/shared';
4
2
  import { notFound, redirect } from 'next/navigation.js';
5
- import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent';
6
- import { serializeAttributeKeys, serializeFlagPoints, serializeMetricSources } from '../../collections/serialize.js';
7
- import { serializePageCollections } from '../pageCollections.js';
8
- import { collectionLabel as resolveLabel } from '../../collections/labels.js';
9
- import { documentEditor, puckCollections } from '../../options.js';
10
3
  import { xpOptionsFromReq } from '../../options/fromReq.js';
11
- import { XP_TESTS_SURFACE } from '../paths.js';
12
- import { SURFACE_COPY } from '../copy/surface.js';
13
- import { routeAccess } from './access.model.js';
4
+ import { surfaceUrls } from '../surfaceUrls.js';
5
+ import { buildSurfaceProps } from './buildSurfaceProps.js';
6
+ import { renderTestsSurface } from './renderTestsSurface.js';
14
7
  export async function ExperimentsRootView({ initPageResult, params, searchParams }) {
15
8
  const { req } = initPageResult;
16
9
  const adminRoute = req.payload.config.routes.admin;
@@ -51,32 +44,23 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
51
44
  path: `${login}?redirect=${encodeURIComponent(currentPath)}`
52
45
  }));
53
46
  }
54
- // Keep the server export importable in plain Node; Next loads the CSS-bearing client boundary when this view renders.
55
- const { RootViewMessage } = await import('../components/RootViewMessage.js');
56
- const options = xpOptionsFromReq(req);
57
- async function permits(access) {
47
+ if (route === 'home' || route === 'goals') {
48
+ let canManage = false;
58
49
  try {
59
- return await access({
50
+ canManage = await xpOptionsFromReq(req).access.manage({
60
51
  req
61
52
  }) === true;
62
- } catch {
63
- return false;
64
- }
53
+ } catch {}
54
+ if (!canManage) return renderTestsSurface(req, null);
55
+ return redirect(route === 'home' ? surfaceUrls.home(adminRoute, await searchParams) : surfaceUrls.goals(adminRoute, await searchParams));
65
56
  }
66
- const canManage = await permits(options.access.manage);
67
- const isAdmin = options.access.admin ? await permits(options.access.admin) : false;
68
- const canReadResults = options.access.readResults ? await permits(options.access.readResults) : canManage;
69
- if (routeAccess({
57
+ const clientProps = await buildSurfaceProps({
58
+ req,
70
59
  route,
71
- hasUser: true,
72
- canManage,
73
- canReadResults
74
- }) !== 'ok') {
75
- return /*#__PURE__*/ _jsx(RootViewMessage, {
76
- children: SURFACE_COPY.noAccess
77
- });
78
- }
79
- if (canManage && id && tail[0] === 'by-slug') {
60
+ id,
61
+ searchParams
62
+ });
63
+ if (clientProps?.canManage && id && tail[0] === 'by-slug') {
80
64
  const result = await req.payload.find({
81
65
  collection: 'xp-experiments',
82
66
  req,
@@ -90,138 +74,7 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
90
74
  }
91
75
  });
92
76
  if (!result.docs[0]) return notFound();
93
- id = String(result.docs[0].id);
77
+ clientProps.id = String(result.docs[0].id);
94
78
  }
95
- const flagPoints = serializeFlagPoints(options);
96
- // xp-state is manager-readable only. Results-only readers do not edit settings.
97
- if (canManage && flagPoints.length) {
98
- const state = await req.payload.findGlobal({
99
- slug: 'xp-state',
100
- req,
101
- overrideAccess: false,
102
- depth: 0
103
- });
104
- const defaults = state.flagDefaults;
105
- for (const point of flagPoints)point.effectiveDefaults = defaults?.[point.id]?.params ?? point.defaultParams;
106
- }
107
- const registrations = [
108
- ...new Set([
109
- ...options.puck?.enabled ? puckCollections(options) : [],
110
- ...options.blocks?.collections ?? []
111
- ].map((entry)=>entry.slug))
112
- ];
113
- const pages = route === 'new' && canManage && registrations.length ? (await Promise.all(registrations.map(async (collection)=>{
114
- try {
115
- const config = req.payload.config.collections.find((item)=>item.slug === collection);
116
- const useAsTitle = config?.admin.useAsTitle ?? 'id';
117
- const collectionLabel = resolveLabel(config?.labels.plural, collection, req);
118
- // appliesTo may inspect any stored field; ordinary Puck lists keep their title-only projection.
119
- const predicateFields = options.blocks?.collections.some((entry)=>entry.slug === collection) ? Object.fromEntries(flattenTopLevelFields(config?.fields ?? []).map((field)=>[
120
- field.name,
121
- true
122
- ])) : {};
123
- const result = await req.payload.find({
124
- collection,
125
- req,
126
- overrideAccess: false,
127
- depth: 0,
128
- limit: 50,
129
- sort: '-updatedAt',
130
- draft: true,
131
- select: {
132
- [useAsTitle]: true,
133
- ...predicateFields
134
- }
135
- });
136
- // The new-test list uses the latest draft's editing intent, as the "Test a block" field condition does (§4.1).
137
- return result.docs.map((doc)=>{
138
- const editor = documentEditor(options, collection, doc);
139
- if (editor !== 'puck' && editor !== 'blocks') return null;
140
- const id = doc.id;
141
- const customUrl = options.admin?.editorUrl?.({
142
- collection,
143
- id,
144
- draft: true,
145
- editor,
146
- doc
147
- });
148
- const title = doc[useAsTitle];
149
- return {
150
- collection,
151
- collectionLabel,
152
- id,
153
- editor,
154
- title: typeof title === 'string' || typeof title === 'number' ? String(title) : String(id),
155
- editorUrl: typeof customUrl === 'string' ? customUrl : formatAdminURL({
156
- adminRoute,
157
- path: `/collections/${collection}/${encodeURIComponent(String(id))}`
158
- })
159
- };
160
- }).filter((page)=>page !== null);
161
- } catch {
162
- return [];
163
- }
164
- }))).flat() : undefined;
165
- const descriptions = options.admin?.descriptions;
166
- const returnToTest = (await searchParams)?.return;
167
- const clientProps = {
168
- ...typeof returnToTest === 'string' && /^[\w-]+$/.test(returnToTest) ? {
169
- returnToTest
170
- } : {},
171
- route,
172
- ...id === undefined ? {} : {
173
- id
174
- },
175
- adminRoute,
176
- apiRoute: req.payload.config.routes.api,
177
- canManage,
178
- canReadResults,
179
- isAdmin,
180
- flagPoints,
181
- ...pages === undefined ? {} : {
182
- pages
183
- },
184
- pageCollections: serializePageCollections(options.pages?.collections ?? [], req),
185
- collectionTitles: Object.fromEntries(req.payload.config.collections.flatMap((collection)=>{
186
- const title = collection.admin.useAsTitle;
187
- return title && title !== 'id' ? [
188
- [
189
- collection.slug,
190
- title
191
- ]
192
- ] : [];
193
- })),
194
- userId: req.user.id,
195
- userCollection: req.payload.config.admin.user,
196
- attributeKeys: serializeAttributeKeys(options),
197
- metricSources: serializeMetricSources(options),
198
- ...descriptions ? {
199
- admin: {
200
- descriptions: {
201
- ...descriptions
202
- }
203
- }
204
- } : {}
205
- };
206
- function TestsSurfaceFallback() {
207
- return /*#__PURE__*/ _jsx(RootViewMessage, {
208
- alert: true,
209
- children: SURFACE_COPY.notInstalled
210
- });
211
- }
212
- const css = accentStyles(options.theme?.accent);
213
- const surface = RenderServerComponent({
214
- Component: XP_TESTS_SURFACE,
215
- Fallback: TestsSurfaceFallback,
216
- importMap: req.payload.importMap,
217
- clientProps
218
- });
219
- return css ? /*#__PURE__*/ _jsxs(_Fragment, {
220
- children: [
221
- /*#__PURE__*/ _jsx("style", {
222
- children: css
223
- }),
224
- surface
225
- ]
226
- }) : surface;
79
+ return renderTestsSurface(req, clientProps);
227
80
  }
@@ -0,0 +1,4 @@
1
+ import type { DocumentViewServerProps } from 'payload';
2
+ type Props = Pick<DocumentViewServerProps, 'initPageResult' | 'params' | 'routeSegments'>;
3
+ export declare function GoalEditRedirect({ initPageResult, params, routeSegments }: Props): Promise<never>;
4
+ export {};
@@ -0,0 +1,14 @@
1
+ import { formatAdminURL } from 'payload/shared';
2
+ import { redirect } from 'next/navigation.js';
3
+ export async function GoalEditRedirect({ initPageResult, params, routeSegments }) {
4
+ const resolved = await params;
5
+ const segments = routeSegments ?? (Array.isArray(resolved?.segments) ? resolved.segments : []);
6
+ const collectionIndex = segments.indexOf('xp-metrics');
7
+ const tail = collectionIndex >= 0 ? segments.slice(collectionIndex + 1) : segments;
8
+ const id = tail[0] ?? resolved?.id ?? initPageResult.docID;
9
+ const path = id === undefined || id === 'create' ? '/experiments/goals/new' : `/experiments/goals/${encodeURIComponent(String(id))}`;
10
+ return redirect(formatAdminURL({
11
+ adminRoute: initPageResult.req.payload.config.routes.admin,
12
+ path
13
+ }));
14
+ }
@@ -0,0 +1,2 @@
1
+ import type { ListViewServerProps } from 'payload';
2
+ export declare function GoalsCollectionListView(props: ListViewServerProps): Promise<string | number | bigint | boolean | Iterable<import("react").ReactNode> | import("react").JSX.Element | null | undefined>;
@@ -0,0 +1,4 @@
1
+ import { renderCollectionList } from './renderCollectionList.js';
2
+ export async function GoalsCollectionListView(props) {
3
+ return renderCollectionList(props, 'goals');
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { ListViewServerProps } from 'payload';
2
+ export declare function TestsCollectionListView(props: ListViewServerProps): Promise<string | number | bigint | boolean | Iterable<import("react").ReactNode> | import("react").JSX.Element | null | undefined>;
@@ -0,0 +1,4 @@
1
+ import { renderCollectionList } from './renderCollectionList.js';
2
+ export async function TestsCollectionListView(props) {
3
+ return renderCollectionList(props, 'home');
4
+ }
@@ -1,2 +1,3 @@
1
1
  import type { ServerProps } from 'payload';
2
+ /** @deprecated Retained for existing import maps until 0.5.0. */
2
3
  export declare function TestsNavLinkServer({ payload, user, permissions }: Pick<ServerProps, 'payload' | 'user' | 'permissions'>): Promise<string | number | bigint | boolean | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined>;
@@ -1,7 +1,7 @@
1
1
  import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent';
2
2
  import { SURFACE_COPY } from '../copy/surface.js';
3
3
  import { XP_TESTS_NAV_CLIENT } from '../paths.js';
4
- export async function TestsNavLinkServer({ payload, user, permissions }) {
4
+ /** @deprecated Retained for existing import maps until 0.5.0. */ export async function TestsNavLinkServer({ payload, user, permissions }) {
5
5
  const testPermissions = permissions?.collections?.['xp-experiments'];
6
6
  if (!user?.collection || testPermissions?.update !== true) return null;
7
7
  const result = await payload.find({
@@ -0,0 +1,8 @@
1
+ import type { AdminViewServerProps, PayloadRequest } from 'payload';
2
+ import type { TestsRoute, TestsSurfaceProps } from '../../types/surface.js';
3
+ export declare function buildSurfaceProps({ req, route, id, searchParams }: {
4
+ req: PayloadRequest;
5
+ route: TestsRoute;
6
+ id?: string;
7
+ searchParams: AdminViewServerProps['searchParams'];
8
+ }): Promise<TestsSurfaceProps | null>;
@@ -0,0 +1,143 @@
1
+ import { flattenTopLevelFields, formatAdminURL } from 'payload/shared';
2
+ import { serializeAttributeKeys, serializeFlagPoints, serializeMetricSources } from '../../collections/serialize.js';
3
+ import { serializePageCollections } from '../pageCollections.js';
4
+ import { collectionLabel as resolveLabel } from '../../collections/labels.js';
5
+ import { documentEditor, puckCollections } from '../../options.js';
6
+ import { xpOptionsFromReq } from '../../options/fromReq.js';
7
+ import { routeAccess } from './access.model.js';
8
+ export async function buildSurfaceProps({ req, route, id, searchParams }) {
9
+ if (!req.user) return null;
10
+ const adminRoute = req.payload.config.routes.admin;
11
+ const options = xpOptionsFromReq(req);
12
+ async function permits(access) {
13
+ try {
14
+ return await access({
15
+ req
16
+ }) === true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ const canManage = await permits(options.access.manage);
22
+ const isAdmin = options.access.admin ? await permits(options.access.admin) : false;
23
+ const canReadResults = options.access.readResults ? await permits(options.access.readResults) : canManage;
24
+ if (routeAccess({
25
+ route,
26
+ hasUser: true,
27
+ canManage,
28
+ canReadResults
29
+ }) !== 'ok') {
30
+ return null;
31
+ }
32
+ const flagPoints = serializeFlagPoints(options);
33
+ // xp-state is manager-readable only. Results-only readers do not edit settings.
34
+ if (canManage && flagPoints.length) {
35
+ const state = await req.payload.findGlobal({
36
+ slug: 'xp-state',
37
+ req,
38
+ overrideAccess: false,
39
+ depth: 0
40
+ });
41
+ const defaults = state.flagDefaults;
42
+ for (const point of flagPoints)point.effectiveDefaults = defaults?.[point.id]?.params ?? point.defaultParams;
43
+ }
44
+ const registrations = [
45
+ ...new Set([
46
+ ...options.puck?.enabled ? puckCollections(options) : [],
47
+ ...options.blocks?.collections ?? []
48
+ ].map((entry)=>entry.slug))
49
+ ];
50
+ const pages = route === 'new' && canManage && registrations.length ? (await Promise.all(registrations.map(async (collection)=>{
51
+ try {
52
+ const config = req.payload.config.collections.find((item)=>item.slug === collection);
53
+ const useAsTitle = config?.admin.useAsTitle ?? 'id';
54
+ const collectionLabel = resolveLabel(config?.labels.plural, collection, req);
55
+ // appliesTo may inspect any stored field; ordinary Puck lists keep their title-only projection.
56
+ const predicateFields = options.blocks?.collections.some((entry)=>entry.slug === collection) ? Object.fromEntries(flattenTopLevelFields(config?.fields ?? []).map((field)=>[
57
+ field.name,
58
+ true
59
+ ])) : {};
60
+ const result = await req.payload.find({
61
+ collection,
62
+ req,
63
+ overrideAccess: false,
64
+ depth: 0,
65
+ limit: 50,
66
+ sort: '-updatedAt',
67
+ draft: true,
68
+ select: {
69
+ [useAsTitle]: true,
70
+ ...predicateFields
71
+ }
72
+ });
73
+ // The new-test list uses the latest draft's editing intent, as the "Test a block" field condition does (§4.1).
74
+ return result.docs.map((doc)=>{
75
+ const editor = documentEditor(options, collection, doc);
76
+ if (editor !== 'puck' && editor !== 'blocks') return null;
77
+ const id = doc.id;
78
+ const customUrl = options.admin?.editorUrl?.({
79
+ collection,
80
+ id,
81
+ draft: true,
82
+ editor,
83
+ doc
84
+ });
85
+ const title = doc[useAsTitle];
86
+ return {
87
+ collection,
88
+ collectionLabel,
89
+ id,
90
+ editor,
91
+ title: typeof title === 'string' || typeof title === 'number' ? String(title) : String(id),
92
+ editorUrl: typeof customUrl === 'string' ? customUrl : formatAdminURL({
93
+ adminRoute,
94
+ path: `/collections/${collection}/${encodeURIComponent(String(id))}`
95
+ })
96
+ };
97
+ }).filter((page)=>page !== null);
98
+ } catch {
99
+ return [];
100
+ }
101
+ }))).flat() : undefined;
102
+ const descriptions = options.admin?.descriptions;
103
+ const returnToTest = (await searchParams)?.return;
104
+ return {
105
+ ...typeof returnToTest === 'string' && /^[\w-]+$/.test(returnToTest) ? {
106
+ returnToTest
107
+ } : {},
108
+ route,
109
+ ...id === undefined ? {} : {
110
+ id
111
+ },
112
+ adminRoute,
113
+ apiRoute: req.payload.config.routes.api,
114
+ canManage,
115
+ canReadResults,
116
+ isAdmin,
117
+ flagPoints,
118
+ ...pages === undefined ? {} : {
119
+ pages
120
+ },
121
+ pageCollections: serializePageCollections(options.pages?.collections ?? [], req),
122
+ collectionTitles: Object.fromEntries(req.payload.config.collections.flatMap((collection)=>{
123
+ const title = collection.admin.useAsTitle;
124
+ return title && title !== 'id' ? [
125
+ [
126
+ collection.slug,
127
+ title
128
+ ]
129
+ ] : [];
130
+ })),
131
+ userId: req.user.id,
132
+ userCollection: req.payload.config.admin.user,
133
+ attributeKeys: serializeAttributeKeys(options),
134
+ metricSources: serializeMetricSources(options),
135
+ ...descriptions ? {
136
+ admin: {
137
+ descriptions: {
138
+ ...descriptions
139
+ }
140
+ }
141
+ } : {}
142
+ };
143
+ }
@@ -0,0 +1,2 @@
1
+ import type { ListViewServerProps } from 'payload';
2
+ export declare function renderCollectionList({ payload, searchParams }: ListViewServerProps, route: 'home' | 'goals'): Promise<string | number | bigint | boolean | Iterable<import("react").ReactNode> | import("react").JSX.Element | null | undefined>;
@@ -0,0 +1,34 @@
1
+ import { createLocalReq } from 'payload';
2
+ import { formatAdminURL } from 'payload/shared';
3
+ import { headers } from 'next/headers.js';
4
+ import { redirect } from 'next/navigation.js';
5
+ import { surfaceUrls } from '../surfaceUrls.js';
6
+ import { buildSurfaceProps } from './buildSurfaceProps.js';
7
+ import { renderTestsSurface } from './renderTestsSurface.js';
8
+ export async function renderCollectionList({ payload, searchParams }, route) {
9
+ const requestHeaders = await headers();
10
+ // Authentication, not an access-checked operation; host access callbacks need the full session user.
11
+ const { user } = await payload.auth({
12
+ headers: requestHeaders
13
+ });
14
+ const adminRoute = payload.config.routes.admin;
15
+ if (!user) {
16
+ const { login } = payload.config.admin.routes;
17
+ const currentPath = route === 'home' ? surfaceUrls.home(adminRoute, await searchParams) : surfaceUrls.goals(adminRoute, await searchParams);
18
+ return redirect(formatAdminURL({
19
+ adminRoute,
20
+ path: `${login}?redirect=${encodeURIComponent(currentPath)}`
21
+ }));
22
+ }
23
+ const req = await createLocalReq({
24
+ user,
25
+ req: {
26
+ headers: requestHeaders
27
+ }
28
+ }, payload);
29
+ return renderTestsSurface(req, await buildSurfaceProps({
30
+ req,
31
+ route,
32
+ searchParams
33
+ }));
34
+ }
@@ -0,0 +1,3 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { TestsSurfaceProps } from '../../types/surface.js';
3
+ export declare function renderTestsSurface(req: PayloadRequest, clientProps: TestsSurfaceProps | null): Promise<string | number | bigint | boolean | Iterable<import("react").ReactNode> | import("react").JSX.Element | null | undefined>;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent';
3
+ import { accentStyles } from '../../ui/accent.js';
4
+ import { xpOptionsFromReq } from '../../options/fromReq.js';
5
+ import { XP_TESTS_SURFACE } from '../paths.js';
6
+ import { SURFACE_COPY } from '../copy/surface.js';
7
+ export async function renderTestsSurface(req, clientProps) {
8
+ // Keep the server export importable in plain Node; Next loads the CSS-bearing client boundary on render.
9
+ const { RootViewMessage } = await import('../components/RootViewMessage.js');
10
+ if (!clientProps) return /*#__PURE__*/ _jsx(RootViewMessage, {
11
+ children: SURFACE_COPY.noAccess
12
+ });
13
+ function TestsSurfaceFallback() {
14
+ return /*#__PURE__*/ _jsx(RootViewMessage, {
15
+ alert: true,
16
+ children: SURFACE_COPY.notInstalled
17
+ });
18
+ }
19
+ const css = accentStyles(xpOptionsFromReq(req).theme?.accent);
20
+ const surface = RenderServerComponent({
21
+ Component: XP_TESTS_SURFACE,
22
+ Fallback: TestsSurfaceFallback,
23
+ importMap: req.payload.importMap,
24
+ clientProps
25
+ });
26
+ return css ? /*#__PURE__*/ _jsxs(_Fragment, {
27
+ children: [
28
+ /*#__PURE__*/ _jsx("style", {
29
+ children: css
30
+ }),
31
+ surface
32
+ ]
33
+ }) : surface;
34
+ }
@@ -1,3 +1,4 @@
1
+ import { SURFACE_COPY } from '../../admin/copy/surface.js';
1
2
  import * as paths from '../../admin/paths.js';
2
3
  import { managedAccess } from '../access.js';
3
4
  import { serializeAttributeKeys, serializeFlagPoints } from '../serialize.js';
@@ -37,6 +38,10 @@ export function experimentsCollection(options) {
37
38
  drafts: false,
38
39
  maxPerDoc: 50
39
40
  },
41
+ labels: {
42
+ singular: SURFACE_COPY.test,
43
+ plural: SURFACE_COPY.navAllTests
44
+ },
40
45
  access: {
41
46
  ...managedAccess(options),
42
47
  delete: async ({ req })=>await options.access.manage({
@@ -51,8 +56,7 @@ export function experimentsCollection(options) {
51
56
  } : false
52
57
  },
53
58
  admin: {
54
- // group:false hides navigation while keeping custom view routes reachable (hidden causes 404s).
55
- group: false,
59
+ group: SURFACE_COPY.title,
56
60
  useAsTitle: 'name',
57
61
  defaultColumns: [
58
62
  'name',
@@ -63,7 +67,7 @@ export function experimentsCollection(options) {
63
67
  components: {
64
68
  views: {
65
69
  list: {
66
- Component: paths.XP_TESTS_LIST_REDIRECT
70
+ Component: paths.XP_TESTS_COLLECTION_LIST
67
71
  },
68
72
  edit: {
69
73
  default: {
@@ -101,8 +105,8 @@ export function experimentsCollection(options) {
101
105
  ],
102
106
  afterError: [
103
107
  ({ error })=>{
104
- // Payload preserves public Error status/message, but omits custom codes.
105
- if (error instanceof XpRefusal && error.code === 'stale-edit') {
108
+ // Payload preserves public Error status/message, but omits custom codes; the admin maps refusals by code.
109
+ if (error instanceof XpRefusal) {
106
110
  return {
107
111
  status: error.status,
108
112
  response: {