@delmaredigital/payload-experiments 0.3.0-rc.4 → 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 +113 -20
- 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 +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/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/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 +22 -3
- package/dist/admin/copy/readiness.js +23 -3
- package/dist/admin/copy/results.d.ts +1 -0
- package/dist/admin/copy/results.js +1 -0
- 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 +6 -0
- package/dist/admin/paths.js +6 -0
- 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/results/DataQualityPanel.js +4 -1
- package/dist/admin/results/results.model.d.ts +3 -0
- package/dist/admin/results/results.model.js +8 -4
- package/dist/admin/surface/TestsHome.js +37 -6
- 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 +4 -1
- 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 +1 -1
- package/dist/admin/surface/home.model.js +8 -4
- package/dist/admin/surface/newTest/NewTest.js +54 -13
- 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 +99 -64
- 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 +91 -9
- 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 +21 -2
- package/dist/admin/surface/results/ResultsHeader.js +2 -5
- package/dist/admin/surface/results/useResults.d.ts +10 -1
- package/dist/admin/surface/test/AdvancedSection.js +111 -104
- package/dist/admin/surface/test/GoalSection.js +90 -7
- 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/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 +53 -15
- package/dist/admin/useApi.d.ts +11 -2
- package/dist/admin/useApi.js +3 -5
- package/dist/admin/views/ExperimentsRootView.js +58 -16
- 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/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 +8 -0
- package/dist/db/locks.js +27 -4
- 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/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 +8 -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/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 +3 -1
- package/dist/exports/puck.js +2 -0
- package/dist/exports/puckEditor.d.ts +3 -0
- package/dist/exports/puckEditor.js +2 -0
- package/dist/exports/rsc.d.ts +3 -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 +157 -95
- package/dist/onInit.js +29 -0
- package/dist/options.d.ts +2 -0
- package/dist/options.js +41 -1
- package/dist/plugin.js +41 -28
- package/dist/puck/copy.d.ts +1 -0
- package/dist/puck/copy.js +1 -0
- package/dist/puck/editor/editor.chrome.css +1 -0
- package/dist/puck/editor/editor.css +1 -0
- package/dist/puck/editor/editor.model.d.ts +4 -2
- package/dist/puck/editor/editor.model.js +6 -3
- 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/integration.d.ts +6 -0
- package/dist/puck/integration.js +21 -0
- 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 +41 -0
- package/dist/server/pageKindSeoContract.d.ts +27 -0
- package/dist/server/pageKindSeoContract.js +80 -0
- package/dist/server/recordContentChange.js +58 -8
- package/dist/server/resolveExperimentBlocks.d.ts +5 -0
- package/dist/server/resolveExperimentBlocks.js +54 -0
- package/dist/server/runResults.js +56 -15
- 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 +13 -1
- package/dist/transitions/checkStart.js +89 -7
- package/dist/transitions/decide.js +24 -0
- 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 +8 -0
- package/dist/transitions/placements.js +148 -1
- package/dist/transitions/preflightData.d.ts +2 -2
- package/dist/transitions/preflightData.js +137 -5
- package/dist/transitions/resolveCaps.d.ts +1 -0
- package/dist/transitions/resolveCaps.js +6 -2
- package/dist/transitions/start.js +45 -1
- package/dist/types/admin.d.ts +1 -1
- package/dist/types/assignments.d.ts +1 -0
- package/dist/types/contracts.d.ts +15 -0
- 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/package.json +8 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Button, SelectInput, useAuth, useConfig, useDocumentInfo, useForm, useFormFields, useFormModified, useServerFunctions } from '@payloadcms/ui';
|
|
4
|
+
import { formatAdminURL } from 'payload/shared';
|
|
5
|
+
import { useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { hostBlocks } from '../../blocks/model.js';
|
|
7
|
+
import { wrapExperimentBlock } from '../../blocks/formState.js';
|
|
8
|
+
import { CreateRefused, createTest } from '../../puck/editor/createTest.js';
|
|
9
|
+
import { createApi } from '../api.js';
|
|
10
|
+
import { BLOCK_COPY, blockSaveNotice, blockTestName } from '../copy/blocks.js';
|
|
11
|
+
import { API_MESSAGES } from '../copy/common.js';
|
|
12
|
+
import { adaptClientBlocksField, BlockChangedError, dataAt, experimentBlockSlug, findClientBlocksField, replaceBlockRow, wrapMode } from './formApi.js';
|
|
13
|
+
import { blockChoices, labelText } from './model.js';
|
|
14
|
+
export function TestBlockField(props) {
|
|
15
|
+
const { permissions } = useAuth();
|
|
16
|
+
const testPermissions = permissions?.collections?.['xp-experiments'];
|
|
17
|
+
return testPermissions?.update === true ? /*#__PURE__*/ _jsx(ManagedTestBlockField, {
|
|
18
|
+
...props
|
|
19
|
+
}) : null;
|
|
20
|
+
}
|
|
21
|
+
function ManagedTestBlockField({ blocksField, blockSlug: suppliedSlug }) {
|
|
22
|
+
const form = useForm(), modified = useFormModified(), document = useDocumentInfo();
|
|
23
|
+
const { config, getEntityConfig } = useConfig(), { getFormState } = useServerFunctions();
|
|
24
|
+
useFormFields(([fields])=>fields);
|
|
25
|
+
const collection = document.collectionSlug ? getEntityConfig({
|
|
26
|
+
collectionSlug: document.collectionSlug
|
|
27
|
+
}) : undefined;
|
|
28
|
+
const clientField = collection ? findClientBlocksField(collection.fields, blocksField) : undefined;
|
|
29
|
+
const field = useMemo(()=>clientField ? adaptClientBlocksField(clientField, config) : undefined, [
|
|
30
|
+
clientField,
|
|
31
|
+
config
|
|
32
|
+
]);
|
|
33
|
+
const [choice, setChoice] = useState(), [busy, setBusy] = useState(false), [error, setError] = useState();
|
|
34
|
+
const wrapping = useRef(false);
|
|
35
|
+
if (!field || !collection || !document.collectionSlug) return null;
|
|
36
|
+
const blockSlug = suppliedSlug ?? experimentBlockSlug(field);
|
|
37
|
+
const rows = hostBlocks(form.getDataByPath(blocksField)), choices = blockChoices(rows, field, blockSlug);
|
|
38
|
+
const wrappers = rows.filter((block)=>block.blockType === blockSlug && typeof block.experiment === 'string');
|
|
39
|
+
async function wrap() {
|
|
40
|
+
if (!field || wrapping.current || !document.collectionSlug || !collection) return;
|
|
41
|
+
setError(undefined);
|
|
42
|
+
if (wrapMode === 'fallback' && modified) {
|
|
43
|
+
setError(BLOCK_COPY.saveFirst);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const selected = blockChoices(form.getDataByPath(blocksField), field, blockSlug).find((item)=>String(item.index) === choice);
|
|
47
|
+
if (!selected) return;
|
|
48
|
+
const title = collection.admin?.useAsTitle ? form.getDataByPath(collection.admin.useAsTitle) : undefined;
|
|
49
|
+
const name = blockTestName(typeof title === 'string' && title.trim() ? title : labelText(collection.labels.singular) ?? collection.slug, selected.label);
|
|
50
|
+
const beforeCreate = JSON.stringify(form.getData());
|
|
51
|
+
wrapping.current = true;
|
|
52
|
+
setBusy(true);
|
|
53
|
+
try {
|
|
54
|
+
const test = await createTest(config.routes.api, {
|
|
55
|
+
kind: 'block',
|
|
56
|
+
name,
|
|
57
|
+
unitType: 'visitor'
|
|
58
|
+
});
|
|
59
|
+
if (wrapMode === 'fallback') {
|
|
60
|
+
if (JSON.stringify(form.getData()) !== beforeCreate) {
|
|
61
|
+
setError(BLOCK_COPY.saveFirst);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (document.id == null || !selected.block.id) {
|
|
65
|
+
setError(BLOCK_COPY.wrapRefusals['wrap-target-missing']);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const result = await createApi(config.routes.api).wrapBlock({
|
|
69
|
+
collection: document.collectionSlug,
|
|
70
|
+
id: document.id,
|
|
71
|
+
field: blocksField,
|
|
72
|
+
blockId: selected.block.id,
|
|
73
|
+
experiment: test.slug
|
|
74
|
+
});
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
setError(result.code && Object.hasOwn(BLOCK_COPY.wrapRefusals, result.code) ? BLOCK_COPY.wrapRefusals[result.code] : API_MESSAGES.failed);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
window.location.reload();
|
|
80
|
+
} else {
|
|
81
|
+
await replaceBlockRow({
|
|
82
|
+
form,
|
|
83
|
+
getFormState,
|
|
84
|
+
collectionSlug: document.collectionSlug,
|
|
85
|
+
id: document.id,
|
|
86
|
+
field: blocksField,
|
|
87
|
+
index: selected.index,
|
|
88
|
+
expectedBlock: selected.block,
|
|
89
|
+
rows: [
|
|
90
|
+
wrapExperimentBlock(selected.block, test.slug, field, {}, blockSlug)
|
|
91
|
+
],
|
|
92
|
+
docPermissions: document.docPermissions,
|
|
93
|
+
docPreferences: await document.getDocPreferences?.()
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
setChoice(undefined);
|
|
97
|
+
} catch (cause) {
|
|
98
|
+
setError(cause instanceof CreateRefused || cause instanceof BlockChangedError ? cause.message : API_MESSAGES.failed);
|
|
99
|
+
} finally{
|
|
100
|
+
wrapping.current = false;
|
|
101
|
+
setBusy(false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
105
|
+
children: [
|
|
106
|
+
/*#__PURE__*/ _jsx("h3", {
|
|
107
|
+
children: BLOCK_COPY.testBlock
|
|
108
|
+
}),
|
|
109
|
+
choices.length ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
110
|
+
children: [
|
|
111
|
+
/*#__PURE__*/ _jsx(SelectInput, {
|
|
112
|
+
name: "xpBlockChoice",
|
|
113
|
+
path: "xpBlockChoice",
|
|
114
|
+
label: BLOCK_COPY.pick,
|
|
115
|
+
options: choices.map((item)=>({
|
|
116
|
+
label: item.option,
|
|
117
|
+
value: String(item.index)
|
|
118
|
+
})),
|
|
119
|
+
value: choice,
|
|
120
|
+
readOnly: busy,
|
|
121
|
+
onChange: (option)=>{
|
|
122
|
+
if (!Array.isArray(option)) setChoice(option?.value);
|
|
123
|
+
}
|
|
124
|
+
}),
|
|
125
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
126
|
+
disabled: busy || choice === undefined,
|
|
127
|
+
onClick: ()=>void wrap(),
|
|
128
|
+
children: BLOCK_COPY.testBlock
|
|
129
|
+
})
|
|
130
|
+
]
|
|
131
|
+
}) : /*#__PURE__*/ _jsx("p", {
|
|
132
|
+
children: BLOCK_COPY.empty
|
|
133
|
+
}),
|
|
134
|
+
error ? /*#__PURE__*/ _jsx("p", {
|
|
135
|
+
role: "alert",
|
|
136
|
+
children: error
|
|
137
|
+
}) : null,
|
|
138
|
+
wrappers.map((block)=>{
|
|
139
|
+
const slug = block.experiment;
|
|
140
|
+
const inSavedDocument = hostBlocks(dataAt(document.data, blocksField)).some((row)=>row.blockType === blockSlug && row.experiment === slug);
|
|
141
|
+
const saved = !modified && inSavedDocument;
|
|
142
|
+
return /*#__PURE__*/ _jsxs("p", {
|
|
143
|
+
children: [
|
|
144
|
+
!inSavedDocument ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
145
|
+
children: [
|
|
146
|
+
blockSaveNotice(Boolean(collection.versions?.drafts)),
|
|
147
|
+
' '
|
|
148
|
+
]
|
|
149
|
+
}) : null,
|
|
150
|
+
/*#__PURE__*/ _jsx("a", {
|
|
151
|
+
"aria-disabled": !saved,
|
|
152
|
+
href: saved ? formatAdminURL({
|
|
153
|
+
adminRoute: config.routes.admin,
|
|
154
|
+
path: `/experiments/by-slug/${encodeURIComponent(slug)}`
|
|
155
|
+
}) : undefined,
|
|
156
|
+
children: BLOCK_COPY.setup
|
|
157
|
+
})
|
|
158
|
+
]
|
|
159
|
+
}, block.id ?? slug);
|
|
160
|
+
})
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ClientConfig, ClientField, Field } from 'payload';
|
|
2
|
+
import type { useForm, useServerFunctions } from '@payloadcms/ui';
|
|
3
|
+
import { type HostBlock } from '../../blocks/model.js';
|
|
4
|
+
/** The only form-state failure with block-specific editor copy. */
|
|
5
|
+
export declare class BlockChangedError extends Error {
|
|
6
|
+
constructor();
|
|
7
|
+
}
|
|
8
|
+
export type RowsField = Extract<Field, {
|
|
9
|
+
type: 'blocks';
|
|
10
|
+
}>;
|
|
11
|
+
export declare const wrapMode: 'form-api' | 'fallback';
|
|
12
|
+
/** Client twin: unnamed layout containers do not contribute to a data path. */
|
|
13
|
+
export declare function findClientBlocksField(fields: readonly ClientField[], name: string): Extract<ClientField, {
|
|
14
|
+
type: 'blocks';
|
|
15
|
+
}> | undefined;
|
|
16
|
+
/** Only schema structure is needed by the pure helpers, never server validators.
|
|
17
|
+
* Resolve references at every depth, including inside the Experiment slots field.
|
|
18
|
+
*/
|
|
19
|
+
export declare function adaptClientBlocksField(field: Extract<ClientField, {
|
|
20
|
+
type: 'blocks';
|
|
21
|
+
}>, config: Pick<ClientConfig, 'blocks'>): RowsField;
|
|
22
|
+
export declare function experimentBlockSlug(field: RowsField): string;
|
|
23
|
+
export declare function rowLocation(path: string): {
|
|
24
|
+
field: string;
|
|
25
|
+
index: number;
|
|
26
|
+
} | undefined;
|
|
27
|
+
export declare function dataAt(data: unknown, path: string): unknown;
|
|
28
|
+
/** Payload 3.90.2 and 4 beta.6 use the same row APIs. Sync replaces the whole
|
|
29
|
+
* wrapper so add/drop/reorder and the server-rendered component slots stay together.
|
|
30
|
+
* N=0 needs UPDATE: removeFieldRow alone serializes the empty field as number 0.
|
|
31
|
+
*/
|
|
32
|
+
export declare function replaceBlockRow(args: {
|
|
33
|
+
form: ReturnType<typeof useForm>;
|
|
34
|
+
getFormState: ReturnType<typeof useServerFunctions>['getFormState'];
|
|
35
|
+
collectionSlug: string;
|
|
36
|
+
id?: string | number;
|
|
37
|
+
field: string;
|
|
38
|
+
index: number;
|
|
39
|
+
rows: HostBlock[];
|
|
40
|
+
expectedBlock?: HostBlock;
|
|
41
|
+
docPermissions?: Parameters<ReturnType<typeof useServerFunctions>['getFormState']>[0]['docPermissions'];
|
|
42
|
+
docPreferences?: Parameters<ReturnType<typeof useServerFunctions>['getFormState']>[0]['docPreferences'];
|
|
43
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { EXPERIMENT_BLOCK_SLUG, hostBlocks } from '../../blocks/model.js';
|
|
2
|
+
import { BLOCK_COPY } from '../copy/blocks.js';
|
|
3
|
+
/** The only form-state failure with block-specific editor copy. */ export class BlockChangedError extends Error {
|
|
4
|
+
constructor(){
|
|
5
|
+
super(BLOCK_COPY.blockChanged);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export const wrapMode = 'form-api';
|
|
9
|
+
/** Client twin: unnamed layout containers do not contribute to a data path. */ export function findClientBlocksField(fields, name) {
|
|
10
|
+
for (const field of fields){
|
|
11
|
+
if (field.type === 'blocks' && field.name === name) return field;
|
|
12
|
+
if (field.type === 'row' || field.type === 'collapsible') {
|
|
13
|
+
const found = findClientBlocksField(field.fields, name);
|
|
14
|
+
if (found) return found;
|
|
15
|
+
}
|
|
16
|
+
if (field.type === 'tabs') for (const tab of field.tabs){
|
|
17
|
+
if ('name' in tab && tab.name) continue;
|
|
18
|
+
const found = findClientBlocksField(tab.fields, name);
|
|
19
|
+
if (found) return found;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Only schema structure is needed by the pure helpers, never server validators.
|
|
24
|
+
* Resolve references at every depth, including inside the Experiment slots field.
|
|
25
|
+
*/ export function adaptClientBlocksField(field, config) {
|
|
26
|
+
function block(block) {
|
|
27
|
+
return {
|
|
28
|
+
...block,
|
|
29
|
+
fields: block.fields.map(adapt)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function adapt(field) {
|
|
33
|
+
if (field.type === 'blocks') return {
|
|
34
|
+
...field,
|
|
35
|
+
blocks: [
|
|
36
|
+
...field.blocks.map(block),
|
|
37
|
+
...(field.blockReferences ?? []).flatMap((reference)=>{
|
|
38
|
+
const resolved = typeof reference === 'string' ? config.blocks?.find((item)=>item.slug === reference) : reference;
|
|
39
|
+
return resolved ? [
|
|
40
|
+
block(resolved)
|
|
41
|
+
] : [];
|
|
42
|
+
})
|
|
43
|
+
],
|
|
44
|
+
blockReferences: []
|
|
45
|
+
};
|
|
46
|
+
if ('fields' in field) return {
|
|
47
|
+
...field,
|
|
48
|
+
fields: field.fields.map(adapt)
|
|
49
|
+
};
|
|
50
|
+
if (field.type === 'tabs') return {
|
|
51
|
+
...field,
|
|
52
|
+
tabs: field.tabs.map((tab)=>({
|
|
53
|
+
...tab,
|
|
54
|
+
fields: tab.fields.map(adapt)
|
|
55
|
+
}))
|
|
56
|
+
};
|
|
57
|
+
return field;
|
|
58
|
+
}
|
|
59
|
+
return adapt(field);
|
|
60
|
+
}
|
|
61
|
+
export function experimentBlockSlug(field) {
|
|
62
|
+
return field.blocks.find((block)=>block.fields.some((field)=>'name' in field && field.name === 'xpBlockStatus'))?.slug ?? EXPERIMENT_BLOCK_SLUG;
|
|
63
|
+
}
|
|
64
|
+
export function rowLocation(path) {
|
|
65
|
+
const match = /^([^.]+)\.(\d+)(?:\.|$)/.exec(path);
|
|
66
|
+
return match ? {
|
|
67
|
+
field: match[1],
|
|
68
|
+
index: Number(match[2])
|
|
69
|
+
} : undefined;
|
|
70
|
+
}
|
|
71
|
+
export function dataAt(data, path) {
|
|
72
|
+
return path.split('.').reduce((value, key)=>value && typeof value === 'object' ? value[key] : undefined, data);
|
|
73
|
+
}
|
|
74
|
+
/** Payload 3.90.2 and 4 beta.6 use the same row APIs. Sync replaces the whole
|
|
75
|
+
* wrapper so add/drop/reorder and the server-rendered component slots stay together.
|
|
76
|
+
* N=0 needs UPDATE: removeFieldRow alone serializes the empty field as number 0.
|
|
77
|
+
*/ export async function replaceBlockRow(args) {
|
|
78
|
+
const { form, getFormState, collectionSlug, id, field, index, rows } = args;
|
|
79
|
+
const data = form.getData(), before = hostBlocks(data[field]);
|
|
80
|
+
if (args.expectedBlock && JSON.stringify(before[index]) !== JSON.stringify(args.expectedBlock)) throw new BlockChangedError();
|
|
81
|
+
const original = JSON.stringify(data[field]);
|
|
82
|
+
const next = [
|
|
83
|
+
...before.slice(0, index),
|
|
84
|
+
...rows,
|
|
85
|
+
...before.slice(index + 1)
|
|
86
|
+
];
|
|
87
|
+
const result = await getFormState({
|
|
88
|
+
collectionSlug,
|
|
89
|
+
id,
|
|
90
|
+
schemaPath: collectionSlug,
|
|
91
|
+
data: {
|
|
92
|
+
...data,
|
|
93
|
+
[field]: next
|
|
94
|
+
},
|
|
95
|
+
docPermissions: args.docPermissions,
|
|
96
|
+
docPreferences: args.docPreferences ?? {
|
|
97
|
+
fields: {}
|
|
98
|
+
},
|
|
99
|
+
operation: id == null ? 'create' : 'update',
|
|
100
|
+
renderAllFields: true,
|
|
101
|
+
skipValidation: true
|
|
102
|
+
});
|
|
103
|
+
if (JSON.stringify(form.getDataByPath(field)) !== original) throw new BlockChangedError();
|
|
104
|
+
if (!result.state) throw new Error('Missing server form state');
|
|
105
|
+
const state = result.state;
|
|
106
|
+
function subFieldState(rowIndex) {
|
|
107
|
+
const prefix = `${field}.${rowIndex}.`;
|
|
108
|
+
return Object.fromEntries(Object.entries(state).filter(([path])=>path.startsWith(prefix)).map(([path, value])=>[
|
|
109
|
+
path.slice(prefix.length),
|
|
110
|
+
value
|
|
111
|
+
]));
|
|
112
|
+
}
|
|
113
|
+
if (!rows.length) {
|
|
114
|
+
form.removeFieldRow({
|
|
115
|
+
path: field,
|
|
116
|
+
rowIndex: index
|
|
117
|
+
});
|
|
118
|
+
if (next.length === 0) form.dispatchFields({
|
|
119
|
+
type: 'UPDATE',
|
|
120
|
+
path: field,
|
|
121
|
+
value: [],
|
|
122
|
+
disableFormData: false
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
form.replaceFieldRow({
|
|
127
|
+
path: field,
|
|
128
|
+
rowIndex: index,
|
|
129
|
+
schemaPath: `${collectionSlug}.${field}`,
|
|
130
|
+
blockType: rows[0].blockType,
|
|
131
|
+
subFieldState: subFieldState(index)
|
|
132
|
+
});
|
|
133
|
+
for(let offset = 1; offset < rows.length; offset++)form.addFieldRow({
|
|
134
|
+
path: field,
|
|
135
|
+
rowIndex: index + offset,
|
|
136
|
+
schemaPath: `${collectionSlug}.${field}`,
|
|
137
|
+
blockType: rows[offset].blockType,
|
|
138
|
+
subFieldState: subFieldState(index + offset)
|
|
139
|
+
});
|
|
140
|
+
// REPLACE_ROW invents a metadata id even when the data id is unchanged.
|
|
141
|
+
// Keep server row metadata (including custom labels) and the wrapper React key:
|
|
142
|
+
// otherwise a sync remounts the status field and loses its save notice.
|
|
143
|
+
const oldRows = form.getFields()[field]?.rows ?? [];
|
|
144
|
+
const serverRows = state[field]?.rows;
|
|
145
|
+
if (serverRows) form.dispatchFields({
|
|
146
|
+
type: 'UPDATE',
|
|
147
|
+
path: field,
|
|
148
|
+
rows: serverRows.map((row)=>({
|
|
149
|
+
...oldRows.find((old)=>old.id === row.id),
|
|
150
|
+
...row
|
|
151
|
+
}))
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Field } from 'payload';
|
|
2
|
+
/** Copy the coordinates supplied by CORE-L2, including every containing array. */
|
|
3
|
+
export declare function injectTestBlock(fields: Field[], registration: {
|
|
4
|
+
field: string;
|
|
5
|
+
blockSlug?: string;
|
|
6
|
+
appliesTo?: (doc: Record<string, unknown>) => boolean;
|
|
7
|
+
}): Field[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { findRegisteredBlocksField } from '../../blocks/formState.js';
|
|
2
|
+
import { XP_TEST_BLOCK } from '../paths.js';
|
|
3
|
+
/** Copy the coordinates supplied by CORE-L2, including every containing array. */ export function injectTestBlock(fields, registration) {
|
|
4
|
+
const found = findRegisteredBlocksField(fields, registration.field);
|
|
5
|
+
if (!found) return fields;
|
|
6
|
+
const ui = {
|
|
7
|
+
name: 'xpTestBlock',
|
|
8
|
+
type: 'ui',
|
|
9
|
+
admin: {
|
|
10
|
+
condition: (data)=>{
|
|
11
|
+
try {
|
|
12
|
+
return registration.appliesTo ? registration.appliesTo(data) === true : true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
components: {
|
|
18
|
+
Field: {
|
|
19
|
+
path: XP_TEST_BLOCK,
|
|
20
|
+
clientProps: {
|
|
21
|
+
blocksField: registration.field,
|
|
22
|
+
blockSlug: registration.blockSlug
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
function insert(value, path) {
|
|
29
|
+
const [key, ...rest] = path;
|
|
30
|
+
if (key === undefined) return value;
|
|
31
|
+
const copy = Array.isArray(value) ? [
|
|
32
|
+
...value
|
|
33
|
+
] : {
|
|
34
|
+
...value
|
|
35
|
+
};
|
|
36
|
+
if (!rest.length) copy.splice(Number(key) + 1, 0, ui);
|
|
37
|
+
else copy[key] = insert(value[key], rest);
|
|
38
|
+
return copy;
|
|
39
|
+
}
|
|
40
|
+
return insert(fields, found.path);
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PlacementStatusResult } from '../../types/endpoints.js';
|
|
2
|
+
import type { RowsField } from './formApi.js';
|
|
3
|
+
export type BlockStatus = PlacementStatusResult['slugs'][string];
|
|
4
|
+
export type LiveBlockStatus = Extract<BlockStatus, {
|
|
5
|
+
status: string;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function liveStatus(status: BlockStatus | null): status is LiveBlockStatus;
|
|
8
|
+
export declare function slotLabel(slot: string, status: BlockStatus | null): string;
|
|
9
|
+
export declare function statusSentence(status: BlockStatus): string;
|
|
10
|
+
export declare function isOrphan(status: BlockStatus): boolean;
|
|
11
|
+
export declare function labelText(value: unknown): string | undefined;
|
|
12
|
+
export declare function blockChoices(value: unknown, field: RowsField, blockSlug: string): {
|
|
13
|
+
block: import("../../blocks/model.js").HostBlock;
|
|
14
|
+
index: number;
|
|
15
|
+
label: string;
|
|
16
|
+
option: string;
|
|
17
|
+
}[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { hostBlocks } from '../../blocks/model.js';
|
|
2
|
+
import { BLOCK_COPY, blockDecidedStatus, blockDraftStatus, blockFinishedStatus, blockPosition } from '../copy/blocks.js';
|
|
3
|
+
export function liveStatus(status) {
|
|
4
|
+
return !!status && 'status' in status;
|
|
5
|
+
}
|
|
6
|
+
export function slotLabel(slot, status) {
|
|
7
|
+
return (liveStatus(status) ? status.arms.find((arm)=>arm.key !== 'holdout' && (arm.slot ?? arm.key) === slot)?.label : undefined) || BLOCK_COPY.slotLabels[slot] || slot;
|
|
8
|
+
}
|
|
9
|
+
export function statusSentence(status) {
|
|
10
|
+
if (!liveStatus(status)) return BLOCK_COPY.deleted;
|
|
11
|
+
const winner = status.arms.find((arm)=>arm.key === status.winnerArm);
|
|
12
|
+
const winnerLabel = winner?.label || BLOCK_COPY.slotLabels[status.winnerArm ?? 'control'] || status.winnerArm || BLOCK_COPY.slotLabels.control;
|
|
13
|
+
switch(status.status){
|
|
14
|
+
case 'draft':
|
|
15
|
+
return blockDraftStatus(status.name);
|
|
16
|
+
case 'running':
|
|
17
|
+
return BLOCK_COPY.running;
|
|
18
|
+
case 'stopped':
|
|
19
|
+
return BLOCK_COPY.stopped;
|
|
20
|
+
case 'decided':
|
|
21
|
+
return blockDecidedStatus(winnerLabel, status.rollout);
|
|
22
|
+
case 'archived':
|
|
23
|
+
return status.winnerArm ? blockFinishedStatus(winnerLabel) : BLOCK_COPY.discarded;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function isOrphan(status) {
|
|
27
|
+
return !liveStatus(status) || status.status === 'archived' && !status.winnerArm;
|
|
28
|
+
}
|
|
29
|
+
export function labelText(value) {
|
|
30
|
+
return typeof value === 'string' ? value : value && typeof value === 'object' ? Object.values(value).find((item)=>typeof item === 'string') : undefined;
|
|
31
|
+
}
|
|
32
|
+
export function blockChoices(value, field, blockSlug) {
|
|
33
|
+
return hostBlocks(value).flatMap((block, index)=>{
|
|
34
|
+
if (block.blockType === blockSlug) return [];
|
|
35
|
+
const label = labelText(field.blocks.find((config)=>config.slug === block.blockType)?.labels?.singular) || block.blockType;
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
block,
|
|
39
|
+
index,
|
|
40
|
+
label,
|
|
41
|
+
option: blockPosition(label, index + 1)
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BlockStatus } from './model.js';
|
|
2
|
+
export declare const validBlockSlug: (slug: unknown) => slug is string;
|
|
3
|
+
/** Only status fields own reads. StrictMode replays the same mount token; a
|
|
4
|
+
* different owner after the last cleanup starts fresh, even in the same commit.
|
|
5
|
+
* Deferred eviction removes every entry with no live owner after that commit.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useBlockStatus(api: string, slug: unknown, owner?: boolean): BlockStatus | null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react';
|
|
2
|
+
import { createApi } from '../api.js';
|
|
3
|
+
const cache = new Map();
|
|
4
|
+
// Subscribers do not create status entries: a label alone owns no cached data.
|
|
5
|
+
const listeners = new Map();
|
|
6
|
+
const keyOf = (api, slug)=>JSON.stringify([
|
|
7
|
+
api,
|
|
8
|
+
slug
|
|
9
|
+
]);
|
|
10
|
+
const publish = (key)=>listeners.get(key)?.callbacks.forEach((listener)=>listener());
|
|
11
|
+
export const validBlockSlug = (slug)=>typeof slug === 'string' && slug.length > 0 && slug.length <= 64;
|
|
12
|
+
/** Only status fields own reads. StrictMode replays the same mount token; a
|
|
13
|
+
* different owner after the last cleanup starts fresh, even in the same commit.
|
|
14
|
+
* Deferred eviction removes every entry with no live owner after that commit.
|
|
15
|
+
*/ export function useBlockStatus(api, slug, owner = false) {
|
|
16
|
+
const key = validBlockSlug(slug) ? keyOf(api, slug) : undefined;
|
|
17
|
+
const token = useRef({});
|
|
18
|
+
const subscribe = useCallback((listener)=>{
|
|
19
|
+
if (!key) return ()=>{};
|
|
20
|
+
let current = listeners.get(key);
|
|
21
|
+
if (!current) {
|
|
22
|
+
current = {
|
|
23
|
+
callbacks: new Set()
|
|
24
|
+
};
|
|
25
|
+
listeners.set(key, current);
|
|
26
|
+
}
|
|
27
|
+
current.callbacks.add(listener);
|
|
28
|
+
return ()=>{
|
|
29
|
+
current.callbacks = new Set([
|
|
30
|
+
...current.callbacks
|
|
31
|
+
].filter((item)=>item !== listener));
|
|
32
|
+
if (!current.callbacks.size) listeners.delete(key);
|
|
33
|
+
};
|
|
34
|
+
}, [
|
|
35
|
+
key
|
|
36
|
+
]);
|
|
37
|
+
const status = useSyncExternalStore(subscribe, ()=>{
|
|
38
|
+
const current = key ? cache.get(key) : undefined;
|
|
39
|
+
// A new owner must not sync with the previous mount's snapshot before its
|
|
40
|
+
// effect registers ownership (or starts the fresh read).
|
|
41
|
+
return current && (!owner || current.mounts.has(token.current)) ? current.value : null;
|
|
42
|
+
}, ()=>null);
|
|
43
|
+
useEffect(()=>{
|
|
44
|
+
if (!key || !owner || !validBlockSlug(slug)) return;
|
|
45
|
+
let current = cache.get(key);
|
|
46
|
+
if (!current || !current.owners && !current.mounts.has(token.current)) {
|
|
47
|
+
current = {
|
|
48
|
+
value: null,
|
|
49
|
+
owners: 0,
|
|
50
|
+
mounts: new WeakSet()
|
|
51
|
+
};
|
|
52
|
+
cache.set(key, current);
|
|
53
|
+
}
|
|
54
|
+
const owned = current;
|
|
55
|
+
owned.owners++;
|
|
56
|
+
owned.mounts.add(token.current);
|
|
57
|
+
publish(key);
|
|
58
|
+
if (!owned.pending) owned.pending = createApi(api).placementStatus({
|
|
59
|
+
slugs: [
|
|
60
|
+
slug
|
|
61
|
+
]
|
|
62
|
+
}).then((result)=>{
|
|
63
|
+
if (cache.get(key) !== owned) return;
|
|
64
|
+
owned.value = result.ok ? result.data?.slugs[slug] ?? null : null;
|
|
65
|
+
publish(key);
|
|
66
|
+
});
|
|
67
|
+
return ()=>{
|
|
68
|
+
owned.owners--;
|
|
69
|
+
queueMicrotask(()=>{
|
|
70
|
+
if (!owned.owners && cache.get(key) === owned) {
|
|
71
|
+
cache.delete(key);
|
|
72
|
+
publish(key);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
}, [
|
|
77
|
+
api,
|
|
78
|
+
key,
|
|
79
|
+
slug,
|
|
80
|
+
owner
|
|
81
|
+
]);
|
|
82
|
+
return status;
|
|
83
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useConfig, useDocumentInfo, useForm } from '@payloadcms/ui';
|
|
2
|
+
import { adaptClientBlocksField, rowLocation } from './formApi.js';
|
|
3
|
+
import type { ClientCollectionConfig } from 'payload';
|
|
4
|
+
import type { ExperimentBlock, HostBlock } from '../../blocks/model.js';
|
|
5
|
+
/** Declared so the d.ts build can name Payload UI's form types through public exports. */
|
|
6
|
+
export type BlockFormApi = {
|
|
7
|
+
form: ReturnType<typeof useForm>;
|
|
8
|
+
document: ReturnType<typeof useDocumentInfo>;
|
|
9
|
+
config: ReturnType<typeof useConfig>['config'];
|
|
10
|
+
collection: ClientCollectionConfig | undefined;
|
|
11
|
+
field: ReturnType<typeof adaptClientBlocksField> | undefined;
|
|
12
|
+
block: ExperimentBlock | undefined;
|
|
13
|
+
manager: boolean;
|
|
14
|
+
replace: (rows: HostBlock[]) => Promise<void>;
|
|
15
|
+
location: ReturnType<typeof rowLocation>;
|
|
16
|
+
};
|
|
17
|
+
export declare function useBlockForm(path: string): BlockFormApi;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useAuth, useConfig, useDocumentInfo, useForm, useFormFields, useServerFunctions } from '@payloadcms/ui';
|
|
3
|
+
import { adaptClientBlocksField, findClientBlocksField, replaceBlockRow, rowLocation } from './formApi.js';
|
|
4
|
+
export function useBlockForm(path) {
|
|
5
|
+
const form = useForm(), document = useDocumentInfo(), { config, getEntityConfig } = useConfig();
|
|
6
|
+
const { getFormState } = useServerFunctions(), { permissions } = useAuth();
|
|
7
|
+
// useForm's getters are stable; subscribe so changed slug/row paths are read again.
|
|
8
|
+
useFormFields(([fields])=>fields);
|
|
9
|
+
const location = rowLocation(path);
|
|
10
|
+
const collection = document.collectionSlug ? getEntityConfig({
|
|
11
|
+
collectionSlug: document.collectionSlug
|
|
12
|
+
}) : undefined;
|
|
13
|
+
const clientField = location && collection ? findClientBlocksField(collection.fields, location.field) : undefined;
|
|
14
|
+
const field = useMemo(()=>clientField ? adaptClientBlocksField(clientField, config) : undefined, [
|
|
15
|
+
clientField,
|
|
16
|
+
config
|
|
17
|
+
]);
|
|
18
|
+
const block = location ? form.getDataByPath(`${location.field}.${location.index}`) : undefined;
|
|
19
|
+
const testPermissions = permissions?.collections?.['xp-experiments'];
|
|
20
|
+
const manager = testPermissions?.update === true;
|
|
21
|
+
async function replace(rows) {
|
|
22
|
+
if (!location || !document.collectionSlug) return;
|
|
23
|
+
await replaceBlockRow({
|
|
24
|
+
form,
|
|
25
|
+
getFormState,
|
|
26
|
+
collectionSlug: document.collectionSlug,
|
|
27
|
+
id: document.id,
|
|
28
|
+
...location,
|
|
29
|
+
rows,
|
|
30
|
+
expectedBlock: block,
|
|
31
|
+
docPermissions: document.docPermissions,
|
|
32
|
+
docPreferences: await document.getDocPreferences?.()
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
form,
|
|
37
|
+
document,
|
|
38
|
+
config,
|
|
39
|
+
collection,
|
|
40
|
+
field,
|
|
41
|
+
block,
|
|
42
|
+
manager,
|
|
43
|
+
replace,
|
|
44
|
+
location
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AddMetricWarning } from '../../types/endpoints.js';
|
|
1
2
|
export declare const ACTIONS_COPY: {
|
|
2
3
|
readonly start: "Start";
|
|
3
4
|
readonly restart: "Start a new round";
|
|
@@ -70,3 +71,4 @@ export declare const CONTENT_ENTRY_REFUSALS: {
|
|
|
70
71
|
};
|
|
71
72
|
readonly fallback: "This page can't be changed right now. Reload to see its current state.";
|
|
72
73
|
};
|
|
74
|
+
export declare function addMetricWarningCopy(warning: AddMetricWarning): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asymmetricGoalsCopy, placementUncheckedCopy } from './readiness.js';
|
|
1
2
|
export const ACTIONS_COPY = {
|
|
2
3
|
start: 'Start',
|
|
3
4
|
restart: 'Start a new round',
|
|
@@ -70,3 +71,11 @@ export const CONTENT_ENTRY_REFUSALS = {
|
|
|
70
71
|
},
|
|
71
72
|
fallback: ACTIONS_COPY.contentEntryFallback
|
|
72
73
|
};
|
|
74
|
+
export function addMetricWarningCopy(warning) {
|
|
75
|
+
if (warning.code === 'goal-added-mid-test') return "Clicks before this goal was added aren't counted (the same for every version).";
|
|
76
|
+
if (warning.code === 'placement-unchecked') {
|
|
77
|
+
const path = warning.detail?.path;
|
|
78
|
+
return placementUncheckedCopy(typeof path === 'string' ? path : 'This page');
|
|
79
|
+
}
|
|
80
|
+
return asymmetricGoalsCopy(warning.detail);
|
|
81
|
+
}
|