@delmaredigital/payload-experiments 0.2.0-rc.3 → 0.2.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 +15 -0
- package/dist/admin/TestsNavLink.d.ts +1 -0
- package/dist/admin/TestsNavLink.js +1 -0
- package/dist/admin/actions/ConfirmDialog.d.ts +1 -0
- package/dist/admin/actions/ConfirmDialog.js +7 -5
- package/dist/admin/components/ChipsInput.d.ts +12 -0
- package/dist/admin/components/ChipsInput.js +96 -0
- package/dist/admin/components/RootViewMessage.d.ts +5 -0
- package/dist/admin/components/RootViewMessage.js +15 -0
- package/dist/admin/components/chipsInput.css +11 -0
- package/dist/admin/copy/audience.d.ts +4 -0
- package/dist/admin/copy/audience.js +4 -0
- package/dist/admin/copy/goals.d.ts +44 -0
- package/dist/admin/copy/goals.js +46 -0
- package/dist/admin/results/ArmTable.js +2 -1
- package/dist/admin/results/CappedCell.js +2 -1
- package/dist/admin/results/CiChart.js +8 -7
- package/dist/admin/results/DataQualityPanel.js +3 -2
- package/dist/admin/results/VerdictCard.js +3 -2
- package/dist/admin/styles.css +8 -8
- package/dist/admin/styles.d.ts +1 -0
- package/dist/admin/styles.js +1 -0
- package/dist/admin/surface/SurfaceShell.d.ts +1 -0
- package/dist/admin/surface/SurfaceShell.js +1 -0
- package/dist/admin/surface/TestsHome.js +1 -1
- package/dist/admin/surface/TestsSurface.js +10 -1
- package/dist/admin/surface/goals/GoalForm.d.ts +7 -0
- package/dist/admin/surface/goals/GoalForm.js +470 -0
- package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +5 -0
- package/dist/admin/surface/goals/GoalsBreadcrumb.js +47 -0
- package/dist/admin/surface/goals/GoalsList.d.ts +6 -0
- package/dist/admin/surface/goals/GoalsList.js +135 -0
- package/dist/admin/surface/goals/data.d.ts +3 -0
- package/dist/admin/surface/goals/data.js +24 -0
- package/dist/admin/surface/goals/goals.css +15 -0
- package/dist/admin/surface/goals/goals.model.d.ts +30 -0
- package/dist/admin/surface/goals/goals.model.js +26 -0
- package/dist/admin/surface/newTest/newTest.css +6 -6
- package/dist/admin/surface/readiness/Banner.js +5 -4
- package/dist/admin/surface/results/ResultsBody.js +3 -2
- package/dist/admin/surface/setting/setting.css +11 -11
- package/dist/admin/surface/surface.css +29 -29
- package/dist/admin/surface/test/GoalSection.js +32 -1
- package/dist/admin/surface/test/TargetingSection.js +27 -10
- package/dist/admin/surface/test/VersionsSection.js +6 -5
- package/dist/admin/surface/test/testPage.css +12 -12
- package/dist/admin/targeting/targeting.model.d.ts +6 -1
- package/dist/admin/targeting/targeting.model.js +18 -2
- package/dist/admin/theme.css +42 -0
- package/dist/admin/theme.d.ts +50 -0
- package/dist/admin/theme.js +49 -0
- package/dist/admin/views/ExperimentsRootView.js +18 -18
- package/dist/collections/metricValidation.d.ts +3 -0
- package/dist/collections/metricValidation.js +9 -0
- package/dist/collections/metrics.js +3 -5
- package/dist/plugin.js +2 -2
- package/dist/puck/editor/CreateExperimentDrawer.d.ts +1 -0
- package/dist/puck/editor/CreateExperimentDrawer.js +34 -25
- package/dist/puck/editor/ExperimentSlotEditor.d.ts +1 -0
- package/dist/puck/editor/ExperimentSlotEditor.js +137 -78
- package/dist/puck/editor/copy.d.ts +3 -1
- package/dist/puck/editor/copy.js +3 -1
- package/dist/puck/editor/editor.css +302 -0
- package/dist/types/surface.d.ts +2 -1
- package/package.json +2 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** every Payload token reference lives in theme.css; porting to Payload 4 edits that file only */
|
|
2
|
+
export declare const theme: {
|
|
3
|
+
readonly color: {
|
|
4
|
+
readonly bg: "var(--xp-bg)";
|
|
5
|
+
readonly text: "var(--xp-text)";
|
|
6
|
+
readonly surface: "var(--xp-surface)";
|
|
7
|
+
readonly surfaceRaised: "var(--xp-surface-raised)";
|
|
8
|
+
readonly border: "var(--xp-border)";
|
|
9
|
+
readonly borderStrong: "var(--xp-border-strong)";
|
|
10
|
+
readonly borderControl: "var(--xp-border-control)";
|
|
11
|
+
readonly inputBorder: "var(--xp-input-border)";
|
|
12
|
+
readonly borderAccent: "var(--xp-border-accent)";
|
|
13
|
+
readonly accent: "var(--xp-accent)";
|
|
14
|
+
readonly textMuted: "var(--xp-text-muted)";
|
|
15
|
+
readonly textStrong: "var(--xp-text-strong)";
|
|
16
|
+
readonly textEmphasis: "var(--xp-text-emphasis)";
|
|
17
|
+
readonly inputBg: "var(--xp-input-bg)";
|
|
18
|
+
readonly overlay: "var(--xp-overlay)";
|
|
19
|
+
readonly successSurface: "var(--xp-success-surface)";
|
|
20
|
+
readonly successBg: "var(--xp-success-bg)";
|
|
21
|
+
readonly success: "var(--xp-success)";
|
|
22
|
+
readonly successText: "var(--xp-success-text)";
|
|
23
|
+
readonly successTextStrong: "var(--xp-success-text-strong)";
|
|
24
|
+
readonly warningSurface: "var(--xp-warning-surface)";
|
|
25
|
+
readonly warningBg: "var(--xp-warning-bg)";
|
|
26
|
+
readonly warning: "var(--xp-warning)";
|
|
27
|
+
readonly warningText: "var(--xp-warning-text)";
|
|
28
|
+
readonly warningTextStrong: "var(--xp-warning-text-strong)";
|
|
29
|
+
readonly errorSurface: "var(--xp-error-surface)";
|
|
30
|
+
readonly error: "var(--xp-error)";
|
|
31
|
+
readonly errorText: "var(--xp-error-text)";
|
|
32
|
+
};
|
|
33
|
+
readonly radius: {
|
|
34
|
+
readonly small: "var(--xp-radius)";
|
|
35
|
+
readonly large: "var(--xp-radius-large)";
|
|
36
|
+
};
|
|
37
|
+
readonly space: {
|
|
38
|
+
readonly 1: "var(--xp-space-1)";
|
|
39
|
+
readonly 2: "var(--xp-space-2)";
|
|
40
|
+
readonly 3: "var(--xp-space-3)";
|
|
41
|
+
readonly 4: "var(--xp-space-4)";
|
|
42
|
+
readonly 5: "var(--xp-space-5)";
|
|
43
|
+
readonly 6: "var(--xp-space-6)";
|
|
44
|
+
};
|
|
45
|
+
readonly font: {
|
|
46
|
+
readonly body: "var(--xp-font-body)";
|
|
47
|
+
readonly small: "var(--xp-font-size-small)";
|
|
48
|
+
readonly size: "var(--xp-font-size-body)";
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** every Payload token reference lives in theme.css; porting to Payload 4 edits that file only */ export const theme = {
|
|
2
|
+
color: {
|
|
3
|
+
bg: 'var(--xp-bg)',
|
|
4
|
+
text: 'var(--xp-text)',
|
|
5
|
+
surface: 'var(--xp-surface)',
|
|
6
|
+
surfaceRaised: 'var(--xp-surface-raised)',
|
|
7
|
+
border: 'var(--xp-border)',
|
|
8
|
+
borderStrong: 'var(--xp-border-strong)',
|
|
9
|
+
borderControl: 'var(--xp-border-control)',
|
|
10
|
+
inputBorder: 'var(--xp-input-border)',
|
|
11
|
+
borderAccent: 'var(--xp-border-accent)',
|
|
12
|
+
accent: 'var(--xp-accent)',
|
|
13
|
+
textMuted: 'var(--xp-text-muted)',
|
|
14
|
+
textStrong: 'var(--xp-text-strong)',
|
|
15
|
+
textEmphasis: 'var(--xp-text-emphasis)',
|
|
16
|
+
inputBg: 'var(--xp-input-bg)',
|
|
17
|
+
overlay: 'var(--xp-overlay)',
|
|
18
|
+
successSurface: 'var(--xp-success-surface)',
|
|
19
|
+
successBg: 'var(--xp-success-bg)',
|
|
20
|
+
success: 'var(--xp-success)',
|
|
21
|
+
successText: 'var(--xp-success-text)',
|
|
22
|
+
successTextStrong: 'var(--xp-success-text-strong)',
|
|
23
|
+
warningSurface: 'var(--xp-warning-surface)',
|
|
24
|
+
warningBg: 'var(--xp-warning-bg)',
|
|
25
|
+
warning: 'var(--xp-warning)',
|
|
26
|
+
warningText: 'var(--xp-warning-text)',
|
|
27
|
+
warningTextStrong: 'var(--xp-warning-text-strong)',
|
|
28
|
+
errorSurface: 'var(--xp-error-surface)',
|
|
29
|
+
error: 'var(--xp-error)',
|
|
30
|
+
errorText: 'var(--xp-error-text)'
|
|
31
|
+
},
|
|
32
|
+
radius: {
|
|
33
|
+
small: 'var(--xp-radius)',
|
|
34
|
+
large: 'var(--xp-radius-large)'
|
|
35
|
+
},
|
|
36
|
+
space: {
|
|
37
|
+
1: 'var(--xp-space-1)',
|
|
38
|
+
2: 'var(--xp-space-2)',
|
|
39
|
+
3: 'var(--xp-space-3)',
|
|
40
|
+
4: 'var(--xp-space-4)',
|
|
41
|
+
5: 'var(--xp-space-5)',
|
|
42
|
+
6: 'var(--xp-space-6)'
|
|
43
|
+
},
|
|
44
|
+
font: {
|
|
45
|
+
body: 'var(--xp-font-body)',
|
|
46
|
+
small: 'var(--xp-font-size-small)',
|
|
47
|
+
size: 'var(--xp-font-size-body)'
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -18,7 +18,11 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
|
|
|
18
18
|
let id;
|
|
19
19
|
if (tail.length === 0) route = 'home';
|
|
20
20
|
else if (tail.length === 1 && tail[0] === 'new') route = 'new';
|
|
21
|
-
else if (tail.length === 1)
|
|
21
|
+
else if (tail.length === 1 && tail[0] === 'goals') route = 'goals';
|
|
22
|
+
else if (tail.length === 2 && tail[0] === 'goals') {
|
|
23
|
+
route = tail[1] === 'new' ? 'goalNew' : 'goal';
|
|
24
|
+
if (route === 'goal') id = tail[1];
|
|
25
|
+
} else if (tail.length === 1) {
|
|
22
26
|
route = 'test';
|
|
23
27
|
id = tail[0];
|
|
24
28
|
} else if (tail.length === 2 && tail[1] === 'results') {
|
|
@@ -41,6 +45,8 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
|
|
|
41
45
|
path: `${login}?redirect=${encodeURIComponent(currentPath)}`
|
|
42
46
|
}));
|
|
43
47
|
}
|
|
48
|
+
// Keep the server export importable in plain Node; Next loads the CSS-bearing client boundary when this view renders.
|
|
49
|
+
const { RootViewMessage } = await import('../components/RootViewMessage.js');
|
|
44
50
|
const options = xpOptionsFromReq(req);
|
|
45
51
|
async function permits(access) {
|
|
46
52
|
try {
|
|
@@ -60,12 +66,7 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
|
|
|
60
66
|
canManage,
|
|
61
67
|
canReadResults
|
|
62
68
|
}) !== 'ok') {
|
|
63
|
-
return /*#__PURE__*/ _jsx(
|
|
64
|
-
style: {
|
|
65
|
-
padding: 'var(--base)',
|
|
66
|
-
color: 'var(--theme-text)',
|
|
67
|
-
background: 'var(--theme-bg)'
|
|
68
|
-
},
|
|
69
|
+
return /*#__PURE__*/ _jsx(RootViewMessage, {
|
|
69
70
|
children: SURFACE_COPY.noAccess
|
|
70
71
|
});
|
|
71
72
|
}
|
|
@@ -131,7 +132,11 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
|
|
|
131
132
|
}
|
|
132
133
|
}))).flat() : undefined;
|
|
133
134
|
const descriptions = options.admin?.descriptions;
|
|
135
|
+
const returnToTest = (await searchParams)?.return;
|
|
134
136
|
const clientProps = {
|
|
137
|
+
...typeof returnToTest === 'string' && /^[\w-]+$/.test(returnToTest) ? {
|
|
138
|
+
returnToTest
|
|
139
|
+
} : {},
|
|
135
140
|
route,
|
|
136
141
|
...id === undefined ? {} : {
|
|
137
142
|
id
|
|
@@ -155,6 +160,12 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
|
|
|
155
160
|
}
|
|
156
161
|
} : {}
|
|
157
162
|
};
|
|
163
|
+
function TestsSurfaceFallback() {
|
|
164
|
+
return /*#__PURE__*/ _jsx(RootViewMessage, {
|
|
165
|
+
alert: true,
|
|
166
|
+
children: SURFACE_COPY.notInstalled
|
|
167
|
+
});
|
|
168
|
+
}
|
|
158
169
|
return RenderServerComponent({
|
|
159
170
|
Component: XP_TESTS_SURFACE,
|
|
160
171
|
Fallback: TestsSurfaceFallback,
|
|
@@ -162,14 +173,3 @@ export async function ExperimentsRootView({ initPageResult, params, searchParams
|
|
|
162
173
|
clientProps
|
|
163
174
|
});
|
|
164
175
|
}
|
|
165
|
-
function TestsSurfaceFallback() {
|
|
166
|
-
return /*#__PURE__*/ _jsx("div", {
|
|
167
|
-
role: "alert",
|
|
168
|
-
style: {
|
|
169
|
-
padding: 'var(--base)',
|
|
170
|
-
color: 'var(--theme-text)',
|
|
171
|
-
background: 'var(--theme-bg)'
|
|
172
|
-
},
|
|
173
|
-
children: SURFACE_COPY.notInstalled
|
|
174
|
-
});
|
|
175
|
-
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Shared with the goals form; preserve Payload's field validation messages. */
|
|
2
|
+
export declare function validateCappingType(value: unknown, capValue: unknown, sourceKind?: string): true | string;
|
|
3
|
+
export declare function validateCappingValue(value: unknown, type: unknown): true | string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Shared with the goals form; preserve Payload's field validation messages. */ export function validateCappingType(value, capValue, sourceKind) {
|
|
2
|
+
const hasCapping = value != null || capValue != null;
|
|
3
|
+
if (hasCapping && sourceKind && sourceKind !== 'revenue') return 'Count and yes-or-no goals cannot use a limit.';
|
|
4
|
+
return !hasCapping || value === 'percentile' || value === 'absolute' || 'Choose how to set the limit.';
|
|
5
|
+
}
|
|
6
|
+
export function validateCappingValue(value, type) {
|
|
7
|
+
if (value == null && !type) return true;
|
|
8
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 && (type === 'absolute' || type === 'percentile' && value > 0 && value <= 1) || 'Enter a valid limit (for past amounts, use a number greater than 0 and at most 1).';
|
|
9
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { XP_METRIC_SOURCE_SELECT } from '../admin/paths.js';
|
|
2
2
|
import { managedAccess } from './access.js';
|
|
3
3
|
import { serializeMetricSources } from './serialize.js';
|
|
4
|
+
import { validateCappingType, validateCappingValue } from './metricValidation.js';
|
|
4
5
|
export function metricsCollection(options) {
|
|
5
6
|
const sources = serializeMetricSources(options);
|
|
6
7
|
return {
|
|
@@ -125,10 +126,8 @@ export function metricsCollection(options) {
|
|
|
125
126
|
description: 'Choose whether the limit comes from past amounts or a fixed amount.'
|
|
126
127
|
},
|
|
127
128
|
validate: (value, { data, siblingData })=>{
|
|
128
|
-
const hasCapping = value != null || siblingData?.value != null;
|
|
129
129
|
const source = sources.find((source)=>source.id === data?.source);
|
|
130
|
-
|
|
131
|
-
return !hasCapping || value === 'percentile' || value === 'absolute' || 'Choose how to set the limit.';
|
|
130
|
+
return validateCappingType(value, siblingData?.value, source?.kind);
|
|
132
131
|
}
|
|
133
132
|
},
|
|
134
133
|
{
|
|
@@ -140,8 +139,7 @@ export function metricsCollection(options) {
|
|
|
140
139
|
description: 'For past amounts, enter how much of the usual range to keep, such as 0.99 to limit only the largest 1%; or enter a fixed amount.'
|
|
141
140
|
},
|
|
142
141
|
validate: (value, { siblingData })=>{
|
|
143
|
-
|
|
144
|
-
return typeof value === 'number' && Number.isFinite(value) && value >= 0 && (siblingData?.type === 'absolute' || siblingData?.type === 'percentile' && value > 0 && value <= 1) || 'Enter a valid limit (for past amounts, use a number greater than 0 and at most 1).';
|
|
142
|
+
return validateCappingValue(value, siblingData?.type);
|
|
145
143
|
}
|
|
146
144
|
}
|
|
147
145
|
]
|
package/dist/plugin.js
CHANGED
|
@@ -35,8 +35,8 @@ export function createExperimentsPlugin(input) {
|
|
|
35
35
|
path: '/experiments/:segments*'
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
-
|
|
39
|
-
...config.admin?.components?.
|
|
38
|
+
beforeNavLinks: [
|
|
39
|
+
...config.admin?.components?.beforeNavLinks ?? [],
|
|
40
40
|
paths.XP_TESTS_NAV_LINK
|
|
41
41
|
]
|
|
42
42
|
}
|
|
@@ -3,8 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useId, useRef, useState } from 'react';
|
|
4
4
|
import { CreateRefused, createTest } from './createTest.js';
|
|
5
5
|
import { editorCopy } from './copy.js';
|
|
6
|
+
import './editor.css';
|
|
6
7
|
export function CreateExperimentDrawer({ onCreate, onClose, initialName = '' }) {
|
|
7
8
|
const titleId = useId();
|
|
9
|
+
const nameId = useId();
|
|
8
10
|
const [name, setName] = useState(initialName);
|
|
9
11
|
const inFlight = useRef(false);
|
|
10
12
|
const [pending, setPending] = useState(false);
|
|
@@ -30,27 +32,26 @@ export function CreateExperimentDrawer({ onCreate, onClose, initialName = '' })
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
return /*#__PURE__*/ _jsxs("aside", {
|
|
35
|
+
className: "xp-slot-root xp-slot-drawer",
|
|
33
36
|
role: "dialog",
|
|
34
37
|
"aria-labelledby": titleId,
|
|
35
|
-
style: {
|
|
36
|
-
padding: 16,
|
|
37
|
-
border: '1px solid #aaa',
|
|
38
|
-
background: 'Canvas',
|
|
39
|
-
color: 'CanvasText'
|
|
40
|
-
},
|
|
41
38
|
children: [
|
|
42
39
|
/*#__PURE__*/ _jsx("h3", {
|
|
40
|
+
className: "xp-slot-title",
|
|
43
41
|
id: titleId,
|
|
44
42
|
children: editorCopy.createLabel
|
|
45
43
|
}),
|
|
46
|
-
/*#__PURE__*/ _jsxs("
|
|
47
|
-
|
|
48
|
-
display: 'block',
|
|
49
|
-
marginBottom: 8
|
|
50
|
-
},
|
|
44
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
45
|
+
className: "xp-slot-field",
|
|
51
46
|
children: [
|
|
52
|
-
|
|
47
|
+
/*#__PURE__*/ _jsx("label", {
|
|
48
|
+
className: "xp-slot-label",
|
|
49
|
+
htmlFor: nameId,
|
|
50
|
+
children: editorCopy.name
|
|
51
|
+
}),
|
|
53
52
|
/*#__PURE__*/ _jsx("input", {
|
|
53
|
+
className: "xp-slot-input",
|
|
54
|
+
id: nameId,
|
|
54
55
|
value: name,
|
|
55
56
|
disabled: pending,
|
|
56
57
|
onChange: (event)=>setName(event.currentTarget.value)
|
|
@@ -58,22 +59,30 @@ export function CreateExperimentDrawer({ onCreate, onClose, initialName = '' })
|
|
|
58
59
|
]
|
|
59
60
|
}),
|
|
60
61
|
error ? /*#__PURE__*/ _jsx("p", {
|
|
62
|
+
className: "xp-slot-error",
|
|
61
63
|
role: "alert",
|
|
62
64
|
children: error
|
|
63
65
|
}) : null,
|
|
64
|
-
/*#__PURE__*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
67
|
+
className: "xp-slot-actions",
|
|
68
|
+
children: [
|
|
69
|
+
/*#__PURE__*/ _jsx("button", {
|
|
70
|
+
className: "xp-slot-button xp-slot-button-primary",
|
|
71
|
+
type: "button",
|
|
72
|
+
disabled: pending || !name.trim(),
|
|
73
|
+
onClick: ()=>{
|
|
74
|
+
void submitNew();
|
|
75
|
+
},
|
|
76
|
+
children: pending ? editorCopy.creating : editorCopy.createLabel
|
|
77
|
+
}),
|
|
78
|
+
/*#__PURE__*/ _jsx("button", {
|
|
79
|
+
className: "xp-slot-button",
|
|
80
|
+
type: "button",
|
|
81
|
+
disabled: pending,
|
|
82
|
+
onClick: onClose,
|
|
83
|
+
children: editorCopy.cancel
|
|
84
|
+
})
|
|
85
|
+
]
|
|
77
86
|
})
|
|
78
87
|
]
|
|
79
88
|
});
|
|
@@ -8,6 +8,7 @@ import { bannerFor, tabsFor } from './editor.model.js';
|
|
|
8
8
|
import { cloneSlot } from './cloneSlot.js';
|
|
9
9
|
import { CreateExperimentDrawer } from './CreateExperimentDrawer.js';
|
|
10
10
|
import { editorCopy } from './copy.js';
|
|
11
|
+
import './editor.css';
|
|
11
12
|
const usePuckSelector = createUsePuck();
|
|
12
13
|
// Public Puck registration enables native controls inside the editor canvas.
|
|
13
14
|
const registerControls = (element)=>registerOverlayPortal(element, {
|
|
@@ -34,8 +35,8 @@ function EditorRender(props) {
|
|
|
34
35
|
const pageTitle = usePuckSelector((state)=>typeof state.appState.data.root.props?.title === 'string' ? state.appState.data.root.props.title : '');
|
|
35
36
|
const selected = usePuckSelector((state)=>state.selectedItem?.props.id === props.id);
|
|
36
37
|
const originalEmpty = usePuckSelector((state)=>{
|
|
37
|
-
const content = state.
|
|
38
|
-
return
|
|
38
|
+
const content = state.getItemById(props.id)?.props.control;
|
|
39
|
+
return !Array.isArray(content) || content.length === 0;
|
|
39
40
|
});
|
|
40
41
|
useEffect(()=>{
|
|
41
42
|
if (experimentId === undefined) return;
|
|
@@ -74,6 +75,10 @@ function EditorRender(props) {
|
|
|
74
75
|
const tabs = tabsFor(experiment);
|
|
75
76
|
const activeSlot = active && active.id === experimentId ? active.slot : undefined;
|
|
76
77
|
const slot = tabs.find((tab)=>tab.slot === activeSlot)?.slot ?? tabs[0]?.slot ?? 'control';
|
|
78
|
+
const slotEmpty = usePuckSelector((state)=>{
|
|
79
|
+
const content = state.getItemById(props.id)?.props[slot];
|
|
80
|
+
return !Array.isArray(content) || content.length === 0;
|
|
81
|
+
});
|
|
77
82
|
const Slot = props[slot];
|
|
78
83
|
const winnerKey = experiment?.decision?.winnerArm;
|
|
79
84
|
const winner = tabs.find((tab)=>tab.slot === winnerKey)?.label;
|
|
@@ -120,105 +125,159 @@ function EditorRender(props) {
|
|
|
120
125
|
});
|
|
121
126
|
}
|
|
122
127
|
return /*#__PURE__*/ _jsxs("section", {
|
|
123
|
-
|
|
124
|
-
border: '1px solid #aaa',
|
|
125
|
-
padding: 12
|
|
126
|
-
},
|
|
128
|
+
className: "xp-slot-root xp-slot-editor",
|
|
127
129
|
children: [
|
|
128
130
|
/*#__PURE__*/ _jsxs("div", {
|
|
131
|
+
className: "xp-slot-chrome",
|
|
129
132
|
ref: registerControls,
|
|
130
133
|
children: [
|
|
131
|
-
/*#__PURE__*/
|
|
132
|
-
|
|
133
|
-
children:
|
|
134
|
+
/*#__PURE__*/ _jsxs("header", {
|
|
135
|
+
className: "xp-slot-header",
|
|
136
|
+
children: [
|
|
137
|
+
/*#__PURE__*/ _jsx("h3", {
|
|
138
|
+
className: "xp-slot-title",
|
|
139
|
+
children: experiment?.name ?? pickerCopy.label
|
|
140
|
+
}),
|
|
141
|
+
experiment ? /*#__PURE__*/ _jsx("span", {
|
|
142
|
+
className: "xp-slot-status",
|
|
143
|
+
"data-status": experiment.status,
|
|
144
|
+
children: pickerCopy.statuses[experiment.status]
|
|
145
|
+
}) : null,
|
|
146
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
147
|
+
className: "xp-slot-actions",
|
|
148
|
+
children: [
|
|
149
|
+
experimentId === undefined ? /*#__PURE__*/ _jsx("button", {
|
|
150
|
+
className: "xp-slot-button xp-slot-button-primary",
|
|
151
|
+
type: "button",
|
|
152
|
+
disabled: !selected,
|
|
153
|
+
onClick: ()=>setCreating(true),
|
|
154
|
+
children: editorCopy.createLabel
|
|
155
|
+
}) : null,
|
|
156
|
+
experiment ? /*#__PURE__*/ _jsx("a", {
|
|
157
|
+
className: "xp-slot-button xp-slot-button-primary",
|
|
158
|
+
href: `/admin/experiments/${encodeURIComponent(String(experimentId))}${experiment.status === 'draft' ? '' : '/results'}`,
|
|
159
|
+
children: experiment.status === 'draft' ? editorCopy.setup : editorCopy.viewResults
|
|
160
|
+
}) : null
|
|
161
|
+
]
|
|
162
|
+
})
|
|
163
|
+
]
|
|
134
164
|
}),
|
|
135
|
-
/*#__PURE__*/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
165
|
+
/*#__PURE__*/ _jsxs("p", {
|
|
166
|
+
className: experiment?.status === 'running' ? 'xp-slot-notice xp-slot-warning' : 'xp-slot-notice',
|
|
167
|
+
role: "status",
|
|
168
|
+
children: [
|
|
169
|
+
experiment?.status === 'running' ? /*#__PURE__*/ _jsxs("svg", {
|
|
170
|
+
className: "xp-slot-warning-icon",
|
|
171
|
+
"aria-hidden": "true",
|
|
172
|
+
viewBox: "0 0 24 24",
|
|
173
|
+
fill: "none",
|
|
174
|
+
stroke: "currentColor",
|
|
175
|
+
strokeWidth: "2",
|
|
176
|
+
children: [
|
|
177
|
+
/*#__PURE__*/ _jsx("path", {
|
|
178
|
+
d: "M12 3 2 21h20L12 3Z"
|
|
179
|
+
}),
|
|
180
|
+
/*#__PURE__*/ _jsx("path", {
|
|
181
|
+
d: "M12 9v5m0 3v1"
|
|
182
|
+
})
|
|
183
|
+
]
|
|
184
|
+
}) : null,
|
|
185
|
+
experimentId === undefined ? bannerFor() : result?.error ? editorCopy.loadError : experiment ? bannerFor(experiment.status, winner) : editorCopy.loading
|
|
186
|
+
]
|
|
140
187
|
}),
|
|
141
188
|
creating ? /*#__PURE__*/ _jsx(CreateExperimentDrawer, {
|
|
142
189
|
initialName: pageTitle,
|
|
143
190
|
onCreate: bind,
|
|
144
191
|
onClose: ()=>setCreating(false)
|
|
145
192
|
}) : null,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
children: editorCopy.setup
|
|
149
|
-
}) : null,
|
|
150
|
-
/*#__PURE__*/ _jsx("div", {
|
|
151
|
-
style: {
|
|
152
|
-
display: 'flex',
|
|
153
|
-
flexWrap: 'wrap',
|
|
154
|
-
gap: 8,
|
|
155
|
-
marginBlock: 8
|
|
156
|
-
},
|
|
157
|
-
children: tabs.filter((tab)=>tab.slot !== 'control').map((tab)=>/*#__PURE__*/ _jsx("button", {
|
|
158
|
-
type: "button",
|
|
159
|
-
disabled: originalEmpty,
|
|
160
|
-
onClick: ()=>requestCopy(tab.slot),
|
|
161
|
-
children: editorCopy.copy(tab.label)
|
|
162
|
-
}, tab.slot))
|
|
163
|
-
}),
|
|
164
|
-
confirming ? /*#__PURE__*/ _jsxs("p", {
|
|
193
|
+
confirming ? /*#__PURE__*/ _jsxs("div", {
|
|
194
|
+
className: "xp-slot-confirm",
|
|
165
195
|
role: "alert",
|
|
166
196
|
children: [
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
type: "button",
|
|
171
|
-
onClick: ()=>copyTo(confirming),
|
|
172
|
-
children: editorCopy.replace
|
|
197
|
+
/*#__PURE__*/ _jsx("p", {
|
|
198
|
+
className: "xp-slot-notice",
|
|
199
|
+
children: editorCopy.replaceConfirm(tabs.find((tab)=>tab.slot === confirming)?.label ?? confirming)
|
|
173
200
|
}),
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
201
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
202
|
+
className: "xp-slot-actions",
|
|
203
|
+
children: [
|
|
204
|
+
/*#__PURE__*/ _jsx("button", {
|
|
205
|
+
className: "xp-slot-button xp-slot-button-primary",
|
|
206
|
+
type: "button",
|
|
207
|
+
onClick: ()=>copyTo(confirming),
|
|
208
|
+
children: editorCopy.replace
|
|
209
|
+
}),
|
|
210
|
+
/*#__PURE__*/ _jsx("button", {
|
|
211
|
+
className: "xp-slot-button",
|
|
212
|
+
type: "button",
|
|
213
|
+
onClick: ()=>setConfirming(null),
|
|
214
|
+
children: editorCopy.cancel
|
|
215
|
+
})
|
|
216
|
+
]
|
|
179
217
|
})
|
|
180
218
|
]
|
|
181
219
|
}) : null,
|
|
182
|
-
/*#__PURE__*/
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
221
|
+
className: "xp-slot-toolbar",
|
|
222
|
+
children: [
|
|
223
|
+
/*#__PURE__*/ _jsx("div", {
|
|
224
|
+
className: "xp-slot-tabs",
|
|
225
|
+
role: "tablist",
|
|
226
|
+
"aria-label": editorCopy.tabs,
|
|
227
|
+
children: tabs.map((tab, index)=>/*#__PURE__*/ _jsx("button", {
|
|
228
|
+
className: "xp-slot-tab",
|
|
229
|
+
type: "button",
|
|
230
|
+
role: "tab",
|
|
231
|
+
id: `${tabId}-${tab.slot}`,
|
|
232
|
+
"aria-selected": tab.slot === slot,
|
|
233
|
+
tabIndex: tab.slot === slot ? 0 : -1,
|
|
234
|
+
"aria-controls": `${tabId}-panel`,
|
|
235
|
+
onClick: ()=>setActive({
|
|
236
|
+
id: experimentId,
|
|
237
|
+
slot: tab.slot
|
|
238
|
+
}),
|
|
239
|
+
onKeyDown: (event)=>{
|
|
240
|
+
const next = event.key === 'ArrowRight' ? (index + 1) % tabs.length : event.key === 'ArrowLeft' ? (index + tabs.length - 1) % tabs.length : event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : undefined;
|
|
241
|
+
if (next === undefined) return;
|
|
242
|
+
event.preventDefault();
|
|
243
|
+
setActive({
|
|
244
|
+
id: experimentId,
|
|
245
|
+
slot: tabs[next].slot
|
|
246
|
+
});
|
|
247
|
+
const buttons = event.currentTarget.parentElement?.querySelectorAll('[role="tab"]');
|
|
248
|
+
buttons?.[next]?.focus();
|
|
249
|
+
},
|
|
250
|
+
children: tab.label
|
|
251
|
+
}, tab.slot))
|
|
252
|
+
}),
|
|
253
|
+
slot !== 'control' ? /*#__PURE__*/ _jsx("div", {
|
|
254
|
+
className: "xp-slot-actions",
|
|
255
|
+
children: /*#__PURE__*/ _jsx("button", {
|
|
256
|
+
className: "xp-slot-button",
|
|
257
|
+
type: "button",
|
|
258
|
+
disabled: !selected || originalEmpty,
|
|
259
|
+
onClick: ()=>requestCopy(slot),
|
|
260
|
+
children: editorCopy.copy
|
|
261
|
+
})
|
|
262
|
+
}) : null
|
|
263
|
+
]
|
|
214
264
|
})
|
|
215
265
|
]
|
|
216
266
|
}),
|
|
217
|
-
/*#__PURE__*/
|
|
267
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
268
|
+
className: slotEmpty ? 'xp-slot-panel xp-slot-panel-empty' : 'xp-slot-panel',
|
|
218
269
|
role: "tabpanel",
|
|
219
270
|
id: `${tabId}-panel`,
|
|
220
271
|
"aria-labelledby": `${tabId}-${slot}`,
|
|
221
|
-
children:
|
|
272
|
+
children: [
|
|
273
|
+
/*#__PURE__*/ _jsx(Slot, {
|
|
274
|
+
className: "xp-slot-drop-target"
|
|
275
|
+
}),
|
|
276
|
+
slotEmpty ? /*#__PURE__*/ _jsx("span", {
|
|
277
|
+
className: "xp-slot-drop-label",
|
|
278
|
+
children: editorCopy.dropZone(slot)
|
|
279
|
+
}) : null
|
|
280
|
+
]
|
|
222
281
|
})
|
|
223
282
|
]
|
|
224
283
|
});
|
|
@@ -10,7 +10,8 @@ export declare const editorCopy: {
|
|
|
10
10
|
readonly running: "Changing this content mid-test mixes two versions in one result";
|
|
11
11
|
readonly decided: (winner: string) => string;
|
|
12
12
|
readonly archived: "This test is archived; the winning content is shown to everyone.";
|
|
13
|
-
readonly copy:
|
|
13
|
+
readonly copy: "Copy Original to this version";
|
|
14
|
+
readonly dropZone: (slot: "control" | "variantA" | "variantB" | "variantC") => string;
|
|
14
15
|
readonly replaceConfirm: (label: string) => string;
|
|
15
16
|
readonly replace: "Replace";
|
|
16
17
|
readonly createLabel: "Test this block";
|
|
@@ -18,6 +19,7 @@ export declare const editorCopy: {
|
|
|
18
19
|
readonly cancel: "Cancel";
|
|
19
20
|
readonly name: "Name";
|
|
20
21
|
readonly setup: "Set up the test";
|
|
22
|
+
readonly viewResults: "View results";
|
|
21
23
|
readonly nameExists: "This name is too close to an existing test. Choose a different name.";
|
|
22
24
|
readonly createError: "Couldn't create the test. Try again.";
|
|
23
25
|
readonly loading: "Loading test…";
|
package/dist/puck/editor/copy.js
CHANGED
|
@@ -10,7 +10,8 @@ export const editorCopy = {
|
|
|
10
10
|
running: 'Changing this content mid-test mixes two versions in one result',
|
|
11
11
|
decided: (winner)=>`Decided: ${winner}. Use 'Make it the content' on the test to finish.`,
|
|
12
12
|
archived: 'This test is archived; the winning content is shown to everyone.',
|
|
13
|
-
copy:
|
|
13
|
+
copy: 'Copy Original to this version',
|
|
14
|
+
dropZone: (slot)=>slot === 'control' ? 'Drop the original content here' : `Drop ${editorCopy.labels[slot]} content here`,
|
|
14
15
|
replaceConfirm: (label)=>`${label} already has content. Replace it with a copy of the Original?`,
|
|
15
16
|
replace: 'Replace',
|
|
16
17
|
createLabel: 'Test this block',
|
|
@@ -18,6 +19,7 @@ export const editorCopy = {
|
|
|
18
19
|
cancel: 'Cancel',
|
|
19
20
|
name: 'Name',
|
|
20
21
|
setup: 'Set up the test',
|
|
22
|
+
viewResults: 'View results',
|
|
21
23
|
nameExists: "This name is too close to an existing test. Choose a different name.",
|
|
22
24
|
createError: "Couldn't create the test. Try again.",
|
|
23
25
|
loading: 'Loading test…',
|