@delmaredigital/payload-experiments 0.2.0-rc.7 → 0.3.0-rc.4
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 +202 -26
- package/dist/admin/PreviewLinks.d.ts +2 -1
- package/dist/admin/PreviewLinks.js +50 -21
- package/dist/admin/TestsNavLink.d.ts +3 -1
- package/dist/admin/TestsNavLink.js +2 -2
- package/dist/admin/actions/ConfirmDialog.d.ts +7 -4
- package/dist/admin/actions/ConfirmDialog.js +37 -82
- package/dist/admin/actions/DecideDialog.d.ts +3 -1
- package/dist/admin/actions/DecideDialog.js +30 -13
- package/dist/admin/actions/MakeContentResult.d.ts +13 -2
- package/dist/admin/actions/MakeContentResult.js +115 -16
- package/dist/admin/actions/actions.model.d.ts +2 -4
- package/dist/admin/actions/actions.model.js +2 -3
- package/dist/admin/allocation.model.js +3 -5
- package/dist/admin/api.d.ts +10 -4
- package/dist/admin/api.js +11 -7
- 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/copy/actions.d.ts +39 -3
- package/dist/admin/copy/actions.js +40 -4
- package/dist/admin/copy/armSelect.js +5 -5
- package/dist/admin/copy/arms.d.ts +1 -6
- package/dist/admin/copy/arms.js +1 -16
- package/dist/admin/copy/audience.d.ts +2 -1
- package/dist/admin/copy/audience.js +3 -2
- package/dist/admin/copy/common.d.ts +13 -6
- package/dist/admin/copy/common.js +14 -8
- package/dist/admin/copy/goals.d.ts +21 -6
- package/dist/admin/copy/goals.js +27 -6
- package/dist/admin/copy/readiness.d.ts +22 -0
- package/dist/admin/copy/readiness.js +21 -0
- package/dist/admin/copy/recommendation.d.ts +0 -3
- package/dist/admin/copy/recommendation.js +0 -3
- package/dist/admin/copy/results.d.ts +43 -2
- package/dist/admin/copy/results.js +44 -3
- package/dist/admin/copy/review.d.ts +2 -2
- package/dist/admin/copy/review.js +2 -2
- package/dist/admin/copy/setting.d.ts +2 -0
- package/dist/admin/copy/setting.js +2 -0
- package/dist/admin/copy/surface.d.ts +7 -5
- package/dist/admin/copy/surface.js +9 -5
- package/dist/admin/copy/testPage.d.ts +10 -0
- package/dist/admin/copy/testPage.js +10 -0
- package/dist/admin/format/date.d.ts +2 -0
- package/dist/admin/format/date.js +10 -0
- package/dist/admin/paths.d.ts +2 -1
- package/dist/admin/paths.js +2 -1
- package/dist/admin/previewLinks.model.d.ts +4 -2
- package/dist/admin/previewLinks.model.js +8 -7
- package/dist/admin/results/CiChart.js +11 -21
- package/dist/admin/results/DataQualityPanel.d.ts +4 -2
- package/dist/admin/results/DataQualityPanel.js +30 -17
- package/dist/admin/results/DataTable.d.ts +16 -0
- package/dist/admin/results/DataTable.js +152 -0
- package/dist/admin/results/StatTile.d.ts +7 -0
- package/dist/admin/results/StatTile.js +22 -0
- package/dist/admin/results/VerdictHero.d.ts +13 -0
- package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
- package/dist/admin/results/legacy.d.ts +16 -0
- package/dist/admin/results/legacy.js +27 -0
- package/dist/admin/results/releasePages.model.d.ts +17 -0
- package/dist/admin/results/releasePages.model.js +10 -0
- package/dist/admin/results/results.css +64 -0
- package/dist/admin/results/results.model.d.ts +11 -7
- package/dist/admin/results/results.model.js +24 -7
- package/dist/admin/results/snapshot.d.ts +14 -0
- package/dist/admin/results/snapshot.js +3 -0
- package/dist/admin/surface/ResultsRoute.d.ts +1 -1
- package/dist/admin/surface/ResultsRoute.js +59 -34
- package/dist/admin/surface/SurfaceShell.d.ts +11 -2
- package/dist/admin/surface/SurfaceShell.js +49 -14
- package/dist/admin/surface/TestsHome.d.ts +6 -1
- package/dist/admin/surface/TestsHome.js +182 -114
- package/dist/admin/surface/TestsSurface.js +56 -16
- package/dist/admin/surface/goals/GoalForm.js +341 -308
- package/dist/admin/surface/goals/GoalsList.js +123 -89
- package/dist/admin/surface/goals/data.d.ts +8 -2
- package/dist/admin/surface/goals/data.js +24 -16
- package/dist/admin/surface/goals/goals.css +10 -13
- package/dist/admin/surface/home.model.d.ts +8 -8
- package/dist/admin/surface/home.model.js +23 -12
- package/dist/admin/surface/newTest/NewTest.js +9 -9
- package/dist/admin/surface/pageBlock/parts.js +31 -26
- package/dist/admin/surface/readiness/ActionDialogs.js +47 -19
- package/dist/admin/surface/readiness/Banner.d.ts +3 -0
- package/dist/admin/surface/readiness/Banner.js +58 -33
- package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
- package/dist/admin/surface/readiness/SaveStatus.js +6 -3
- package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
- package/dist/admin/surface/readiness/readiness.model.js +25 -9
- package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
- package/dist/admin/surface/readiness/useTransition.js +2 -1
- package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
- package/dist/admin/surface/results/ReleasePages.js +116 -0
- package/dist/admin/surface/results/ResultsBody.d.ts +11 -1
- package/dist/admin/surface/results/ResultsBody.js +277 -105
- package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
- package/dist/admin/surface/results/ResultsHeader.js +177 -0
- package/dist/admin/surface/results/recommendation.model.d.ts +3 -2
- package/dist/admin/surface/results/recommendation.model.js +12 -9
- package/dist/admin/surface/results/useResults.d.ts +21 -3
- package/dist/admin/surface/results/useResults.js +2 -1
- package/dist/admin/surface/setting/parts.js +75 -88
- package/dist/admin/surface/setting/setting.css +3 -15
- package/dist/admin/surface/surface.css +61 -47
- package/dist/admin/surface/test/AdvancedSection.js +55 -48
- package/dist/admin/surface/test/GoalSection.js +46 -47
- package/dist/admin/surface/test/LengthSection.js +46 -34
- package/dist/admin/surface/test/Rail.d.ts +4 -0
- package/dist/admin/surface/test/Rail.js +83 -0
- package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
- package/dist/admin/surface/test/ReadinessBar.js +33 -0
- package/dist/admin/surface/test/Section.d.ts +1 -1
- package/dist/admin/surface/test/Section.js +36 -16
- package/dist/admin/surface/test/TargetingSection.js +168 -142
- package/dist/admin/surface/test/TestPage.d.ts +2 -1
- package/dist/admin/surface/test/TestPage.js +146 -68
- package/dist/admin/surface/test/VersionsSection.js +151 -120
- package/dist/admin/surface/test/fields.d.ts +11 -4
- package/dist/admin/surface/test/fields.js +84 -85
- package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
- package/dist/admin/surface/test/jumpToSection.js +10 -0
- package/dist/admin/surface/test/pageParts.js +9 -3
- package/dist/admin/surface/test/testPage.css +81 -14
- 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/useTestDocument.js +12 -5
- package/dist/admin/targeting/targeting.model.d.ts +1 -3
- package/dist/admin/targeting/targeting.model.js +3 -8
- package/dist/admin/theme.css +115 -36
- package/dist/admin/theme.d.ts +38 -0
- package/dist/admin/theme.js +38 -1
- package/dist/admin/useApi.d.ts +17 -2
- package/dist/admin/useLayerUsage.js +14 -23
- package/dist/admin/views/ExperimentsRootView.js +12 -2
- 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/collections/experiments/config.js +25 -0
- package/dist/collections/experiments/lockHook.js +3 -1
- package/dist/collections/identities.js +6 -1
- package/dist/collections/metrics.js +7 -0
- package/dist/collections/metricsGuards.d.ts +9 -0
- package/dist/collections/metricsGuards.js +33 -0
- package/dist/contentDone.d.ts +4 -0
- package/dist/contentDone.js +5 -0
- package/dist/db/appends.d.ts +6 -2
- package/dist/db/appends.js +10 -4
- package/dist/db/identities.d.ts +8 -7
- package/dist/db/identities.js +9 -6
- package/dist/db/locks.d.ts +6 -0
- package/dist/db/locks.js +13 -1
- package/dist/db/metricUsage.d.ts +19 -0
- package/dist/db/metricUsage.js +54 -0
- package/dist/db/names.d.ts +5 -0
- package/dist/db/names.js +5 -0
- package/dist/db/pageOccupancy.d.ts +12 -0
- package/dist/db/pageOccupancy.js +45 -0
- package/dist/db/publishedContent.d.ts +10 -0
- package/dist/db/publishedContent.js +43 -0
- package/dist/db/results.d.ts +2 -0
- package/dist/db/results.js +6 -2
- package/dist/db/snapshots.d.ts +18 -6
- package/dist/db/snapshots.js +25 -8
- package/dist/db/sweep.js +11 -6
- package/dist/endpoints/index.js +19 -1
- package/dist/endpoints/results.js +5 -0
- package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
- package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
- package/dist/endpoints/summary.js +20 -2
- package/dist/endpoints/transitions.d.ts +15 -0
- package/dist/endpoints/transitions.js +36 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +28 -0
- package/dist/exports/adminRsc.d.ts +1 -0
- package/dist/exports/adminRsc.js +1 -0
- package/dist/exports/index.d.ts +2 -1
- package/dist/exports/index.js +1 -0
- package/dist/exports/puck.d.ts +2 -1
- package/dist/exports/puck.js +1 -1
- package/dist/exports/puckEditor.d.ts +4 -1
- package/dist/exports/puckEditor.js +3 -1
- package/dist/exports/rsc.d.ts +6 -1
- package/dist/exports/rsc.js +2 -0
- package/dist/hooks/pageGuard.d.ts +5 -0
- package/dist/hooks/pageGuard.js +139 -0
- package/dist/lib/fetchAllDocs.d.ts +16 -0
- package/dist/lib/fetchAllDocs.js +56 -0
- package/dist/log.js +6 -3
- package/dist/manifest/compile.d.ts +5 -0
- package/dist/manifest/compile.js +5 -1
- package/dist/options.js +16 -0
- package/dist/plugin.js +30 -2
- 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 -5
- package/dist/puck/copy.js +6 -5
- 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 +121 -0
- package/dist/puck/editor/editor.css +288 -204
- package/dist/puck/editor/editor.model.d.ts +4 -1
- package/dist/puck/editor/editor.model.js +10 -7
- package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
- package/dist/puck/editor/experimentOutlinePlugin.js +89 -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/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/results/aggregate.js +4 -3
- package/dist/results/analyse.d.ts +4 -1
- package/dist/results/analyse.js +27 -11
- package/dist/results/cuped.js +3 -2
- package/dist/results/identityCredit.d.ts +15 -7
- package/dist/results/identityCredit.js +32 -33
- package/dist/results/rolloutUnits.js +2 -1
- package/dist/results/snapshot.js +8 -2
- package/dist/results/srm.js +2 -1
- package/dist/sanitizeReadinessDetail.d.ts +7 -0
- package/dist/sanitizeReadinessDetail.js +63 -0
- package/dist/server/linkIdentity.d.ts +10 -1
- package/dist/server/linkIdentity.js +17 -15
- package/dist/server/metadata.d.ts +5 -0
- package/dist/server/metadata.js +18 -3
- package/dist/server/normaliseContentRelationships.d.ts +7 -0
- package/dist/server/normaliseContentRelationships.js +62 -0
- package/dist/server/recordContentChange.d.ts +1 -1
- package/dist/server/recordContentChange.js +17 -8
- package/dist/server/runResults.d.ts +3 -2
- package/dist/server/runResults.js +157 -35
- package/dist/targeting/values.d.ts +4 -0
- package/dist/targeting/values.js +10 -0
- package/dist/transitions/addMetric.js +8 -0
- package/dist/transitions/archive.js +2 -3
- package/dist/transitions/checkStart.d.ts +10 -3
- package/dist/transitions/checkStart.js +25 -0
- package/dist/transitions/contentFields.d.ts +7 -0
- package/dist/transitions/contentFields.js +20 -0
- package/dist/transitions/decide.js +22 -2
- package/dist/transitions/dismissContentEntry.d.ts +8 -0
- package/dist/transitions/dismissContentEntry.js +45 -0
- package/dist/transitions/makeContent.d.ts +5 -2
- package/dist/transitions/makeContent.js +217 -118
- package/dist/transitions/phaseRecord.d.ts +8 -2
- package/dist/transitions/placements.d.ts +3 -6
- package/dist/transitions/placements.js +12 -1
- package/dist/transitions/preflightData.d.ts +9 -1
- package/dist/transitions/preflightData.js +138 -20
- package/dist/transitions/releasePages.d.ts +8 -0
- package/dist/transitions/releasePages.js +26 -0
- package/dist/transitions/restoreContentEntry.d.ts +8 -0
- package/dist/transitions/restoreContentEntry.js +47 -0
- package/dist/transitions/start.js +62 -7
- package/dist/types/contracts.d.ts +24 -4
- package/dist/types/endpoints.d.ts +34 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/reasons.d.ts +3 -0
- package/dist/types/reasons.js +1 -0
- package/dist/types/snapshot.d.ts +6 -1
- package/dist/ui/Banner.d.ts +2 -0
- package/dist/ui/Banner.js +2 -0
- package/dist/ui/Breadcrumb.d.ts +7 -0
- package/dist/ui/Breadcrumb.js +25 -0
- package/dist/ui/Button.d.ts +15 -0
- package/dist/ui/Button.js +32 -0
- package/dist/ui/Card.d.ts +12 -0
- package/dist/ui/Card.js +40 -0
- package/dist/ui/Checkbox.d.ts +2 -0
- package/dist/ui/Checkbox.js +2 -0
- package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
- package/dist/{admin/components → ui}/ChipsInput.js +20 -12
- package/dist/ui/Dialog.d.ts +13 -0
- package/dist/ui/Dialog.js +85 -0
- package/dist/ui/EmptyState.d.ts +2 -0
- package/dist/ui/EmptyState.js +2 -0
- package/dist/ui/Feedback.d.ts +24 -0
- package/dist/ui/Feedback.js +40 -0
- package/dist/ui/Inputs.d.ts +34 -0
- package/dist/ui/Inputs.js +225 -0
- package/dist/ui/NumberInput.d.ts +2 -0
- package/dist/ui/NumberInput.js +2 -0
- package/dist/ui/Range.d.ts +2 -0
- package/dist/ui/Range.js +2 -0
- package/dist/ui/SearchInput.d.ts +2 -0
- package/dist/ui/SearchInput.js +2 -0
- package/dist/ui/SectionCard.d.ts +2 -0
- package/dist/ui/SectionCard.js +2 -0
- package/dist/ui/Select.d.ts +2 -0
- package/dist/ui/Select.js +2 -0
- package/dist/ui/StatusChip.d.ts +2 -0
- package/dist/ui/StatusChip.js +2 -0
- package/dist/ui/Tabs.d.ts +16 -0
- package/dist/ui/Tabs.js +61 -0
- package/dist/ui/TextInput.d.ts +2 -0
- package/dist/ui/TextInput.js +2 -0
- package/dist/ui/Textarea.d.ts +2 -0
- package/dist/ui/Textarea.js +2 -0
- package/dist/ui/Toggle.d.ts +2 -0
- package/dist/ui/Toggle.js +2 -0
- package/dist/ui/accent.d.ts +40 -0
- package/dist/ui/accent.js +139 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.js +9 -0
- package/dist/ui/ui.css +92 -0
- package/package.json +5 -2
- package/dist/admin/components/chipsInput.css +0 -11
- package/dist/admin/results/ArmTable.d.ts +0 -8
- package/dist/admin/results/ArmTable.js +0 -113
- package/dist/admin/results/VerdictCard.d.ts +0 -9
- package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
- package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
- package/dist/admin/surface/test/TestHeader.d.ts +0 -4
- package/dist/admin/surface/test/TestHeader.js +0 -70
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { previousPublishedContent } from '../db/appends.js';
|
|
2
|
+
import { lockPagePaths } from '../db/locks.js';
|
|
3
|
+
import { txExecutor } from '../db/executor.js';
|
|
4
|
+
import { normalisePath } from '../runtime/normalisePath.js';
|
|
5
|
+
import { XpNoTransactionError, XpRefusal } from '../errors.js';
|
|
6
|
+
import { pageGuardCopy, READINESS_COPY } from '../admin/copy/readiness.js';
|
|
7
|
+
// createLocalReq replaces context with a shallow copy on nested calls. Keep
|
|
8
|
+
// the shared stack in that context so both the outer req and its proxies see it.
|
|
9
|
+
const operations = (req)=>req.context.xpPageGuardOperations;
|
|
10
|
+
export const pageGuardBeforeOperation = ({ args, operation })=>{
|
|
11
|
+
if (operation !== 'update' && operation !== 'restoreVersion') return;
|
|
12
|
+
const call = args;
|
|
13
|
+
let collections = operations(call.req);
|
|
14
|
+
if (!collections) call.req.context.xpPageGuardOperations = collections = new Map();
|
|
15
|
+
const slug = call.collection.config.slug;
|
|
16
|
+
const stack = collections.get(slug) ?? [];
|
|
17
|
+
stack.push({
|
|
18
|
+
draft: call.draft === true,
|
|
19
|
+
restore: operation === 'restoreVersion'
|
|
20
|
+
});
|
|
21
|
+
collections.set(slug, stack);
|
|
22
|
+
};
|
|
23
|
+
export const pageGuardAfterOperation = ({ req, collection, operation, result })=>{
|
|
24
|
+
// beforeOperation reports by-id updates as 'update'; afterOperation reports 'updateByID'.
|
|
25
|
+
if (operation === 'update' || operation === 'updateByID' || operation === 'restoreVersion') {
|
|
26
|
+
const collections = operations(req);
|
|
27
|
+
const stack = collections?.get(collection.slug);
|
|
28
|
+
stack?.pop();
|
|
29
|
+
if (!stack?.length) collections?.delete(collection.slug);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
async function protect(req, path) {
|
|
34
|
+
// Serialise with Start (CORE-52 path locks, taken there after the experiment lock): a concurrent Start
|
|
35
|
+
// either commits first, so this check sees it, or waits and then sees this change. Raw SQL on the
|
|
36
|
+
// host's transaction; without one there is nothing to hold the lock for.
|
|
37
|
+
if (await req.transactionID != null) {
|
|
38
|
+
try {
|
|
39
|
+
await lockPagePaths(txExecutor(req), [
|
|
40
|
+
normalisePath(path)
|
|
41
|
+
]);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// A request id without a database session (e.g. a transaction already killed) has no lock to take.
|
|
44
|
+
if (!(error instanceof XpNoTransactionError)) throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// page guard: the hook must see test placements regardless of the editor's access
|
|
48
|
+
const tests = await req.payload.find({
|
|
49
|
+
collection: 'xp-experiments',
|
|
50
|
+
req,
|
|
51
|
+
overrideAccess: true,
|
|
52
|
+
depth: 0,
|
|
53
|
+
limit: 0,
|
|
54
|
+
where: {
|
|
55
|
+
and: [
|
|
56
|
+
{
|
|
57
|
+
kind: {
|
|
58
|
+
equals: 'page'
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
status: {
|
|
63
|
+
in: [
|
|
64
|
+
'running',
|
|
65
|
+
'decided',
|
|
66
|
+
'archived'
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
for (const test of tests.docs){
|
|
74
|
+
const decision = test.decision;
|
|
75
|
+
const archived = test.status === 'archived';
|
|
76
|
+
if (archived && (!decision?.winnerArm || decision.discardedAt || decision.pagesReleasedAt)) continue;
|
|
77
|
+
// page guard: the hook must see test placements regardless of the editor's access
|
|
78
|
+
const phases = await req.payload.find({
|
|
79
|
+
collection: 'xp-phases',
|
|
80
|
+
req,
|
|
81
|
+
overrideAccess: true,
|
|
82
|
+
depth: 0,
|
|
83
|
+
limit: 1,
|
|
84
|
+
sort: '-phase',
|
|
85
|
+
where: {
|
|
86
|
+
experiment: {
|
|
87
|
+
equals: test.id
|
|
88
|
+
},
|
|
89
|
+
phaseKind: {
|
|
90
|
+
equals: 'test'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const phase = phases.docs[0];
|
|
95
|
+
if (!phase) throw new XpRefusal('invalid-phase', READINESS_COPY.missingVersions, {
|
|
96
|
+
experimentId: test.id
|
|
97
|
+
}, 409);
|
|
98
|
+
const arms = phase.arms;
|
|
99
|
+
const protectedArms = archived ? arms.filter((arm)=>arm.kind === 'control' || arm.key === decision.winnerArm) : arms;
|
|
100
|
+
const matched = protectedArms.find((arm)=>arm.pageTarget !== undefined && normalisePath(arm.pageTarget) === normalisePath(path));
|
|
101
|
+
if (matched) {
|
|
102
|
+
const role = archived && matched.key !== decision.winnerArm ? 'original' : 'winner';
|
|
103
|
+
throw new XpRefusal('locked-placement', pageGuardCopy(String(test.name), test.status, role), {
|
|
104
|
+
experimentId: test.id
|
|
105
|
+
}, 409);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function registration(req, slug) {
|
|
110
|
+
return (req.payload.config.custom?.xp).pages?.collections.find((entry)=>entry.slug === slug);
|
|
111
|
+
}
|
|
112
|
+
export const pageGuardBeforeDelete = async ({ req, id, collection })=>{
|
|
113
|
+
const page = registration(req, collection.slug);
|
|
114
|
+
if (!page) return;
|
|
115
|
+
const published = await previousPublishedContent(req, collection.slug, id);
|
|
116
|
+
if (!published || published._status === 'draft') return;
|
|
117
|
+
const path = page.pathOf(published);
|
|
118
|
+
if (path !== null) await protect(req, path);
|
|
119
|
+
};
|
|
120
|
+
export const pageGuardBeforeChange = async ({ req, collection, originalDoc, operation, data })=>{
|
|
121
|
+
if (operation !== 'update') return data;
|
|
122
|
+
const page = registration(req, collection.slug);
|
|
123
|
+
if (!page) return data;
|
|
124
|
+
const drafts = Boolean(collection.versions && collection.versions.drafts);
|
|
125
|
+
// updateDocument computes isSavingDraft from draftArg and _status before
|
|
126
|
+
// beforeChange, and writes the main row only when !isSavingDraft.
|
|
127
|
+
const call = operations(req)?.get(collection.slug)?.at(-1);
|
|
128
|
+
if (drafts && call?.draft && (call.restore || data._status !== 'published')) return data;
|
|
129
|
+
const published = await previousPublishedContent(req, collection.slug, originalDoc.id);
|
|
130
|
+
if (!published || published._status === 'draft') return data;
|
|
131
|
+
const path = page.pathOf(published);
|
|
132
|
+
const unpublishing = drafts && data._status === 'draft';
|
|
133
|
+
const trashing = collection.trash && data.deletedAt != null && !originalDoc.deletedAt;
|
|
134
|
+
if (path !== null && (trashing || unpublishing || page.pathOf({
|
|
135
|
+
...originalDoc,
|
|
136
|
+
...data
|
|
137
|
+
}) !== path)) await protect(req, path);
|
|
138
|
+
return data;
|
|
139
|
+
};
|
|
@@ -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/log.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { XpNoTransactionError, XpNotImplemented, XpRefusal } from './errors.js';
|
|
1
|
+
import { ANALYSIS_ERROR_MESSAGES, XpAnalysisError, XpNoTransactionError, XpNotImplemented, XpRefusal } from './errors.js';
|
|
2
2
|
import { ResultsBudgetError } from './db/pgExecutor.js';
|
|
3
3
|
/** Deliberately construct a plain object: host serializers must never see the error. */ export function safeError(err) {
|
|
4
4
|
try {
|
|
@@ -10,7 +10,8 @@ import { ResultsBudgetError } from './db/pgExecutor.js';
|
|
|
10
10
|
name: typeof name === 'string' ? name : 'Error'
|
|
11
11
|
};
|
|
12
12
|
const code = 'code' in err ? err.code : undefined;
|
|
13
|
-
|
|
13
|
+
const analysisCode = err instanceof XpAnalysisError && typeof code === 'string' && Object.hasOwn(ANALYSIS_ERROR_MESSAGES, code) ? code : undefined;
|
|
14
|
+
if (typeof code === 'string' && (/^[0-9A-Z]{5}$/.test(code) || err instanceof XpRefusal || analysisCode)) result.code = code;
|
|
14
15
|
const message = err.message;
|
|
15
16
|
const stack = err.stack;
|
|
16
17
|
// Remove the entire V8 header first: message lines can impersonate frames.
|
|
@@ -22,7 +23,9 @@ import { ResultsBudgetError } from './db/pgExecutor.js';
|
|
|
22
23
|
}
|
|
23
24
|
// XpRefusal includes stored slugs; XpLockedFieldError includes request keys
|
|
24
25
|
// and stored status. Neither class's message is safe to send to host logs.
|
|
25
|
-
|
|
26
|
+
// An analysis error's logged text always comes from the fixed table by code, never from err.message.
|
|
27
|
+
if (analysisCode) result.message = ANALYSIS_ERROR_MESSAGES[analysisCode];
|
|
28
|
+
else if (err instanceof XpNoTransactionError || err instanceof XpNotImplemented || err instanceof ResultsBudgetError) {
|
|
26
29
|
if (typeof message === 'string') result.message = message;
|
|
27
30
|
}
|
|
28
31
|
return result;
|
|
@@ -35,6 +35,7 @@ export interface ManifestInputExperiment {
|
|
|
35
35
|
winnerArm?: string | null;
|
|
36
36
|
discardedAt?: string | null;
|
|
37
37
|
holdoutReleasedAt?: string | null;
|
|
38
|
+
pagesReleasedAt?: string | null;
|
|
38
39
|
} | null;
|
|
39
40
|
currentPhase: ManifestPhase | null;
|
|
40
41
|
lastTestPhase?: ManifestPhase | null;
|
|
@@ -50,6 +51,10 @@ export declare function loadManifestInputs(req: PayloadRequest, { excludeIds, pr
|
|
|
50
51
|
pruneFlagDefaultIds?: DocId[];
|
|
51
52
|
}): Promise<ManifestInputs>;
|
|
52
53
|
/** Pure compiler. Assignment config comes exclusively from the frozen phase. */
|
|
54
|
+
/**
|
|
55
|
+
* Pure manifest builder (exported from `/rsc` for host tests). Pass the same `attributeKeys` as the plugin options,
|
|
56
|
+
* or legacy untyped targeting rules are dropped. Its input shape may change in a pre-1.0 minor.
|
|
57
|
+
*/
|
|
53
58
|
export declare function compileManifest(inputs: ManifestInputs, revision: number, { attributeKeys, onInvalid }?: ManifestCompileOptions): Manifest;
|
|
54
59
|
/** Take the state lock before reading inputs. Under READ COMMITTED, a builder
|
|
55
60
|
* waiting for another transition then sees that transition's committed changes,
|
package/dist/manifest/compile.js
CHANGED
|
@@ -107,7 +107,10 @@ export async function loadManifestInputs(req, { excludeIds = [], pruneFlagDefaul
|
|
|
107
107
|
});
|
|
108
108
|
inputs.flagDefaults = defaults;
|
|
109
109
|
}
|
|
110
|
-
/** Pure compiler. Assignment config comes exclusively from the frozen phase. */
|
|
110
|
+
/** Pure compiler. Assignment config comes exclusively from the frozen phase. */ /**
|
|
111
|
+
* Pure manifest builder (exported from `/rsc` for host tests). Pass the same `attributeKeys` as the plugin options,
|
|
112
|
+
* or legacy untyped targeting rules are dropped. Its input shape may change in a pre-1.0 minor.
|
|
113
|
+
*/ export function compileManifest(inputs, revision, { attributeKeys, onInvalid } = {}) {
|
|
111
114
|
const excluded = new Set(inputs.excludeIds?.map(String));
|
|
112
115
|
const prunedDefaults = new Set(inputs.pruneFlagDefaultIds?.map(String));
|
|
113
116
|
const manifest = {
|
|
@@ -126,6 +129,7 @@ export async function loadManifestInputs(req, { excludeIds = [], pruneFlagDefaul
|
|
|
126
129
|
const { decision, currentPhase: phase } = experiment;
|
|
127
130
|
if (experiment.status === 'archived') {
|
|
128
131
|
if (!decision?.winnerArm || decision.discardedAt) continue;
|
|
132
|
+
if (experiment.kind === 'page' && decision.pagesReleasedAt) continue;
|
|
129
133
|
const testPhase = experiment.lastTestPhase ?? (phase?.phaseKind === 'test' ? phase : null);
|
|
130
134
|
if (!testPhase) throw new XpRefusal('invalid-status', `Missing last test phase for experiment ${experiment.slug}`, undefined, 409);
|
|
131
135
|
const winner = testPhase.arms.find((arm)=>arm.key === decision.winnerArm);
|
package/dist/options.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateAccent } from './ui/accent.js';
|
|
1
2
|
import { DEFAULT_MAX_BODY_BYTES } from './constants.js';
|
|
2
3
|
const normalisedCollections = new WeakMap();
|
|
3
4
|
/** Resolve once per registration list; every consumer shares the same field defaults. */ export function puckCollections(options) {
|
|
@@ -54,6 +55,21 @@ export function resolveOptions(input) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
if (input.pages !== undefined) {
|
|
59
|
+
if (!input.pages || !Array.isArray(input.pages.collections) || input.pages.collections.length === 0) throw new Error('Experiments pages.collections must be a non-empty array');
|
|
60
|
+
const slugs = new Set();
|
|
61
|
+
for (const entry of input.pages.collections){
|
|
62
|
+
if (!entry || typeof entry.slug !== 'string' || !entry.slug.trim() || slugs.has(entry.slug) || typeof entry.pathOf !== 'function' || typeof entry.findByPath !== 'function') {
|
|
63
|
+
throw new Error('Experiments pages.collections requires unique slugs, pathOf and findByPath functions');
|
|
64
|
+
}
|
|
65
|
+
slugs.add(entry.slug);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (input.theme !== undefined) {
|
|
69
|
+
if (!input.theme || typeof input.theme !== 'object' || Array.isArray(input.theme)) throw new Error('Experiments theme must be an object');
|
|
70
|
+
for (const key of Object.keys(input.theme))if (key !== 'accent') throw new Error(`Unknown experiments theme option: ${key}`);
|
|
71
|
+
validateAccent(input.theme.accent);
|
|
72
|
+
}
|
|
57
73
|
const ids = new Set();
|
|
58
74
|
for (const point of input.flagPoints ?? []){
|
|
59
75
|
if (typeof point.id !== 'string' || !point.id || ids.has(point.id)) throw new Error('Experiments flag point ids must be unique nonempty strings');
|
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildCollections } from './collections/index.js';
|
|
2
2
|
import { buildEndpoints } from './endpoints/index.js';
|
|
3
3
|
import { contentChangesBeforeHook, contentChangesHook } from './hooks/contentChanges.js';
|
|
4
|
+
import { pageGuardAfterOperation, pageGuardBeforeChange, pageGuardBeforeDelete, pageGuardBeforeOperation } from './hooks/pageGuard.js';
|
|
4
5
|
import { createOnInit } from './onInit.js';
|
|
5
6
|
import { puckCollections, resolveOptions } from './options.js';
|
|
6
7
|
import * as paths from './admin/paths.js';
|
|
@@ -11,6 +12,9 @@ export function createExperimentsPlugin(input) {
|
|
|
11
12
|
for (const { slug } of registrations){
|
|
12
13
|
if (!config.collections?.some((collection)=>collection.slug === slug)) throw new Error(`Unknown experiments puck collection: ${slug}`);
|
|
13
14
|
}
|
|
15
|
+
for (const { slug } of options.pages?.collections ?? []){
|
|
16
|
+
if (!config.collections?.some((collection)=>collection.slug === slug)) throw new Error(`Unknown experiments page collection: ${slug}`);
|
|
17
|
+
}
|
|
14
18
|
// The frozen factories consume registration/access metadata, never TUser callbacks.
|
|
15
19
|
// Erase the user subtype only at this non-generic factory boundary.
|
|
16
20
|
const { collections, globals } = buildCollections(options);
|
|
@@ -26,6 +30,10 @@ export function createExperimentsPlugin(input) {
|
|
|
26
30
|
xpTestsSurface: {
|
|
27
31
|
path: paths.XP_TESTS_SURFACE,
|
|
28
32
|
type: 'component'
|
|
33
|
+
},
|
|
34
|
+
xpTestsNav: {
|
|
35
|
+
path: paths.XP_TESTS_NAV_CLIENT,
|
|
36
|
+
type: 'component'
|
|
29
37
|
}
|
|
30
38
|
},
|
|
31
39
|
components: {
|
|
@@ -50,15 +58,35 @@ export function createExperimentsPlugin(input) {
|
|
|
50
58
|
collections: [
|
|
51
59
|
...(config.collections ?? []).map((collection)=>{
|
|
52
60
|
const puck = options.puck?.enabled && registrations.some((entry)=>entry.slug === collection.slug);
|
|
53
|
-
|
|
61
|
+
const page = options.pages?.collections.some((entry)=>entry.slug === collection.slug);
|
|
62
|
+
if (!puck && !page) return collection;
|
|
54
63
|
return {
|
|
55
64
|
...collection,
|
|
56
65
|
hooks: {
|
|
57
66
|
...collection.hooks,
|
|
58
67
|
beforeChange: [
|
|
59
68
|
...collection.hooks?.beforeChange ?? [],
|
|
60
|
-
|
|
69
|
+
...page ? [
|
|
70
|
+
pageGuardBeforeChange
|
|
71
|
+
] : [],
|
|
72
|
+
...puck ? [
|
|
73
|
+
contentChangesBeforeHook
|
|
74
|
+
] : []
|
|
61
75
|
],
|
|
76
|
+
...page ? {
|
|
77
|
+
beforeOperation: [
|
|
78
|
+
...collection.hooks?.beforeOperation ?? [],
|
|
79
|
+
pageGuardBeforeOperation
|
|
80
|
+
],
|
|
81
|
+
beforeDelete: [
|
|
82
|
+
...collection.hooks?.beforeDelete ?? [],
|
|
83
|
+
pageGuardBeforeDelete
|
|
84
|
+
],
|
|
85
|
+
afterOperation: [
|
|
86
|
+
pageGuardAfterOperation,
|
|
87
|
+
...collection.hooks?.afterOperation ?? []
|
|
88
|
+
]
|
|
89
|
+
} : {},
|
|
62
90
|
...puck ? {
|
|
63
91
|
afterChange: [
|
|
64
92
|
...collection.hooks?.afterChange ?? [],
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useEffect, useId, useState } from 'react';
|
|
4
|
+
import { fetchAllDocs } from '../lib/fetchAllDocs.js';
|
|
5
|
+
import { usePuckRoutes } from './hostContext.js';
|
|
4
6
|
import { pickerCopy } from './copy.js';
|
|
5
7
|
function isExperimentOption(value) {
|
|
6
8
|
if (!value || typeof value !== 'object') return false;
|
|
@@ -8,14 +10,17 @@ function isExperimentOption(value) {
|
|
|
8
10
|
return (typeof doc.id === 'string' || typeof doc.id === 'number') && typeof doc.slug === 'string' && typeof doc.name === 'string' && typeof doc.status === 'string' && Object.hasOwn(pickerCopy.statuses, doc.status);
|
|
9
11
|
}
|
|
10
12
|
export function ExperimentPicker({ value, onChange }) {
|
|
13
|
+
const { api } = usePuckRoutes();
|
|
14
|
+
const id = useId();
|
|
11
15
|
const [experiments, setExperiments] = useState([]);
|
|
16
|
+
const [search, setSearch] = useState('');
|
|
12
17
|
const [state, setState] = useState('loading');
|
|
13
18
|
useEffect(()=>{
|
|
14
19
|
let active = true;
|
|
15
20
|
const controller = new AbortController();
|
|
16
21
|
async function load() {
|
|
17
22
|
try {
|
|
18
|
-
const response = await
|
|
23
|
+
const response = await fetchAllDocs(`${api}/xp-experiments?where[kind][equals]=block&depth=0&sort=id`, {
|
|
19
24
|
credentials: 'include',
|
|
20
25
|
signal: controller.signal
|
|
21
26
|
});
|
|
@@ -24,13 +29,7 @@ export function ExperimentPicker({ value, onChange }) {
|
|
|
24
29
|
setState(response.status === 403 ? 'forbidden' : 'error');
|
|
25
30
|
return;
|
|
26
31
|
}
|
|
27
|
-
|
|
28
|
-
if (!active) return;
|
|
29
|
-
if (!data || typeof data !== 'object' || !('docs' in data) || !Array.isArray(data.docs)) {
|
|
30
|
-
setState('error');
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
setExperiments(data.docs.filter(isExperimentOption));
|
|
32
|
+
setExperiments(response.docs.filter(isExperimentOption).sort((a, b)=>(Date.parse(b.updatedAt ?? '') || 0) - (Date.parse(a.updatedAt ?? '') || 0)));
|
|
34
33
|
setState('ready');
|
|
35
34
|
} catch {
|
|
36
35
|
if (active) setState('error');
|
|
@@ -41,12 +40,38 @@ export function ExperimentPicker({ value, onChange }) {
|
|
|
41
40
|
active = false;
|
|
42
41
|
controller.abort();
|
|
43
42
|
};
|
|
44
|
-
}, [
|
|
43
|
+
}, [
|
|
44
|
+
api
|
|
45
|
+
]);
|
|
45
46
|
const selected = value ? String(value.id) : '';
|
|
47
|
+
const query = search.trim().toLocaleLowerCase('en-US');
|
|
48
|
+
const visible = experiments.filter((doc)=>String(doc.id) === selected || doc.name.toLocaleLowerCase('en-US').includes(query) || doc.slug.toLocaleLowerCase('en-US').includes(query));
|
|
46
49
|
return /*#__PURE__*/ _jsxs("div", {
|
|
50
|
+
className: "xp-ui-field",
|
|
47
51
|
children: [
|
|
52
|
+
experiments.length > 8 ? /*#__PURE__*/ _jsxs("div", {
|
|
53
|
+
className: "xp-ui-field",
|
|
54
|
+
children: [
|
|
55
|
+
/*#__PURE__*/ _jsx("label", {
|
|
56
|
+
htmlFor: `${id}-search`,
|
|
57
|
+
children: pickerCopy.search
|
|
58
|
+
}),
|
|
59
|
+
/*#__PURE__*/ _jsx("input", {
|
|
60
|
+
id: `${id}-search`,
|
|
61
|
+
type: "search",
|
|
62
|
+
className: "xp-ui-input",
|
|
63
|
+
value: search,
|
|
64
|
+
onChange: (event)=>setSearch(event.currentTarget.value)
|
|
65
|
+
})
|
|
66
|
+
]
|
|
67
|
+
}) : null,
|
|
68
|
+
/*#__PURE__*/ _jsx("label", {
|
|
69
|
+
htmlFor: id,
|
|
70
|
+
children: pickerCopy.label
|
|
71
|
+
}),
|
|
48
72
|
/*#__PURE__*/ _jsxs("select", {
|
|
49
|
-
|
|
73
|
+
id: id,
|
|
74
|
+
className: "xp-ui-input xp-ui-select",
|
|
50
75
|
value: selected,
|
|
51
76
|
disabled: state === 'loading',
|
|
52
77
|
onChange: (event)=>{
|
|
@@ -65,7 +90,7 @@ export function ExperimentPicker({ value, onChange }) {
|
|
|
65
90
|
value: selected,
|
|
66
91
|
children: value.slug
|
|
67
92
|
}) : null,
|
|
68
|
-
|
|
93
|
+
visible.map((doc)=>/*#__PURE__*/ _jsxs("option", {
|
|
69
94
|
value: String(doc.id),
|
|
70
95
|
children: [
|
|
71
96
|
doc.name,
|
|
@@ -75,13 +100,19 @@ export function ExperimentPicker({ value, onChange }) {
|
|
|
75
100
|
}, doc.id))
|
|
76
101
|
]
|
|
77
102
|
}),
|
|
78
|
-
state === 'loading' ? /*#__PURE__*/ _jsx("
|
|
103
|
+
state === 'loading' ? /*#__PURE__*/ _jsx("div", {
|
|
104
|
+
className: "xp-ui-banner",
|
|
79
105
|
role: "status",
|
|
80
|
-
children:
|
|
106
|
+
children: /*#__PURE__*/ _jsx("p", {
|
|
107
|
+
children: pickerCopy.loading
|
|
108
|
+
})
|
|
81
109
|
}) : null,
|
|
82
|
-
state === 'error' || state === 'forbidden' ? /*#__PURE__*/ _jsx("
|
|
110
|
+
state === 'error' || state === 'forbidden' ? /*#__PURE__*/ _jsx("div", {
|
|
111
|
+
className: "xp-ui-banner xp-ui-banner--error",
|
|
83
112
|
role: "alert",
|
|
84
|
-
children:
|
|
113
|
+
children: /*#__PURE__*/ _jsx("p", {
|
|
114
|
+
children: pickerCopy[state]
|
|
115
|
+
})
|
|
85
116
|
}) : null
|
|
86
117
|
]
|
|
87
118
|
});
|
|
@@ -1,3 +1,53 @@
|
|
|
1
1
|
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
2
|
import type { ExperimentSlotProps } from '../types/puck.js';
|
|
3
|
-
|
|
3
|
+
import { type PuckRoutes } from './routes.js';
|
|
4
|
+
export declare function createExperimentSlot({ routes: inputRoutes }?: {
|
|
5
|
+
routes?: PuckRoutes;
|
|
6
|
+
}): ComponentConfig<ExperimentSlotProps>;
|
|
7
|
+
export declare const ExperimentSlot: {
|
|
8
|
+
render: import("@puckeditor/core").PuckComponent<ExperimentSlotProps>;
|
|
9
|
+
label?: string;
|
|
10
|
+
defaultProps?: ExperimentSlotProps | undefined;
|
|
11
|
+
fields?: import("@puckeditor/core").Fields<ExperimentSlotProps, {}> | undefined;
|
|
12
|
+
renderFields?: ((props: {
|
|
13
|
+
fields: Partial<Record<keyof ExperimentSlotProps, import("react").ReactNode>>;
|
|
14
|
+
}) => import("react").ReactElement) | undefined;
|
|
15
|
+
permissions?: Partial<import("@puckeditor/core").Permissions>;
|
|
16
|
+
inline?: boolean;
|
|
17
|
+
resolveFields?: ((data: Omit<import("@puckeditor/core").ComponentData<ExperimentSlotProps, string, Record<string, import("@puckeditor/core").DefaultComponentProps>>, "type">, params: {
|
|
18
|
+
changed: Partial<Record<keyof ExperimentSlotProps, boolean> & {
|
|
19
|
+
id: string;
|
|
20
|
+
}>;
|
|
21
|
+
fields: import("@puckeditor/core").Fields<ExperimentSlotProps, {}>;
|
|
22
|
+
lastFields: import("@puckeditor/core").Fields<ExperimentSlotProps, {}>;
|
|
23
|
+
lastData: Omit<import("@puckeditor/core").ComponentData<ExperimentSlotProps, string, Record<string, import("@puckeditor/core").DefaultComponentProps>>, "type"> | null;
|
|
24
|
+
metadata: import("@puckeditor/core").ComponentMetadata;
|
|
25
|
+
appState: import("@puckeditor/core").AppState;
|
|
26
|
+
parent: import("@puckeditor/core").ComponentData | null;
|
|
27
|
+
}) => import("@puckeditor/core").Fields<ExperimentSlotProps, {}> | Promise<import("@puckeditor/core").Fields<ExperimentSlotProps, {}>>) | undefined;
|
|
28
|
+
resolveData?: ((data: Omit<import("@puckeditor/core").ComponentData<ExperimentSlotProps, string, Record<string, import("@puckeditor/core").DefaultComponentProps>>, "type">, params: {
|
|
29
|
+
changed: Partial<Record<keyof ExperimentSlotProps, boolean> & {
|
|
30
|
+
id: string;
|
|
31
|
+
}>;
|
|
32
|
+
lastData: Omit<import("@puckeditor/core").ComponentData<ExperimentSlotProps, string, Record<string, import("@puckeditor/core").DefaultComponentProps>>, "type"> | null;
|
|
33
|
+
metadata: import("@puckeditor/core").ComponentMetadata;
|
|
34
|
+
trigger: import("@puckeditor/core").ResolveDataTrigger;
|
|
35
|
+
parent: import("@puckeditor/core").ComponentData | null;
|
|
36
|
+
root: import("@puckeditor/core").RootData;
|
|
37
|
+
}) => (Omit<Omit<import("@puckeditor/core").ComponentData<ExperimentSlotProps, string, Record<string, import("@puckeditor/core").DefaultComponentProps>>, "type">, "props"> & {
|
|
38
|
+
props?: Partial<ExperimentSlotProps> | undefined;
|
|
39
|
+
}) | Promise<Omit<Omit<import("@puckeditor/core").ComponentData<ExperimentSlotProps, string, Record<string, import("@puckeditor/core").DefaultComponentProps>>, "type">, "props"> & {
|
|
40
|
+
props?: Partial<ExperimentSlotProps> | undefined;
|
|
41
|
+
}>) | undefined;
|
|
42
|
+
resolvePermissions?: ((data: Omit<import("@puckeditor/core").ComponentData<ExperimentSlotProps, string, Record<string, import("@puckeditor/core").DefaultComponentProps>>, "type">, params: {
|
|
43
|
+
changed: Partial<Record<keyof ExperimentSlotProps, boolean> & {
|
|
44
|
+
id: string;
|
|
45
|
+
}>;
|
|
46
|
+
lastPermissions: Partial<import("@puckeditor/core").Permissions>;
|
|
47
|
+
permissions: Partial<import("@puckeditor/core").Permissions>;
|
|
48
|
+
appState: import("@puckeditor/core").AppState;
|
|
49
|
+
lastData: Omit<import("@puckeditor/core").ComponentData<ExperimentSlotProps, string, Record<string, import("@puckeditor/core").DefaultComponentProps>>, "type"> | null;
|
|
50
|
+
parent: import("@puckeditor/core").ComponentData | null;
|
|
51
|
+
}) => Promise<Partial<import("@puckeditor/core").Permissions>> | Partial<import("@puckeditor/core").Permissions>) | undefined;
|
|
52
|
+
metadata?: import("@puckeditor/core").ComponentMetadata;
|
|
53
|
+
} & import("@puckeditor/core").ComponentConfigExtensions;
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { ViewportExposure } from '../client/ViewportExposure.js';
|
|
3
3
|
import { experimentSlotDefaultProps, experimentSlotFields, experimentSlotLabel } from './slotConfig.js';
|
|
4
|
+
import { createExperimentSlotField } from './experimentSlotField.js';
|
|
5
|
+
import { resolvePuckRoutes } from './routes.js';
|
|
4
6
|
import { slotToRender } from './slot.model.js';
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
export function createExperimentSlot({ routes: inputRoutes } = {}) {
|
|
8
|
+
const routes = resolvePuckRoutes(inputRoutes);
|
|
9
|
+
return {
|
|
10
|
+
label: experimentSlotLabel,
|
|
11
|
+
fields: {
|
|
12
|
+
...experimentSlotFields,
|
|
13
|
+
experiment: createExperimentSlotField({
|
|
14
|
+
routes
|
|
15
|
+
})
|
|
16
|
+
},
|
|
17
|
+
defaultProps: experimentSlotDefaultProps,
|
|
18
|
+
render: (props)=>{
|
|
19
|
+
const Slot = props[slotToRender(props)];
|
|
20
|
+
return /*#__PURE__*/ _jsx(ViewportExposure, {
|
|
21
|
+
expose: props.xpExpose,
|
|
22
|
+
children: /*#__PURE__*/ _jsx(Slot, {})
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export const ExperimentSlot = createExperimentSlot();
|
package/dist/puck/copy.d.ts
CHANGED
|
@@ -5,11 +5,12 @@ export declare const slotLabels: {
|
|
|
5
5
|
readonly variantC: "Variant C";
|
|
6
6
|
};
|
|
7
7
|
export declare const pickerCopy: {
|
|
8
|
-
readonly label: "
|
|
9
|
-
readonly none: "No
|
|
10
|
-
readonly
|
|
11
|
-
readonly
|
|
12
|
-
readonly
|
|
8
|
+
readonly label: "Test";
|
|
9
|
+
readonly none: "No test";
|
|
10
|
+
readonly search: "Search tests";
|
|
11
|
+
readonly loading: "Loading tests…";
|
|
12
|
+
readonly error: "Couldn't load tests.";
|
|
13
|
+
readonly forbidden: "You can't see tests. Ask an admin.";
|
|
13
14
|
readonly statuses: {
|
|
14
15
|
readonly draft: "Draft";
|
|
15
16
|
readonly running: "Running";
|
package/dist/puck/copy.js
CHANGED
|
@@ -6,11 +6,12 @@ export const slotLabels = {
|
|
|
6
6
|
variantC: ARM_LABELS.variantC
|
|
7
7
|
};
|
|
8
8
|
export const pickerCopy = {
|
|
9
|
-
label: '
|
|
10
|
-
none: 'No
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
label: 'Test',
|
|
10
|
+
none: 'No test',
|
|
11
|
+
search: 'Search tests',
|
|
12
|
+
loading: 'Loading tests…',
|
|
13
|
+
error: "Couldn't load tests.",
|
|
14
|
+
forbidden: "You can't see tests. Ask an admin.",
|
|
14
15
|
statuses: {
|
|
15
16
|
draft: 'Draft',
|
|
16
17
|
running: 'Running',
|