@delmaredigital/payload-experiments 0.3.0-rc.1 → 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.
- package/README.md +172 -21
- package/dist/admin/MetricSourceSelect.js +3 -3
- package/dist/admin/PlacementStatus.d.ts +3 -0
- package/dist/admin/PlacementStatus.js +54 -0
- package/dist/admin/PreviewLinks.d.ts +2 -1
- package/dist/admin/PreviewLinks.js +47 -26
- package/dist/admin/TestsNavLink.d.ts +3 -1
- package/dist/admin/TestsNavLink.js +2 -2
- package/dist/admin/actions/ConfirmDialog.d.ts +2 -1
- package/dist/admin/actions/ConfirmDialog.js +2 -2
- package/dist/admin/actions/DecideDialog.d.ts +2 -1
- package/dist/admin/actions/DecideDialog.js +2 -2
- package/dist/admin/api.d.ts +11 -2
- package/dist/admin/api.js +20 -2
- package/dist/admin/armSelect/ArmSelectField.js +14 -22
- package/dist/admin/arms/arms.model.d.ts +1 -21
- package/dist/admin/arms/arms.model.js +1 -153
- package/dist/admin/blocks/BlockPanel.d.ts +4 -0
- package/dist/admin/blocks/BlockPanel.js +153 -0
- package/dist/admin/blocks/TestBlockField.d.ts +5 -0
- package/dist/admin/blocks/TestBlockField.js +163 -0
- package/dist/admin/blocks/formApi.d.ts +43 -0
- package/dist/admin/blocks/formApi.js +153 -0
- package/dist/admin/blocks/inject.d.ts +7 -0
- package/dist/admin/blocks/inject.js +41 -0
- package/dist/admin/blocks/model.d.ts +17 -0
- package/dist/admin/blocks/model.js +45 -0
- package/dist/admin/blocks/statusCache.d.ts +7 -0
- package/dist/admin/blocks/statusCache.js +83 -0
- package/dist/admin/blocks/useBlockForm.d.ts +17 -0
- package/dist/admin/blocks/useBlockForm.js +46 -0
- package/dist/admin/copy/actions.d.ts +2 -0
- package/dist/admin/copy/actions.js +9 -0
- package/dist/admin/copy/arms.d.ts +1 -6
- package/dist/admin/copy/arms.js +1 -16
- package/dist/admin/copy/blocks.d.ts +27 -0
- package/dist/admin/copy/blocks.js +38 -0
- package/dist/admin/copy/goals.d.ts +2 -0
- package/dist/admin/copy/goals.js +4 -1
- package/dist/admin/copy/newTest.d.ts +10 -0
- package/dist/admin/copy/newTest.js +10 -0
- package/dist/admin/copy/pages.d.ts +26 -0
- package/dist/admin/copy/pages.js +26 -0
- package/dist/admin/copy/readiness.d.ts +25 -3
- package/dist/admin/copy/readiness.js +25 -3
- package/dist/admin/copy/results.d.ts +3 -0
- package/dist/admin/copy/results.js +4 -1
- package/dist/admin/copy/review.d.ts +1 -1
- package/dist/admin/copy/review.js +1 -1
- package/dist/admin/copy/surface.d.ts +0 -1
- package/dist/admin/copy/surface.js +0 -1
- package/dist/admin/copy/testPage.d.ts +4 -0
- package/dist/admin/copy/testPage.js +4 -0
- package/dist/admin/goals/XpGoalField.d.ts +2 -0
- package/dist/admin/goals/XpGoalField.js +152 -0
- package/dist/admin/metricSource.model.d.ts +2 -0
- package/dist/admin/metricSource.model.js +4 -0
- package/dist/admin/pageCollections.d.ts +10 -0
- package/dist/admin/pageCollections.js +20 -0
- package/dist/admin/paths.d.ts +8 -1
- package/dist/admin/paths.js +8 -1
- package/dist/admin/placementStatus.model.d.ts +2 -0
- package/dist/admin/placementStatus.model.js +19 -0
- package/dist/admin/placements/placementLabels.d.ts +11 -0
- package/dist/admin/placements/placementLabels.js +21 -0
- package/dist/admin/placements/placementLabels.model.d.ts +13 -0
- package/dist/admin/placements/placementLabels.model.js +12 -0
- package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
- package/dist/admin/placements/usePlacementLabels.js +102 -0
- package/dist/admin/previewLinks.model.d.ts +2 -0
- package/dist/admin/previewLinks.model.js +3 -0
- package/dist/admin/results/DataQualityPanel.js +4 -1
- package/dist/admin/results/legacy.d.ts +16 -0
- package/dist/admin/results/legacy.js +27 -0
- package/dist/admin/results/results.model.d.ts +9 -6
- package/dist/admin/results/results.model.js +22 -9
- package/dist/admin/results/snapshot.d.ts +14 -0
- package/dist/admin/results/snapshot.js +3 -0
- package/dist/admin/surface/TestsHome.js +43 -15
- package/dist/admin/surface/TestsSurface.js +5 -1
- package/dist/admin/surface/contracts.d.ts +6 -5
- package/dist/admin/surface/goals/GoalForm.js +67 -34
- package/dist/admin/surface/goals/GoalsList.js +3 -3
- package/dist/admin/surface/goals/data.js +19 -16
- package/dist/admin/surface/goals/goals.model.d.ts +1 -2
- package/dist/admin/surface/goals/goals.model.js +1 -3
- package/dist/admin/surface/home.model.d.ts +2 -1
- package/dist/admin/surface/home.model.js +8 -4
- package/dist/admin/surface/newTest/NewTest.js +58 -17
- package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
- package/dist/admin/surface/newTest/PageWizard.js +209 -0
- package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
- package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
- package/dist/admin/surface/pagePicker/PagePicker.d.ts +10 -0
- package/dist/admin/surface/pagePicker/PagePicker.js +172 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +25 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
- package/dist/admin/surface/readiness/ActionDialogs.js +103 -69
- package/dist/admin/surface/readiness/Banner.js +30 -6
- package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
- package/dist/admin/surface/readiness/readiness.model.js +95 -6
- package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
- package/dist/admin/surface/results/ReleasePages.js +3 -7
- package/dist/admin/surface/results/ResultsBody.js +23 -3
- package/dist/admin/surface/results/ResultsHeader.js +2 -5
- package/dist/admin/surface/results/recommendation.model.d.ts +3 -2
- package/dist/admin/surface/results/recommendation.model.js +10 -3
- package/dist/admin/surface/results/useResults.d.ts +10 -1
- package/dist/admin/surface/surface.css +0 -1
- package/dist/admin/surface/test/AdvancedSection.js +111 -104
- package/dist/admin/surface/test/GoalSection.js +97 -22
- package/dist/admin/surface/test/LengthSection.js +1 -1
- package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
- package/dist/admin/surface/test/PageHoldout.js +67 -0
- package/dist/admin/surface/test/TestPage.js +2 -1
- package/dist/admin/surface/test/VersionsSection.js +12 -5
- package/dist/admin/surface/test/fields.d.ts +13 -4
- package/dist/admin/surface/test/fields.js +46 -14
- package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
- package/dist/admin/surface/test/jumpToSection.js +4 -3
- package/dist/admin/surface/test/pageParts.d.ts +4 -1
- package/dist/admin/surface/test/pageParts.js +79 -2
- package/dist/admin/surface/test/testPage.css +8 -1
- package/dist/admin/surface/test/testPage.model.d.ts +2 -3
- package/dist/admin/surface/test/testPage.model.js +1 -21
- package/dist/admin/surface/testDocument.model.d.ts +1 -0
- package/dist/admin/surface/testDocument.model.js +2 -1
- package/dist/admin/surface/useTestDocument.d.ts +11 -2
- package/dist/admin/surface/useTestDocument.js +55 -16
- package/dist/admin/useApi.d.ts +11 -2
- package/dist/admin/useApi.js +3 -5
- package/dist/admin/useLayerUsage.js +14 -23
- package/dist/admin/views/ExperimentsRootView.js +58 -16
- package/dist/admin/views/TestsNavLinkServer.d.ts +2 -0
- package/dist/admin/views/TestsNavLinkServer.js +43 -0
- package/dist/arms/copy.d.ts +6 -0
- package/dist/arms/copy.js +16 -0
- package/dist/arms/model.d.ts +23 -0
- package/dist/arms/model.js +173 -0
- package/dist/blocks/bindings.d.ts +3 -0
- package/dist/blocks/bindings.js +34 -0
- package/dist/blocks/createExperimentBlock.d.ts +6 -0
- package/dist/blocks/createExperimentBlock.js +77 -0
- package/dist/blocks/formState.d.ts +31 -0
- package/dist/blocks/formState.js +146 -0
- package/dist/blocks/model.d.ts +41 -0
- package/dist/blocks/model.js +34 -0
- package/dist/client/ExperimentGroup.d.ts +6 -0
- package/dist/client/ExperimentGroup.js +9 -0
- package/dist/client/ExperimentsProvider.js +20 -4
- package/dist/client/clickGoals.d.ts +4 -0
- package/dist/client/clickGoals.js +43 -0
- package/dist/collections/experiments/config.js +12 -2
- package/dist/collections/experiments/deleteGuard.d.ts +2 -1
- package/dist/collections/experiments/deleteGuard.js +12 -1
- package/dist/collections/experiments/editability.js +4 -2
- package/dist/collections/experiments/pickSlug.d.ts +2 -0
- package/dist/collections/experiments/pickSlug.js +8 -0
- package/dist/collections/labels.d.ts +3 -0
- package/dist/collections/labels.js +20 -0
- package/dist/collections/metrics.js +12 -2
- package/dist/collections/phases.js +1 -0
- package/dist/collections/serialize.js +7 -0
- package/dist/db/appends.d.ts +6 -2
- package/dist/db/appends.js +10 -4
- package/dist/db/clickGoals.d.ts +9 -0
- package/dist/db/clickGoals.js +22 -0
- package/dist/db/delete.d.ts +1 -0
- package/dist/db/delete.js +1 -0
- package/dist/db/locks.d.ts +14 -0
- package/dist/db/locks.js +36 -1
- package/dist/db/names.d.ts +4 -0
- package/dist/db/names.js +4 -0
- package/dist/db/pageBindings.d.ts +4 -0
- package/dist/db/pageBindings.js +17 -0
- package/dist/db/pageBusy.d.ts +2 -0
- package/dist/db/pageBusy.js +10 -0
- package/dist/db/pageContent.d.ts +10 -0
- package/dist/db/pageContent.js +79 -0
- package/dist/db/pageOccupancy.d.ts +12 -0
- package/dist/db/pageOccupancy.js +45 -0
- package/dist/db/placements.d.ts +33 -0
- package/dist/db/placements.js +54 -0
- package/dist/db/slugs.d.ts +8 -0
- package/dist/db/slugs.js +40 -0
- package/dist/db/snapshots.d.ts +6 -0
- package/dist/db/snapshots.js +9 -5
- package/dist/endpoints/clickKeys.d.ts +5 -0
- package/dist/endpoints/clickKeys.js +17 -0
- package/dist/endpoints/events.js +4 -1
- package/dist/endpoints/index.js +28 -0
- package/dist/endpoints/pages.d.ts +5 -0
- package/dist/endpoints/pages.js +120 -0
- package/dist/endpoints/placementStatus.d.ts +6 -0
- package/dist/endpoints/placementStatus.js +238 -0
- package/dist/endpoints/readBody.d.ts +2 -0
- package/dist/endpoints/readBody.js +24 -0
- package/dist/endpoints/results.js +5 -1
- package/dist/endpoints/summary.js +11 -4
- package/dist/endpoints/wrapBlock.d.ts +6 -0
- package/dist/endpoints/wrapBlock.js +76 -0
- package/dist/errors.d.ts +3 -3
- package/dist/errors.js +4 -4
- package/dist/events/rows.d.ts +1 -1
- package/dist/events/rows.js +5 -3
- package/dist/exports/adminClient.d.ts +5 -0
- package/dist/exports/adminClient.js +5 -0
- package/dist/exports/adminRsc.d.ts +1 -0
- package/dist/exports/adminRsc.js +1 -0
- package/dist/exports/blocks.d.ts +2 -0
- package/dist/exports/blocks.js +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/index.d.ts +6 -1
- package/dist/exports/index.js +3 -0
- package/dist/exports/puck.d.ts +5 -2
- package/dist/exports/puck.js +3 -1
- package/dist/exports/puckEditor.d.ts +6 -1
- package/dist/exports/puckEditor.js +4 -1
- package/dist/exports/rsc.d.ts +4 -0
- package/dist/exports/rsc.js +2 -0
- package/dist/fields/xpGoal.d.ts +3 -0
- package/dist/fields/xpGoal.js +28 -0
- package/dist/globals/state.js +10 -0
- package/dist/goals/cache.d.ts +3 -0
- package/dist/goals/cache.js +35 -0
- package/dist/goals/fairness.d.ts +40 -0
- package/dist/goals/fairness.js +200 -0
- package/dist/goals/key.d.ts +4 -0
- package/dist/goals/key.js +3 -0
- package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
- package/dist/goals/picker/clickGoalPicker.model.js +45 -0
- package/dist/goals/picker/copy.d.ts +16 -0
- package/dist/goals/picker/copy.js +15 -0
- package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
- package/dist/goals/picker/useClickGoalPicker.js +220 -0
- package/dist/hooks/pageGuard.d.ts +5 -1
- package/dist/hooks/pageGuard.js +160 -80
- package/dist/lib/fetchAllDocs.d.ts +16 -0
- package/dist/lib/fetchAllDocs.js +56 -0
- package/dist/onInit.js +29 -0
- package/dist/options.d.ts +2 -0
- package/dist/options.js +43 -10
- package/dist/plugin.js +45 -28
- package/dist/puck/ExperimentPicker.js +47 -16
- package/dist/puck/ExperimentSlot.d.ts +51 -1
- package/dist/puck/ExperimentSlot.js +23 -12
- package/dist/puck/copy.d.ts +6 -4
- package/dist/puck/copy.js +6 -4
- package/dist/puck/editor/CreateExperimentDrawer.js +46 -56
- package/dist/puck/editor/ExperimentSlotEditor.d.ts +52 -1
- package/dist/puck/editor/ExperimentSlotEditor.js +292 -137
- package/dist/puck/editor/ResultsReadout.d.ts +6 -0
- package/dist/puck/editor/ResultsReadout.js +99 -0
- package/dist/puck/editor/VersionBuilder.d.ts +34 -0
- package/dist/puck/editor/VersionBuilder.js +317 -0
- package/dist/puck/editor/armWrites.d.ts +7 -0
- package/dist/puck/editor/armWrites.js +71 -0
- package/dist/puck/editor/armsCache.d.ts +14 -0
- package/dist/puck/editor/armsCache.js +78 -0
- package/dist/puck/editor/copy.d.ts +33 -1
- package/dist/puck/editor/copy.js +45 -8
- package/dist/puck/editor/createTest.d.ts +1 -1
- package/dist/puck/editor/createTest.js +25 -14
- package/dist/puck/editor/editor.chrome.css +48 -161
- package/dist/puck/editor/editor.css +48 -161
- package/dist/puck/editor/editor.model.d.ts +8 -3
- package/dist/puck/editor/editor.model.js +16 -10
- package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
- package/dist/puck/editor/experimentOutlinePlugin.js +89 -0
- package/dist/puck/editor/integration.d.ts +10 -0
- package/dist/puck/editor/integration.js +30 -0
- package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
- package/dist/puck/editor/xpGoalPuckEditorField.js +134 -0
- package/dist/puck/experimentSlotField.d.ts +2 -0
- package/dist/puck/experimentSlotField.js +11 -3
- package/dist/puck/hostContext.d.ts +10 -0
- package/dist/puck/hostContext.js +12 -0
- package/dist/puck/integration.d.ts +6 -0
- package/dist/puck/integration.js +21 -0
- package/dist/puck/routes.d.ts +12 -0
- package/dist/puck/routes.js +11 -0
- package/dist/puck/slotConfig.d.ts +1 -1
- package/dist/puck/slotConfig.js +2 -2
- package/dist/puck/xpGoalField.d.ts +3 -0
- package/dist/puck/xpGoalField.js +6 -0
- package/dist/results/health.d.ts +4 -0
- package/dist/results/health.js +27 -0
- package/dist/results/snapshot.js +15 -2
- package/dist/sanitizeReadinessDetail.js +51 -1
- package/dist/server/metadata.js +9 -2
- package/dist/server/normaliseContentRelationships.d.ts +7 -0
- package/dist/server/normaliseContentRelationships.js +62 -0
- package/dist/server/pageKindSeoContract.d.ts +27 -0
- package/dist/server/pageKindSeoContract.js +80 -0
- package/dist/server/recordContentChange.d.ts +1 -1
- package/dist/server/recordContentChange.js +72 -13
- package/dist/server/resolveExperimentBlocks.d.ts +5 -0
- package/dist/server/resolveExperimentBlocks.js +54 -0
- package/dist/server/runResults.js +92 -19
- package/dist/server/toClientAssignments.js +2 -1
- package/dist/transitions/addMetric.d.ts +2 -1
- package/dist/transitions/addMetric.js +28 -1
- package/dist/transitions/checkStart.d.ts +17 -1
- package/dist/transitions/checkStart.js +99 -7
- package/dist/transitions/decide.js +40 -1
- package/dist/transitions/deleteExperiment.d.ts +3 -1
- package/dist/transitions/deleteExperiment.js +27 -1
- package/dist/transitions/kernel.js +4 -0
- package/dist/transitions/makeContent.d.ts +5 -0
- package/dist/transitions/makeContent.js +39 -11
- package/dist/transitions/pageCollection.d.ts +10 -0
- package/dist/transitions/pageCollection.js +20 -0
- package/dist/transitions/pageVersions.d.ts +10 -0
- package/dist/transitions/pageVersions.js +44 -0
- package/dist/transitions/phaseRecord.d.ts +1 -0
- package/dist/transitions/placementInteractions.d.ts +40 -0
- package/dist/transitions/placementInteractions.js +94 -0
- package/dist/transitions/placements.d.ts +10 -6
- package/dist/transitions/placements.js +156 -2
- package/dist/transitions/preflightData.d.ts +8 -0
- package/dist/transitions/preflightData.js +248 -36
- package/dist/transitions/resolveCaps.d.ts +1 -0
- package/dist/transitions/resolveCaps.js +6 -2
- package/dist/transitions/start.js +77 -3
- package/dist/types/admin.d.ts +1 -1
- package/dist/types/assignments.d.ts +1 -0
- package/dist/types/contracts.d.ts +31 -10
- package/dist/types/endpoints.d.ts +92 -5
- package/dist/types/index.d.ts +3 -0
- package/dist/types/puck.d.ts +7 -1
- package/dist/types/snapshot.d.ts +17 -4
- package/dist/types/surface.d.ts +13 -0
- package/dist/ui/Tabs.d.ts +4 -2
- package/dist/ui/Tabs.js +33 -26
- package/dist/ui/accent.d.ts +22 -3
- package/dist/ui/accent.js +74 -16
- package/package.json +12 -2
package/dist/admin/useApi.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
|
-
import { useConfig } from '@payloadcms/ui';
|
|
4
3
|
import { createApi } from './api.js';
|
|
5
|
-
export function useApi() {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
config.routes.api
|
|
4
|
+
export function useApi(apiRoute) {
|
|
5
|
+
return useMemo(()=>createApi(apiRoute), [
|
|
6
|
+
apiRoute
|
|
9
7
|
]);
|
|
10
8
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
import { fetchAllDocs } from '../lib/fetchAllDocs.js';
|
|
2
3
|
import { useConfig } from '@payloadcms/ui';
|
|
3
4
|
import { useEffect, useState } from 'react';
|
|
4
5
|
import { layerUsage } from './layerUsage.model.js';
|
|
@@ -13,33 +14,23 @@ export function useLayerExperiments(layerId) {
|
|
|
13
14
|
if (id == null) return;
|
|
14
15
|
const controller = new AbortController();
|
|
15
16
|
async function load() {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
signal: controller.signal
|
|
29
|
-
});
|
|
30
|
-
if (!response.ok) {
|
|
31
|
-
if (!controller.signal.aborted) setState(null);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const data = await response.json();
|
|
35
|
-
docs.push(...data.docs);
|
|
36
|
-
if (!data.hasNextPage) break;
|
|
37
|
-
page++;
|
|
17
|
+
const query = new URLSearchParams({
|
|
18
|
+
depth: '0',
|
|
19
|
+
'where[layer][equals]': id,
|
|
20
|
+
'where[status][in]': 'running,decided'
|
|
21
|
+
});
|
|
22
|
+
const response = await fetchAllDocs(`${api}/xp-experiments?${query}`, {
|
|
23
|
+
credentials: 'include',
|
|
24
|
+
signal: controller.signal
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
if (!controller.signal.aborted) setState(null);
|
|
28
|
+
return;
|
|
38
29
|
}
|
|
39
30
|
if (!controller.signal.aborted) setState({
|
|
40
31
|
api,
|
|
41
32
|
id: id,
|
|
42
|
-
usage: layerUsage(docs)
|
|
33
|
+
usage: layerUsage(response.docs)
|
|
43
34
|
});
|
|
44
35
|
}
|
|
45
36
|
void load().catch(()=>{
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { accentStyles } from '../../ui/accent.js';
|
|
3
|
-
import { formatAdminURL } from 'payload/shared';
|
|
3
|
+
import { flattenTopLevelFields, formatAdminURL } from 'payload/shared';
|
|
4
4
|
import { notFound, redirect } from 'next/navigation.js';
|
|
5
5
|
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent';
|
|
6
6
|
import { serializeAttributeKeys, serializeFlagPoints, serializeMetricSources } from '../../collections/serialize.js';
|
|
7
|
-
import {
|
|
7
|
+
import { serializePageCollections } from '../pageCollections.js';
|
|
8
|
+
import { collectionLabel as resolveLabel } from '../../collections/labels.js';
|
|
9
|
+
import { documentEditor, puckCollections } from '../../options.js';
|
|
8
10
|
import { xpOptionsFromReq } from '../../options/fromReq.js';
|
|
9
11
|
import { XP_TESTS_SURFACE } from '../paths.js';
|
|
10
12
|
import { SURFACE_COPY } from '../copy/surface.js';
|
|
@@ -26,6 +28,9 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
|
|
|
26
28
|
} else if (tail.length === 1) {
|
|
27
29
|
route = 'test';
|
|
28
30
|
id = tail[0];
|
|
31
|
+
} else if (tail.length === 2 && tail[0] === 'by-slug') {
|
|
32
|
+
route = 'test';
|
|
33
|
+
id = tail[1];
|
|
29
34
|
} else if (tail.length === 2 && tail[1] === 'results') {
|
|
30
35
|
route = 'results';
|
|
31
36
|
id = tail[0];
|
|
@@ -71,6 +76,22 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
|
|
|
71
76
|
children: SURFACE_COPY.noAccess
|
|
72
77
|
});
|
|
73
78
|
}
|
|
79
|
+
if (canManage && id && tail[0] === 'by-slug') {
|
|
80
|
+
const result = await req.payload.find({
|
|
81
|
+
collection: 'xp-experiments',
|
|
82
|
+
req,
|
|
83
|
+
overrideAccess: false,
|
|
84
|
+
depth: 0,
|
|
85
|
+
limit: 1,
|
|
86
|
+
where: {
|
|
87
|
+
slug: {
|
|
88
|
+
equals: id
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
if (!result.docs[0]) return notFound();
|
|
93
|
+
id = String(result.docs[0].id);
|
|
94
|
+
}
|
|
74
95
|
const flagPoints = serializeFlagPoints(options);
|
|
75
96
|
// xp-state is manager-readable only. Results-only readers do not edit settings.
|
|
76
97
|
if (canManage && flagPoints.length) {
|
|
@@ -83,20 +104,22 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
|
|
|
83
104
|
const defaults = state.flagDefaults;
|
|
84
105
|
for (const point of flagPoints)point.effectiveDefaults = defaults?.[point.id]?.params ?? point.defaultParams;
|
|
85
106
|
}
|
|
86
|
-
const registrations =
|
|
87
|
-
|
|
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)=>{
|
|
88
114
|
try {
|
|
89
115
|
const config = req.payload.config.collections.find((item)=>item.slug === collection);
|
|
90
116
|
const useAsTitle = config?.admin.useAsTitle ?? 'id';
|
|
91
|
-
const
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
byLanguage.en,
|
|
98
|
-
...Object.values(byLanguage)
|
|
99
|
-
].find((value)=>typeof value === 'string') ?? collection;
|
|
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
|
+
])) : {};
|
|
100
123
|
const result = await req.payload.find({
|
|
101
124
|
collection,
|
|
102
125
|
req,
|
|
@@ -106,28 +129,35 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
|
|
|
106
129
|
sort: '-updatedAt',
|
|
107
130
|
draft: true,
|
|
108
131
|
select: {
|
|
109
|
-
[useAsTitle]: true
|
|
132
|
+
[useAsTitle]: true,
|
|
133
|
+
...predicateFields
|
|
110
134
|
}
|
|
111
135
|
});
|
|
136
|
+
// The new-test list uses the latest draft's editing intent, as the "Test a block" field condition does (§4.1).
|
|
112
137
|
return result.docs.map((doc)=>{
|
|
138
|
+
const editor = documentEditor(options, collection, doc);
|
|
139
|
+
if (editor !== 'puck' && editor !== 'blocks') return null;
|
|
113
140
|
const id = doc.id;
|
|
114
141
|
const customUrl = options.admin?.editorUrl?.({
|
|
115
142
|
collection,
|
|
116
143
|
id,
|
|
117
|
-
draft: true
|
|
144
|
+
draft: true,
|
|
145
|
+
editor,
|
|
146
|
+
doc
|
|
118
147
|
});
|
|
119
148
|
const title = doc[useAsTitle];
|
|
120
149
|
return {
|
|
121
150
|
collection,
|
|
122
151
|
collectionLabel,
|
|
123
152
|
id,
|
|
153
|
+
editor,
|
|
124
154
|
title: typeof title === 'string' || typeof title === 'number' ? String(title) : String(id),
|
|
125
155
|
editorUrl: typeof customUrl === 'string' ? customUrl : formatAdminURL({
|
|
126
156
|
adminRoute,
|
|
127
157
|
path: `/collections/${collection}/${encodeURIComponent(String(id))}`
|
|
128
158
|
})
|
|
129
159
|
};
|
|
130
|
-
});
|
|
160
|
+
}).filter((page)=>page !== null);
|
|
131
161
|
} catch {
|
|
132
162
|
return [];
|
|
133
163
|
}
|
|
@@ -151,6 +181,18 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
|
|
|
151
181
|
...pages === undefined ? {} : {
|
|
152
182
|
pages
|
|
153
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,
|
|
154
196
|
attributeKeys: serializeAttributeKeys(options),
|
|
155
197
|
metricSources: serializeMetricSources(options),
|
|
156
198
|
...descriptions ? {
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { ServerProps } from 'payload';
|
|
2
|
+
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>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent';
|
|
2
|
+
import { SURFACE_COPY } from '../copy/surface.js';
|
|
3
|
+
import { XP_TESTS_NAV_CLIENT } from '../paths.js';
|
|
4
|
+
export async function TestsNavLinkServer({ payload, user, permissions }) {
|
|
5
|
+
const testPermissions = permissions?.collections?.['xp-experiments'];
|
|
6
|
+
if (!user?.collection || testPermissions?.update !== true) return null;
|
|
7
|
+
const result = await payload.find({
|
|
8
|
+
collection: 'payload-preferences',
|
|
9
|
+
where: {
|
|
10
|
+
and: [
|
|
11
|
+
{
|
|
12
|
+
key: {
|
|
13
|
+
equals: 'nav'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
'user.relationTo': {
|
|
18
|
+
equals: user.collection
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
'user.value': {
|
|
23
|
+
equals: user.id
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
limit: 1,
|
|
29
|
+
pagination: false,
|
|
30
|
+
depth: 0,
|
|
31
|
+
user,
|
|
32
|
+
overrideAccess: false
|
|
33
|
+
});
|
|
34
|
+
const value = result.docs[0]?.value;
|
|
35
|
+
// Resolve the CSS-bearing client boundary through Payload's public import map.
|
|
36
|
+
return RenderServerComponent({
|
|
37
|
+
Component: XP_TESTS_NAV_CLIENT,
|
|
38
|
+
importMap: payload.importMap,
|
|
39
|
+
clientProps: {
|
|
40
|
+
isOpen: value?.groups?.[SURFACE_COPY.title]?.open ?? true
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const ISSUE_COPY = {
|
|
2
|
+
'weights-total': 'Weights must add up to 100%. They add up to {total}% now.',
|
|
3
|
+
'control-count': 'Include exactly one Original.',
|
|
4
|
+
'control-first': 'Put the Original first.',
|
|
5
|
+
'holdout-last': 'Put the holdout last.',
|
|
6
|
+
'holdout-count': 'Include only one holdout.',
|
|
7
|
+
'variant-count': 'Include up to three variants.',
|
|
8
|
+
'duplicate-key': 'Each group must have a different standard name.',
|
|
9
|
+
'duplicate-slot': 'Each group must have a different content slot.',
|
|
10
|
+
'slot-required': 'Choose the matching content slot for this group.',
|
|
11
|
+
'invalid-key': 'Use the standard group names: Original, Variant A–C, Holdout.'
|
|
12
|
+
};
|
|
13
|
+
export function issueMessage(issue, total) {
|
|
14
|
+
const copy = Object.hasOwn(ISSUE_COPY, issue.code) ? ISSUE_COPY[issue.code] : issue.message;
|
|
15
|
+
return copy.replace('{total}', String(total));
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ArmIssue } from '../collections/experiments/armInvariants.js';
|
|
2
|
+
import type { Arm, ArmKey, ExperimentKind } from '../types/contracts.js';
|
|
3
|
+
export declare function defaultArms(kind?: ExperimentKind): Arm[];
|
|
4
|
+
/** Keep content slots in step with the experiment kind, including an unset kind. */
|
|
5
|
+
export declare function withSlotsFor(arms: Arm[], kind?: ExperimentKind): Arm[];
|
|
6
|
+
export declare function totalWeight(arms: Arm[]): number;
|
|
7
|
+
export declare function addVariant(arms: Arm[], kind?: ExperimentKind): Arm[];
|
|
8
|
+
export declare function removeArm(arms: Arm[], key: ArmKey): Arm[];
|
|
9
|
+
export declare function addHoldoutArm(arms: Arm[]): Arm[];
|
|
10
|
+
export declare function removeHoldout(arms: Arm[]): Arm[];
|
|
11
|
+
export declare function splitEvenly(arms: Arm[]): Arm[];
|
|
12
|
+
export declare function setWeight(arms: Arm[], key: ArmKey, weight: number): Arm[];
|
|
13
|
+
export declare function issuesByRow(arms: Arm[], kind?: ExperimentKind): {
|
|
14
|
+
rows: ArmIssue[][];
|
|
15
|
+
form: ArmIssue[];
|
|
16
|
+
};
|
|
17
|
+
export declare const ARM_FIELDS: readonly ["key", "label", "weight", "kind", "slot", "params", "pageTarget"];
|
|
18
|
+
/** Payload arrays store each row's values at separate field paths. */
|
|
19
|
+
export declare function readArms(fields: Record<string, {
|
|
20
|
+
value?: unknown;
|
|
21
|
+
}>, path?: string): Arm[];
|
|
22
|
+
/** Keep the edited share exact and distribute the remaining whole percentages proportionally. */
|
|
23
|
+
export declare function balanceWeight(arms: Arm[], key: ArmKey, weight: number): Arm[];
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { addHoldout, checkArms } from '../collections/experiments/armInvariants.js';
|
|
2
|
+
import { ARM_LABELS as GROUP_LABELS } from '../labels/arms.js';
|
|
3
|
+
const VARIANTS = [
|
|
4
|
+
'variantA',
|
|
5
|
+
'variantB',
|
|
6
|
+
'variantC'
|
|
7
|
+
];
|
|
8
|
+
export function defaultArms(kind) {
|
|
9
|
+
return withSlotsFor([
|
|
10
|
+
{
|
|
11
|
+
key: 'control',
|
|
12
|
+
kind: 'control',
|
|
13
|
+
label: GROUP_LABELS.control,
|
|
14
|
+
weight: 50
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: 'variantA',
|
|
18
|
+
kind: 'variant',
|
|
19
|
+
label: GROUP_LABELS.variantA,
|
|
20
|
+
weight: 50
|
|
21
|
+
}
|
|
22
|
+
], kind);
|
|
23
|
+
}
|
|
24
|
+
/** Keep content slots in step with the experiment kind, including an unset kind. */ export function withSlotsFor(arms, kind) {
|
|
25
|
+
const next = arms.map((arm)=>{
|
|
26
|
+
if (kind === 'block' && arm.kind !== 'holdout') return arm.slot === arm.key ? arm : {
|
|
27
|
+
...arm,
|
|
28
|
+
slot: arm.key
|
|
29
|
+
};
|
|
30
|
+
// Postgres returns null for unset columns; null counts as "no slot" so a loaded doc is not rewritten.
|
|
31
|
+
if (arm.slot == null) return arm;
|
|
32
|
+
const { slot: _slot, ...rest } = arm;
|
|
33
|
+
return rest;
|
|
34
|
+
});
|
|
35
|
+
return next.every((arm, index)=>arm === arms[index]) ? arms : next;
|
|
36
|
+
}
|
|
37
|
+
const finiteWeight = (weight)=>Number.isFinite(weight) ? weight : 0;
|
|
38
|
+
export function totalWeight(arms) {
|
|
39
|
+
return arms.reduce((sum, arm)=>sum + finiteWeight(arm.weight), 0);
|
|
40
|
+
}
|
|
41
|
+
export function addVariant(arms, kind) {
|
|
42
|
+
if (!arms.some((arm)=>arm.kind === 'control')) return arms;
|
|
43
|
+
const key = VARIANTS.find((candidate)=>!arms.some((arm)=>arm.key === candidate));
|
|
44
|
+
if (!key || arms.filter((arm)=>arm.kind === 'variant').length >= 3) return arms;
|
|
45
|
+
const block = kind === 'block' || kind === undefined && arms.some((arm)=>arm.slot !== undefined);
|
|
46
|
+
const variant = {
|
|
47
|
+
key,
|
|
48
|
+
kind: 'variant',
|
|
49
|
+
label: GROUP_LABELS[key],
|
|
50
|
+
weight: 0,
|
|
51
|
+
...block ? {
|
|
52
|
+
slot: key
|
|
53
|
+
} : {}
|
|
54
|
+
};
|
|
55
|
+
const index = arms.findIndex((arm)=>arm.kind === 'holdout');
|
|
56
|
+
const result = [
|
|
57
|
+
...arms
|
|
58
|
+
];
|
|
59
|
+
result.splice(index < 0 ? arms.length : index, 0, variant);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
export function removeArm(arms, key) {
|
|
63
|
+
if (arms.some((arm)=>arm.key === key && arm.kind === 'control')) return arms;
|
|
64
|
+
if (key === 'holdout') return removeHoldout(arms);
|
|
65
|
+
return arms.filter((arm)=>arm.key !== key);
|
|
66
|
+
}
|
|
67
|
+
export function addHoldoutArm(arms) {
|
|
68
|
+
if (!arms.some((arm)=>arm.kind === 'control') || arms.some((arm)=>arm.kind === 'holdout' || arm.key === 'holdout')) return arms;
|
|
69
|
+
return addHoldout(arms.map((arm)=>Number.isFinite(arm.weight) ? arm : {
|
|
70
|
+
...arm,
|
|
71
|
+
weight: 0
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
// Largest remainder, with equal fractional remainders awarded to Original first.
|
|
75
|
+
function distribute(arms, total, proportional) {
|
|
76
|
+
if (!arms.length) return arms;
|
|
77
|
+
const weightTotal = totalWeight(arms);
|
|
78
|
+
const useWeights = proportional && weightTotal > 0;
|
|
79
|
+
const denominator = useWeights ? weightTotal : arms.length;
|
|
80
|
+
const numerators = arms.map((arm)=>(useWeights ? finiteWeight(arm.weight) : 1) * total);
|
|
81
|
+
const weights = numerators.map((numerator)=>Math.floor(numerator / denominator));
|
|
82
|
+
const order = arms.map((_, i)=>i).sort((a, b)=>numerators[b] % denominator - numerators[a] % denominator || Number(arms[b].kind === 'control') - Number(arms[a].kind === 'control') || a - b);
|
|
83
|
+
const remainder = total - weights.reduce((sum, weight)=>sum + weight, 0);
|
|
84
|
+
for(let i = 0; i < remainder; i++)weights[order[i % order.length]]++;
|
|
85
|
+
return arms.map((arm, i)=>({
|
|
86
|
+
...arm,
|
|
87
|
+
weight: weights[i]
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
export function removeHoldout(arms) {
|
|
91
|
+
if (!arms.some((arm)=>arm.kind === 'holdout')) return arms;
|
|
92
|
+
return distribute(arms.filter((arm)=>arm.kind !== 'holdout'), totalWeight(arms), true);
|
|
93
|
+
}
|
|
94
|
+
export function splitEvenly(arms) {
|
|
95
|
+
const holdout = arms.find((arm)=>arm.kind === 'holdout');
|
|
96
|
+
const weight = finiteWeight(holdout?.weight ?? 0);
|
|
97
|
+
return [
|
|
98
|
+
...distribute(arms.filter((arm)=>arm.kind !== 'holdout'), 100 - weight, false),
|
|
99
|
+
...holdout ? [
|
|
100
|
+
{
|
|
101
|
+
...holdout,
|
|
102
|
+
weight
|
|
103
|
+
}
|
|
104
|
+
] : []
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
export function setWeight(arms, key, weight) {
|
|
108
|
+
return arms.map((arm)=>arm.key === key ? {
|
|
109
|
+
...arm,
|
|
110
|
+
weight
|
|
111
|
+
} : arm);
|
|
112
|
+
}
|
|
113
|
+
export function issuesByRow(arms, kind) {
|
|
114
|
+
const rows = arms.map(()=>[]);
|
|
115
|
+
const form = [];
|
|
116
|
+
for (const issue of checkArms(arms, kind)){
|
|
117
|
+
const index = issue.armIndex ?? arms.findIndex((arm)=>arm.key === issue.armKey);
|
|
118
|
+
if (index >= 0 && index < arms.length) rows[index].push(issue);
|
|
119
|
+
else form.push(issue);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
rows,
|
|
123
|
+
form
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export const ARM_FIELDS = [
|
|
127
|
+
'key',
|
|
128
|
+
'label',
|
|
129
|
+
'weight',
|
|
130
|
+
'kind',
|
|
131
|
+
'slot',
|
|
132
|
+
'params',
|
|
133
|
+
'pageTarget'
|
|
134
|
+
];
|
|
135
|
+
/** Payload arrays store each row's values at separate field paths. */ export function readArms(fields, path = 'arms') {
|
|
136
|
+
const indices = new Set();
|
|
137
|
+
for (const name of Object.keys(fields)){
|
|
138
|
+
if (!name.startsWith(`${path}.`)) continue;
|
|
139
|
+
const [index, field] = name.slice(path.length + 1).split('.');
|
|
140
|
+
if (/^\d+$/.test(index) && field === 'key') indices.add(Number(index));
|
|
141
|
+
}
|
|
142
|
+
return [
|
|
143
|
+
...indices
|
|
144
|
+
].sort((a, b)=>a - b).map((index)=>Object.fromEntries(ARM_FIELDS.flatMap((field)=>{
|
|
145
|
+
const value = fields[`${path}.${index}.${field}`]?.value;
|
|
146
|
+
return value === undefined ? [] : [
|
|
147
|
+
[
|
|
148
|
+
field,
|
|
149
|
+
value
|
|
150
|
+
]
|
|
151
|
+
];
|
|
152
|
+
})));
|
|
153
|
+
}
|
|
154
|
+
/** Keep the edited share exact and distribute the remaining whole percentages proportionally. */ export function balanceWeight(arms, key, weight) {
|
|
155
|
+
if (!arms.some((arm)=>arm.key === key) || !Number.isInteger(weight) || weight < 0 || weight > 100) return arms;
|
|
156
|
+
const others = arms.filter((arm)=>arm.key !== key);
|
|
157
|
+
if (!others.length) return arms;
|
|
158
|
+
const total = others.reduce((sum, arm)=>sum + Math.max(0, Number.isFinite(arm.weight) ? arm.weight : 0), 0);
|
|
159
|
+
const denominator = total || others.length;
|
|
160
|
+
const raw = others.map((arm)=>(total ? Math.max(0, Number.isFinite(arm.weight) ? arm.weight : 0) : 1) * (100 - weight) / denominator);
|
|
161
|
+
const whole = raw.map(Math.floor);
|
|
162
|
+
const order = raw.map((_, i)=>i).sort((a, b)=>raw[b] - whole[b] - (raw[a] - whole[a]) || a - b);
|
|
163
|
+
const remainder = 100 - weight - whole.reduce((sum, value)=>sum + value, 0);
|
|
164
|
+
for(let i = 0; i < remainder; i++)whole[order[i]]++;
|
|
165
|
+
const shares = new Map(others.map((arm, i)=>[
|
|
166
|
+
arm.key,
|
|
167
|
+
whole[i]
|
|
168
|
+
]));
|
|
169
|
+
return setWeight(arms, key, weight).map((arm)=>shares.has(arm.key) ? {
|
|
170
|
+
...arm,
|
|
171
|
+
weight: shares.get(arm.key)
|
|
172
|
+
} : arm);
|
|
173
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ResolvedOptions } from '../types/contracts.js';
|
|
2
|
+
/** Same document-kind rule as the scan; Payload blocks are top level, Puck is recursive. */
|
|
3
|
+
export declare function placementSlugs(options: Pick<ResolvedOptions, 'blocks' | 'puck'>, collection: string, doc: Record<string, unknown> | undefined): string[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { documentEditor, puckCollections } from '../options.js';
|
|
2
|
+
import { EXPERIMENT_BLOCK_SLUG, hostBlocks, isObject } from './model.js';
|
|
3
|
+
/** Same document-kind rule as the scan; Payload blocks are top level, Puck is recursive. */ export function placementSlugs(options, collection, doc) {
|
|
4
|
+
if (!doc) return [];
|
|
5
|
+
const editor = documentEditor(options, collection, doc);
|
|
6
|
+
if (editor === 'blocks') {
|
|
7
|
+
const registration = options.blocks?.collections.find((r)=>r.slug === collection);
|
|
8
|
+
if (!registration) return [];
|
|
9
|
+
return [
|
|
10
|
+
...new Set(hostBlocks(doc[registration.field]).flatMap((block)=>block.blockType === (registration.blockSlug ?? EXPERIMENT_BLOCK_SLUG) && typeof block.experiment === 'string' ? [
|
|
11
|
+
block.experiment
|
|
12
|
+
] : []))
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
if (editor !== 'puck') return [];
|
|
16
|
+
const registration = puckCollections(options).find((r)=>r.slug === collection);
|
|
17
|
+
const found = new Set();
|
|
18
|
+
function walk(v) {
|
|
19
|
+
if (Array.isArray(v)) {
|
|
20
|
+
v.forEach(walk);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!isObject(v)) return;
|
|
24
|
+
if (v.type === 'ExperimentSlot' && isObject(v.props)) {
|
|
25
|
+
const ref = v.props.experiment, slug = typeof ref === 'string' ? ref : isObject(ref) ? ref.slug : undefined;
|
|
26
|
+
if (typeof slug === 'string') found.add(slug);
|
|
27
|
+
}
|
|
28
|
+
Object.values(v).forEach(walk);
|
|
29
|
+
}
|
|
30
|
+
if (registration) walk(doc[registration.field]);
|
|
31
|
+
return [
|
|
32
|
+
...found
|
|
33
|
+
];
|
|
34
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { XP_BLOCK_STATUS, XP_BLOCK_ROW_LABEL, XP_COPY_ORIGINAL } from '../admin/paths.js';
|
|
2
|
+
import { BLOCK_SLOTS, EXPERIMENT_BLOCK_SLUG } from './model.js';
|
|
3
|
+
export function validateExperimentSlots(value) {
|
|
4
|
+
if (!Array.isArray(value)) return true;
|
|
5
|
+
const slots = value.map((row)=>row?.slot);
|
|
6
|
+
return new Set(slots).size === slots.length ? true : 'Each version appears once';
|
|
7
|
+
}
|
|
8
|
+
export function createExperimentBlock({ blocks, slug = EXPERIMENT_BLOCK_SLUG }) {
|
|
9
|
+
if (blocks.some((block)=>block.slug === slug)) throw new Error('Experiment blocks cannot be nested.');
|
|
10
|
+
return {
|
|
11
|
+
slug,
|
|
12
|
+
labels: {
|
|
13
|
+
singular: 'Experiment',
|
|
14
|
+
plural: 'Experiments'
|
|
15
|
+
},
|
|
16
|
+
fields: [
|
|
17
|
+
{
|
|
18
|
+
name: 'xpBlockStatus',
|
|
19
|
+
type: 'ui',
|
|
20
|
+
admin: {
|
|
21
|
+
components: {
|
|
22
|
+
Field: XP_BLOCK_STATUS
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'experiment',
|
|
28
|
+
label: 'Test',
|
|
29
|
+
type: 'text',
|
|
30
|
+
required: true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'slots',
|
|
34
|
+
label: 'Versions',
|
|
35
|
+
type: 'array',
|
|
36
|
+
validate: validateExperimentSlots,
|
|
37
|
+
admin: {
|
|
38
|
+
components: {
|
|
39
|
+
RowLabel: XP_BLOCK_ROW_LABEL
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
fields: [
|
|
43
|
+
{
|
|
44
|
+
name: 'xpCopyOriginal',
|
|
45
|
+
type: 'ui',
|
|
46
|
+
admin: {
|
|
47
|
+
components: {
|
|
48
|
+
Field: XP_COPY_ORIGINAL
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'slot',
|
|
54
|
+
label: 'Version',
|
|
55
|
+
type: 'select',
|
|
56
|
+
required: true,
|
|
57
|
+
options: BLOCK_SLOTS.map((value, index)=>({
|
|
58
|
+
value,
|
|
59
|
+
label: [
|
|
60
|
+
'Original',
|
|
61
|
+
'Variant A',
|
|
62
|
+
'Variant B',
|
|
63
|
+
'Variant C'
|
|
64
|
+
][index]
|
|
65
|
+
}))
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'blocks',
|
|
69
|
+
label: 'Content',
|
|
70
|
+
type: 'blocks',
|
|
71
|
+
blocks
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Block, Field, SanitizedConfig } from 'payload';
|
|
2
|
+
import { type ExperimentBlock, type HostBlock } from './model.js';
|
|
3
|
+
type RowConfig = {
|
|
4
|
+
blocks?: Block[];
|
|
5
|
+
localization?: SanitizedConfig['localization'];
|
|
6
|
+
};
|
|
7
|
+
type RowsField = Extract<Field, {
|
|
8
|
+
type: 'array' | 'blocks';
|
|
9
|
+
}>;
|
|
10
|
+
/** Mirror Payload 3.90.2's collections/operations/utilities/copyDataWithFreshRowIDs.js:
|
|
11
|
+
* walk configured row/subfield containers only. Mint ids for array/blocks rows, leaving
|
|
12
|
+
* opaque values (including json, richText, relationships and uploads) unchanged.
|
|
13
|
+
* Global block definitions resolve blockReferences; inline slot blocks retain host configs.
|
|
14
|
+
*/
|
|
15
|
+
export declare function freshBlockIds<T>(value: T, field: RowsField, config?: RowConfig, mint?: () => string): T;
|
|
16
|
+
export declare function wrapExperimentBlock(block: HostBlock, experiment: string, field: RowsField, config?: RowConfig, blockSlug?: string): ExperimentBlock;
|
|
17
|
+
export declare function syncExperimentSlots(block: ExperimentBlock, arms: readonly {
|
|
18
|
+
key: string;
|
|
19
|
+
kind?: string;
|
|
20
|
+
slot?: string | null;
|
|
21
|
+
}[], field: RowsField, config?: RowConfig): ExperimentBlock;
|
|
22
|
+
export declare function copyOriginalIntoVersion(block: ExperimentBlock, slot: string, field: RowsField, config?: RowConfig): ExperimentBlock;
|
|
23
|
+
export declare function unwrapExperimentBlock(block: HostBlock, field: RowsField, slot?: string, config?: RowConfig): HostBlock[];
|
|
24
|
+
/** Config coordinates include unnamed layout containers. No field component is injected here. */
|
|
25
|
+
export declare function findRegisteredBlocksField(fields: readonly Field[], name: string): {
|
|
26
|
+
field: Extract<Field, {
|
|
27
|
+
type: 'blocks';
|
|
28
|
+
}>;
|
|
29
|
+
path: Array<string | number>;
|
|
30
|
+
} | undefined;
|
|
31
|
+
export {};
|