@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.
- package/README.md +10 -8
- package/dist/admin/TestsDashboardCards.d.ts +1 -0
- package/dist/admin/TestsDashboardCards.js +4 -8
- package/dist/admin/TestsNavLink.d.ts +1 -0
- package/dist/admin/TestsNavLink.js +4 -10
- package/dist/admin/api.d.ts +7 -0
- package/dist/admin/api.js +27 -0
- package/dist/admin/blocks/BlockPanel.js +65 -19
- package/dist/admin/blocks/formApi.d.ts +3 -0
- package/dist/admin/blocks/formApi.js +9 -3
- package/dist/admin/blocks/statusCache.d.ts +2 -0
- package/dist/admin/blocks/statusCache.js +11 -2
- package/dist/admin/copy/blocks.d.ts +16 -0
- package/dist/admin/copy/blocks.js +27 -1
- package/dist/admin/copy/common.d.ts +1 -0
- package/dist/admin/copy/common.js +1 -1
- package/dist/admin/copy/pages.d.ts +2 -4
- package/dist/admin/copy/pages.js +3 -5
- package/dist/admin/copy/readiness.d.ts +1 -0
- package/dist/admin/copy/readiness.js +1 -0
- package/dist/admin/goals/XpGoalField.d.ts +2 -0
- package/dist/admin/goals/XpGoalField.js +51 -35
- package/dist/admin/goals/xpGoalField.css +48 -0
- package/dist/admin/paths.d.ts +3 -0
- package/dist/admin/paths.js +3 -0
- package/dist/admin/surface/TestsHome.js +2 -4
- package/dist/admin/surface/TestsSurface.js +2 -5
- package/dist/admin/surface/goals/GoalForm.js +4 -10
- package/dist/admin/surface/goals/GoalsList.js +2 -1
- package/dist/admin/surface/home.model.d.ts +2 -2
- package/dist/admin/surface/newTest/NewTest.js +2 -4
- package/dist/admin/surface/newTest/PageWizard.js +7 -2
- package/dist/admin/surface/pagePicker/CopyRoute.d.ts +1 -1
- package/dist/admin/surface/pagePicker/{PagePicker.d.ts → PageCombobox.d.ts} +3 -3
- package/dist/admin/surface/pagePicker/PageCombobox.js +395 -0
- package/dist/admin/surface/pagePicker/pagePicker.css +10 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +2 -0
- package/dist/admin/surface/readiness/readiness.model.js +1 -0
- package/dist/admin/surface/results/ResultsHeader.js +3 -8
- package/dist/admin/surface/results/useResults.d.ts +10 -0
- package/dist/admin/surface/test/GoalSection.js +4 -7
- package/dist/admin/surface/test/TestPage.js +3 -8
- package/dist/admin/surface/test/pageParts.js +10 -39
- package/dist/admin/surface/test/testPage.css +0 -5
- package/dist/admin/surface/useTestDocument.d.ts +4 -0
- package/dist/admin/surfaceUrls.d.ts +7 -0
- package/dist/admin/surfaceUrls.js +19 -0
- package/dist/admin/theme.css +2 -0
- package/dist/admin/useApi.d.ts +10 -0
- package/dist/admin/views/CollectionListRedirect.d.ts +1 -0
- package/dist/admin/views/CollectionListRedirect.js +1 -1
- package/dist/admin/views/ExperimentsRootView.js +18 -165
- package/dist/admin/views/GoalEditRedirect.d.ts +4 -0
- package/dist/admin/views/GoalEditRedirect.js +14 -0
- package/dist/admin/views/GoalsCollectionListView.d.ts +2 -0
- package/dist/admin/views/GoalsCollectionListView.js +4 -0
- package/dist/admin/views/TestsCollectionListView.d.ts +2 -0
- package/dist/admin/views/TestsCollectionListView.js +4 -0
- package/dist/admin/views/TestsNavLinkServer.d.ts +1 -0
- package/dist/admin/views/TestsNavLinkServer.js +1 -1
- package/dist/admin/views/buildSurfaceProps.d.ts +8 -0
- package/dist/admin/views/buildSurfaceProps.js +143 -0
- package/dist/admin/views/renderCollectionList.d.ts +2 -0
- package/dist/admin/views/renderCollectionList.js +34 -0
- package/dist/admin/views/renderTestsSurface.d.ts +3 -0
- package/dist/admin/views/renderTestsSurface.js +34 -0
- package/dist/collections/experiments/config.js +9 -5
- package/dist/collections/metrics.js +16 -4
- package/dist/exports/adminRsc.d.ts +3 -0
- package/dist/exports/adminRsc.js +3 -0
- package/dist/fields/xpGoal.js +2 -14
- package/dist/goals/validate.d.ts +3 -0
- package/dist/goals/validate.js +14 -0
- package/dist/hooks/puckGoals.d.ts +3 -0
- package/dist/hooks/puckGoals.js +38 -0
- package/dist/plugin.js +12 -18
- package/dist/puck/editor/editor.chrome.css +13 -0
- package/dist/puck/editor/editor.css +105 -0
- package/dist/puck/editor/xpGoalPuckEditorField.js +28 -20
- package/dist/sanitizeReadinessDetail.js +2 -0
- package/dist/transitions/checkStart.d.ts +2 -1
- package/dist/transitions/checkStart.js +3 -1
- package/dist/transitions/decide.js +1 -0
- package/dist/transitions/start.js +2 -1
- package/dist/types/endpoints.d.ts +1 -1
- package/dist/ui/ui.css +4 -0
- package/package.json +1 -1
- 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
|
+
};
|
package/dist/admin/theme.css
CHANGED
package/dist/admin/useApi.d.ts
CHANGED
|
@@ -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,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 {
|
|
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 {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
const options = xpOptionsFromReq(req);
|
|
57
|
-
async function permits(access) {
|
|
47
|
+
if (route === 'home' || route === 'goals') {
|
|
48
|
+
let canManage = false;
|
|
58
49
|
try {
|
|
59
|
-
|
|
50
|
+
canManage = await xpOptionsFromReq(req).access.manage({
|
|
60
51
|
req
|
|
61
52
|
}) === true;
|
|
62
|
-
} catch {
|
|
63
|
-
|
|
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
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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: {
|