@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/api.js
CHANGED
|
@@ -3,11 +3,14 @@ import { CONTENT_ENTRY_REFUSALS } from './copy/actions.js';
|
|
|
3
3
|
export function createApi(apiBase) {
|
|
4
4
|
const base = `${apiBase.replace(/\/$/, '')}/xp/experiments`;
|
|
5
5
|
const route = (id, action)=>`${base}/${encodeURIComponent(id)}/${action}`;
|
|
6
|
-
async function request(path, method, body, options) {
|
|
6
|
+
async function request(path, method, body, options, signal) {
|
|
7
7
|
try {
|
|
8
8
|
const response = await fetch(path(), {
|
|
9
9
|
method,
|
|
10
10
|
credentials: 'include',
|
|
11
|
+
...signal ? {
|
|
12
|
+
signal
|
|
13
|
+
} : {},
|
|
11
14
|
...body === undefined ? {} : {
|
|
12
15
|
headers: {
|
|
13
16
|
'Content-Type': 'application/json'
|
|
@@ -48,7 +51,7 @@ export function createApi(apiBase) {
|
|
|
48
51
|
code
|
|
49
52
|
} : {},
|
|
50
53
|
message: refusalMessage(code, response.status, typeof error?.message === 'string' ? error.message : undefined, options),
|
|
51
|
-
retryable: code === 'commit-unverified' || response.status >= 500 && response.status !== 501
|
|
54
|
+
retryable: code === 'page-busy' || code === 'commit-unverified' || response.status >= 500 && response.status !== 501
|
|
52
55
|
};
|
|
53
56
|
} catch {
|
|
54
57
|
return {
|
|
@@ -61,6 +64,20 @@ export function createApi(apiBase) {
|
|
|
61
64
|
}
|
|
62
65
|
const post = (id, action, body, options)=>request(()=>route(id, action), 'POST', body, options);
|
|
63
66
|
return {
|
|
67
|
+
wrapBlock: (body)=>request(()=>`${apiBase.replace(/\/$/, '')}/xp/blocks/wrap`, 'POST', body),
|
|
68
|
+
pages: (query, signal)=>request(()=>`${apiBase.replace(/\/$/, '')}/xp/pages?${new URLSearchParams({
|
|
69
|
+
collection: query.collection,
|
|
70
|
+
...query.q ? {
|
|
71
|
+
q: query.q
|
|
72
|
+
} : {},
|
|
73
|
+
...query.path !== undefined ? {
|
|
74
|
+
path: query.path
|
|
75
|
+
} : {},
|
|
76
|
+
...query.page ? {
|
|
77
|
+
page: String(query.page)
|
|
78
|
+
} : {}
|
|
79
|
+
})}`, 'GET', undefined, undefined, signal),
|
|
80
|
+
placementStatus: (body)=>request(()=>`${apiBase.replace(/\/$/, '')}/xp/placements/status`, 'POST', body),
|
|
64
81
|
start: (id)=>post(id, 'start'),
|
|
65
82
|
allocate: (id, body, options)=>post(id, 'allocate', body, options),
|
|
66
83
|
addMetric: (id, body, options)=>post(id, 'add-metric', body, options),
|
|
@@ -72,6 +89,7 @@ export function createApi(apiBase) {
|
|
|
72
89
|
dismissContentEntry: (id, body)=>post(id, 'content-report/dismiss', body, CONTENT_ENTRY_REFUSALS),
|
|
73
90
|
restoreContentEntry: (id, body)=>post(id, 'content-report/restore', body, CONTENT_ENTRY_REFUSALS),
|
|
74
91
|
archive: (id, body)=>post(id, 'archive', body),
|
|
92
|
+
clickKeys: (id)=>request(()=>route(id, 'click-keys'), 'GET'),
|
|
75
93
|
preflight: (id)=>request(()=>route(id, 'preflight'), 'GET'),
|
|
76
94
|
previewToken: (id, arm)=>request(()=>`${route(id, 'preview-token')}?arm=${encodeURIComponent(arm)}`, 'GET'),
|
|
77
95
|
results: (id, options = {})=>request(()=>{
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { fetchAllDocs } from '../../lib/fetchAllDocs.js';
|
|
3
4
|
import { Banner, Button, SelectInput, useConfig, useField } from '@payloadcms/ui';
|
|
4
5
|
import { useEffect, useState } from 'react';
|
|
5
6
|
import { bindArmSelect, experimentId } from './armSelect.model.js';
|
|
6
7
|
import { ARM_SELECT_COPY } from '../copy/armSelect.js';
|
|
8
|
+
/** Only authored copy reaches the field; transport or parse errors read as unavailable. */ const knownCopy = (reason)=>reason instanceof Error && Object.values(ARM_SELECT_COPY).includes(reason.message) ? reason.message : ARM_SELECT_COPY.unavailable;
|
|
7
9
|
export function ArmSelectField({ path, field, readOnly }) {
|
|
8
10
|
const { config } = useConfig();
|
|
9
11
|
const api = config.routes.api;
|
|
@@ -26,27 +28,17 @@ export function ArmSelectField({ path, field, readOnly }) {
|
|
|
26
28
|
if (!point) return;
|
|
27
29
|
const controller = new AbortController();
|
|
28
30
|
async function list(statuses) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
credentials: 'include',
|
|
41
|
-
signal: controller.signal
|
|
42
|
-
});
|
|
43
|
-
if (!response.ok) throw new Error(response.status === 403 ? ARM_SELECT_COPY.forbidden : ARM_SELECT_COPY.unavailable);
|
|
44
|
-
const data = await response.json();
|
|
45
|
-
docs.push(...data.docs);
|
|
46
|
-
if (!data.hasNextPage) break;
|
|
47
|
-
page++;
|
|
48
|
-
}
|
|
49
|
-
return docs;
|
|
31
|
+
const query = new URLSearchParams({
|
|
32
|
+
depth: '0',
|
|
33
|
+
'where[flagPoint][equals]': point,
|
|
34
|
+
'where[status][in]': statuses
|
|
35
|
+
});
|
|
36
|
+
const response = await fetchAllDocs(`${api}/xp-experiments?${query}`, {
|
|
37
|
+
credentials: 'include',
|
|
38
|
+
signal: controller.signal
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) throw new Error(response.status === 403 ? ARM_SELECT_COPY.forbidden : ARM_SELECT_COPY.unavailable);
|
|
41
|
+
return response.docs;
|
|
50
42
|
}
|
|
51
43
|
void Promise.all([
|
|
52
44
|
list('running,decided'),
|
|
@@ -63,7 +55,7 @@ export function ArmSelectField({ path, field, readOnly }) {
|
|
|
63
55
|
}).catch((reason)=>{
|
|
64
56
|
if (!controller.signal.aborted) setError({
|
|
65
57
|
key,
|
|
66
|
-
message: reason
|
|
58
|
+
message: knownCopy(reason)
|
|
67
59
|
});
|
|
68
60
|
});
|
|
69
61
|
return ()=>controller.abort();
|
|
@@ -1,21 +1 @@
|
|
|
1
|
-
|
|
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[];
|
|
1
|
+
export * from '../../arms/model.js';
|
|
@@ -1,153 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { GROUP_LABELS } from '../copy/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
|
-
}
|
|
1
|
+
export * from '../../arms/model.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { UIFieldClientProps } from 'payload';
|
|
2
|
+
export declare function BlockStatusField({ path }: UIFieldClientProps): import("react").JSX.Element | null;
|
|
3
|
+
export declare function BlockSlotRowLabel(): import("react").JSX.Element;
|
|
4
|
+
export declare function CopyOriginalField({ path }: UIFieldClientProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Banner, Button, ConfirmationModal, useModal, useFormInitializing, useRowLabel } from '@payloadcms/ui';
|
|
4
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
5
|
+
import { copyOriginalIntoVersion, syncExperimentSlots, unwrapExperimentBlock } from '../../blocks/formState.js';
|
|
6
|
+
import { BLOCK_COPY, blockSyncNotice } from '../copy/blocks.js';
|
|
7
|
+
import { API_MESSAGES } from '../copy/common.js';
|
|
8
|
+
import { isOrphan, liveStatus, slotLabel, statusSentence } from './model.js';
|
|
9
|
+
import { useBlockStatus } from './statusCache.js';
|
|
10
|
+
import { useBlockForm } from './useBlockForm.js';
|
|
11
|
+
export function BlockStatusField({ path }) {
|
|
12
|
+
const context = useBlockForm(path), { block, field, manager, config, collection, replace } = context;
|
|
13
|
+
const initializing = useFormInitializing();
|
|
14
|
+
const status = useBlockStatus(config.routes.api, block?.experiment, true);
|
|
15
|
+
const synced = useRef(false), syncing = useRef(false);
|
|
16
|
+
const [notice, setNotice] = useState(false), [error, setError] = useState(), [busy, setBusy] = useState(false);
|
|
17
|
+
const [syncFailed, setSyncFailed] = useState(false);
|
|
18
|
+
const canSync = !initializing && manager && block && field && liveStatus(status) && [
|
|
19
|
+
'draft',
|
|
20
|
+
'stopped'
|
|
21
|
+
].includes(status.status);
|
|
22
|
+
async function sync() {
|
|
23
|
+
if (!canSync || syncing.current) return;
|
|
24
|
+
syncing.current = true;
|
|
25
|
+
setBusy(true);
|
|
26
|
+
setSyncFailed(false);
|
|
27
|
+
try {
|
|
28
|
+
const next = syncExperimentSlots(block, status.arms, field);
|
|
29
|
+
if (JSON.stringify(next.slots) !== JSON.stringify(block.slots)) {
|
|
30
|
+
await replace([
|
|
31
|
+
next
|
|
32
|
+
]);
|
|
33
|
+
setNotice(true);
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
setSyncFailed(true);
|
|
37
|
+
} finally{
|
|
38
|
+
syncing.current = false;
|
|
39
|
+
setBusy(false);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
useEffect(()=>{
|
|
43
|
+
if (synced.current || !canSync) return;
|
|
44
|
+
synced.current = true;
|
|
45
|
+
void sync();
|
|
46
|
+
});
|
|
47
|
+
if (!status) return null;
|
|
48
|
+
async function unwrap() {
|
|
49
|
+
if (!block || !field || busy) return;
|
|
50
|
+
setBusy(true);
|
|
51
|
+
setError(undefined);
|
|
52
|
+
try {
|
|
53
|
+
await replace(unwrapExperimentBlock(block, field));
|
|
54
|
+
} catch {
|
|
55
|
+
setError(API_MESSAGES.failed);
|
|
56
|
+
} finally{
|
|
57
|
+
setBusy(false);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const sentence = statusSentence(status);
|
|
61
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
62
|
+
children: [
|
|
63
|
+
liveStatus(status) && status.status === 'running' ? /*#__PURE__*/ _jsx(Banner, {
|
|
64
|
+
children: sentence
|
|
65
|
+
}) : /*#__PURE__*/ _jsx("p", {
|
|
66
|
+
children: sentence
|
|
67
|
+
}),
|
|
68
|
+
notice ? /*#__PURE__*/ _jsx("p", {
|
|
69
|
+
role: "status",
|
|
70
|
+
children: blockSyncNotice(Boolean(collection?.versions?.drafts))
|
|
71
|
+
}) : null,
|
|
72
|
+
manager && isOrphan(status) ? /*#__PURE__*/ _jsx(Button, {
|
|
73
|
+
disabled: busy,
|
|
74
|
+
onClick: ()=>void unwrap(),
|
|
75
|
+
children: BLOCK_COPY.unwrap
|
|
76
|
+
}) : null,
|
|
77
|
+
error ? /*#__PURE__*/ _jsx("p", {
|
|
78
|
+
role: "alert",
|
|
79
|
+
children: error
|
|
80
|
+
}) : null,
|
|
81
|
+
syncFailed ? /*#__PURE__*/ _jsx("p", {
|
|
82
|
+
role: "alert",
|
|
83
|
+
children: API_MESSAGES.failed
|
|
84
|
+
}) : null,
|
|
85
|
+
syncFailed && canSync ? /*#__PURE__*/ _jsx(Button, {
|
|
86
|
+
disabled: busy,
|
|
87
|
+
onClick: ()=>void sync(),
|
|
88
|
+
children: BLOCK_COPY.retry
|
|
89
|
+
}) : null
|
|
90
|
+
]
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function BlockSlotRowLabel() {
|
|
94
|
+
const { data, path } = useRowLabel();
|
|
95
|
+
const { config, block } = useBlockForm(path);
|
|
96
|
+
const status = useBlockStatus(config.routes.api, block?.experiment);
|
|
97
|
+
return /*#__PURE__*/ _jsx("span", {
|
|
98
|
+
children: slotLabel(data?.slot ?? '', status)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export function CopyOriginalField({ path }) {
|
|
102
|
+
const { block, field, form, manager, config, replace } = useBlockForm(path);
|
|
103
|
+
const status = useBlockStatus(config.routes.api, block?.experiment);
|
|
104
|
+
const rowPath = path.slice(0, path.lastIndexOf('.'));
|
|
105
|
+
const slot = form.getDataByPath(`${rowPath}.slot`);
|
|
106
|
+
const [busy, setBusy] = useState(false), [error, setError] = useState();
|
|
107
|
+
const { openModal } = useModal();
|
|
108
|
+
const modalSlug = `xp-copy-original-${useId()}`;
|
|
109
|
+
if (!manager || !field || !block || !slot || slot === 'control' || !liveStatus(status) || ![
|
|
110
|
+
'draft',
|
|
111
|
+
'stopped',
|
|
112
|
+
'running'
|
|
113
|
+
].includes(status.status)) return null;
|
|
114
|
+
async function copy() {
|
|
115
|
+
if (!block || !field || busy || !slot) return;
|
|
116
|
+
setBusy(true);
|
|
117
|
+
setError(undefined);
|
|
118
|
+
try {
|
|
119
|
+
await replace([
|
|
120
|
+
copyOriginalIntoVersion(block, slot, field)
|
|
121
|
+
]);
|
|
122
|
+
} catch {
|
|
123
|
+
setError(API_MESSAGES.failed);
|
|
124
|
+
} finally{
|
|
125
|
+
setBusy(false);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function requestCopy() {
|
|
129
|
+
if (block?.slots.find((row)=>row.slot === slot)?.blocks?.length) openModal(modalSlug);
|
|
130
|
+
else void copy();
|
|
131
|
+
}
|
|
132
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
133
|
+
children: [
|
|
134
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
135
|
+
disabled: busy,
|
|
136
|
+
onClick: requestCopy,
|
|
137
|
+
children: BLOCK_COPY.copy
|
|
138
|
+
}),
|
|
139
|
+
/*#__PURE__*/ _jsx(ConfirmationModal, {
|
|
140
|
+
modalSlug: modalSlug,
|
|
141
|
+
heading: BLOCK_COPY.copy,
|
|
142
|
+
body: BLOCK_COPY.confirmCopy,
|
|
143
|
+
confirmLabel: BLOCK_COPY.replace,
|
|
144
|
+
cancelLabel: BLOCK_COPY.cancel,
|
|
145
|
+
onConfirm: copy
|
|
146
|
+
}),
|
|
147
|
+
error ? /*#__PURE__*/ _jsx("p", {
|
|
148
|
+
role: "alert",
|
|
149
|
+
children: error
|
|
150
|
+
}) : null
|
|
151
|
+
]
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Button, SelectInput, useAuth, useConfig, useDocumentInfo, useForm, useFormFields, useFormModified, useServerFunctions } from '@payloadcms/ui';
|
|
4
|
+
import { formatAdminURL } from 'payload/shared';
|
|
5
|
+
import { useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { hostBlocks } from '../../blocks/model.js';
|
|
7
|
+
import { wrapExperimentBlock } from '../../blocks/formState.js';
|
|
8
|
+
import { CreateRefused, createTest } from '../../puck/editor/createTest.js';
|
|
9
|
+
import { createApi } from '../api.js';
|
|
10
|
+
import { BLOCK_COPY, blockSaveNotice, blockTestName } from '../copy/blocks.js';
|
|
11
|
+
import { API_MESSAGES } from '../copy/common.js';
|
|
12
|
+
import { adaptClientBlocksField, BlockChangedError, dataAt, experimentBlockSlug, findClientBlocksField, replaceBlockRow, wrapMode } from './formApi.js';
|
|
13
|
+
import { blockChoices, labelText } from './model.js';
|
|
14
|
+
export function TestBlockField(props) {
|
|
15
|
+
const { permissions } = useAuth();
|
|
16
|
+
const testPermissions = permissions?.collections?.['xp-experiments'];
|
|
17
|
+
return testPermissions?.update === true ? /*#__PURE__*/ _jsx(ManagedTestBlockField, {
|
|
18
|
+
...props
|
|
19
|
+
}) : null;
|
|
20
|
+
}
|
|
21
|
+
function ManagedTestBlockField({ blocksField, blockSlug: suppliedSlug }) {
|
|
22
|
+
const form = useForm(), modified = useFormModified(), document = useDocumentInfo();
|
|
23
|
+
const { config, getEntityConfig } = useConfig(), { getFormState } = useServerFunctions();
|
|
24
|
+
useFormFields(([fields])=>fields);
|
|
25
|
+
const collection = document.collectionSlug ? getEntityConfig({
|
|
26
|
+
collectionSlug: document.collectionSlug
|
|
27
|
+
}) : undefined;
|
|
28
|
+
const clientField = collection ? findClientBlocksField(collection.fields, blocksField) : undefined;
|
|
29
|
+
const field = useMemo(()=>clientField ? adaptClientBlocksField(clientField, config) : undefined, [
|
|
30
|
+
clientField,
|
|
31
|
+
config
|
|
32
|
+
]);
|
|
33
|
+
const [choice, setChoice] = useState(), [busy, setBusy] = useState(false), [error, setError] = useState();
|
|
34
|
+
const wrapping = useRef(false);
|
|
35
|
+
if (!field || !collection || !document.collectionSlug) return null;
|
|
36
|
+
const blockSlug = suppliedSlug ?? experimentBlockSlug(field);
|
|
37
|
+
const rows = hostBlocks(form.getDataByPath(blocksField)), choices = blockChoices(rows, field, blockSlug);
|
|
38
|
+
const wrappers = rows.filter((block)=>block.blockType === blockSlug && typeof block.experiment === 'string');
|
|
39
|
+
async function wrap() {
|
|
40
|
+
if (!field || wrapping.current || !document.collectionSlug || !collection) return;
|
|
41
|
+
setError(undefined);
|
|
42
|
+
if (wrapMode === 'fallback' && modified) {
|
|
43
|
+
setError(BLOCK_COPY.saveFirst);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const selected = blockChoices(form.getDataByPath(blocksField), field, blockSlug).find((item)=>String(item.index) === choice);
|
|
47
|
+
if (!selected) return;
|
|
48
|
+
const title = collection.admin?.useAsTitle ? form.getDataByPath(collection.admin.useAsTitle) : undefined;
|
|
49
|
+
const name = blockTestName(typeof title === 'string' && title.trim() ? title : labelText(collection.labels.singular) ?? collection.slug, selected.label);
|
|
50
|
+
const beforeCreate = JSON.stringify(form.getData());
|
|
51
|
+
wrapping.current = true;
|
|
52
|
+
setBusy(true);
|
|
53
|
+
try {
|
|
54
|
+
const test = await createTest(config.routes.api, {
|
|
55
|
+
kind: 'block',
|
|
56
|
+
name,
|
|
57
|
+
unitType: 'visitor'
|
|
58
|
+
});
|
|
59
|
+
if (wrapMode === 'fallback') {
|
|
60
|
+
if (JSON.stringify(form.getData()) !== beforeCreate) {
|
|
61
|
+
setError(BLOCK_COPY.saveFirst);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (document.id == null || !selected.block.id) {
|
|
65
|
+
setError(BLOCK_COPY.wrapRefusals['wrap-target-missing']);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const result = await createApi(config.routes.api).wrapBlock({
|
|
69
|
+
collection: document.collectionSlug,
|
|
70
|
+
id: document.id,
|
|
71
|
+
field: blocksField,
|
|
72
|
+
blockId: selected.block.id,
|
|
73
|
+
experiment: test.slug
|
|
74
|
+
});
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
setError(result.code && Object.hasOwn(BLOCK_COPY.wrapRefusals, result.code) ? BLOCK_COPY.wrapRefusals[result.code] : API_MESSAGES.failed);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
window.location.reload();
|
|
80
|
+
} else {
|
|
81
|
+
await replaceBlockRow({
|
|
82
|
+
form,
|
|
83
|
+
getFormState,
|
|
84
|
+
collectionSlug: document.collectionSlug,
|
|
85
|
+
id: document.id,
|
|
86
|
+
field: blocksField,
|
|
87
|
+
index: selected.index,
|
|
88
|
+
expectedBlock: selected.block,
|
|
89
|
+
rows: [
|
|
90
|
+
wrapExperimentBlock(selected.block, test.slug, field, {}, blockSlug)
|
|
91
|
+
],
|
|
92
|
+
docPermissions: document.docPermissions,
|
|
93
|
+
docPreferences: await document.getDocPreferences?.()
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
setChoice(undefined);
|
|
97
|
+
} catch (cause) {
|
|
98
|
+
setError(cause instanceof CreateRefused || cause instanceof BlockChangedError ? cause.message : API_MESSAGES.failed);
|
|
99
|
+
} finally{
|
|
100
|
+
wrapping.current = false;
|
|
101
|
+
setBusy(false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
105
|
+
children: [
|
|
106
|
+
/*#__PURE__*/ _jsx("h3", {
|
|
107
|
+
children: BLOCK_COPY.testBlock
|
|
108
|
+
}),
|
|
109
|
+
choices.length ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
110
|
+
children: [
|
|
111
|
+
/*#__PURE__*/ _jsx(SelectInput, {
|
|
112
|
+
name: "xpBlockChoice",
|
|
113
|
+
path: "xpBlockChoice",
|
|
114
|
+
label: BLOCK_COPY.pick,
|
|
115
|
+
options: choices.map((item)=>({
|
|
116
|
+
label: item.option,
|
|
117
|
+
value: String(item.index)
|
|
118
|
+
})),
|
|
119
|
+
value: choice,
|
|
120
|
+
readOnly: busy,
|
|
121
|
+
onChange: (option)=>{
|
|
122
|
+
if (!Array.isArray(option)) setChoice(option?.value);
|
|
123
|
+
}
|
|
124
|
+
}),
|
|
125
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
126
|
+
disabled: busy || choice === undefined,
|
|
127
|
+
onClick: ()=>void wrap(),
|
|
128
|
+
children: BLOCK_COPY.testBlock
|
|
129
|
+
})
|
|
130
|
+
]
|
|
131
|
+
}) : /*#__PURE__*/ _jsx("p", {
|
|
132
|
+
children: BLOCK_COPY.empty
|
|
133
|
+
}),
|
|
134
|
+
error ? /*#__PURE__*/ _jsx("p", {
|
|
135
|
+
role: "alert",
|
|
136
|
+
children: error
|
|
137
|
+
}) : null,
|
|
138
|
+
wrappers.map((block)=>{
|
|
139
|
+
const slug = block.experiment;
|
|
140
|
+
const inSavedDocument = hostBlocks(dataAt(document.data, blocksField)).some((row)=>row.blockType === blockSlug && row.experiment === slug);
|
|
141
|
+
const saved = !modified && inSavedDocument;
|
|
142
|
+
return /*#__PURE__*/ _jsxs("p", {
|
|
143
|
+
children: [
|
|
144
|
+
!inSavedDocument ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
145
|
+
children: [
|
|
146
|
+
blockSaveNotice(Boolean(collection.versions?.drafts)),
|
|
147
|
+
' '
|
|
148
|
+
]
|
|
149
|
+
}) : null,
|
|
150
|
+
/*#__PURE__*/ _jsx("a", {
|
|
151
|
+
"aria-disabled": !saved,
|
|
152
|
+
href: saved ? formatAdminURL({
|
|
153
|
+
adminRoute: config.routes.admin,
|
|
154
|
+
path: `/experiments/by-slug/${encodeURIComponent(slug)}`
|
|
155
|
+
}) : undefined,
|
|
156
|
+
children: BLOCK_COPY.setup
|
|
157
|
+
})
|
|
158
|
+
]
|
|
159
|
+
}, block.id ?? slug);
|
|
160
|
+
})
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ClientConfig, ClientField, Field } from 'payload';
|
|
2
|
+
import type { useForm, useServerFunctions } from '@payloadcms/ui';
|
|
3
|
+
import { type HostBlock } from '../../blocks/model.js';
|
|
4
|
+
/** The only form-state failure with block-specific editor copy. */
|
|
5
|
+
export declare class BlockChangedError extends Error {
|
|
6
|
+
constructor();
|
|
7
|
+
}
|
|
8
|
+
export type RowsField = Extract<Field, {
|
|
9
|
+
type: 'blocks';
|
|
10
|
+
}>;
|
|
11
|
+
export declare const wrapMode: 'form-api' | 'fallback';
|
|
12
|
+
/** Client twin: unnamed layout containers do not contribute to a data path. */
|
|
13
|
+
export declare function findClientBlocksField(fields: readonly ClientField[], name: string): Extract<ClientField, {
|
|
14
|
+
type: 'blocks';
|
|
15
|
+
}> | undefined;
|
|
16
|
+
/** Only schema structure is needed by the pure helpers, never server validators.
|
|
17
|
+
* Resolve references at every depth, including inside the Experiment slots field.
|
|
18
|
+
*/
|
|
19
|
+
export declare function adaptClientBlocksField(field: Extract<ClientField, {
|
|
20
|
+
type: 'blocks';
|
|
21
|
+
}>, config: Pick<ClientConfig, 'blocks'>): RowsField;
|
|
22
|
+
export declare function experimentBlockSlug(field: RowsField): string;
|
|
23
|
+
export declare function rowLocation(path: string): {
|
|
24
|
+
field: string;
|
|
25
|
+
index: number;
|
|
26
|
+
} | undefined;
|
|
27
|
+
export declare function dataAt(data: unknown, path: string): unknown;
|
|
28
|
+
/** Payload 3.90.2 and 4 beta.6 use the same row APIs. Sync replaces the whole
|
|
29
|
+
* wrapper so add/drop/reorder and the server-rendered component slots stay together.
|
|
30
|
+
* N=0 needs UPDATE: removeFieldRow alone serializes the empty field as number 0.
|
|
31
|
+
*/
|
|
32
|
+
export declare function replaceBlockRow(args: {
|
|
33
|
+
form: ReturnType<typeof useForm>;
|
|
34
|
+
getFormState: ReturnType<typeof useServerFunctions>['getFormState'];
|
|
35
|
+
collectionSlug: string;
|
|
36
|
+
id?: string | number;
|
|
37
|
+
field: string;
|
|
38
|
+
index: number;
|
|
39
|
+
rows: HostBlock[];
|
|
40
|
+
expectedBlock?: HostBlock;
|
|
41
|
+
docPermissions?: Parameters<ReturnType<typeof useServerFunctions>['getFormState']>[0]['docPermissions'];
|
|
42
|
+
docPreferences?: Parameters<ReturnType<typeof useServerFunctions>['getFormState']>[0]['docPreferences'];
|
|
43
|
+
}): Promise<void>;
|