@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.
Files changed (65) hide show
  1. package/README.md +15 -0
  2. package/dist/admin/TestsNavLink.d.ts +1 -0
  3. package/dist/admin/TestsNavLink.js +1 -0
  4. package/dist/admin/actions/ConfirmDialog.d.ts +1 -0
  5. package/dist/admin/actions/ConfirmDialog.js +7 -5
  6. package/dist/admin/components/ChipsInput.d.ts +12 -0
  7. package/dist/admin/components/ChipsInput.js +96 -0
  8. package/dist/admin/components/RootViewMessage.d.ts +5 -0
  9. package/dist/admin/components/RootViewMessage.js +15 -0
  10. package/dist/admin/components/chipsInput.css +11 -0
  11. package/dist/admin/copy/audience.d.ts +4 -0
  12. package/dist/admin/copy/audience.js +4 -0
  13. package/dist/admin/copy/goals.d.ts +44 -0
  14. package/dist/admin/copy/goals.js +46 -0
  15. package/dist/admin/results/ArmTable.js +2 -1
  16. package/dist/admin/results/CappedCell.js +2 -1
  17. package/dist/admin/results/CiChart.js +8 -7
  18. package/dist/admin/results/DataQualityPanel.js +3 -2
  19. package/dist/admin/results/VerdictCard.js +3 -2
  20. package/dist/admin/styles.css +8 -8
  21. package/dist/admin/styles.d.ts +1 -0
  22. package/dist/admin/styles.js +1 -0
  23. package/dist/admin/surface/SurfaceShell.d.ts +1 -0
  24. package/dist/admin/surface/SurfaceShell.js +1 -0
  25. package/dist/admin/surface/TestsHome.js +1 -1
  26. package/dist/admin/surface/TestsSurface.js +10 -1
  27. package/dist/admin/surface/goals/GoalForm.d.ts +7 -0
  28. package/dist/admin/surface/goals/GoalForm.js +470 -0
  29. package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +5 -0
  30. package/dist/admin/surface/goals/GoalsBreadcrumb.js +47 -0
  31. package/dist/admin/surface/goals/GoalsList.d.ts +6 -0
  32. package/dist/admin/surface/goals/GoalsList.js +135 -0
  33. package/dist/admin/surface/goals/data.d.ts +3 -0
  34. package/dist/admin/surface/goals/data.js +24 -0
  35. package/dist/admin/surface/goals/goals.css +15 -0
  36. package/dist/admin/surface/goals/goals.model.d.ts +30 -0
  37. package/dist/admin/surface/goals/goals.model.js +26 -0
  38. package/dist/admin/surface/newTest/newTest.css +6 -6
  39. package/dist/admin/surface/readiness/Banner.js +5 -4
  40. package/dist/admin/surface/results/ResultsBody.js +3 -2
  41. package/dist/admin/surface/setting/setting.css +11 -11
  42. package/dist/admin/surface/surface.css +29 -29
  43. package/dist/admin/surface/test/GoalSection.js +32 -1
  44. package/dist/admin/surface/test/TargetingSection.js +27 -10
  45. package/dist/admin/surface/test/VersionsSection.js +6 -5
  46. package/dist/admin/surface/test/testPage.css +12 -12
  47. package/dist/admin/targeting/targeting.model.d.ts +6 -1
  48. package/dist/admin/targeting/targeting.model.js +18 -2
  49. package/dist/admin/theme.css +42 -0
  50. package/dist/admin/theme.d.ts +50 -0
  51. package/dist/admin/theme.js +49 -0
  52. package/dist/admin/views/ExperimentsRootView.js +18 -18
  53. package/dist/collections/metricValidation.d.ts +3 -0
  54. package/dist/collections/metricValidation.js +9 -0
  55. package/dist/collections/metrics.js +3 -5
  56. package/dist/plugin.js +2 -2
  57. package/dist/puck/editor/CreateExperimentDrawer.d.ts +1 -0
  58. package/dist/puck/editor/CreateExperimentDrawer.js +34 -25
  59. package/dist/puck/editor/ExperimentSlotEditor.d.ts +1 -0
  60. package/dist/puck/editor/ExperimentSlotEditor.js +137 -78
  61. package/dist/puck/editor/copy.d.ts +3 -1
  62. package/dist/puck/editor/copy.js +3 -1
  63. package/dist/puck/editor/editor.css +302 -0
  64. package/dist/types/surface.d.ts +2 -1
  65. package/package.json +2 -1
@@ -0,0 +1,470 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { useRouter } from 'next/navigation.js';
5
+ import { Link } from '@payloadcms/ui';
6
+ import { formatAdminURL } from 'payload/shared';
7
+ import { GOALS_COPY as C, goalUsedBy } from '../../copy/goals.js';
8
+ import { metricSourceHint, metricSourceOptions } from '../../metricSource.model.js';
9
+ import { validateCappingType, validateCappingValue } from '../../../collections/metricValidation.js';
10
+ import { goalSlug, goalUsage } from './goals.model.js';
11
+ import { goalPages, goalResponse } from './data.js';
12
+ import { GoalsBreadcrumb } from './GoalsBreadcrumb.js';
13
+ import './goals.css';
14
+ const empty = ()=>({
15
+ name: '',
16
+ slug: '',
17
+ source: '',
18
+ direction: 'increase',
19
+ attributionWindowHours: 168,
20
+ minSampleSize: 150,
21
+ capType: '',
22
+ capValue: '',
23
+ warningPercent: '5'
24
+ });
25
+ export function GoalForm({ surface, id, returnToTest }) {
26
+ const router = useRouter();
27
+ const base = surface.apiRoute.replace(/\/$/, '');
28
+ const [draft, setDraft] = useState(empty);
29
+ const [goal, setGoal] = useState();
30
+ const [usedBy, setUsedBy] = useState(0);
31
+ const [ready, setReady] = useState(false);
32
+ const [loadError, setLoadError] = useState('');
33
+ const [error, setError] = useState('');
34
+ const [fieldErrors, setFieldErrors] = useState({});
35
+ const [pending, setPending] = useState(null);
36
+ const [slugEdited, setSlugEdited] = useState(false);
37
+ const [attempt, setAttempt] = useState(0);
38
+ const inFlight = useRef(false);
39
+ const returnQuery = returnToTest ? `?${new URLSearchParams({
40
+ return: returnToTest
41
+ })}` : '';
42
+ const listUrl = formatAdminURL({
43
+ adminRoute: surface.adminRoute,
44
+ path: `/experiments/goals${returnQuery}`
45
+ });
46
+ useEffect(()=>{
47
+ const controller = new AbortController();
48
+ setReady(false);
49
+ setLoadError('');
50
+ void (async ()=>{
51
+ try {
52
+ if (id !== undefined) {
53
+ const [loaded, tests] = await Promise.all([
54
+ fetch(`${base}/xp-metrics/${encodeURIComponent(id)}?depth=0`, {
55
+ credentials: 'include',
56
+ signal: controller.signal
57
+ }).then((response)=>goalResponse(response, C.loadError)),
58
+ goalPages(base, 'xp-experiments', controller.signal)
59
+ ]);
60
+ if (loaded.id == null) throw new Error(C.missing);
61
+ if (controller.signal.aborted) return;
62
+ const defaultCap = surface.metricSources.some((source)=>source.id === loaded.source && source.kind === 'revenue') && loaded.capping?.type == null && loaded.capping?.value == null;
63
+ setGoal(loaded);
64
+ setUsedBy(goalUsage(tests).get(loaded.id) ?? 0);
65
+ setDraft({
66
+ ...empty(),
67
+ ...loaded,
68
+ capType: loaded.capping?.type ?? (defaultCap ? 'percentile' : ''),
69
+ capValue: loaded.capping?.value == null ? defaultCap ? '0.99' : '' : String(loaded.capping.value),
70
+ warningPercent: String(Number(((loaded.guardrailThreshold ?? 0.05) * 100).toPrecision(12)))
71
+ });
72
+ setSlugEdited(true);
73
+ }
74
+ if (!controller.signal.aborted) setReady(true);
75
+ } catch (cause) {
76
+ if (!controller.signal.aborted) setLoadError(cause instanceof Error ? cause.message : C.loadError);
77
+ }
78
+ })();
79
+ return ()=>controller.abort();
80
+ }, [
81
+ base,
82
+ id,
83
+ attempt
84
+ ]);
85
+ const source = surface.metricSources.find((source)=>source.id === draft.source);
86
+ const hint = metricSourceHint(source);
87
+ const options = metricSourceOptions(surface.metricSources);
88
+ const set = (key, value)=>setDraft((current)=>({
89
+ ...current,
90
+ [key]: value
91
+ }));
92
+ async function submit() {
93
+ if (inFlight.current || !ready) return;
94
+ const capValue = draft.capValue.trim() === '' ? null : Number(draft.capValue);
95
+ const typeError = validateCappingType(draft.capType || null, capValue, source?.kind);
96
+ const valueError = validateCappingValue(capValue, draft.capType || null);
97
+ const warningPercent = draft.warningPercent.trim() === '' ? 5 : Number(draft.warningPercent);
98
+ const errors = {
99
+ ...typeError !== true ? {
100
+ capType: typeError
101
+ } : {},
102
+ ...valueError !== true ? {
103
+ capValue: valueError
104
+ } : {},
105
+ ...!Number.isFinite(warningPercent) || warningPercent < 0 ? {
106
+ warningPercent: C.thresholdError
107
+ } : {}
108
+ };
109
+ setFieldErrors(errors);
110
+ if (Object.keys(errors).length) return;
111
+ inFlight.current = true;
112
+ setPending('save');
113
+ setError('');
114
+ try {
115
+ // Use an explicit body: loaded metadata and presentation fields never go back to Payload.
116
+ const body = {
117
+ name: draft.name,
118
+ slug: usedBy && goal ? goal.slug : draft.slug,
119
+ source: draft.source,
120
+ direction: draft.direction,
121
+ attributionWindowHours: draft.attributionWindowHours,
122
+ minSampleSize: draft.minSampleSize,
123
+ capping: {
124
+ type: draft.capType || null,
125
+ value: capValue
126
+ },
127
+ guardrailThreshold: warningPercent / 100
128
+ };
129
+ const response = await fetch(`${base}/xp-metrics${id === undefined ? '' : `/${encodeURIComponent(id)}`}`, {
130
+ method: id === undefined ? 'POST' : 'PATCH',
131
+ credentials: 'include',
132
+ headers: {
133
+ 'Content-Type': 'application/json'
134
+ },
135
+ body: JSON.stringify(body)
136
+ });
137
+ const saved = await goalResponse(response, C.saveError);
138
+ if (saved.doc?.id == null) throw new Error(C.saveError);
139
+ router.push(id === undefined && returnToTest ? formatAdminURL({
140
+ adminRoute: surface.adminRoute,
141
+ path: `/experiments/${encodeURIComponent(returnToTest)}?${new URLSearchParams({
142
+ goal: String(saved.doc.id)
143
+ })}`
144
+ }) : listUrl);
145
+ } catch (cause) {
146
+ setError(cause instanceof Error ? cause.message : C.saveError);
147
+ inFlight.current = false;
148
+ setPending(null);
149
+ }
150
+ }
151
+ async function remove() {
152
+ if (inFlight.current || !ready || usedBy || id === undefined) return;
153
+ inFlight.current = true;
154
+ setPending('delete');
155
+ setError('');
156
+ try {
157
+ const response = await fetch(`${base}/xp-metrics/${encodeURIComponent(id)}`, {
158
+ method: 'DELETE',
159
+ credentials: 'include'
160
+ });
161
+ await goalResponse(response, C.deleteError);
162
+ router.push(listUrl);
163
+ } catch (cause) {
164
+ setError(cause instanceof Error ? cause.message : C.deleteError);
165
+ inFlight.current = false;
166
+ setPending(null);
167
+ }
168
+ }
169
+ return /*#__PURE__*/ _jsxs("div", {
170
+ className: "xp-goals",
171
+ children: [
172
+ /*#__PURE__*/ _jsx(GoalsBreadcrumb, {
173
+ adminRoute: surface.adminRoute,
174
+ name: goal?.name ?? (id === undefined ? C.newGoal : undefined),
175
+ returnQuery: returnQuery
176
+ }),
177
+ /*#__PURE__*/ _jsx("h1", {
178
+ children: goal?.name ?? (id === undefined ? C.newGoal : C.title)
179
+ }),
180
+ loadError ? /*#__PURE__*/ _jsxs("p", {
181
+ role: "alert",
182
+ children: [
183
+ loadError,
184
+ ' ',
185
+ /*#__PURE__*/ _jsx("button", {
186
+ type: "button",
187
+ onClick: ()=>setAttempt((n)=>n + 1),
188
+ children: C.retry
189
+ })
190
+ ]
191
+ }) : !ready ? /*#__PURE__*/ _jsx("p", {
192
+ role: "status",
193
+ children: C.loading
194
+ }) : /*#__PURE__*/ _jsxs("form", {
195
+ className: "xp-goal-form",
196
+ onSubmit: (event)=>{
197
+ event.preventDefault();
198
+ void submit();
199
+ },
200
+ children: [
201
+ /*#__PURE__*/ _jsxs("fieldset", {
202
+ disabled: pending !== null,
203
+ children: [
204
+ /*#__PURE__*/ _jsxs("label", {
205
+ className: "xp-field",
206
+ children: [
207
+ C.name,
208
+ /*#__PURE__*/ _jsx("input", {
209
+ required: true,
210
+ value: draft.name,
211
+ onChange: (event)=>{
212
+ const name = event.currentTarget.value;
213
+ setDraft((current)=>({
214
+ ...current,
215
+ name,
216
+ ...!slugEdited && !usedBy ? {
217
+ slug: goalSlug(name)
218
+ } : {}
219
+ }));
220
+ }
221
+ })
222
+ ]
223
+ }),
224
+ /*#__PURE__*/ _jsxs("label", {
225
+ className: "xp-field",
226
+ children: [
227
+ C.slug,
228
+ /*#__PURE__*/ _jsx("input", {
229
+ required: true,
230
+ value: draft.slug,
231
+ readOnly: usedBy > 0,
232
+ "aria-describedby": "xp-goal-slug-help",
233
+ onChange: (event)=>{
234
+ setSlugEdited(true);
235
+ set('slug', event.currentTarget.value);
236
+ }
237
+ })
238
+ ]
239
+ }),
240
+ /*#__PURE__*/ _jsx("p", {
241
+ id: "xp-goal-slug-help",
242
+ children: usedBy ? C.slugLocked : C.slugHelp
243
+ }),
244
+ /*#__PURE__*/ _jsxs("label", {
245
+ className: "xp-field",
246
+ children: [
247
+ C.source,
248
+ /*#__PURE__*/ _jsxs("select", {
249
+ required: true,
250
+ value: draft.source,
251
+ onChange: (event)=>{
252
+ const sourceId = event.currentTarget.value;
253
+ const revenue = surface.metricSources.find((source)=>source.id === sourceId)?.kind === 'revenue';
254
+ setDraft((current)=>({
255
+ ...current,
256
+ source: sourceId,
257
+ capType: revenue ? 'percentile' : '',
258
+ capValue: revenue ? '0.99' : ''
259
+ }));
260
+ },
261
+ children: [
262
+ /*#__PURE__*/ _jsx("option", {
263
+ value: "",
264
+ children: C.chooseSource
265
+ }),
266
+ options.map((option)=>/*#__PURE__*/ _jsx("option", {
267
+ value: option.value,
268
+ children: option.label
269
+ }, option.value)),
270
+ draft.source && !options.some((option)=>option.value === draft.source) ? /*#__PURE__*/ _jsx("option", {
271
+ value: draft.source,
272
+ children: draft.source
273
+ }) : null
274
+ ]
275
+ })
276
+ ]
277
+ }),
278
+ hint ? /*#__PURE__*/ _jsx("p", {
279
+ children: hint
280
+ }) : null,
281
+ /*#__PURE__*/ _jsxs("label", {
282
+ className: "xp-field",
283
+ children: [
284
+ C.direction,
285
+ /*#__PURE__*/ _jsxs("select", {
286
+ value: draft.direction,
287
+ onChange: (event)=>set('direction', event.currentTarget.value),
288
+ children: [
289
+ /*#__PURE__*/ _jsx("option", {
290
+ value: "increase",
291
+ children: C.increase
292
+ }),
293
+ /*#__PURE__*/ _jsx("option", {
294
+ value: "decrease",
295
+ children: C.decrease
296
+ })
297
+ ]
298
+ })
299
+ ]
300
+ }),
301
+ /*#__PURE__*/ _jsxs("label", {
302
+ className: "xp-field",
303
+ children: [
304
+ C.window,
305
+ /*#__PURE__*/ _jsx("input", {
306
+ required: true,
307
+ type: "number",
308
+ min: "1",
309
+ step: "any",
310
+ value: draft.attributionWindowHours ?? '',
311
+ onChange: (event)=>set('attributionWindowHours', event.currentTarget.value === '' ? null : Number(event.currentTarget.value))
312
+ })
313
+ ]
314
+ }),
315
+ /*#__PURE__*/ _jsxs("label", {
316
+ className: "xp-field",
317
+ children: [
318
+ C.minimum,
319
+ /*#__PURE__*/ _jsx("input", {
320
+ required: true,
321
+ type: "number",
322
+ min: "1",
323
+ step: "any",
324
+ value: draft.minSampleSize ?? '',
325
+ onChange: (event)=>set('minSampleSize', event.currentTarget.value === '' ? null : Number(event.currentTarget.value))
326
+ })
327
+ ]
328
+ }),
329
+ /*#__PURE__*/ _jsxs("details", {
330
+ children: [
331
+ /*#__PURE__*/ _jsx("summary", {
332
+ children: C.advanced
333
+ }),
334
+ /*#__PURE__*/ _jsxs("fieldset", {
335
+ children: [
336
+ /*#__PURE__*/ _jsx("legend", {
337
+ children: C.capping
338
+ }),
339
+ /*#__PURE__*/ _jsxs("label", {
340
+ className: "xp-field",
341
+ children: [
342
+ C.capType,
343
+ /*#__PURE__*/ _jsxs("select", {
344
+ required: source?.kind === 'revenue',
345
+ value: draft.capType,
346
+ "aria-invalid": fieldErrors.capType ? true : undefined,
347
+ "aria-describedby": fieldErrors.capType ? 'xp-goal-cap-type-error' : undefined,
348
+ onChange: (event)=>{
349
+ const capType = event.currentTarget.value;
350
+ setDraft((current)=>({
351
+ ...current,
352
+ capType,
353
+ capValue: capType ? current.capValue : ''
354
+ }));
355
+ },
356
+ children: [
357
+ source?.kind !== 'revenue' ? /*#__PURE__*/ _jsx("option", {
358
+ value: "",
359
+ children: C.noCap
360
+ }) : null,
361
+ /*#__PURE__*/ _jsx("option", {
362
+ value: "percentile",
363
+ children: C.percentile
364
+ }),
365
+ /*#__PURE__*/ _jsx("option", {
366
+ value: "absolute",
367
+ children: C.absolute
368
+ })
369
+ ]
370
+ })
371
+ ]
372
+ }),
373
+ fieldErrors.capType ? /*#__PURE__*/ _jsx("p", {
374
+ id: "xp-goal-cap-type-error",
375
+ role: "alert",
376
+ children: fieldErrors.capType
377
+ }) : null,
378
+ /*#__PURE__*/ _jsxs("label", {
379
+ className: "xp-field",
380
+ children: [
381
+ C.capValue,
382
+ /*#__PURE__*/ _jsx("input", {
383
+ type: "number",
384
+ step: "any",
385
+ min: "0",
386
+ disabled: !draft.capType,
387
+ value: draft.capValue,
388
+ "aria-invalid": fieldErrors.capValue ? true : undefined,
389
+ "aria-describedby": fieldErrors.capValue ? 'xp-goal-cap-value-error' : undefined,
390
+ onChange: (event)=>set('capValue', event.currentTarget.value)
391
+ })
392
+ ]
393
+ }),
394
+ /*#__PURE__*/ _jsx("p", {
395
+ children: C.capHelp
396
+ }),
397
+ fieldErrors.capValue ? /*#__PURE__*/ _jsx("p", {
398
+ id: "xp-goal-cap-value-error",
399
+ role: "alert",
400
+ children: fieldErrors.capValue
401
+ }) : null
402
+ ]
403
+ }),
404
+ /*#__PURE__*/ _jsxs("label", {
405
+ className: "xp-field",
406
+ children: [
407
+ C.threshold,
408
+ /*#__PURE__*/ _jsx("input", {
409
+ type: "number",
410
+ min: "0",
411
+ step: "any",
412
+ value: draft.warningPercent,
413
+ "aria-invalid": fieldErrors.warningPercent ? true : undefined,
414
+ "aria-describedby": fieldErrors.warningPercent ? 'xp-goal-threshold-error' : undefined,
415
+ onInvalid: ()=>setFieldErrors((current)=>({
416
+ ...current,
417
+ warningPercent: C.thresholdError
418
+ })),
419
+ onChange: (event)=>set('warningPercent', event.currentTarget.value)
420
+ })
421
+ ]
422
+ }),
423
+ fieldErrors.warningPercent ? /*#__PURE__*/ _jsx("p", {
424
+ id: "xp-goal-threshold-error",
425
+ role: "alert",
426
+ children: fieldErrors.warningPercent
427
+ }) : null
428
+ ]
429
+ })
430
+ ]
431
+ }),
432
+ error ? /*#__PURE__*/ _jsx("p", {
433
+ role: "alert",
434
+ children: error
435
+ }) : null,
436
+ /*#__PURE__*/ _jsxs("div", {
437
+ className: "xp-tests-toolbar",
438
+ children: [
439
+ /*#__PURE__*/ _jsx("button", {
440
+ type: "submit",
441
+ disabled: pending !== null,
442
+ children: pending === 'save' ? C.saving : C.save
443
+ }),
444
+ /*#__PURE__*/ _jsx(Link, {
445
+ className: "xp-goal-cancel",
446
+ href: listUrl,
447
+ children: C.cancel
448
+ }),
449
+ id === undefined ? null : /*#__PURE__*/ _jsxs(_Fragment, {
450
+ children: [
451
+ /*#__PURE__*/ _jsx("button", {
452
+ type: "button",
453
+ disabled: pending !== null || usedBy > 0,
454
+ "aria-describedby": usedBy ? 'xp-goal-used' : undefined,
455
+ onClick: ()=>void remove(),
456
+ children: pending === 'delete' ? C.deleting : C.remove
457
+ }),
458
+ usedBy ? /*#__PURE__*/ _jsx("span", {
459
+ id: "xp-goal-used",
460
+ children: goalUsedBy(usedBy)
461
+ }) : null
462
+ ]
463
+ })
464
+ ]
465
+ })
466
+ ]
467
+ })
468
+ ]
469
+ });
470
+ }
@@ -0,0 +1,5 @@
1
+ export declare function GoalsBreadcrumb({ adminRoute, name, returnQuery }: {
2
+ adminRoute: string;
3
+ name?: string;
4
+ returnQuery?: string;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Link } from '@payloadcms/ui';
4
+ import { formatAdminURL } from 'payload/shared';
5
+ import { GOALS_COPY as C } from '../../copy/goals.js';
6
+ import { SURFACE_COPY } from '../../copy/surface.js';
7
+ export function GoalsBreadcrumb({ adminRoute, name, returnQuery = '' }) {
8
+ return /*#__PURE__*/ _jsxs("nav", {
9
+ className: "xp-goals-breadcrumb",
10
+ "aria-label": C.breadcrumb,
11
+ children: [
12
+ /*#__PURE__*/ _jsx(Link, {
13
+ href: formatAdminURL({
14
+ adminRoute,
15
+ path: '/experiments'
16
+ }),
17
+ children: SURFACE_COPY.title
18
+ }),
19
+ /*#__PURE__*/ _jsx("span", {
20
+ "aria-hidden": "true",
21
+ children: "›"
22
+ }),
23
+ name ? /*#__PURE__*/ _jsxs(_Fragment, {
24
+ children: [
25
+ /*#__PURE__*/ _jsx(Link, {
26
+ href: formatAdminURL({
27
+ adminRoute,
28
+ path: `/experiments/goals${returnQuery}`
29
+ }),
30
+ children: C.title
31
+ }),
32
+ /*#__PURE__*/ _jsx("span", {
33
+ "aria-hidden": "true",
34
+ children: "›"
35
+ }),
36
+ /*#__PURE__*/ _jsx("span", {
37
+ "aria-current": "page",
38
+ children: name
39
+ })
40
+ ]
41
+ }) : /*#__PURE__*/ _jsx("span", {
42
+ "aria-current": "page",
43
+ children: C.title
44
+ })
45
+ ]
46
+ });
47
+ }
@@ -0,0 +1,6 @@
1
+ import type { SurfaceProps } from '../contracts.js';
2
+ import './goals.css';
3
+ export declare function GoalsList({ surface, returnToTest }: {
4
+ surface: SurfaceProps;
5
+ returnToTest?: string;
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,135 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { Link, Pill } from '@payloadcms/ui';
5
+ import { formatAdminURL } from 'payload/shared';
6
+ import { GOALS_COPY as C } from '../../copy/goals.js';
7
+ import { goalUsage } from './goals.model.js';
8
+ import { goalPages } from './data.js';
9
+ import { GoalsBreadcrumb } from './GoalsBreadcrumb.js';
10
+ import './goals.css';
11
+ export function GoalsList({ surface, returnToTest }) {
12
+ const [data, setData] = useState();
13
+ const [error, setError] = useState('');
14
+ const [attempt, setAttempt] = useState(0);
15
+ const returnQuery = returnToTest ? `?${new URLSearchParams({
16
+ return: returnToTest
17
+ })}` : '';
18
+ useEffect(()=>{
19
+ const controller = new AbortController();
20
+ setData(undefined);
21
+ setError('');
22
+ const base = surface.apiRoute.replace(/\/$/, '');
23
+ void Promise.all([
24
+ goalPages(base, 'xp-metrics', controller.signal),
25
+ goalPages(base, 'xp-experiments', controller.signal)
26
+ ]).then(([goals, tests])=>{
27
+ if (!controller.signal.aborted) setData({
28
+ goals,
29
+ tests
30
+ });
31
+ }).catch((cause)=>{
32
+ if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : C.loadError);
33
+ });
34
+ return ()=>controller.abort();
35
+ }, [
36
+ surface.apiRoute,
37
+ attempt
38
+ ]);
39
+ const usage = goalUsage(data?.tests ?? []);
40
+ const newGoal = /*#__PURE__*/ _jsx(Link, {
41
+ className: "xp-tests-primary",
42
+ href: formatAdminURL({
43
+ adminRoute: surface.adminRoute,
44
+ path: `/experiments/goals/new${returnQuery}`
45
+ }),
46
+ children: C.newGoal
47
+ });
48
+ return /*#__PURE__*/ _jsxs("div", {
49
+ className: "xp-goals",
50
+ children: [
51
+ /*#__PURE__*/ _jsx(GoalsBreadcrumb, {
52
+ adminRoute: surface.adminRoute
53
+ }),
54
+ /*#__PURE__*/ _jsxs("div", {
55
+ className: "xp-tests-toolbar",
56
+ children: [
57
+ /*#__PURE__*/ _jsx("h1", {
58
+ children: C.title
59
+ }),
60
+ newGoal
61
+ ]
62
+ }),
63
+ error ? /*#__PURE__*/ _jsxs("p", {
64
+ role: "alert",
65
+ children: [
66
+ error,
67
+ ' ',
68
+ /*#__PURE__*/ _jsx("button", {
69
+ type: "button",
70
+ onClick: ()=>setAttempt((n)=>n + 1),
71
+ children: C.retry
72
+ })
73
+ ]
74
+ }) : !data ? /*#__PURE__*/ _jsx("p", {
75
+ role: "status",
76
+ children: C.loading
77
+ }) : !data.goals.length ? /*#__PURE__*/ _jsx("p", {
78
+ children: C.empty
79
+ }) : /*#__PURE__*/ _jsx("div", {
80
+ className: "xp-goals-table",
81
+ children: /*#__PURE__*/ _jsxs("table", {
82
+ children: [
83
+ /*#__PURE__*/ _jsx("thead", {
84
+ children: /*#__PURE__*/ _jsx("tr", {
85
+ children: [
86
+ C.name,
87
+ C.source,
88
+ C.directionColumn,
89
+ C.usedBy,
90
+ C.status
91
+ ].map((label)=>/*#__PURE__*/ _jsx("th", {
92
+ scope: "col",
93
+ children: label
94
+ }, label))
95
+ })
96
+ }),
97
+ /*#__PURE__*/ _jsx("tbody", {
98
+ children: data.goals.map((goal)=>{
99
+ const count = usage.get(goal.id) ?? 0;
100
+ return /*#__PURE__*/ _jsxs("tr", {
101
+ children: [
102
+ /*#__PURE__*/ _jsx("th", {
103
+ scope: "row",
104
+ children: /*#__PURE__*/ _jsx(Link, {
105
+ href: formatAdminURL({
106
+ adminRoute: surface.adminRoute,
107
+ path: `/experiments/goals/${encodeURIComponent(String(goal.id))}${returnQuery}`
108
+ }),
109
+ children: goal.name
110
+ })
111
+ }),
112
+ /*#__PURE__*/ _jsx("td", {
113
+ children: surface.metricSources.find((source)=>source.id === goal.source)?.label ?? goal.source
114
+ }),
115
+ /*#__PURE__*/ _jsx("td", {
116
+ children: goal.direction === 'decrease' ? C.decrease : C.increase
117
+ }),
118
+ /*#__PURE__*/ _jsx("td", {
119
+ children: count
120
+ }),
121
+ /*#__PURE__*/ _jsx("td", {
122
+ children: /*#__PURE__*/ _jsx(Pill, {
123
+ children: count ? C.inUse : C.unused
124
+ })
125
+ })
126
+ ]
127
+ }, `${typeof goal.id}:${goal.id}`);
128
+ })
129
+ })
130
+ ]
131
+ })
132
+ })
133
+ ]
134
+ });
135
+ }