@delmaredigital/payload-experiments 0.2.0-rc.2 → 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/endpoints/preflight.js +3 -2
- package/dist/endpoints/previewToken.js +3 -2
- package/dist/endpoints/recompute.js +3 -2
- package/dist/endpoints/results.js +3 -2
- package/dist/endpoints/routeId.d.ts +5 -0
- package/dist/endpoints/routeId.js +13 -0
- package/dist/endpoints/summary.js +32 -21
- package/dist/endpoints/transitions.js +3 -2
- 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,3 @@
|
|
|
1
|
+
/** REST refusals remain the server's words, including nested field validation errors. */
|
|
2
|
+
export declare function goalResponse(response: Response, fallback: string): Promise<unknown>;
|
|
3
|
+
export declare function goalPages<T>(base: string, collection: 'xp-metrics' | 'xp-experiments', signal: AbortSignal): Promise<T[]>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { GOALS_COPY as C } from '../../copy/goals.js';
|
|
2
|
+
import { payloadFieldErrors } from '../testDocument.model.js';
|
|
3
|
+
/** REST refusals remain the server's words, including nested field validation errors. */ export async function goalResponse(response, fallback) {
|
|
4
|
+
const body = await response.json().catch(()=>undefined);
|
|
5
|
+
if (!response.ok) {
|
|
6
|
+
const messages = Object.values(payloadFieldErrors(body, '_section', fallback));
|
|
7
|
+
throw new Error(messages.length ? messages.join(' ') : fallback);
|
|
8
|
+
}
|
|
9
|
+
if (body == null) throw new Error(fallback);
|
|
10
|
+
return body;
|
|
11
|
+
}
|
|
12
|
+
export async function goalPages(base, collection, signal) {
|
|
13
|
+
const all = [];
|
|
14
|
+
for(let page = 1;; page++){
|
|
15
|
+
const response = await fetch(`${base}/${collection}?depth=0&limit=100&page=${page}`, {
|
|
16
|
+
credentials: 'include',
|
|
17
|
+
signal
|
|
18
|
+
});
|
|
19
|
+
const body = await goalResponse(response, C.loadError);
|
|
20
|
+
if (!Array.isArray(body.docs)) throw new Error(C.loadError);
|
|
21
|
+
all.push(...body.docs);
|
|
22
|
+
if (!body.hasNextPage) return all;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.xp-goals { display: grid; gap: var(--xp-space-4); }
|
|
2
|
+
.xp-goals-breadcrumb { display: flex; flex-wrap: wrap; gap: var(--xp-space-2); align-items: center; }
|
|
3
|
+
.xp-goals-table { overflow-x: auto; }
|
|
4
|
+
.xp-goals-table table { width: 100%; border-collapse: collapse; text-align: left; }
|
|
5
|
+
.xp-goals-table th, .xp-goals-table td { padding: var(--xp-space-3); border-bottom: 1px solid var(--xp-border); }
|
|
6
|
+
.xp-goal-form { max-width: 48rem; display: grid; gap: var(--xp-space-4); }
|
|
7
|
+
.xp-goal-form fieldset { border: 0; margin: 0; padding: 0; min-width: 0; display: grid; gap: var(--xp-space-3); }
|
|
8
|
+
.xp-goal-form .xp-field { display: grid; gap: var(--xp-space-2); }
|
|
9
|
+
.xp-goal-form input, .xp-goal-form select {
|
|
10
|
+
padding: var(--xp-space-3); color: var(--xp-text); background: var(--xp-input-bg); border: 1px solid var(--xp-input-border);
|
|
11
|
+
}
|
|
12
|
+
.xp-goal-form input[readonly] { background: var(--xp-surface); }
|
|
13
|
+
.xp-goal-form details { padding: var(--xp-space-4); border: 1px solid var(--xp-border); }
|
|
14
|
+
.xp-goal-form summary { cursor: pointer; margin-bottom: var(--xp-space-3); }
|
|
15
|
+
.xp-goal-cancel { padding: var(--xp-space-2) var(--xp-space-4); color: var(--xp-text); }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DocId } from '../../../types/contracts.js';
|
|
2
|
+
export interface Goal {
|
|
3
|
+
id: DocId;
|
|
4
|
+
name: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
source: string;
|
|
7
|
+
direction: 'increase' | 'decrease';
|
|
8
|
+
attributionWindowHours?: number | null;
|
|
9
|
+
minSampleSize?: number | null;
|
|
10
|
+
guardrailThreshold?: number | null;
|
|
11
|
+
capping?: {
|
|
12
|
+
type?: 'percentile' | 'absolute' | null;
|
|
13
|
+
value?: number | null;
|
|
14
|
+
} | null;
|
|
15
|
+
}
|
|
16
|
+
type Reference = DocId | {
|
|
17
|
+
id: DocId;
|
|
18
|
+
} | null;
|
|
19
|
+
export interface GoalUsageTest {
|
|
20
|
+
id: DocId;
|
|
21
|
+
metrics?: {
|
|
22
|
+
primary?: Reference;
|
|
23
|
+
secondary?: Reference[] | null;
|
|
24
|
+
guardrails?: Reference[] | null;
|
|
25
|
+
} | null;
|
|
26
|
+
}
|
|
27
|
+
/** Count each experiment once per goal, retaining the API's id types. */
|
|
28
|
+
export declare function goalUsage(tests: readonly GoalUsageTest[]): Map<DocId, number>;
|
|
29
|
+
export declare function goalSlug(name: string): string;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Count each experiment once per goal, retaining the API's id types. */ export function goalUsage(tests) {
|
|
2
|
+
const byGoal = new Map();
|
|
3
|
+
for (const test of tests){
|
|
4
|
+
const references = [
|
|
5
|
+
test.metrics?.primary,
|
|
6
|
+
...test.metrics?.secondary ?? [],
|
|
7
|
+
...test.metrics?.guardrails ?? []
|
|
8
|
+
];
|
|
9
|
+
for (const reference of references){
|
|
10
|
+
const id = reference && typeof reference === 'object' ? reference.id : reference;
|
|
11
|
+
if (id == null) continue;
|
|
12
|
+
const used = byGoal.get(id) ?? new Set();
|
|
13
|
+
used.add(test.id);
|
|
14
|
+
byGoal.set(id, used);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return new Map([
|
|
18
|
+
...byGoal
|
|
19
|
+
].map(([id, tests])=>[
|
|
20
|
+
id,
|
|
21
|
+
tests.size
|
|
22
|
+
]));
|
|
23
|
+
}
|
|
24
|
+
export function goalSlug(name) {
|
|
25
|
+
return name.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
26
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
.xp-new-test { max-width: 60rem; padding: var(--
|
|
2
|
-
.xp-new-test-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: var(--
|
|
3
|
-
.xp-new-test-card { display: flex; flex-direction: column; gap: .75rem; padding: 1.5rem; border: 1px solid var(--
|
|
1
|
+
.xp-new-test { max-width: 60rem; padding: var(--xp-space-4); color: var(--xp-text); }
|
|
2
|
+
.xp-new-test-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: var(--xp-space-4); }
|
|
3
|
+
.xp-new-test-card { display: flex; flex-direction: column; gap: .75rem; padding: 1.5rem; border: 1px solid var(--xp-border-strong); border-radius: .5rem; background: var(--xp-surface); color: var(--xp-text); text-align: left; cursor: pointer; font: inherit; }
|
|
4
4
|
.xp-new-test-card strong { font-size: 1.25rem; }
|
|
5
|
-
.xp-new-test-card:hover { background: var(--
|
|
5
|
+
.xp-new-test-card:hover { background: var(--xp-surface-raised); }
|
|
6
6
|
.xp-new-test label { display: block; margin-block: .75rem; }
|
|
7
|
-
.xp-new-test input:not([type=radio]), .xp-new-test textarea, .xp-new-test select { display: block; width: 100%; max-width: 36rem; padding: .5rem; color: var(--
|
|
7
|
+
.xp-new-test input:not([type=radio]), .xp-new-test textarea, .xp-new-test select { display: block; width: 100%; max-width: 36rem; padding: .5rem; color: var(--xp-text); background: var(--xp-input-bg); border: 1px solid var(--xp-input-border); font: inherit; }
|
|
8
8
|
.xp-new-test-choice span { display: block; margin-left: 1.5rem; }
|
|
9
|
-
.xp-new-test-actions { display: flex; gap: 1rem; margin-top: var(--
|
|
9
|
+
.xp-new-test-actions { display: flex; gap: 1rem; margin-top: var(--xp-space-4); }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { theme } from '../../theme.js';
|
|
3
4
|
import { useEffect, useState } from 'react';
|
|
4
5
|
import { Button } from '@payloadcms/ui';
|
|
5
6
|
import { actionGroupLabel, availableActions } from '../../actions/actions.model.js';
|
|
@@ -72,10 +73,10 @@ function BannerContent({ test, surface }) {
|
|
|
72
73
|
className: `xp-readiness xp-readiness--${setup && ready ? 'ready' : 'neutral'}`,
|
|
73
74
|
style: {
|
|
74
75
|
padding: '1rem',
|
|
75
|
-
border:
|
|
76
|
-
borderRadius:
|
|
77
|
-
background: setup && ready ?
|
|
78
|
-
color:
|
|
76
|
+
border: `1px solid ${theme.color.borderStrong}`,
|
|
77
|
+
borderRadius: theme.radius.small,
|
|
78
|
+
background: setup && ready ? theme.color.successSurface : theme.color.surface,
|
|
79
|
+
color: theme.color.text
|
|
79
80
|
},
|
|
80
81
|
children: [
|
|
81
82
|
/*#__PURE__*/ _jsx("p", {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { theme } from '../../theme.js';
|
|
3
4
|
import '../../styles.js';
|
|
4
5
|
import { useEffect, useRef, useState } from 'react';
|
|
5
6
|
import { Button } from '@payloadcms/ui';
|
|
@@ -106,7 +107,7 @@ function ResultsContent({ id, surface }) {
|
|
|
106
107
|
return /*#__PURE__*/ _jsxs("div", {
|
|
107
108
|
className: "xp-results",
|
|
108
109
|
style: {
|
|
109
|
-
color:
|
|
110
|
+
color: theme.color.text
|
|
110
111
|
},
|
|
111
112
|
children: [
|
|
112
113
|
phases.length > 0 ? /*#__PURE__*/ _jsxs("label", {
|
|
@@ -148,7 +149,7 @@ function ResultsContent({ id, surface }) {
|
|
|
148
149
|
children: [
|
|
149
150
|
/*#__PURE__*/ _jsxs("div", {
|
|
150
151
|
style: {
|
|
151
|
-
background:
|
|
152
|
+
background: theme.color.surface,
|
|
152
153
|
paddingTop: '1rem'
|
|
153
154
|
},
|
|
154
155
|
children: [
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
width: 100%;
|
|
6
6
|
padding: 0.65rem;
|
|
7
7
|
margin-top: 0.4rem;
|
|
8
|
-
border: 1px solid var(--
|
|
9
|
-
border-radius: var(--
|
|
10
|
-
background: var(--
|
|
11
|
-
color: var(--
|
|
8
|
+
border: 1px solid var(--xp-border);
|
|
9
|
+
border-radius: var(--xp-radius);
|
|
10
|
+
background: var(--xp-input-bg);
|
|
11
|
+
color: var(--xp-text);
|
|
12
12
|
}
|
|
13
13
|
.xp-setting-values { display: grid; gap: 1rem; }
|
|
14
14
|
.xp-setting-value { white-space: pre-wrap; }
|
|
15
|
-
.xp-setting-lock { color: var(--
|
|
15
|
+
.xp-setting-lock { color: var(--xp-text-muted); }
|
|
16
16
|
.xp-setting-parameter fieldset { border: 0; padding: 0; margin: 0; }
|
|
17
17
|
.xp-setting-segments { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.4rem; }
|
|
18
18
|
.xp-setting-segments label { position: relative; }
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
.xp-setting-segments span {
|
|
21
21
|
display: block;
|
|
22
22
|
padding: 0.5rem 0.8rem;
|
|
23
|
-
border: 1px solid var(--
|
|
24
|
-
border-radius: var(--
|
|
25
|
-
background: var(--
|
|
26
|
-
color: var(--
|
|
23
|
+
border: 1px solid var(--xp-border);
|
|
24
|
+
border-radius: var(--xp-radius);
|
|
25
|
+
background: var(--xp-input-bg);
|
|
26
|
+
color: var(--xp-text);
|
|
27
27
|
}
|
|
28
|
-
.xp-setting-segments input:checked + span { background: var(--
|
|
29
|
-
.xp-setting-segments input:focus-visible + span { outline: 2px solid var(--
|
|
28
|
+
.xp-setting-segments input:checked + span { background: var(--xp-border); border-color: var(--xp-text); }
|
|
29
|
+
.xp-setting-segments input:focus-visible + span { outline: 2px solid var(--xp-text); outline-offset: 2px; }
|
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
.xp-tests-surface {
|
|
2
2
|
min-height: 100dvh;
|
|
3
3
|
width: 100%;
|
|
4
|
-
color: var(--
|
|
5
|
-
background: var(--
|
|
4
|
+
color: var(--xp-text);
|
|
5
|
+
background: var(--xp-bg);
|
|
6
6
|
}
|
|
7
7
|
.xp-tests-shell-header {
|
|
8
8
|
display: flex;
|
|
9
9
|
align-items: center;
|
|
10
|
-
gap: var(--
|
|
11
|
-
padding:
|
|
12
|
-
border-bottom: 1px solid var(--
|
|
10
|
+
gap: var(--xp-space-4);
|
|
11
|
+
padding: var(--xp-space-5) var(--xp-space-6);
|
|
12
|
+
border-bottom: 1px solid var(--xp-border);
|
|
13
13
|
}
|
|
14
|
-
.xp-tests-main { padding:
|
|
15
|
-
.xp-tests-surface a { color: var(--
|
|
14
|
+
.xp-tests-main { padding: var(--xp-space-6); }
|
|
15
|
+
.xp-tests-surface a { color: var(--xp-text); }
|
|
16
16
|
.xp-tests-toolbar, .xp-tests-card-heading {
|
|
17
17
|
display: flex;
|
|
18
18
|
flex-wrap: wrap;
|
|
19
19
|
align-items: center;
|
|
20
|
-
gap: var(--
|
|
20
|
+
gap: var(--xp-space-4);
|
|
21
21
|
}
|
|
22
|
-
.xp-tests-toolbar { margin-bottom:
|
|
22
|
+
.xp-tests-toolbar { margin-bottom: var(--xp-space-6); }
|
|
23
23
|
.xp-tests-toolbar h1 { margin: 0; margin-right: auto; }
|
|
24
|
-
.xp-tests-toolbar label { display: grid; gap:
|
|
24
|
+
.xp-tests-toolbar label { display: grid; gap: var(--xp-space-1); }
|
|
25
25
|
.xp-tests-toolbar input, .xp-tests-surface button {
|
|
26
|
-
color: var(--
|
|
27
|
-
background: var(--
|
|
28
|
-
border: 1px solid var(--
|
|
29
|
-
border-radius: var(--
|
|
30
|
-
padding:
|
|
26
|
+
color: var(--xp-text);
|
|
27
|
+
background: var(--xp-input-bg);
|
|
28
|
+
border: 1px solid var(--xp-input-border);
|
|
29
|
+
border-radius: var(--xp-radius);
|
|
30
|
+
padding: var(--xp-space-2);
|
|
31
31
|
}
|
|
32
32
|
.xp-tests-primary {
|
|
33
|
-
padding:
|
|
34
|
-
border: 1px solid var(--
|
|
35
|
-
border-radius: var(--
|
|
33
|
+
padding: var(--xp-space-2) var(--xp-space-4);
|
|
34
|
+
border: 1px solid var(--xp-accent);
|
|
35
|
+
border-radius: var(--xp-radius);
|
|
36
36
|
}
|
|
37
|
-
.xp-tests-group { margin-bottom:
|
|
38
|
-
.xp-tests-group summary { cursor: pointer; padding: var(--
|
|
39
|
-
.xp-tests-cards { display: grid; gap: var(--
|
|
37
|
+
.xp-tests-group { margin-bottom: var(--xp-space-6); }
|
|
38
|
+
.xp-tests-group summary { cursor: pointer; padding: var(--xp-space-4) 0; }
|
|
39
|
+
.xp-tests-cards { display: grid; gap: var(--xp-space-4); }
|
|
40
40
|
.xp-tests-card {
|
|
41
|
-
border: 1px solid var(--
|
|
42
|
-
border-radius: var(--
|
|
43
|
-
padding:
|
|
44
|
-
background: var(--
|
|
41
|
+
border: 1px solid var(--xp-border);
|
|
42
|
+
border-radius: var(--xp-radius-large);
|
|
43
|
+
padding: var(--xp-space-5);
|
|
44
|
+
background: var(--xp-surface);
|
|
45
45
|
}
|
|
46
46
|
.xp-tests-card h3 { margin: 0; margin-right: auto; }
|
|
47
|
-
.xp-tests-health { width:
|
|
48
|
-
.xp-tests-health--red { background: var(--
|
|
49
|
-
.xp-tests-health--amber { background: var(--
|
|
50
|
-
.xp-tests-health--green { background: var(--
|
|
47
|
+
.xp-tests-health { width: var(--xp-space-2); height: var(--xp-space-2); border-radius: 50%; }
|
|
48
|
+
.xp-tests-health--red { background: var(--xp-error); }
|
|
49
|
+
.xp-tests-health--amber { background: var(--xp-warning); }
|
|
50
|
+
.xp-tests-health--green { background: var(--xp-success); }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
|
+
import { formatAdminURL } from 'payload/shared';
|
|
4
5
|
import { TEST_PAGE as C } from '../../copy/testPage.js';
|
|
5
6
|
import { metricSourceOptions } from '../../metricSource.model.js';
|
|
6
7
|
import { reviewView } from '../../review/review.model.js';
|
|
@@ -46,6 +47,31 @@ export function GoalSection({ test, surface }) {
|
|
|
46
47
|
]);
|
|
47
48
|
const doc = test.doc;
|
|
48
49
|
const reason = reasonFor(test, 'metrics');
|
|
50
|
+
useEffect(()=>{
|
|
51
|
+
if (!loaded) return;
|
|
52
|
+
const url = new URL(window.location.href);
|
|
53
|
+
const goalId = url.searchParams.get('goal');
|
|
54
|
+
if (goalId == null) return;
|
|
55
|
+
url.searchParams.delete('goal');
|
|
56
|
+
window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`);
|
|
57
|
+
const goal = metrics.find((metric)=>String(metric.id) === goalId);
|
|
58
|
+
if (!reason && doc.metrics?.primary == null && goal) {
|
|
59
|
+
save(test, 'metrics', ()=>({
|
|
60
|
+
metrics: {
|
|
61
|
+
...doc.metrics,
|
|
62
|
+
primary: goal.id,
|
|
63
|
+
guardrails: doc.metrics?.guardrails?.filter((id)=>id !== goal.id),
|
|
64
|
+
secondary: doc.metrics?.secondary?.filter((id)=>id !== goal.id)
|
|
65
|
+
}
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
}, [
|
|
69
|
+
loaded,
|
|
70
|
+
metrics,
|
|
71
|
+
reason,
|
|
72
|
+
doc.metrics,
|
|
73
|
+
test
|
|
74
|
+
]);
|
|
49
75
|
const sources = metricSourceOptions(surface.metricSources);
|
|
50
76
|
const unknownGoal = loaded ? C.missingGoal : C.goalUnavailable;
|
|
51
77
|
const options = metrics.map((metric)=>({
|
|
@@ -149,7 +175,12 @@ export function GoalSection({ test, surface }) {
|
|
|
149
175
|
children: C.goalHelp
|
|
150
176
|
}),
|
|
151
177
|
/*#__PURE__*/ _jsx("a", {
|
|
152
|
-
href:
|
|
178
|
+
href: formatAdminURL({
|
|
179
|
+
adminRoute: surface.adminRoute,
|
|
180
|
+
path: `/experiments/goals${!reason ? `?${new URLSearchParams({
|
|
181
|
+
return: String(doc.id)
|
|
182
|
+
})}` : ''}`
|
|
183
|
+
}),
|
|
153
184
|
children: C.manageGoals
|
|
154
185
|
}),
|
|
155
186
|
/*#__PURE__*/ _jsx(FieldErrors, {
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useId, useState } from 'react';
|
|
4
|
+
import { ChipsInput } from '../../components/ChipsInput.js';
|
|
5
|
+
import { AUDIENCE_COPY } from '../../copy/audience.js';
|
|
6
|
+
import { coerceValue, emptyListHint, humaniseAttribute, opLabel, opsFor, readListValue, readTargetingRules, valueInput } from '../../targeting/targeting.model.js';
|
|
4
7
|
import { TEST_PAGE as C, NUMBER_COMPARISONS } from '../../copy/testPage.js';
|
|
5
8
|
import { CheckboxList, FieldErrors, reasonFor, save, SelectEdit, TextEdit } from './fields.js';
|
|
6
9
|
import { Section } from './Section.js';
|
|
7
10
|
export function TargetingSection({ test, surface }) {
|
|
11
|
+
const id = useId();
|
|
8
12
|
const doc = test.doc;
|
|
9
|
-
const rules = doc.targeting ?? [];
|
|
13
|
+
const rules = readTargetingRules(doc.targeting ?? []);
|
|
14
|
+
// Removing a row invalidates index-based drafts, including unfinished chip text.
|
|
15
|
+
const [rowsRevision, setRowsRevision] = useState(0);
|
|
10
16
|
const reason = reasonFor(test, 'targeting');
|
|
11
17
|
const update = (transform)=>save(test, 'targeting', ()=>({
|
|
12
|
-
targeting: transform(test.doc.targeting ?? [])
|
|
18
|
+
targeting: transform(readTargetingRules(test.doc.targeting ?? []))
|
|
13
19
|
}));
|
|
14
20
|
const keys = Object.keys(surface.attributeKeys);
|
|
15
21
|
return /*#__PURE__*/ _jsx(Section, {
|
|
@@ -73,20 +79,20 @@ export function TargetingSection({ test, surface }) {
|
|
|
73
79
|
})),
|
|
74
80
|
onCommit: (value)=>{
|
|
75
81
|
const op = value;
|
|
82
|
+
const current = readTargetingRules(test.doc.targeting ?? [])[index];
|
|
76
83
|
setRule({
|
|
77
84
|
op,
|
|
78
|
-
value: coerceValue(attribute.type, op,
|
|
85
|
+
value: coerceValue(attribute.type, op, current?.value) ?? ''
|
|
79
86
|
});
|
|
80
87
|
}
|
|
81
88
|
}),
|
|
82
89
|
input.kind === 'multiselect' ? /*#__PURE__*/ _jsx(CheckboxList, {
|
|
83
90
|
label: C.value,
|
|
84
91
|
reason: reason,
|
|
85
|
-
value:
|
|
92
|
+
value: readListValue(rule.value),
|
|
86
93
|
options: input.options ?? [],
|
|
87
94
|
onToggle: (value, checked)=>{
|
|
88
|
-
const
|
|
89
|
-
const values = Array.isArray(current) ? current.map(String) : [];
|
|
95
|
+
const values = readListValue(test.doc.targeting?.[index]?.value);
|
|
90
96
|
changeValue(checked ? [
|
|
91
97
|
...new Set([
|
|
92
98
|
...values,
|
|
@@ -94,7 +100,15 @@ export function TargetingSection({ test, surface }) {
|
|
|
94
100
|
])
|
|
95
101
|
] : values.filter((saved)=>saved !== value));
|
|
96
102
|
}
|
|
97
|
-
}) : input.kind === '
|
|
103
|
+
}) : input.kind === 'chips' ? /*#__PURE__*/ _jsx(ChipsInput, {
|
|
104
|
+
id: `${id}-${index}-value`,
|
|
105
|
+
label: C.value,
|
|
106
|
+
value: readListValue(rule.value),
|
|
107
|
+
disabled: Boolean(reason),
|
|
108
|
+
description: reason ?? AUDIENCE_COPY.listDescription,
|
|
109
|
+
placeholder: AUDIENCE_COPY.listPlaceholder,
|
|
110
|
+
onChange: changeValue
|
|
111
|
+
}, `${rule.attribute}-${rule.op}`) : input.kind === 'select' ? /*#__PURE__*/ _jsx(SelectEdit, {
|
|
98
112
|
label: C.value,
|
|
99
113
|
reason: reason,
|
|
100
114
|
value: String(rule.value),
|
|
@@ -131,11 +145,14 @@ export function TargetingSection({ test, surface }) {
|
|
|
131
145
|
}),
|
|
132
146
|
!reason ? /*#__PURE__*/ _jsx("button", {
|
|
133
147
|
type: "button",
|
|
134
|
-
onClick: ()=>
|
|
148
|
+
onClick: ()=>{
|
|
149
|
+
setRowsRevision((revision)=>revision + 1);
|
|
150
|
+
update((current)=>current.filter((_, i)=>i !== index));
|
|
151
|
+
},
|
|
135
152
|
children: C.removeRule
|
|
136
153
|
}) : null
|
|
137
154
|
]
|
|
138
|
-
}, index);
|
|
155
|
+
}, `${rowsRevision}-${index}`);
|
|
139
156
|
}),
|
|
140
157
|
!reason ? /*#__PURE__*/ _jsx("button", {
|
|
141
158
|
type: "button",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { theme } from '../../theme.js';
|
|
3
4
|
import { useEffect, useRef, useState } from 'react';
|
|
4
5
|
import { TEST_PAGE as C } from '../../copy/testPage.js';
|
|
5
6
|
import { ARMS_COPY, GROUP_LABELS, issueMessage } from '../../copy/arms.js';
|
|
@@ -9,11 +10,11 @@ import { balanceWeight, orderedVersions, trafficShares } from './testPage.model.
|
|
|
9
10
|
import { FieldErrors, ReadOnly, reasonFor, save, TextEdit } from './fields.js';
|
|
10
11
|
import { Section } from './Section.js';
|
|
11
12
|
const COLOURS = [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
theme.color.success,
|
|
14
|
+
theme.color.accent,
|
|
15
|
+
theme.color.warning,
|
|
16
|
+
theme.color.error,
|
|
17
|
+
theme.color.inputBorder
|
|
17
18
|
];
|
|
18
19
|
export function VersionsSection({ test, surface, parts }) {
|
|
19
20
|
const doc = test.doc;
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
.xp-test-page { color: var(--
|
|
2
|
-
.xp-test-header { position: sticky; top: 0; z-index: 10; background: var(--
|
|
1
|
+
.xp-test-page { color: var(--xp-text); background: var(--xp-bg); max-width: 90rem; margin: 0 auto; padding: 1.5rem; }
|
|
2
|
+
.xp-test-header { position: sticky; top: 0; z-index: 10; background: var(--xp-bg); padding: 1rem 0; border-bottom: 1px solid var(--xp-border); }
|
|
3
3
|
.xp-header-meta, .xp-section-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; margin-block: 1rem; }
|
|
4
|
-
.xp-kind-chip, .xp-status-pill { border: 1px solid var(--
|
|
5
|
-
.xp-status-pill--success, .xp-content-ready, .xp-section-check--done { color: var(--
|
|
6
|
-
.xp-content-empty, .xp-status-pill--warning { color: var(--
|
|
7
|
-
.xp-test-section { padding-block: 1.5rem; border-bottom: 1px solid var(--
|
|
4
|
+
.xp-kind-chip, .xp-status-pill { border: 1px solid var(--xp-border-control); border-radius: 1rem; padding: .25rem .75rem; }
|
|
5
|
+
.xp-status-pill--success, .xp-content-ready, .xp-section-check--done { color: var(--xp-success-text); }
|
|
6
|
+
.xp-content-empty, .xp-status-pill--warning { color: var(--xp-warning-text); }
|
|
7
|
+
.xp-test-section { padding-block: 1.5rem; border-bottom: 1px solid var(--xp-border); scroll-margin-top: 15rem; }
|
|
8
8
|
.xp-version-rows { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); }
|
|
9
|
-
.xp-version-row { border: 1px solid var(--
|
|
9
|
+
.xp-version-row { border: 1px solid var(--xp-border); border-radius: var(--xp-radius); padding: 1rem; }
|
|
10
10
|
.xp-version-colour { display: inline-block; width: .75rem; height: .75rem; margin-right: .5rem; border-radius: 50%; }
|
|
11
|
-
.xp-traffic-bar { display: flex; height: 1.25rem; background: var(--
|
|
11
|
+
.xp-traffic-bar { display: flex; height: 1.25rem; background: var(--xp-surface-raised); margin-block: 1.5rem; overflow: hidden; border-radius: var(--xp-radius); }
|
|
12
12
|
.xp-traffic-bar > span { display: block; }
|
|
13
13
|
.xp-field { display: flex; flex-direction: column; gap: .5rem; margin-block: 1rem; }
|
|
14
|
-
.xp-field input, .xp-field textarea, .xp-field select { font: inherit; color: var(--
|
|
14
|
+
.xp-field input, .xp-field textarea, .xp-field select { font: inherit; color: var(--xp-text); background: var(--xp-input-bg); border: 1px solid var(--xp-border-control); border-radius: var(--xp-radius); padding: .65rem; }
|
|
15
15
|
.xp-field textarea { min-height: 6rem; resize: vertical; }
|
|
16
16
|
.xp-field input[type=range] { padding: 0; }
|
|
17
17
|
.xp-read-only { padding-block: .5rem; }
|
|
18
18
|
.xp-read-only p { margin-block: .5rem; }
|
|
19
|
-
.xp-field-error { color: var(--
|
|
20
|
-
.xp-test-page button { cursor: pointer; color: var(--
|
|
19
|
+
.xp-field-error { color: var(--xp-error-text); }
|
|
20
|
+
.xp-test-page button { cursor: pointer; color: var(--xp-text); background: var(--xp-surface-raised); border: 1px solid var(--xp-border-control); padding: .65rem 1rem; border-radius: var(--xp-radius); font: inherit; }
|
|
21
21
|
.xp-test-page button:disabled { opacity: .5; cursor: default; }
|
|
22
22
|
.xp-test-page summary { cursor: pointer; padding-block: .75rem; }
|
|
23
|
-
.xp-targeting-row { border-bottom: 1px solid var(--
|
|
23
|
+
.xp-targeting-row { border-bottom: 1px solid var(--xp-border); padding-bottom: 1rem; }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TargetingRule } from '../../types/contracts.js';
|
|
1
2
|
import type { SerializedAttributeKeys } from '../../types/admin.js';
|
|
2
3
|
import type { TargetingOp } from '../../targeting/ops.js';
|
|
3
4
|
type Attribute = SerializedAttributeKeys[string];
|
|
@@ -5,13 +6,17 @@ export type AttributeType = Attribute['type'];
|
|
|
5
6
|
export declare function opsFor(type?: AttributeType): readonly TargetingOp[];
|
|
6
7
|
export declare function opLabel(type: AttributeType, op: TargetingOp): string;
|
|
7
8
|
export declare function valueInput(attribute?: Attribute, op?: TargetingOp): {
|
|
8
|
-
kind: 'text' | 'number' | 'boolean' | 'select' | 'multiselect';
|
|
9
|
+
kind: 'text' | 'number' | 'boolean' | 'select' | 'multiselect' | 'chips';
|
|
9
10
|
options?: {
|
|
10
11
|
label: string;
|
|
11
12
|
value: string;
|
|
12
13
|
}[];
|
|
13
14
|
};
|
|
14
15
|
export declare function coerceValue(type: AttributeType, op: TargetingOp, raw: unknown): string | number | boolean | string[] | null;
|
|
16
|
+
/** Only the read boundary accepts legacy comma-separated document values. */
|
|
17
|
+
export declare function readListValue(raw: unknown): string[];
|
|
18
|
+
/** Normalize the read boundary before any whole-group edit. */
|
|
19
|
+
export declare function readTargetingRules(rules: TargetingRule[]): TargetingRule[];
|
|
15
20
|
export declare function rowPath(path: string): string;
|
|
16
21
|
export declare function emptyListHint(op: TargetingOp, value: unknown): string | undefined;
|
|
17
22
|
export declare function humaniseAttribute(key: string): string;
|
|
@@ -17,6 +17,9 @@ export function valueInput(attribute, op = 'eq') {
|
|
|
17
17
|
value
|
|
18
18
|
}))
|
|
19
19
|
};
|
|
20
|
+
if (many) return {
|
|
21
|
+
kind: 'chips'
|
|
22
|
+
};
|
|
20
23
|
return {
|
|
21
24
|
kind: attribute?.type === 'number' ? 'number' : attribute?.type === 'boolean' ? 'boolean' : 'text'
|
|
22
25
|
};
|
|
@@ -24,7 +27,11 @@ export function valueInput(attribute, op = 'eq') {
|
|
|
24
27
|
export function coerceValue(type, op, raw) {
|
|
25
28
|
if (!isOpValid(type, op)) return null;
|
|
26
29
|
if (type === 'string') {
|
|
27
|
-
if (op === 'in' || op === 'nin') return
|
|
30
|
+
if (op === 'in' || op === 'nin') return [
|
|
31
|
+
...new Set((Array.isArray(raw) ? raw.map(String) : [
|
|
32
|
+
String(raw ?? '')
|
|
33
|
+
]).map((value)=>value.trim()).filter(Boolean))
|
|
34
|
+
];
|
|
28
35
|
return raw == null ? '' : String(raw);
|
|
29
36
|
}
|
|
30
37
|
if (type === 'number') {
|
|
@@ -34,12 +41,21 @@ export function coerceValue(type, op, raw) {
|
|
|
34
41
|
}
|
|
35
42
|
return raw === true || raw === 'true' ? true : raw === false || raw === 'false' ? false : null;
|
|
36
43
|
}
|
|
44
|
+
/** Only the read boundary accepts legacy comma-separated document values. */ export function readListValue(raw) {
|
|
45
|
+
return coerceValue('string', 'in', typeof raw === 'string' ? raw.split(',') : raw);
|
|
46
|
+
}
|
|
47
|
+
/** Normalize the read boundary before any whole-group edit. */ export function readTargetingRules(rules) {
|
|
48
|
+
return rules.map((rule)=>rule.op === 'in' || rule.op === 'nin' ? {
|
|
49
|
+
...rule,
|
|
50
|
+
value: readListValue(rule.value)
|
|
51
|
+
} : rule);
|
|
52
|
+
}
|
|
37
53
|
export function rowPath(path) {
|
|
38
54
|
return path.slice(0, Math.max(0, path.lastIndexOf('.')));
|
|
39
55
|
}
|
|
40
56
|
export function emptyListHint(op, value) {
|
|
41
57
|
if (op !== 'in' && op !== 'nin') return undefined;
|
|
42
|
-
const list =
|
|
58
|
+
const list = readListValue(value);
|
|
43
59
|
return Array.isArray(list) && list.length === 0 ? AUDIENCE_COPY.emptyListNote : undefined;
|
|
44
60
|
}
|
|
45
61
|
export function humaniseAttribute(key) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* The Payload token boundary. Recompute aliases for either admin theme. */
|
|
2
|
+
:root, [data-theme] {
|
|
3
|
+
--xp-bg: var(--theme-bg);
|
|
4
|
+
--xp-text: var(--theme-text);
|
|
5
|
+
--xp-surface: var(--theme-elevation-50);
|
|
6
|
+
--xp-surface-raised: var(--theme-elevation-100);
|
|
7
|
+
--xp-border: var(--theme-elevation-150);
|
|
8
|
+
--xp-border-strong: var(--theme-elevation-200);
|
|
9
|
+
--xp-border-control: var(--theme-elevation-250);
|
|
10
|
+
--xp-input-border: var(--theme-elevation-300);
|
|
11
|
+
--xp-border-accent: var(--theme-elevation-400);
|
|
12
|
+
--xp-accent: var(--theme-elevation-500);
|
|
13
|
+
--xp-text-muted: var(--theme-elevation-600);
|
|
14
|
+
--xp-text-strong: var(--theme-elevation-800);
|
|
15
|
+
--xp-text-emphasis: var(--theme-elevation-900);
|
|
16
|
+
--xp-input-bg: var(--theme-input-bg);
|
|
17
|
+
--xp-overlay: var(--theme-overlay);
|
|
18
|
+
--xp-success-surface: var(--theme-success-100);
|
|
19
|
+
--xp-success-bg: var(--theme-success-150);
|
|
20
|
+
--xp-success: var(--theme-success-500);
|
|
21
|
+
--xp-success-text: var(--theme-success-600);
|
|
22
|
+
--xp-success-text-strong: var(--theme-success-850);
|
|
23
|
+
--xp-warning-surface: var(--theme-warning-100);
|
|
24
|
+
--xp-warning-bg: var(--theme-warning-150);
|
|
25
|
+
--xp-warning: var(--theme-warning-500);
|
|
26
|
+
--xp-warning-text: var(--theme-warning-600);
|
|
27
|
+
--xp-warning-text-strong: var(--theme-warning-850);
|
|
28
|
+
--xp-error-surface: var(--theme-error-100);
|
|
29
|
+
--xp-error: var(--theme-error-500);
|
|
30
|
+
--xp-error-text: var(--theme-error-600);
|
|
31
|
+
--xp-radius: var(--style-radius-s);
|
|
32
|
+
--xp-radius-large: var(--style-radius-m);
|
|
33
|
+
--xp-space-1: calc(var(--base) * 0.25);
|
|
34
|
+
--xp-space-2: calc(var(--base) * 0.5);
|
|
35
|
+
--xp-space-3: calc(var(--base) * 0.75);
|
|
36
|
+
--xp-space-4: calc(var(--base) * 1);
|
|
37
|
+
--xp-space-5: calc(var(--base) * 1.5);
|
|
38
|
+
--xp-space-6: calc(var(--base) * 2);
|
|
39
|
+
--xp-font-body: var(--font-body);
|
|
40
|
+
--xp-font-size-small: calc(var(--base) * 0.65);
|
|
41
|
+
--xp-font-size-body: calc(var(--base) * 0.75);
|
|
42
|
+
}
|