@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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { fetchAllDocs } from '../../lib/fetchAllDocs.js';
|
|
4
|
+
import { CLICK_KEY } from '../key.js';
|
|
5
|
+
import { clickGoalBody, clickGoalCreateError, clickGoalSlug, definedClickGoals, undefinedClickKey } from './clickGoalPicker.model.js';
|
|
6
|
+
const goalLists = new Map();
|
|
7
|
+
function publish(list, state) {
|
|
8
|
+
// A read may finish after its last row unmounts and a new list has started.
|
|
9
|
+
if (!list.listeners.length || goalLists.get(state.api) !== list) return;
|
|
10
|
+
list.state = state;
|
|
11
|
+
list.listeners.forEach((listener)=>listener(state));
|
|
12
|
+
}
|
|
13
|
+
/** Mounted rows at the same API base share the in-flight read and its result. */ function loadGoals(api) {
|
|
14
|
+
const cached = goalLists.get(api);
|
|
15
|
+
if (cached && (cached.state.status === 'loading' || cached.state.status === 'ready')) return cached;
|
|
16
|
+
const list = cached ?? {
|
|
17
|
+
state: {
|
|
18
|
+
api,
|
|
19
|
+
status: 'loading',
|
|
20
|
+
goals: []
|
|
21
|
+
},
|
|
22
|
+
listeners: []
|
|
23
|
+
};
|
|
24
|
+
if (cached) publish(list, {
|
|
25
|
+
api,
|
|
26
|
+
status: 'loading',
|
|
27
|
+
goals: []
|
|
28
|
+
});
|
|
29
|
+
goalLists.set(api, list);
|
|
30
|
+
void (async ()=>{
|
|
31
|
+
try {
|
|
32
|
+
const result = await fetchAllDocs(`${api}/xp-metrics?where[source][like]=click:&depth=0`, {
|
|
33
|
+
credentials: 'include'
|
|
34
|
+
});
|
|
35
|
+
if (result.ok) {
|
|
36
|
+
publish(list, {
|
|
37
|
+
api,
|
|
38
|
+
status: 'ready',
|
|
39
|
+
goals: definedClickGoals(result.docs)
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
publish(list, {
|
|
44
|
+
api,
|
|
45
|
+
status: result.status === 403 ? 'not-manager' : 'error',
|
|
46
|
+
goals: []
|
|
47
|
+
});
|
|
48
|
+
} catch {
|
|
49
|
+
publish(list, {
|
|
50
|
+
api,
|
|
51
|
+
status: 'error',
|
|
52
|
+
goals: []
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
return list;
|
|
57
|
+
}
|
|
58
|
+
const emptyPanel = ()=>({
|
|
59
|
+
open: false,
|
|
60
|
+
name: '',
|
|
61
|
+
slug: '',
|
|
62
|
+
slugEdited: false,
|
|
63
|
+
advanced: false,
|
|
64
|
+
pending: false,
|
|
65
|
+
errors: {}
|
|
66
|
+
});
|
|
67
|
+
/** UI-free state shared by the two editor hosts. A refused read silently removes management. */ export function useClickGoalPicker(apiRoute, { enabled = true, onSelect }) {
|
|
68
|
+
const api = apiRoute.replace(/\/$/, '');
|
|
69
|
+
const [loaded, setLoaded] = useState({
|
|
70
|
+
api,
|
|
71
|
+
status: 'loading',
|
|
72
|
+
goals: []
|
|
73
|
+
});
|
|
74
|
+
const [panel, setPanel] = useState(emptyPanel);
|
|
75
|
+
const flight = useRef(false);
|
|
76
|
+
const binding = useRef({
|
|
77
|
+
api,
|
|
78
|
+
enabled,
|
|
79
|
+
live: true,
|
|
80
|
+
onSelect
|
|
81
|
+
});
|
|
82
|
+
binding.current.api = api;
|
|
83
|
+
binding.current.enabled = enabled;
|
|
84
|
+
binding.current.onSelect = onSelect;
|
|
85
|
+
useEffect(()=>{
|
|
86
|
+
binding.current.live = true;
|
|
87
|
+
return ()=>{
|
|
88
|
+
binding.current.live = false;
|
|
89
|
+
};
|
|
90
|
+
}, []);
|
|
91
|
+
useEffect(()=>{
|
|
92
|
+
if (!enabled) return;
|
|
93
|
+
setPanel(emptyPanel());
|
|
94
|
+
const list = loadGoals(api);
|
|
95
|
+
setLoaded(list.state);
|
|
96
|
+
list.listeners.push(setLoaded);
|
|
97
|
+
return ()=>{
|
|
98
|
+
list.listeners = list.listeners.filter((listener)=>listener !== setLoaded);
|
|
99
|
+
if (!list.listeners.length && goalLists.get(api) === list) goalLists.delete(api);
|
|
100
|
+
};
|
|
101
|
+
}, [
|
|
102
|
+
api,
|
|
103
|
+
enabled
|
|
104
|
+
]);
|
|
105
|
+
const status = !enabled ? 'not-manager' : loaded.api === api ? loaded.status : 'loading';
|
|
106
|
+
const goals = status === 'ready' ? loaded.goals : [];
|
|
107
|
+
const canManage = status === 'ready';
|
|
108
|
+
async function create() {
|
|
109
|
+
if (!canManage || !panel.open || flight.current || !panel.name.trim()) return;
|
|
110
|
+
if (!CLICK_KEY.test(panel.slug)) {
|
|
111
|
+
setPanel((current)=>({
|
|
112
|
+
...current,
|
|
113
|
+
advanced: true,
|
|
114
|
+
errors: clickGoalCreateError(panel.slug)
|
|
115
|
+
}));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
flight.current = true;
|
|
119
|
+
setPanel((current)=>({
|
|
120
|
+
...current,
|
|
121
|
+
pending: true,
|
|
122
|
+
errors: {}
|
|
123
|
+
}));
|
|
124
|
+
const current = ()=>binding.current.live && binding.current.enabled && binding.current.api === api;
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetch(`${api}/xp-metrics`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
credentials: 'include',
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'application/json'
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify(clickGoalBody(panel.name, panel.slug))
|
|
133
|
+
});
|
|
134
|
+
const body = await response.json().catch(()=>undefined);
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
if (!current()) return;
|
|
137
|
+
if (response.status === 403) {
|
|
138
|
+
setLoaded({
|
|
139
|
+
api,
|
|
140
|
+
status: 'not-manager',
|
|
141
|
+
goals: []
|
|
142
|
+
});
|
|
143
|
+
setPanel(emptyPanel());
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const errors = clickGoalCreateError(panel.slug, body);
|
|
147
|
+
setPanel((value)=>({
|
|
148
|
+
...value,
|
|
149
|
+
pending: false,
|
|
150
|
+
advanced: value.advanced || Boolean(errors.slug),
|
|
151
|
+
errors
|
|
152
|
+
}));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const doc = body?.doc;
|
|
156
|
+
if (!doc || typeof doc.slug !== 'string' || typeof doc.name !== 'string' || !isFiniteId(doc.id)) throw new Error('response');
|
|
157
|
+
const list = goalLists.get(api);
|
|
158
|
+
if (list) publish(list, {
|
|
159
|
+
api,
|
|
160
|
+
status: 'ready',
|
|
161
|
+
goals: [
|
|
162
|
+
...list.state.goals.filter((goal)=>goal.id !== doc.id),
|
|
163
|
+
doc
|
|
164
|
+
]
|
|
165
|
+
});
|
|
166
|
+
if (!current()) return;
|
|
167
|
+
binding.current.onSelect(doc.slug);
|
|
168
|
+
setPanel(emptyPanel());
|
|
169
|
+
} catch {
|
|
170
|
+
if (current()) setPanel((value)=>({
|
|
171
|
+
...value,
|
|
172
|
+
pending: false,
|
|
173
|
+
errors: clickGoalCreateError(panel.slug)
|
|
174
|
+
}));
|
|
175
|
+
} finally{
|
|
176
|
+
flight.current = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
status,
|
|
181
|
+
goals,
|
|
182
|
+
canManage,
|
|
183
|
+
undefinedKey: (value)=>undefinedClickKey(status, goals, value),
|
|
184
|
+
panel,
|
|
185
|
+
open: ()=>{
|
|
186
|
+
if (canManage) setPanel({
|
|
187
|
+
...emptyPanel(),
|
|
188
|
+
open: true
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
cancel: ()=>{
|
|
192
|
+
if (!flight.current) setPanel(emptyPanel());
|
|
193
|
+
},
|
|
194
|
+
setName: (name)=>setPanel((value)=>{
|
|
195
|
+
const slug = value.slugEdited ? value.slug : clickGoalSlug(name);
|
|
196
|
+
const invalid = !value.slugEdited && !CLICK_KEY.test(slug);
|
|
197
|
+
return {
|
|
198
|
+
...value,
|
|
199
|
+
name,
|
|
200
|
+
slug,
|
|
201
|
+
advanced: value.advanced || invalid,
|
|
202
|
+
errors: invalid ? clickGoalCreateError(slug) : {}
|
|
203
|
+
};
|
|
204
|
+
}),
|
|
205
|
+
setSlug: (slug)=>setPanel((value)=>({
|
|
206
|
+
...value,
|
|
207
|
+
slug,
|
|
208
|
+
slugEdited: true,
|
|
209
|
+
errors: {}
|
|
210
|
+
})),
|
|
211
|
+
setAdvanced: (advanced)=>setPanel((value)=>({
|
|
212
|
+
...value,
|
|
213
|
+
advanced
|
|
214
|
+
})),
|
|
215
|
+
create
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function isFiniteId(id) {
|
|
219
|
+
return typeof id === 'string' || typeof id === 'number' && Number.isFinite(id);
|
|
220
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type { CollectionAfterOperationHook, CollectionBeforeChangeHook, CollectionBeforeDeleteHook, CollectionBeforeOperationHook } from 'payload';
|
|
1
|
+
import type { CollectionAfterChangeHook, CollectionAfterOperationHook, CollectionBeforeChangeHook, CollectionBeforeDeleteHook, CollectionBeforeOperationHook } from 'payload';
|
|
2
|
+
import { type PlacementTest } from '../db/placements.js';
|
|
2
3
|
export declare const pageGuardBeforeOperation: CollectionBeforeOperationHook;
|
|
3
4
|
export declare const pageGuardAfterOperation: CollectionAfterOperationHook;
|
|
5
|
+
export declare function guardExperimentIds(tests: readonly PlacementTest[], slugs: readonly string[], collection: string, paths: readonly string[], held: unknown, tags?: Set<string>): string[];
|
|
4
6
|
export declare const pageGuardBeforeDelete: CollectionBeforeDeleteHook;
|
|
5
7
|
export declare const pageGuardBeforeChange: CollectionBeforeChangeHook;
|
|
8
|
+
/** Unwrapped: these writes must fail the publish closed, unlike the best-effort audit. */
|
|
9
|
+
export declare const pageGuardAfterChange: CollectionAfterChangeHook;
|
package/dist/hooks/pageGuard.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { previousPublishedContent } from '../db/appends.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { previousPublishedContent, storedContent } from '../db/appends.js';
|
|
2
|
+
import { lockPlacementScope, sortLockKeys } from '../db/locks.js';
|
|
3
|
+
import { txExecutor, pooledExecutor } from '../db/executor.js';
|
|
4
|
+
import { normalisePath } from '../runtime/normalisePath.js';
|
|
5
|
+
import { XpNoTransactionError, XpRefusal } from '../errors.js';
|
|
6
|
+
import { placementGuardCopy, pageGuardCopy, READINESS_COPY } from '../admin/copy/readiness.js';
|
|
7
|
+
import { placementSlugs } from '../blocks/bindings.js';
|
|
8
|
+
import { isObject } from '../blocks/model.js';
|
|
9
|
+
import { placementTests, appendPlacementDoc } from '../db/placements.js';
|
|
10
|
+
import { isPageBusy } from '../db/pageBusy.js';
|
|
4
11
|
// createLocalReq replaces context with a shallow copy on nested calls. Keep
|
|
5
12
|
// the shared stack in that context so both the outer req and its proxies see it.
|
|
6
13
|
const operations = (req)=>req.context.xpPageGuardOperations;
|
|
7
14
|
export const pageGuardBeforeOperation = ({ args, operation })=>{
|
|
8
|
-
if (operation !== 'update' && operation !== 'restoreVersion') return;
|
|
15
|
+
if (operation !== 'create' && operation !== 'update' && operation !== 'restoreVersion') return;
|
|
9
16
|
const call = args;
|
|
10
17
|
let collections = operations(call.req);
|
|
11
18
|
if (!collections) call.req.context.xpPageGuardOperations = collections = new Map();
|
|
@@ -19,7 +26,7 @@ export const pageGuardBeforeOperation = ({ args, operation })=>{
|
|
|
19
26
|
};
|
|
20
27
|
export const pageGuardAfterOperation = ({ req, collection, operation, result })=>{
|
|
21
28
|
// beforeOperation reports by-id updates as 'update'; afterOperation reports 'updateByID'.
|
|
22
|
-
if (operation === 'update' || operation === 'updateByID' || operation === 'restoreVersion') {
|
|
29
|
+
if (operation === 'create' || operation === 'update' || operation === 'updateByID' || operation === 'restoreVersion') {
|
|
23
30
|
const collections = operations(req);
|
|
24
31
|
const stack = collections?.get(collection.slug);
|
|
25
32
|
stack?.pop();
|
|
@@ -27,95 +34,168 @@ export const pageGuardAfterOperation = ({ req, collection, operation, result })=
|
|
|
27
34
|
}
|
|
28
35
|
return result;
|
|
29
36
|
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
37
|
+
function optionsOf(req) {
|
|
38
|
+
return req.payload.config.custom?.xp;
|
|
39
|
+
}
|
|
40
|
+
function registration(req, slug) {
|
|
41
|
+
return optionsOf(req).pages?.collections.find((entry)=>entry.slug === slug);
|
|
42
|
+
}
|
|
43
|
+
const active = (test)=>test.status === 'running' || test.status === 'decided';
|
|
44
|
+
const pathsOf = (req, collection, rows)=>{
|
|
45
|
+
const page = registration(req, collection);
|
|
46
|
+
return page ? sortLockKeys(rows.flatMap((row)=>{
|
|
47
|
+
const path = row ? page.pathOf(row) : null;
|
|
48
|
+
return path === null ? [] : [
|
|
49
|
+
normalisePath(path)
|
|
50
|
+
];
|
|
51
|
+
})) : [];
|
|
52
|
+
};
|
|
53
|
+
/** The later audit also recognises arm-select tags; include those ids in its lock superset. */ function taggedIds(value, ids = new Set()) {
|
|
54
|
+
if (Array.isArray(value)) value.forEach((v)=>taggedIds(v, ids));
|
|
55
|
+
else if (isObject(value)) {
|
|
56
|
+
if (typeof value.experiment === 'string' && typeof value.arm === 'string') ids.add(value.experiment);
|
|
57
|
+
Object.values(value).forEach((v)=>taggedIds(v, ids));
|
|
58
|
+
}
|
|
59
|
+
return ids;
|
|
60
|
+
}
|
|
61
|
+
export function guardExperimentIds(tests, slugs, collection, paths, held, tags = new Set()) {
|
|
62
|
+
const excluded = new Set(Array.isArray(held) ? held.map(String) : []);
|
|
63
|
+
return sortLockKeys(tests.filter((test)=>slugs.includes(test.slug) || tags.has(String(test.id)) || test.kind === 'page' && (!test.pageCollection || test.pageCollection === collection) && test.arms.some((a)=>typeof a.pageTarget === 'string' && paths.includes(normalisePath(a.pageTarget)))).map((test)=>String(test.id)).filter((id)=>!excluded.has(id)));
|
|
64
|
+
}
|
|
65
|
+
function protect(tests, path) {
|
|
66
|
+
for (const test of tests){
|
|
67
|
+
if (test.kind !== 'page' || !active(test) && test.status !== 'archived') continue;
|
|
59
68
|
const archived = test.status === 'archived';
|
|
60
|
-
if (archived && (!
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
limit: 1,
|
|
68
|
-
sort: '-phase',
|
|
69
|
-
where: {
|
|
70
|
-
experiment: {
|
|
71
|
-
equals: test.id
|
|
72
|
-
},
|
|
73
|
-
phaseKind: {
|
|
74
|
-
equals: 'test'
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
const phase = phases.docs[0];
|
|
79
|
-
if (!phase) throw new XpRefusal('invalid-phase', READINESS_COPY.missingVersions, {
|
|
69
|
+
if (archived && (!test.winnerArm || test.discardedAt || test.pagesReleasedAt)) continue;
|
|
70
|
+
if (!test.arms.length) throw new XpRefusal('invalid-phase', READINESS_COPY.missingVersions, {
|
|
71
|
+
experimentId: test.id
|
|
72
|
+
}, 409);
|
|
73
|
+
const arms = archived ? test.arms.filter((a)=>a.kind === 'control' || a.key === test.winnerArm) : test.arms;
|
|
74
|
+
const matched = arms.find((a)=>typeof a.pageTarget === 'string' && normalisePath(a.pageTarget) === path);
|
|
75
|
+
if (matched) throw new XpRefusal('locked-placement', pageGuardCopy(test.name, test.status, archived && matched.key !== test.winnerArm ? 'original' : 'winner'), {
|
|
80
76
|
experimentId: test.id
|
|
81
77
|
}, 409);
|
|
82
|
-
const arms = phase.arms;
|
|
83
|
-
const protectedArms = archived ? arms.filter((arm)=>arm.kind === 'control' || arm.key === decision.winnerArm) : arms;
|
|
84
|
-
if (protectedArms.some((arm)=>arm.pageTarget === path)) {
|
|
85
|
-
throw new XpRefusal('locked-placement', pageGuardCopy(String(test.name), test.status), {
|
|
86
|
-
experimentId: test.id
|
|
87
|
-
}, 409);
|
|
88
|
-
}
|
|
89
78
|
}
|
|
90
79
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
80
|
+
function refuseInteraction(tests, beforeSlugs, afterSlugs, beforePaths, afterPaths, collection) {
|
|
81
|
+
const blocks = tests.filter((test)=>test.kind === 'block' && active(test) && afterSlugs.includes(test.slug));
|
|
82
|
+
const pages = tests.filter((test)=>test.kind === 'page' && active(test) && (!test.pageCollection || test.pageCollection === collection));
|
|
83
|
+
for (const page of pages){
|
|
84
|
+
if (!page.arms.some((a)=>typeof a.pageTarget === 'string' && afterPaths.includes(normalisePath(a.pageTarget)))) continue;
|
|
85
|
+
const block = blocks[0];
|
|
86
|
+
if (!block) continue;
|
|
87
|
+
const preExisting = beforeSlugs.includes(block.slug) && page.arms.some((a)=>typeof a.pageTarget === 'string' && beforePaths.includes(normalisePath(a.pageTarget)));
|
|
88
|
+
throw new XpRefusal('interacting-tests', placementGuardCopy(page, block, preExisting), {
|
|
89
|
+
experimentId: page.id,
|
|
90
|
+
blockId: block.id
|
|
91
|
+
}, 409);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function mapBusy(error) {
|
|
95
|
+
if (isPageBusy(error)) throw new XpRefusal('page-busy', 'This page is being updated by a test right now. Try again in a moment.', undefined, 409, {
|
|
96
|
+
cause: error
|
|
97
|
+
});
|
|
98
|
+
throw error;
|
|
93
99
|
}
|
|
94
100
|
export const pageGuardBeforeDelete = async ({ req, id, collection })=>{
|
|
95
101
|
const page = registration(req, collection.slug);
|
|
96
102
|
if (!page) return;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
try {
|
|
104
|
+
const published = await previousPublishedContent(req, collection.slug, id);
|
|
105
|
+
if (!published || published._status === 'draft') return;
|
|
106
|
+
const paths = pathsOf(req, collection.slug, [
|
|
107
|
+
published
|
|
108
|
+
]);
|
|
109
|
+
const exec = await req.transactionID != null ? txExecutor(req) : pooledExecutor(req.payload);
|
|
110
|
+
let tests = await placementTests(exec);
|
|
111
|
+
if (await req.transactionID != null) {
|
|
112
|
+
await lockPlacementScope(exec, guardExperimentIds(tests, placementSlugs(optionsOf(req), collection.slug, published), collection.slug, paths, req.context.xpHeldExperiments, taggedIds(published)), paths);
|
|
113
|
+
tests = await placementTests(exec);
|
|
114
|
+
}
|
|
115
|
+
for (const path of paths)protect(tests.filter((t)=>!t.pageCollection || t.pageCollection === collection.slug), path);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
mapBusy(error);
|
|
118
|
+
}
|
|
101
119
|
};
|
|
102
120
|
export const pageGuardBeforeChange = async ({ req, collection, originalDoc, operation, data })=>{
|
|
103
|
-
if (operation !== 'update') return data;
|
|
104
|
-
const page = registration(req, collection.slug);
|
|
105
|
-
if (!page) return data;
|
|
106
121
|
const drafts = Boolean(collection.versions && collection.versions.drafts);
|
|
107
|
-
// updateDocument computes isSavingDraft from draftArg and _status before
|
|
108
|
-
// beforeChange, and writes the main row only when !isSavingDraft.
|
|
109
122
|
const call = operations(req)?.get(collection.slug)?.at(-1);
|
|
110
123
|
if (drafts && call?.draft && (call.restore || data._status !== 'published')) return data;
|
|
111
|
-
const
|
|
112
|
-
if (!published || published._status === 'draft') return data;
|
|
113
|
-
const path = page.pathOf(published);
|
|
114
|
-
const unpublishing = drafts && data._status === 'draft';
|
|
115
|
-
const trashing = collection.trash && data.deletedAt != null && !originalDoc.deletedAt;
|
|
116
|
-
if (path !== null && (trashing || unpublishing || page.pathOf({
|
|
124
|
+
const after = operation === 'create' ? data : {
|
|
117
125
|
...originalDoc,
|
|
118
126
|
...data
|
|
119
|
-
}
|
|
120
|
-
|
|
127
|
+
};
|
|
128
|
+
const options = optionsOf(req);
|
|
129
|
+
try {
|
|
130
|
+
const published = operation === 'create' ? undefined : await previousPublishedContent(req, collection.slug, originalDoc.id);
|
|
131
|
+
const before = published?._status === 'draft' ? undefined : published;
|
|
132
|
+
const beforeSlugs = placementSlugs(options, collection.slug, before);
|
|
133
|
+
const afterSlugs = placementSlugs(options, collection.slug, after);
|
|
134
|
+
const beforePaths = pathsOf(req, collection.slug, [
|
|
135
|
+
before
|
|
136
|
+
]), afterPaths = pathsOf(req, collection.slug, [
|
|
137
|
+
after
|
|
138
|
+
]);
|
|
139
|
+
const paths = sortLockKeys([
|
|
140
|
+
...beforePaths,
|
|
141
|
+
...afterPaths
|
|
142
|
+
]);
|
|
143
|
+
const exec = await req.transactionID != null ? txExecutor(req) : pooledExecutor(req.payload);
|
|
144
|
+
let tests = await placementTests(exec);
|
|
145
|
+
const publishes = !drafts || after._status === 'published';
|
|
146
|
+
const noTransaction = ()=>{
|
|
147
|
+
if (publishes && tests.some((t)=>t.kind === 'block' && active(t) && afterSlugs.includes(t.slug))) throw new XpNoTransactionError('Save this page with transactions on.');
|
|
148
|
+
};
|
|
149
|
+
if (await req.transactionID == null) noTransaction();
|
|
150
|
+
else {
|
|
151
|
+
try {
|
|
152
|
+
await lockPlacementScope(exec, guardExperimentIds(tests, [
|
|
153
|
+
...beforeSlugs,
|
|
154
|
+
...afterSlugs
|
|
155
|
+
], collection.slug, paths, req.context.xpHeldExperiments, taggedIds([
|
|
156
|
+
before,
|
|
157
|
+
after
|
|
158
|
+
])), paths);
|
|
159
|
+
tests = await placementTests(exec);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (!(error instanceof XpNoTransactionError)) throw error;
|
|
162
|
+
// Only a session lost after discovery reaches this fallback. The first discovery
|
|
163
|
+
// read and delete have no fallback: a dead session fails those operations closed.
|
|
164
|
+
tests = await placementTests(pooledExecutor(req.payload));
|
|
165
|
+
noTransaction();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (operation === 'update' && before) {
|
|
169
|
+
const page = registration(req, collection.slug);
|
|
170
|
+
const old = page?.pathOf(before);
|
|
171
|
+
const unpublishing = drafts && data._status === 'draft';
|
|
172
|
+
const trashing = collection.trash && data.deletedAt != null && !originalDoc.deletedAt;
|
|
173
|
+
if (old != null && (unpublishing || trashing || page.pathOf(after) !== old)) protect(tests.filter((t)=>!t.pageCollection || t.pageCollection === collection.slug), normalisePath(old));
|
|
174
|
+
}
|
|
175
|
+
if (publishes) refuseInteraction(tests, beforeSlugs, afterSlugs, beforePaths, afterPaths, collection.slug);
|
|
176
|
+
return data;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
mapBusy(error);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
/** Unwrapped: these writes must fail the publish closed, unlike the best-effort audit. */ export const pageGuardAfterChange = async ({ req, collection, doc })=>{
|
|
182
|
+
const call = operations(req)?.get(collection.slug)?.at(-1);
|
|
183
|
+
const drafts = Boolean(collection.versions && collection.versions.drafts);
|
|
184
|
+
if (drafts && (doc._status !== 'published' || call?.draft && call.restore)) return doc;
|
|
185
|
+
if (await req.transactionID == null) return doc;
|
|
186
|
+
try {
|
|
187
|
+
const stored = await storedContent(req, collection.slug, doc.id);
|
|
188
|
+
if (!stored) throw new Error('The saved placement row was not found.');
|
|
189
|
+
const slugs = placementSlugs(optionsOf(req), collection.slug, stored);
|
|
190
|
+
const exec = txExecutor(req);
|
|
191
|
+
for (const test of (await placementTests(exec))){
|
|
192
|
+
if (test.kind === 'block' && active(test) && slugs.includes(test.slug)) await appendPlacementDoc(exec, test.id, test.phase, {
|
|
193
|
+
collection: collection.slug,
|
|
194
|
+
id: doc.id
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return doc;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
mapBusy(error);
|
|
200
|
+
}
|
|
121
201
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type DocsResult<T> = {
|
|
2
|
+
ok: true;
|
|
3
|
+
docs: T[];
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
status: number;
|
|
7
|
+
body: unknown;
|
|
8
|
+
reason?: 'page-limit-exceeded';
|
|
9
|
+
};
|
|
10
|
+
/** A successful REST response did not contain a readable document list. */
|
|
11
|
+
export declare class DocsResponseError extends Error {
|
|
12
|
+
}
|
|
13
|
+
/** Read every Payload REST page with the caller's credentials and cancellation. Transport,
|
|
14
|
+
* invalid successful responses and aborts throw; HTTP refusals retain their status/body. Keeps the first document per id; refuses
|
|
15
|
+
* incomplete lists after 1000 pages with page-limit-exceeded. */
|
|
16
|
+
export declare function fetchAllDocs<T>(url: string, options?: Pick<RequestInit, 'credentials' | 'signal'>): Promise<DocsResult<T>>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** A successful REST response did not contain a readable document list. */ export class DocsResponseError extends Error {
|
|
2
|
+
}
|
|
3
|
+
/** Read every Payload REST page with the caller's credentials and cancellation. Transport,
|
|
4
|
+
* invalid successful responses and aborts throw; HTTP refusals retain their status/body. Keeps the first document per id; refuses
|
|
5
|
+
* incomplete lists after 1000 pages with page-limit-exceeded. */ export async function fetchAllDocs(url, options = {}) {
|
|
6
|
+
const [path, query] = url.split('?');
|
|
7
|
+
const params = new URLSearchParams(query);
|
|
8
|
+
params.set('limit', '100');
|
|
9
|
+
const docs = [];
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
for(let page = 1; page <= 1000; page++){
|
|
12
|
+
options.signal?.throwIfAborted();
|
|
13
|
+
params.set('page', String(page));
|
|
14
|
+
const response = await fetch(`${path}?${params}`, options);
|
|
15
|
+
const body = await response.json().catch(()=>{
|
|
16
|
+
if (response.ok) throw new DocsResponseError();
|
|
17
|
+
return undefined;
|
|
18
|
+
});
|
|
19
|
+
options.signal?.throwIfAborted();
|
|
20
|
+
if (!response.ok) return {
|
|
21
|
+
ok: false,
|
|
22
|
+
status: response.status,
|
|
23
|
+
body
|
|
24
|
+
};
|
|
25
|
+
const data = body;
|
|
26
|
+
if (!Array.isArray(data?.docs)) throw new DocsResponseError();
|
|
27
|
+
for (const doc of data.docs){
|
|
28
|
+
const id = doc && typeof doc === 'object' && 'id' in doc ? doc.id : undefined;
|
|
29
|
+
if (typeof id === 'string' || typeof id === 'number') {
|
|
30
|
+
if (seen.has(id)) continue;
|
|
31
|
+
seen.add(id);
|
|
32
|
+
}
|
|
33
|
+
docs.push(doc);
|
|
34
|
+
}
|
|
35
|
+
if (data.docs.length === 0) return {
|
|
36
|
+
ok: true,
|
|
37
|
+
docs
|
|
38
|
+
};
|
|
39
|
+
if (typeof data.totalPages === 'number' && page >= data.totalPages) return {
|
|
40
|
+
ok: true,
|
|
41
|
+
docs
|
|
42
|
+
};
|
|
43
|
+
if (data.hasNextPage === false || data.hasNextPage !== true && !(typeof data.totalPages === 'number' && page < data.totalPages)) {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
docs
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
status: 0,
|
|
53
|
+
body: undefined,
|
|
54
|
+
reason: 'page-limit-exceeded'
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/onInit.js
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
1
|
+
import { findRegisteredBlocksField } from './blocks/formState.js';
|
|
2
|
+
import { EXPERIMENT_BLOCK_SLUG } from './blocks/model.js';
|
|
3
|
+
import { contentBlocks } from './transitions/contentFields.js';
|
|
1
4
|
import { validateDimensions } from './events/dimensions.js';
|
|
2
5
|
import { assertPostgresAdapter, probeTransactions } from './db/adapter.js';
|
|
3
6
|
import { hasHostAttributesOf } from './server/serverRuntime.js';
|
|
7
|
+
// Diagnostic only: named containers are never accepted by the top-level readers.
|
|
8
|
+
function registeredPaths(fields, name, parent = []) {
|
|
9
|
+
return fields.flatMap((field)=>{
|
|
10
|
+
const path = 'name' in field && field.name ? [
|
|
11
|
+
...parent,
|
|
12
|
+
field.name
|
|
13
|
+
] : parent;
|
|
14
|
+
if (field.type === 'blocks' && field.name === name) return [
|
|
15
|
+
path.join('.')
|
|
16
|
+
];
|
|
17
|
+
if (field.type === 'tabs') return field.tabs.flatMap((tab)=>registeredPaths(tab.fields, name, 'name' in tab && tab.name ? [
|
|
18
|
+
...path,
|
|
19
|
+
tab.name
|
|
20
|
+
] : path));
|
|
21
|
+
return 'fields' in field ? registeredPaths(field.fields, name, path) : [];
|
|
22
|
+
});
|
|
23
|
+
}
|
|
4
24
|
export function createOnInit(options) {
|
|
5
25
|
return async (payload)=>{
|
|
26
|
+
for (const registration of options.blocks?.collections ?? []){
|
|
27
|
+
const config = payload.config.collections.find((c)=>c.slug === registration.slug);
|
|
28
|
+
const found = findRegisteredBlocksField(config?.fields ?? [], registration.field);
|
|
29
|
+
if (!found) throw new Error(`Experiments blocks field ${registration.slug}.${registeredPaths(config?.fields ?? [], registration.field)[0] ?? registration.field} must be a top-level blocks field and must contain ${registration.blockSlug ?? EXPERIMENT_BLOCK_SLUG}`);
|
|
30
|
+
if (!contentBlocks(found.field, payload.config.blocks).some((b)=>b.slug === (registration.blockSlug ?? EXPERIMENT_BLOCK_SLUG))) {
|
|
31
|
+
throw new Error(`Experiments blocks field ${registration.slug}.${registration.field} must contain ${registration.blockSlug ?? EXPERIMENT_BLOCK_SLUG}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (options.metricSources?.some((source)=>source.id.startsWith('click:'))) throw new Error('metricSources ids starting with click: are reserved for click goals');
|
|
6
35
|
validateDimensions(options.events?.dimensions);
|
|
7
36
|
assertPostgresAdapter(payload);
|
|
8
37
|
await probeTransactions(payload);
|
package/dist/options.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export declare function puckCollections(options: Pick<ExperimentsPluginOptions,
|
|
|
7
7
|
export declare function resolveOptions<TUser extends {
|
|
8
8
|
id: string | number;
|
|
9
9
|
}>(input: ExperimentsPluginOptions<TUser>): ResolvedOptions<TUser>;
|
|
10
|
+
/** One published-row rule shared by scans, bindings, writes and editor links. */
|
|
11
|
+
export declare function documentEditor(options: Pick<ExperimentsPluginOptions, 'puck' | 'blocks'>, collection: string, doc: Record<string, unknown>): 'puck' | 'blocks' | 'page';
|