@delmaredigital/payload-experiments 0.4.0-rc.1 → 0.4.0-rc.3
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 +283 -773
- package/dist/admin/TestsDashboardCards.d.ts +1 -0
- package/dist/admin/TestsDashboardCards.js +4 -8
- package/dist/admin/TestsNavLink.d.ts +1 -0
- package/dist/admin/TestsNavLink.js +4 -10
- package/dist/admin/api.d.ts +7 -0
- package/dist/admin/api.js +27 -0
- package/dist/admin/blocks/BlockPanel.js +65 -19
- package/dist/admin/blocks/formApi.d.ts +3 -0
- package/dist/admin/blocks/formApi.js +9 -3
- package/dist/admin/blocks/statusCache.d.ts +2 -0
- package/dist/admin/blocks/statusCache.js +11 -2
- package/dist/admin/copy/blocks.d.ts +16 -0
- package/dist/admin/copy/blocks.js +27 -1
- package/dist/admin/copy/common.d.ts +1 -0
- package/dist/admin/copy/common.js +1 -1
- package/dist/admin/copy/goals.d.ts +1 -0
- package/dist/admin/copy/goals.js +1 -0
- package/dist/admin/copy/pages.d.ts +2 -4
- package/dist/admin/copy/pages.js +3 -5
- package/dist/admin/copy/readiness.d.ts +1 -0
- package/dist/admin/copy/readiness.js +1 -0
- package/dist/admin/goals/XpGoalField.d.ts +2 -0
- package/dist/admin/goals/XpGoalField.js +51 -35
- package/dist/admin/goals/xpGoalField.css +48 -0
- package/dist/admin/paths.d.ts +3 -0
- package/dist/admin/paths.js +3 -0
- package/dist/admin/surface/TestsHome.js +2 -4
- package/dist/admin/surface/TestsSurface.js +2 -5
- package/dist/admin/surface/goals/GoalForm.js +18 -18
- package/dist/admin/surface/goals/GoalsList.js +2 -1
- package/dist/admin/surface/home.model.d.ts +2 -2
- package/dist/admin/surface/newTest/NewTest.js +2 -4
- package/dist/admin/surface/newTest/PageWizard.js +7 -2
- package/dist/admin/surface/pagePicker/CopyRoute.d.ts +1 -1
- package/dist/admin/surface/pagePicker/{PagePicker.d.ts → PageCombobox.d.ts} +3 -3
- package/dist/admin/surface/pagePicker/PageCombobox.js +395 -0
- package/dist/admin/surface/pagePicker/pagePicker.css +10 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +2 -0
- package/dist/admin/surface/readiness/readiness.model.js +1 -0
- package/dist/admin/surface/results/ResultsHeader.js +3 -8
- package/dist/admin/surface/results/useResults.d.ts +10 -0
- package/dist/admin/surface/test/GoalSection.js +4 -7
- package/dist/admin/surface/test/Section.js +9 -8
- package/dist/admin/surface/test/TestPage.js +3 -8
- package/dist/admin/surface/test/pageParts.js +10 -39
- package/dist/admin/surface/test/testPage.css +0 -5
- package/dist/admin/surface/useTestDocument.d.ts +4 -0
- package/dist/admin/surfaceUrls.d.ts +7 -0
- package/dist/admin/surfaceUrls.js +19 -0
- package/dist/admin/theme.css +2 -0
- package/dist/admin/useApi.d.ts +10 -0
- package/dist/admin/views/CollectionListRedirect.d.ts +1 -0
- package/dist/admin/views/CollectionListRedirect.js +1 -1
- package/dist/admin/views/ExperimentsRootView.js +18 -165
- package/dist/admin/views/GoalEditRedirect.d.ts +4 -0
- package/dist/admin/views/GoalEditRedirect.js +14 -0
- package/dist/admin/views/GoalsCollectionListView.d.ts +2 -0
- package/dist/admin/views/GoalsCollectionListView.js +4 -0
- package/dist/admin/views/TestsCollectionListView.d.ts +2 -0
- package/dist/admin/views/TestsCollectionListView.js +4 -0
- package/dist/admin/views/TestsNavLinkServer.d.ts +1 -0
- package/dist/admin/views/TestsNavLinkServer.js +1 -1
- package/dist/admin/views/buildSurfaceProps.d.ts +8 -0
- package/dist/admin/views/buildSurfaceProps.js +143 -0
- package/dist/admin/views/renderCollectionList.d.ts +2 -0
- package/dist/admin/views/renderCollectionList.js +34 -0
- package/dist/admin/views/renderTestsSurface.d.ts +3 -0
- package/dist/admin/views/renderTestsSurface.js +34 -0
- package/dist/collections/experiments/config.js +9 -5
- package/dist/collections/metrics.js +16 -4
- package/dist/exports/adminRsc.d.ts +3 -0
- package/dist/exports/adminRsc.js +3 -0
- package/dist/fields/xpGoal.js +2 -14
- package/dist/goals/picker/copy.d.ts +2 -0
- package/dist/goals/picker/copy.js +7 -0
- package/dist/goals/validate.d.ts +3 -0
- package/dist/goals/validate.js +15 -0
- package/dist/hooks/puckGoals.d.ts +5 -0
- package/dist/hooks/puckGoals.js +62 -0
- package/dist/plugin.js +12 -18
- package/dist/puck/editor/editor.chrome.css +13 -0
- package/dist/puck/editor/editor.css +105 -0
- package/dist/puck/editor/xpGoalPuckEditorField.js +28 -20
- package/dist/sanitizeReadinessDetail.js +2 -0
- package/dist/transitions/checkStart.d.ts +2 -1
- package/dist/transitions/checkStart.js +3 -1
- package/dist/transitions/decide.js +1 -0
- package/dist/transitions/start.js +2 -1
- package/dist/types/endpoints.d.ts +1 -1
- package/dist/ui/ui.css +4 -0
- package/package.json +1 -1
- package/dist/admin/surface/pagePicker/PagePicker.js +0 -172
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SURFACE_COPY } from '../../admin/copy/surface.js';
|
|
1
2
|
import * as paths from '../../admin/paths.js';
|
|
2
3
|
import { managedAccess } from '../access.js';
|
|
3
4
|
import { serializeAttributeKeys, serializeFlagPoints } from '../serialize.js';
|
|
@@ -37,6 +38,10 @@ export function experimentsCollection(options) {
|
|
|
37
38
|
drafts: false,
|
|
38
39
|
maxPerDoc: 50
|
|
39
40
|
},
|
|
41
|
+
labels: {
|
|
42
|
+
singular: SURFACE_COPY.test,
|
|
43
|
+
plural: SURFACE_COPY.navAllTests
|
|
44
|
+
},
|
|
40
45
|
access: {
|
|
41
46
|
...managedAccess(options),
|
|
42
47
|
delete: async ({ req })=>await options.access.manage({
|
|
@@ -51,8 +56,7 @@ export function experimentsCollection(options) {
|
|
|
51
56
|
} : false
|
|
52
57
|
},
|
|
53
58
|
admin: {
|
|
54
|
-
|
|
55
|
-
group: false,
|
|
59
|
+
group: SURFACE_COPY.title,
|
|
56
60
|
useAsTitle: 'name',
|
|
57
61
|
defaultColumns: [
|
|
58
62
|
'name',
|
|
@@ -63,7 +67,7 @@ export function experimentsCollection(options) {
|
|
|
63
67
|
components: {
|
|
64
68
|
views: {
|
|
65
69
|
list: {
|
|
66
|
-
Component: paths.
|
|
70
|
+
Component: paths.XP_TESTS_COLLECTION_LIST
|
|
67
71
|
},
|
|
68
72
|
edit: {
|
|
69
73
|
default: {
|
|
@@ -101,8 +105,8 @@ export function experimentsCollection(options) {
|
|
|
101
105
|
],
|
|
102
106
|
afterError: [
|
|
103
107
|
({ error })=>{
|
|
104
|
-
// Payload preserves public Error status/message, but omits custom codes.
|
|
105
|
-
if (error instanceof XpRefusal
|
|
108
|
+
// Payload preserves public Error status/message, but omits custom codes; the admin maps refusals by code.
|
|
109
|
+
if (error instanceof XpRefusal) {
|
|
106
110
|
return {
|
|
107
111
|
status: error.status,
|
|
108
112
|
response: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { SURFACE_COPY } from '../admin/copy/surface.js';
|
|
1
2
|
import { CLICK_KEY, isClickSource, validClickSource } from '../goals/key.js';
|
|
2
|
-
import { XP_METRIC_SOURCE_SELECT } from '../admin/paths.js';
|
|
3
|
+
import { XP_METRIC_SOURCE_SELECT, XP_GOALS_COLLECTION_LIST, XP_GOAL_EDIT_REDIRECT } from '../admin/paths.js';
|
|
3
4
|
import { managedAccess } from './access.js';
|
|
4
5
|
import { serializeMetricSources } from './serialize.js';
|
|
5
6
|
import { validateCappingType, validateCappingValue } from './metricValidation.js';
|
|
@@ -11,14 +12,25 @@ export function metricsCollection(options) {
|
|
|
11
12
|
graphQL: false,
|
|
12
13
|
disableDuplicate: true,
|
|
13
14
|
versions: false,
|
|
14
|
-
// group:false hides navigation while keeping collection routes reachable (hidden causes 404s).
|
|
15
15
|
labels: {
|
|
16
16
|
singular: 'Goal',
|
|
17
17
|
plural: 'Goals'
|
|
18
18
|
},
|
|
19
19
|
admin: {
|
|
20
|
-
group:
|
|
21
|
-
useAsTitle: 'name'
|
|
20
|
+
group: SURFACE_COPY.title,
|
|
21
|
+
useAsTitle: 'name',
|
|
22
|
+
components: {
|
|
23
|
+
views: {
|
|
24
|
+
list: {
|
|
25
|
+
Component: XP_GOALS_COLLECTION_LIST
|
|
26
|
+
},
|
|
27
|
+
edit: {
|
|
28
|
+
default: {
|
|
29
|
+
Component: XP_GOAL_EDIT_REDIRECT
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
22
34
|
},
|
|
23
35
|
access: managedAccess(options),
|
|
24
36
|
hooks: {
|
|
@@ -2,3 +2,6 @@ export { ExperimentsRootView } from '../admin/views/ExperimentsRootView.js';
|
|
|
2
2
|
export { CollectionListRedirect } from '../admin/views/CollectionListRedirect.js';
|
|
3
3
|
export { CollectionEditRedirect } from '../admin/views/CollectionEditRedirect.js';
|
|
4
4
|
export { TestsNavLinkServer } from '../admin/views/TestsNavLinkServer.js';
|
|
5
|
+
export { TestsCollectionListView } from '../admin/views/TestsCollectionListView.js';
|
|
6
|
+
export { GoalsCollectionListView } from '../admin/views/GoalsCollectionListView.js';
|
|
7
|
+
export { GoalEditRedirect } from '../admin/views/GoalEditRedirect.js';
|
package/dist/exports/adminRsc.js
CHANGED
|
@@ -2,3 +2,6 @@ export { ExperimentsRootView } from '../admin/views/ExperimentsRootView.js';
|
|
|
2
2
|
export { CollectionListRedirect } from '../admin/views/CollectionListRedirect.js';
|
|
3
3
|
export { CollectionEditRedirect } from '../admin/views/CollectionEditRedirect.js';
|
|
4
4
|
export { TestsNavLinkServer } from '../admin/views/TestsNavLinkServer.js';
|
|
5
|
+
export { TestsCollectionListView } from '../admin/views/TestsCollectionListView.js';
|
|
6
|
+
export { GoalsCollectionListView } from '../admin/views/GoalsCollectionListView.js';
|
|
7
|
+
export { GoalEditRedirect } from '../admin/views/GoalEditRedirect.js';
|
package/dist/fields/xpGoal.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { XP_GOAL_FIELD } from '../admin/paths.js';
|
|
2
|
-
import {
|
|
3
|
-
import { pooledExecutor } from '../db/executor.js';
|
|
4
|
-
import { clickKeys } from '../goals/cache.js';
|
|
5
|
-
import { CLICK_KEY } from '../goals/key.js';
|
|
2
|
+
import { validateClickGoal } from '../goals/validate.js';
|
|
6
3
|
/** The host renders this value as data-xp-goal on its interactive element. */ export function xpGoalField() {
|
|
7
4
|
return {
|
|
8
5
|
name: 'xpGoal',
|
|
@@ -14,15 +11,6 @@ import { CLICK_KEY } from '../goals/key.js';
|
|
|
14
11
|
Field: XP_GOAL_FIELD
|
|
15
12
|
}
|
|
16
13
|
},
|
|
17
|
-
validate:
|
|
18
|
-
if (!value) return true;
|
|
19
|
-
if (!CLICK_KEY.test(value)) return 'Use a click goal short name.';
|
|
20
|
-
const options = req.payload.config.custom?.xp;
|
|
21
|
-
if (!options || await options.access.manage({
|
|
22
|
-
req
|
|
23
|
-
}) !== true) return true;
|
|
24
|
-
const keys = await clickKeys(req.payload);
|
|
25
|
-
return !keys || keys.has(value) || (await definedClickKeys(pooledExecutor(req.payload))).has(value) || 'No click goal has this name yet. Create it with "+ New click goal".';
|
|
26
|
-
}
|
|
14
|
+
validate: (value, { req })=>validateClickGoal(value, req)
|
|
27
15
|
};
|
|
28
16
|
}
|
|
@@ -10,7 +10,9 @@ export declare const CLICK_GOAL_COPY: {
|
|
|
10
10
|
readonly createGoal: "Create goal";
|
|
11
11
|
readonly cancel: "Cancel";
|
|
12
12
|
readonly undefinedKey: "No click goal has this name yet. Create it with \"+ New click goal\".";
|
|
13
|
+
readonly invalidKey: "Use a click goal short name.";
|
|
13
14
|
readonly slugRule: "Use 1–64 lowercase letters, numbers, underscores or hyphens, starting with a letter or number.";
|
|
14
15
|
readonly duplicateSlug: "Another goal already uses this short name. Change it under Advanced.";
|
|
15
16
|
readonly saveError: "Couldn't save the goal. Try again.";
|
|
16
17
|
};
|
|
18
|
+
export declare function puckGoalRefusal(rawKey: string, block: string, malformed: boolean, more?: number): string;
|
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
createGoal: 'Create goal',
|
|
10
10
|
cancel: 'Cancel',
|
|
11
11
|
undefinedKey: 'No click goal has this name yet. Create it with "+ New click goal".',
|
|
12
|
+
invalidKey: 'Use a click goal short name.',
|
|
12
13
|
slugRule: 'Use 1–64 lowercase letters, numbers, underscores or hyphens, starting with a letter or number.',
|
|
13
14
|
duplicateSlug: 'Another goal already uses this short name. Change it under Advanced.',
|
|
14
15
|
saveError: "Couldn't save the goal. Try again."
|
|
15
16
|
};
|
|
17
|
+
export function puckGoalRefusal(rawKey, block, malformed, more = 0) {
|
|
18
|
+
// A malformed key can be any length; keep the dialog readable.
|
|
19
|
+
const key = rawKey.length > 64 ? `${rawKey.slice(0, 64)}…` : rawKey;
|
|
20
|
+
const sentence = malformed ? `Click goal "${key}" on the ${block} block isn't a valid short name. Use lowercase letters, numbers, - and _, then publish.` : `Click goal "${key}" doesn't exist yet. Create it with "+ New click goal" on the ${block} block, then publish.`;
|
|
21
|
+
return sentence + (more > 0 ? ` (and ${more} more)` : '');
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { definedClickKeys } from '../db/clickGoals.js';
|
|
2
|
+
import { pooledExecutor } from '../db/executor.js';
|
|
3
|
+
import { clickKeys } from './cache.js';
|
|
4
|
+
import { CLICK_KEY } from './key.js';
|
|
5
|
+
import { CLICK_GOAL_COPY } from './picker/copy.js';
|
|
6
|
+
/** Shared publish validation for Payload fields and Puck JSON props. */ export async function validateClickGoal(value, req) {
|
|
7
|
+
if (!value) return true;
|
|
8
|
+
if (!CLICK_KEY.test(value)) return CLICK_GOAL_COPY.invalidKey;
|
|
9
|
+
const options = req.payload.config.custom?.xp;
|
|
10
|
+
if (!options || await options.access.manage({
|
|
11
|
+
req
|
|
12
|
+
}) !== true) return true;
|
|
13
|
+
const keys = await clickKeys(req.payload);
|
|
14
|
+
return !keys || keys.has(value) || (await definedClickKeys(pooledExecutor(req.payload))).has(value) || CLICK_GOAL_COPY.undefinedKey;
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CollectionBeforeChangeHook } from 'payload';
|
|
2
|
+
/** Host component labels are not passed to the plugin; display their type as words. */
|
|
3
|
+
export declare function puckBlockName(rawType: string): string;
|
|
4
|
+
/** Validate only the Puck JSON included in a publishing write. */
|
|
5
|
+
export declare function puckGoalsBeforeChange(field: string): CollectionBeforeChangeHook;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ValidationError } from 'payload';
|
|
2
|
+
import { validateClickGoal } from '../goals/validate.js';
|
|
3
|
+
import { CLICK_KEY } from '../goals/key.js';
|
|
4
|
+
import { puckGoalRefusal } from '../goals/picker/copy.js';
|
|
5
|
+
/** Host component labels are not passed to the plugin; display their type as words. */ export function puckBlockName(rawType) {
|
|
6
|
+
// Cap before any regex: the type comes from saved page JSON and the first pattern backtracks on long capital runs.
|
|
7
|
+
const type = rawType.slice(0, 64);
|
|
8
|
+
const words = type.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2').replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/([A-Za-z])([0-9])/g, '$1 $2').replace(/[\s_-]+/g, ' ').trim();
|
|
9
|
+
return words ? words[0].toUpperCase() + words.slice(1) : 'Page';
|
|
10
|
+
}
|
|
11
|
+
/** Validate only the Puck JSON included in a publishing write. */ export function puckGoalsBeforeChange(field) {
|
|
12
|
+
return async function validatePuckGoals({ collection, data, originalDoc, req }) {
|
|
13
|
+
const drafts = Boolean(collection.versions && collection.versions.drafts);
|
|
14
|
+
if (drafts && data._status !== 'published') return data;
|
|
15
|
+
// Publishing a stored draft without resending its JSON publishes the draft's JSON.
|
|
16
|
+
let value = data[field] !== undefined ? data[field] : drafts ? originalDoc?.[field] : undefined;
|
|
17
|
+
if (value === undefined) return data;
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
try {
|
|
20
|
+
value = JSON.parse(value);
|
|
21
|
+
} catch {
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const errors = [];
|
|
26
|
+
let first;
|
|
27
|
+
// Holds the raw component type; it is humanised once, for the first refusal only.
|
|
28
|
+
async function walk(value, path, block = '') {
|
|
29
|
+
if (!value || typeof value !== 'object') return;
|
|
30
|
+
const item = value;
|
|
31
|
+
if (typeof item.type === 'string' && item.props && typeof item.props === 'object' && !Array.isArray(item.props)) {
|
|
32
|
+
block = item.type;
|
|
33
|
+
}
|
|
34
|
+
for (const [key, prop] of Object.entries(value)){
|
|
35
|
+
const propPath = `${path}.${key}`;
|
|
36
|
+
if (key === 'xpGoal' && typeof prop === 'string') {
|
|
37
|
+
const result = await validateClickGoal(prop, req);
|
|
38
|
+
if (result !== true) {
|
|
39
|
+
first ??= {
|
|
40
|
+
key: prop,
|
|
41
|
+
block
|
|
42
|
+
};
|
|
43
|
+
errors.push({
|
|
44
|
+
path: propPath,
|
|
45
|
+
message: result
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
} else await walk(prop, propPath, block);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
await walk(value, field);
|
|
52
|
+
if (first) {
|
|
53
|
+
const error = new ValidationError({
|
|
54
|
+
collection: collection.slug,
|
|
55
|
+
errors
|
|
56
|
+
});
|
|
57
|
+
error.message = puckGoalRefusal(first.key, first.block ? puckBlockName(first.block) : 'Page', !CLICK_KEY.test(first.key), errors.length - 1);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
return data;
|
|
61
|
+
};
|
|
62
|
+
}
|
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildCollections } from './collections/index.js';
|
|
2
2
|
import { buildEndpoints } from './endpoints/index.js';
|
|
3
3
|
import { contentChangesBeforeHook, contentChangesHook } from './hooks/contentChanges.js';
|
|
4
|
+
import { puckGoalsBeforeChange } from './hooks/puckGoals.js';
|
|
4
5
|
import { pageGuardAfterChange, pageGuardAfterOperation, pageGuardBeforeChange, pageGuardBeforeDelete, pageGuardBeforeOperation } from './hooks/pageGuard.js';
|
|
5
6
|
import { createOnInit } from './onInit.js';
|
|
6
7
|
import { puckCollections, resolveOptions } from './options.js';
|
|
@@ -23,8 +24,6 @@ export function createExperimentsPlugin(input) {
|
|
|
23
24
|
// Erase the user subtype only at this non-generic factory boundary.
|
|
24
25
|
const { collections, globals } = buildCollections(options);
|
|
25
26
|
const onInit = createOnInit(options);
|
|
26
|
-
const beforeDashboard = config.admin?.components?.beforeDashboard ?? [];
|
|
27
|
-
const beforeNavLinks = config.admin?.components?.beforeNavLinks ?? [];
|
|
28
27
|
return {
|
|
29
28
|
...config,
|
|
30
29
|
admin: {
|
|
@@ -34,10 +33,6 @@ export function createExperimentsPlugin(input) {
|
|
|
34
33
|
xpTestsSurface: {
|
|
35
34
|
path: paths.XP_TESTS_SURFACE,
|
|
36
35
|
type: 'component'
|
|
37
|
-
},
|
|
38
|
-
xpTestsNav: {
|
|
39
|
-
path: paths.XP_TESTS_NAV_CLIENT,
|
|
40
|
-
type: 'component'
|
|
41
36
|
}
|
|
42
37
|
},
|
|
43
38
|
components: {
|
|
@@ -48,20 +43,16 @@ export function createExperimentsPlugin(input) {
|
|
|
48
43
|
Component: paths.XP_TESTS_VIEW,
|
|
49
44
|
path: '/experiments/:segments*'
|
|
50
45
|
}
|
|
51
|
-
}
|
|
52
|
-
beforeDashboard: beforeDashboard.includes(paths.XP_TESTS_DASHBOARD_CARDS) ? beforeDashboard : [
|
|
53
|
-
...beforeDashboard,
|
|
54
|
-
paths.XP_TESTS_DASHBOARD_CARDS
|
|
55
|
-
],
|
|
56
|
-
beforeNavLinks: beforeNavLinks.includes(paths.XP_TESTS_NAV_LINK) ? beforeNavLinks : [
|
|
57
|
-
...beforeNavLinks,
|
|
58
|
-
paths.XP_TESTS_NAV_LINK
|
|
59
|
-
]
|
|
46
|
+
}
|
|
60
47
|
}
|
|
61
48
|
},
|
|
62
49
|
collections: [
|
|
50
|
+
...[
|
|
51
|
+
'xp-experiments',
|
|
52
|
+
'xp-metrics'
|
|
53
|
+
].map((slug)=>collections.find((collection)=>collection.slug === slug)),
|
|
63
54
|
...(config.collections ?? []).map((collection)=>{
|
|
64
|
-
const puck = options.puck?.enabled && registrations.
|
|
55
|
+
const puck = options.puck?.enabled && registrations.find((entry)=>entry.slug === collection.slug);
|
|
65
56
|
const page = options.pages?.collections.some((entry)=>entry.slug === collection.slug);
|
|
66
57
|
const blocks = options.blocks?.collections.find((entry)=>entry.slug === collection.slug);
|
|
67
58
|
if (!puck && !page && !blocks) return collection;
|
|
@@ -87,7 +78,10 @@ export function createExperimentsPlugin(input) {
|
|
|
87
78
|
beforeChange: [
|
|
88
79
|
...collection.hooks?.beforeChange ?? [],
|
|
89
80
|
pageGuardBeforeChange,
|
|
90
|
-
contentChangesBeforeHook
|
|
81
|
+
contentChangesBeforeHook,
|
|
82
|
+
...puck ? [
|
|
83
|
+
puckGoalsBeforeChange(puck.field)
|
|
84
|
+
] : []
|
|
91
85
|
],
|
|
92
86
|
beforeOperation: [
|
|
93
87
|
...collection.hooks?.beforeOperation ?? [],
|
|
@@ -109,7 +103,7 @@ export function createExperimentsPlugin(input) {
|
|
|
109
103
|
}
|
|
110
104
|
};
|
|
111
105
|
}),
|
|
112
|
-
...collections
|
|
106
|
+
...collections.filter((collection)=>collection.slug !== 'xp-experiments' && collection.slug !== 'xp-metrics')
|
|
113
107
|
],
|
|
114
108
|
globals: [
|
|
115
109
|
...config.globals ?? [],
|
|
@@ -120,3 +120,16 @@
|
|
|
120
120
|
.xp-slot-readout > .xp-ui-button { justify-self: start; }
|
|
121
121
|
.xp-slot-editor .xp-ui-tablist { flex: 1 1 auto; min-width: 0; flex-wrap: wrap; }
|
|
122
122
|
.xp-slot-root.xp-puck-field { display: grid; gap: var(--xp-space-3); min-width: 0; }
|
|
123
|
+
|
|
124
|
+
/* Puck's inline field sidebar stays light, including under Payload's dark-mode text overrides. */
|
|
125
|
+
[class*="_PuckFields"] .xp-slot-root .xp-ui-button--secondary { color: var(--xp-text); }
|
|
126
|
+
[class*="_PuckFields"] .xp-slot-root .xp-ui-button--primary { color: var(--xp-accent-fg); }
|
|
127
|
+
[class*="_PuckFields"] .xp-slot-root .xp-ui-error { color: var(--xp-error); }
|
|
128
|
+
[class*="_PuckFields"] .xp-slot-root .xp-ui-help { color: var(--xp-text-2); }
|
|
129
|
+
[class*="_PuckFields"] .xp-slot-root .xp-ui-field label { color: var(--xp-text); }
|
|
130
|
+
[class*="_PuckFields"] .xp-puck-field .xp-ui-button:disabled {
|
|
131
|
+
opacity: 1;
|
|
132
|
+
background: var(--xp-surface-3);
|
|
133
|
+
border-color: var(--xp-border-strong);
|
|
134
|
+
color: var(--xp-text-2);
|
|
135
|
+
}
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
--xp-space-3: 12px;
|
|
56
56
|
--xp-space-4: 16px;
|
|
57
57
|
--xp-space-5: 20px;
|
|
58
|
+
--xp-field-gap: calc((20 / 13) * 1rem);
|
|
58
59
|
--xp-space-6: 24px;
|
|
59
60
|
--xp-space-7: 28px;
|
|
60
61
|
--xp-space-8: 32px;
|
|
@@ -141,6 +142,7 @@
|
|
|
141
142
|
--xp-space-3: 12px;
|
|
142
143
|
--xp-space-4: 16px;
|
|
143
144
|
--xp-space-5: 20px;
|
|
145
|
+
--xp-field-gap: calc((20 / 13) * 1rem);
|
|
144
146
|
--xp-space-6: 24px;
|
|
145
147
|
--xp-space-7: 28px;
|
|
146
148
|
--xp-space-8: 32px;
|
|
@@ -170,6 +172,92 @@
|
|
|
170
172
|
--xp-font-size-body: 15px;
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
[class*="_PuckFields"] .xp-slot-root {
|
|
176
|
+
--xp-bg: rgb(245, 245, 245);
|
|
177
|
+
--xp-surface: rgb(255, 255, 255);
|
|
178
|
+
--xp-surface-2: rgb(235, 235, 235);
|
|
179
|
+
--xp-surface-3: rgb(221, 221, 221);
|
|
180
|
+
--xp-border: rgb(235, 235, 235);
|
|
181
|
+
--xp-border-strong: rgb(195, 195, 195);
|
|
182
|
+
--xp-text: rgb(34, 34, 34);
|
|
183
|
+
--xp-text-2: rgb(87, 87, 87);
|
|
184
|
+
--xp-text-3: rgb(141, 141, 141);
|
|
185
|
+
--xp-accent: rgb(47, 47, 47);
|
|
186
|
+
--xp-accent-hover: rgb(20, 20, 20);
|
|
187
|
+
--xp-accent-active: rgb(7, 7, 7);
|
|
188
|
+
--xp-accent-subtle: rgb(208, 208, 208);
|
|
189
|
+
--xp-accent-fg: rgb(255, 255, 255);
|
|
190
|
+
--xp-accent-text: rgb(34, 34, 34);
|
|
191
|
+
--xp-accent-text-decoration: none;
|
|
192
|
+
--xp-focus: rgb(47, 47, 47);
|
|
193
|
+
--xp-series-muted: rgb(181, 181, 181);
|
|
194
|
+
--xp-success: #1a6a3a;
|
|
195
|
+
--xp-success-bg: #e8f4ec;
|
|
196
|
+
--xp-success-border: #bddfc9;
|
|
197
|
+
--xp-warning: #8a5a0b;
|
|
198
|
+
--xp-warning-bg: #fbf3e1;
|
|
199
|
+
--xp-warning-border: #ecd6a2;
|
|
200
|
+
--xp-error: #a63030;
|
|
201
|
+
--xp-error-bg: #fbeaea;
|
|
202
|
+
--xp-error-border: #efc3c3;
|
|
203
|
+
--xp-series-a: #2a78d6;
|
|
204
|
+
--xp-series-b: #eb6834;
|
|
205
|
+
--xp-series-c: #8056b3;
|
|
206
|
+
--xp-radius-control: 8px;
|
|
207
|
+
--xp-radius-card: 12px;
|
|
208
|
+
--xp-radius-chip: 6px;
|
|
209
|
+
--xp-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
210
|
+
--xp-mono: 'SF Mono', Menlo, Consolas, Monaco, monospace;
|
|
211
|
+
--xp-text-display: 600 28px/32px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
212
|
+
--xp-text-title: 600 20px/28px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
213
|
+
--xp-text-body: 400 15px/22px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
214
|
+
--xp-text-small: 400 13px/18px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
215
|
+
--xp-motion: 120ms;
|
|
216
|
+
--xp-space-half: 2px;
|
|
217
|
+
--xp-space-one-half: 6px;
|
|
218
|
+
--xp-version-share-width: 96px;
|
|
219
|
+
--xp-version-share-width-mobile: 84px;
|
|
220
|
+
--xp-version-state-width: 192px;
|
|
221
|
+
--xp-version-remove-width: 128px;
|
|
222
|
+
--xp-targeting-attribute-width: 150px;
|
|
223
|
+
--xp-targeting-comparison-width: 140px;
|
|
224
|
+
--xp-legend-key-size: 10px;
|
|
225
|
+
--xp-legend-key-radius: 3px;
|
|
226
|
+
--xp-space-1: 4px;
|
|
227
|
+
--xp-space-2: 8px;
|
|
228
|
+
--xp-space-3: 12px;
|
|
229
|
+
--xp-space-4: 16px;
|
|
230
|
+
--xp-space-5: 20px;
|
|
231
|
+
--xp-field-gap: calc((20 / 13) * 1rem);
|
|
232
|
+
--xp-space-6: 24px;
|
|
233
|
+
--xp-space-7: 28px;
|
|
234
|
+
--xp-space-8: 32px;
|
|
235
|
+
--xp-surface-raised: rgb(235, 235, 235);
|
|
236
|
+
--xp-border-control: rgb(195, 195, 195);
|
|
237
|
+
--xp-input-border: rgb(181, 181, 181);
|
|
238
|
+
--xp-border-accent: rgb(154, 154, 154);
|
|
239
|
+
--xp-text-muted: rgb(87, 87, 87);
|
|
240
|
+
--xp-text-strong: rgb(34, 34, 34);
|
|
241
|
+
--xp-text-emphasis: rgb(34, 34, 34);
|
|
242
|
+
--xp-input-bg: rgb(255, 255, 255);
|
|
243
|
+
--xp-overlay: rgba(5, 5, 5, 0.5);
|
|
244
|
+
--xp-success-surface: #e8f4ec;
|
|
245
|
+
--xp-success-text: #1a6a3a;
|
|
246
|
+
--xp-success-text-strong: #1a6a3a;
|
|
247
|
+
--xp-warning-surface: #fbf3e1;
|
|
248
|
+
--xp-warning-text: #8a5a0b;
|
|
249
|
+
--xp-warning-text-strong: #8a5a0b;
|
|
250
|
+
--xp-error-surface: #fbeaea;
|
|
251
|
+
--xp-error-text: #a63030;
|
|
252
|
+
--xp-radius: 8px;
|
|
253
|
+
--xp-radius-large: 12px;
|
|
254
|
+
--xp-dash-card-gap: 12px;
|
|
255
|
+
--xp-dash-card-gap-small: 8px;
|
|
256
|
+
--xp-font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
257
|
+
--xp-font-size-small: 13px;
|
|
258
|
+
--xp-font-size-body: 15px;
|
|
259
|
+
}
|
|
260
|
+
|
|
173
261
|
/* Shared primitives: no host palette references, and no global element resets. */
|
|
174
262
|
.xp-ui-chips, .xp-ui-chips-control, .xp-ui-chips-item, .xp-ui-button, .xp-ui-input, .xp-ui-toggle, .xp-ui-tab, .xp-ui-dialog, .xp-ui-card { box-sizing: border-box; }
|
|
175
263
|
.xp-ui-button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 36px; padding: 7px 14px; border-radius: var(--xp-radius-control); border: 1px solid transparent; font: 500 14px/20px var(--xp-font); color: var(--xp-text); background: transparent; cursor: pointer; white-space: normal; overflow-wrap: anywhere; max-width: 100%; min-width: 0; text-decoration: none; transition: background-color var(--xp-motion), border-color var(--xp-motion), color var(--xp-motion); }
|
|
@@ -246,6 +334,10 @@
|
|
|
246
334
|
.xp-ui-chips p:not(.xp-ui-error) { margin: 0; color: var(--xp-text-muted); font-size: var(--xp-font-size-small); }
|
|
247
335
|
/* Quiet actions and breadcrumb links stay neutral; only the selected tab takes the text accent. */
|
|
248
336
|
.xp-ui-button-group { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; max-width: 100%; }
|
|
337
|
+
.xp-ui-inline-row { display: flex; flex-wrap: wrap; align-items: end; gap: var(--xp-space-3); min-width: 0; }
|
|
338
|
+
.xp-ui-inline-row > .xp-ui-field { flex: 1 1 auto; min-width: 0; }
|
|
339
|
+
.xp-ui-inline-row > .xp-ui-button { flex: 0 0 auto; }
|
|
340
|
+
.xp-ui-subpanel { display: grid; gap: var(--xp-space-3); border: 1px solid var(--xp-border); border-radius: var(--xp-radius-card); padding: var(--xp-space-4); }
|
|
249
341
|
@media (prefers-reduced-motion: reduce) {
|
|
250
342
|
.xp-ui-button, .xp-ui-input, .xp-ui-toggle, .xp-ui-toggle::before, .xp-ui-chips-control { transition: none; }
|
|
251
343
|
}
|
|
@@ -385,3 +477,16 @@
|
|
|
385
477
|
.xp-slot-readout > .xp-ui-button { justify-self: start; }
|
|
386
478
|
.xp-slot-editor .xp-ui-tablist { flex: 1 1 auto; min-width: 0; flex-wrap: wrap; }
|
|
387
479
|
.xp-slot-root.xp-puck-field { display: grid; gap: var(--xp-space-3); min-width: 0; }
|
|
480
|
+
|
|
481
|
+
/* Puck's inline field sidebar stays light, including under Payload's dark-mode text overrides. */
|
|
482
|
+
[class*="_PuckFields"] .xp-slot-root .xp-ui-button--secondary { color: var(--xp-text); }
|
|
483
|
+
[class*="_PuckFields"] .xp-slot-root .xp-ui-button--primary { color: var(--xp-accent-fg); }
|
|
484
|
+
[class*="_PuckFields"] .xp-slot-root .xp-ui-error { color: var(--xp-error); }
|
|
485
|
+
[class*="_PuckFields"] .xp-slot-root .xp-ui-help { color: var(--xp-text-2); }
|
|
486
|
+
[class*="_PuckFields"] .xp-slot-root .xp-ui-field label { color: var(--xp-text); }
|
|
487
|
+
[class*="_PuckFields"] .xp-puck-field .xp-ui-button:disabled {
|
|
488
|
+
opacity: 1;
|
|
489
|
+
background: var(--xp-surface-3);
|
|
490
|
+
border-color: var(--xp-border-strong);
|
|
491
|
+
color: var(--xp-text-2);
|
|
492
|
+
}
|
|
@@ -30,22 +30,32 @@ function XpGoalPuckEditor({ value, onChange, api, readOnly }) {
|
|
|
30
30
|
}),
|
|
31
31
|
picker.canManage && !readOnly ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
32
32
|
children: [
|
|
33
|
-
/*#__PURE__*/ _jsxs(
|
|
34
|
-
|
|
35
|
-
label: C.choose,
|
|
36
|
-
value: picker.goals.some((goal)=>goal.slug === value) ? value : '',
|
|
37
|
-
onChange: (event)=>{
|
|
38
|
-
if (event.currentTarget.value) onChange(event.currentTarget.value);
|
|
39
|
-
},
|
|
33
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
34
|
+
className: "xp-ui-inline-row",
|
|
40
35
|
children: [
|
|
41
|
-
/*#__PURE__*/
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
/*#__PURE__*/ _jsxs(Select, {
|
|
37
|
+
id: `${id}-picker`,
|
|
38
|
+
label: C.choose,
|
|
39
|
+
value: picker.goals.some((goal)=>goal.slug === value) ? value : '',
|
|
40
|
+
onChange: (event)=>{
|
|
41
|
+
if (event.currentTarget.value) onChange(event.currentTarget.value);
|
|
42
|
+
},
|
|
43
|
+
children: [
|
|
44
|
+
/*#__PURE__*/ _jsx("option", {
|
|
45
|
+
value: "",
|
|
46
|
+
children: C.choose
|
|
47
|
+
}),
|
|
48
|
+
picker.goals.map((goal)=>/*#__PURE__*/ _jsx("option", {
|
|
49
|
+
value: goal.slug,
|
|
50
|
+
children: goal.name
|
|
51
|
+
}, goal.id))
|
|
52
|
+
]
|
|
44
53
|
}),
|
|
45
|
-
picker.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
!picker.panel.open ? /*#__PURE__*/ _jsx(Button, {
|
|
55
|
+
type: "button",
|
|
56
|
+
onClick: picker.open,
|
|
57
|
+
children: C.newGoal
|
|
58
|
+
}) : null
|
|
49
59
|
]
|
|
50
60
|
}),
|
|
51
61
|
picker.undefinedKey(value ?? '') ? /*#__PURE__*/ _jsx("p", {
|
|
@@ -53,11 +63,8 @@ function XpGoalPuckEditor({ value, onChange, api, readOnly }) {
|
|
|
53
63
|
role: "status",
|
|
54
64
|
children: C.undefinedKey
|
|
55
65
|
}) : null,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
onClick: picker.open,
|
|
59
|
-
children: C.newGoal
|
|
60
|
-
}) : /*#__PURE__*/ _jsxs("section", {
|
|
66
|
+
picker.panel.open ? /*#__PURE__*/ _jsxs("section", {
|
|
67
|
+
className: "xp-ui-subpanel",
|
|
61
68
|
"aria-labelledby": `${id}-title`,
|
|
62
69
|
children: [
|
|
63
70
|
/*#__PURE__*/ _jsx("h3", {
|
|
@@ -100,6 +107,7 @@ function XpGoalPuckEditor({ value, onChange, api, readOnly }) {
|
|
|
100
107
|
children: [
|
|
101
108
|
/*#__PURE__*/ _jsx(Button, {
|
|
102
109
|
type: "button",
|
|
110
|
+
variant: "primary",
|
|
103
111
|
disabled: picker.panel.pending || !picker.panel.name.trim(),
|
|
104
112
|
onClick: ()=>void picker.create(),
|
|
105
113
|
children: C.createGoal
|
|
@@ -113,7 +121,7 @@ function XpGoalPuckEditor({ value, onChange, api, readOnly }) {
|
|
|
113
121
|
]
|
|
114
122
|
})
|
|
115
123
|
]
|
|
116
|
-
})
|
|
124
|
+
}) : null
|
|
117
125
|
]
|
|
118
126
|
}) : null
|
|
119
127
|
]
|
|
@@ -63,6 +63,8 @@ const failure = (value)=>value === 'placements-failed' || value === 'forbidden'
|
|
|
63
63
|
} : undefined;
|
|
64
64
|
case 'no-placements':
|
|
65
65
|
return failure(detail) ? detail : undefined;
|
|
66
|
+
case 'placement-removed':
|
|
67
|
+
return undefined;
|
|
66
68
|
case 'page-in-use':
|
|
67
69
|
{
|
|
68
70
|
if (!object(detail) || !Array.isArray(detail.paths)) return undefined;
|
|
@@ -2,13 +2,14 @@ import { type GoalContent } from '../goals/fairness.js';
|
|
|
2
2
|
import type { Arm, DocId, ExperimentKind, ExposurePolicy, FlagPoint, MetricSource, ResolvedOptions, Status, TargetingRule, UnitType } from '../types/contracts.js';
|
|
3
3
|
import type { ArmIssueCode, PreflightResult } from '../types/endpoints.js';
|
|
4
4
|
import type { BlockPlacement, FlagPlacement } from './placements.js';
|
|
5
|
-
export type ReadinessCode = ArmIssueCode | 'goal-in-some-versions' | 'placement-unchecked' | 'invalid-status' | 'no-planned-duration' | 'no-primary-metric' | 'no-reach-per-day' | 'placement-unpublished' | 'duplicate-version-row' | 'interacting-tests' | 'version-identical' | 'version-same-address' | 'unknown-collection' | 'page-no-targets' | 'page-in-use' | 'page-target-unpublished' | 'page-targets-unchecked' | 'version-page-unavailable' | 'version-page-unchecked' | 'page-user-unit' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'no-placements' | 'empty-slot' | 'make-content-unsupported' | 'layer-unit-mismatch' | 'layer-full' | 'unknown-metric-source' | 'unit-source-mismatch' | 'block-path-rule' | 'undeclared-targeting-key' | 'invalid-targeting-rule';
|
|
5
|
+
export type ReadinessCode = ArmIssueCode | 'goal-in-some-versions' | 'placement-unchecked' | 'invalid-status' | 'no-planned-duration' | 'no-primary-metric' | 'no-reach-per-day' | 'placement-unpublished' | 'duplicate-version-row' | 'interacting-tests' | 'version-identical' | 'version-same-address' | 'unknown-collection' | 'page-no-targets' | 'page-in-use' | 'page-target-unpublished' | 'page-targets-unchecked' | 'version-page-unavailable' | 'version-page-unchecked' | 'page-user-unit' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'no-placements' | 'placement-removed' | 'empty-slot' | 'make-content-unsupported' | 'layer-unit-mismatch' | 'layer-full' | 'unknown-metric-source' | 'unit-source-mismatch' | 'block-path-rule' | 'undeclared-targeting-key' | 'invalid-targeting-rule';
|
|
6
6
|
export interface StartExperiment {
|
|
7
7
|
id: DocId;
|
|
8
8
|
slug: string;
|
|
9
9
|
status: Status;
|
|
10
10
|
kind: ExperimentKind;
|
|
11
11
|
unitType: UnitType;
|
|
12
|
+
phase?: number;
|
|
12
13
|
pageCollection?: string | null;
|
|
13
14
|
arms: Arm[];
|
|
14
15
|
allocation: number;
|
|
@@ -114,7 +114,9 @@ function invalidParams(arms, point) {
|
|
|
114
114
|
}))
|
|
115
115
|
});
|
|
116
116
|
const placements = input.placements ?? [];
|
|
117
|
-
|
|
117
|
+
const placementRemoved = e.kind === 'block' && e.status === 'stopped' && (e.phase ?? 0) >= 1 && placements.length === 0 && input.placementError === undefined;
|
|
118
|
+
add('no-placements', placementRemoved || e.kind !== 'block' || !input.placementError && placements.length > 0, input.placementError);
|
|
119
|
+
add('placement-removed', !placementRemoved);
|
|
118
120
|
const unpublished = placements.filter((p)=>!p.published && !placements.some((other)=>other.published && other.collection === p.collection && String(other.id) === String(p.id)));
|
|
119
121
|
add('placement-unpublished', unpublished.length === 0, unpublished.map(placementDetail));
|
|
120
122
|
const duplicates = placements.filter((p)=>p.published && p.duplicateRows);
|
|
@@ -57,6 +57,7 @@ export async function decide(req, { experimentId, winnerArm, note, confirmation
|
|
|
57
57
|
...current,
|
|
58
58
|
arms: test.arms
|
|
59
59
|
}, placements);
|
|
60
|
+
if (rescan.placements.length === 0) refuse('placement-removed');
|
|
60
61
|
await refreshPlacementDocs(exec, experimentId, test.phase, rescan.docs);
|
|
61
62
|
}
|
|
62
63
|
if (current.kind === 'page' && current.status === 'stopped') {
|
|
@@ -5,7 +5,7 @@ import { lockPagePaths } from '../db/locks.js';
|
|
|
5
5
|
import { lockMetricsForFreeze } from '../db/metricUsage.js';
|
|
6
6
|
import { DEFAULT_SEQUENTIAL_TUNING_PARAMETER } from '../constants.js';
|
|
7
7
|
import { claimSlices } from '../db/slices.js';
|
|
8
|
-
import { asymmetricGoalsCopy, sameAddressCopy, interactingTestsCopy, pageInUseCopy, unpublishedPageCopy } from '../admin/copy/readiness.js';
|
|
8
|
+
import { READINESS_COPY, asymmetricGoalsCopy, sameAddressCopy, interactingTestsCopy, pageInUseCopy, unpublishedPageCopy } from '../admin/copy/readiness.js';
|
|
9
9
|
import { GROUP_LABELS } from '../admin/copy/arms.js';
|
|
10
10
|
import { XpRefusal } from '../errors.js';
|
|
11
11
|
import { isOpValid } from '../targeting/ops.js';
|
|
@@ -43,6 +43,7 @@ export function refuse(code, detail) {
|
|
|
43
43
|
return d ? interactingTestsCopy(d.path, d.status, d.name, d.version) : 'Finish one of the tests first.';
|
|
44
44
|
})(),
|
|
45
45
|
'unknown-collection': "That page type isn't set up for tests.",
|
|
46
|
+
'placement-removed': READINESS_COPY.placementRemoved,
|
|
46
47
|
'page-in-use': pageInUseCopy(detail?.paths?.[0] ?? 'this page'),
|
|
47
48
|
'version-same-address': (()=>{
|
|
48
49
|
const d = detail;
|
|
@@ -7,7 +7,7 @@ import type { Snapshot, VerdictCode } from './snapshot.js';
|
|
|
7
7
|
export type ArmIssueCode = 'weights-total' | 'control-count' | 'control-first' | 'holdout-last' | 'holdout-count' | 'variant-count' | 'duplicate-key' | 'duplicate-slot' | 'slot-required' | 'invalid-key';
|
|
8
8
|
/** Content-report dismiss/restore refusals (CORE-44). */
|
|
9
9
|
export type ContentEntryRefusal = 'not-decided' | 'entry-not-found' | 'entry-ok' | 'entry-dismissed' | 'entry-not-dismissed';
|
|
10
|
-
export type RefusalCode = ArmIssueCode | 'goal-in-some-versions' | 'placement-unchecked' | 'placement-unpublished' | 'duplicate-version-row' | 'interacting-tests' | 'placements-changed' | 'page-busy' | 'wrap-target-missing' | 'version-identical' | 'version-same-address' | 'unknown-collection' | 'not-found' | ContentEntryRefusal | 'no-planned-duration' | 'no-primary-metric' | 'no-placements' | 'empty-slot' | 'page-in-use' | 'page-no-targets' | 'page-target-unpublished' | 'page-user-unit' | 'version-page-unavailable' | 'version-page-unchecked' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'layer-full' | 'layer-unit-mismatch' | 'unknown-metric-source' | 'unit-source-mismatch' | 'no-reach-per-day' | 'invalid-status' | 'stale-draft' | 'stale-edit' | 'commit-unverified' | 'allocation-lower' | 'confirmation-required' | 'invalid-winner' | 'slices-taken' | 'locked-placement' | 'not-implemented' | 'forbidden' | 'unauthenticated' | 'body-too-large' | 'invalid-body' | 'too-many-ids' | 'invalid-phase' | 'invalid-ids' | 'recompute-failed' | 'analysis-failed' | 'undeclared-targeting-key' | 'invalid-targeting-rule' | 'block-path-rule' | 'duplicate-metric';
|
|
10
|
+
export type RefusalCode = ArmIssueCode | 'goal-in-some-versions' | 'placement-unchecked' | 'placement-unpublished' | 'duplicate-version-row' | 'interacting-tests' | 'placements-changed' | 'page-busy' | 'wrap-target-missing' | 'version-identical' | 'version-same-address' | 'unknown-collection' | 'not-found' | ContentEntryRefusal | 'no-planned-duration' | 'no-primary-metric' | 'no-placements' | 'placement-removed' | 'empty-slot' | 'page-in-use' | 'page-no-targets' | 'page-target-unpublished' | 'page-user-unit' | 'version-page-unavailable' | 'version-page-unchecked' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'layer-full' | 'layer-unit-mismatch' | 'unknown-metric-source' | 'unit-source-mismatch' | 'no-reach-per-day' | 'invalid-status' | 'stale-draft' | 'stale-edit' | 'commit-unverified' | 'allocation-lower' | 'confirmation-required' | 'invalid-winner' | 'slices-taken' | 'locked-placement' | 'not-implemented' | 'forbidden' | 'unauthenticated' | 'body-too-large' | 'invalid-body' | 'too-many-ids' | 'invalid-phase' | 'invalid-ids' | 'recompute-failed' | 'analysis-failed' | 'undeclared-targeting-key' | 'invalid-targeting-rule' | 'block-path-rule' | 'duplicate-metric';
|
|
11
11
|
export type ErrorEnvelope = {
|
|
12
12
|
errors: Array<{
|
|
13
13
|
code: RefusalCode;
|