@byline/admin 4.15.0 → 4.17.0

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 (51) hide show
  1. package/dist/abilities.js +2 -0
  2. package/dist/fields/relation/relation-field.js +3 -2
  3. package/dist/fields/relation/relation-many-field.js +3 -2
  4. package/dist/fields/relation/relation-picker.d.ts +2 -2
  5. package/dist/fields/relation/relation-summary.d.ts +2 -2
  6. package/dist/forms/document-actions.js +30 -25
  7. package/dist/forms/form-renderer.d.ts +17 -3
  8. package/dist/forms/form-renderer.js +18 -9
  9. package/dist/forms/tree-placement-widget.js +3 -2
  10. package/dist/forms/use-form-layout.d.ts +3 -3
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/modules/analytics/abilities.d.ts +14 -0
  14. package/dist/modules/analytics/abilities.js +21 -0
  15. package/dist/modules/analytics/components/dashboard.d.ts +20 -0
  16. package/dist/modules/analytics/components/dashboard.js +308 -0
  17. package/dist/modules/analytics/components/dashboard.module.js +29 -0
  18. package/dist/modules/analytics/components/dashboard.test.node.d.ts +8 -0
  19. package/dist/modules/analytics/components/dashboard_module.css +268 -0
  20. package/dist/modules/analytics/components/timeseries.d.ts +59 -0
  21. package/dist/modules/analytics/components/timeseries.js +253 -0
  22. package/dist/modules/analytics/components/timeseries.module.js +17 -0
  23. package/dist/modules/analytics/components/timeseries_module.css +126 -0
  24. package/dist/modules/analytics/index.d.ts +9 -0
  25. package/dist/modules/analytics/index.js +1 -0
  26. package/dist/modules/analytics/types.d.ts +20 -0
  27. package/dist/modules/analytics/types.js +1 -0
  28. package/package.json +21 -11
  29. package/src/abilities.ts +2 -0
  30. package/src/fields/relation/relation-field.tsx +5 -3
  31. package/src/fields/relation/relation-many-field.tsx +5 -3
  32. package/src/fields/relation/relation-picker.tsx +2 -2
  33. package/src/fields/relation/relation-summary.tsx +2 -2
  34. package/src/forms/document-actions.test.tsx +157 -0
  35. package/src/forms/document-actions.tsx +93 -74
  36. package/src/forms/form-renderer-capabilities.test.tsx +155 -0
  37. package/src/forms/form-renderer-submit.test.tsx +194 -0
  38. package/src/forms/form-renderer.tsx +59 -25
  39. package/src/forms/tree-placement-widget.tsx +3 -2
  40. package/src/forms/use-form-layout.ts +3 -3
  41. package/src/index.ts +1 -0
  42. package/src/modules/analytics/abilities.ts +33 -0
  43. package/src/modules/analytics/components/dashboard.module.css +330 -0
  44. package/src/modules/analytics/components/dashboard.test.node.ts +193 -0
  45. package/src/modules/analytics/components/dashboard.tsx +371 -0
  46. package/src/modules/analytics/components/timeseries.module.css +151 -0
  47. package/src/modules/analytics/components/timeseries.tsx +332 -0
  48. package/src/modules/analytics/index.ts +14 -0
  49. package/src/modules/analytics/types.ts +31 -0
  50. package/dist/forms/__probe-nested.test.node.d.ts +0 -1
  51. package/dist/forms/__probe-two.test.node.d.ts +0 -1
@@ -0,0 +1,253 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useRef, useState } from "react";
4
+ import { useTranslation } from "@byline/i18n/react";
5
+ import clsx from "clsx";
6
+ import timeseries_module from "./timeseries.module.js";
7
+ const VIEWBOX_WIDTH = 900;
8
+ const VIEWBOX_HEIGHT = 180;
9
+ const COLUMN_GAP_RATIO = 0.38;
10
+ const MAX_COLUMN_WIDTH = 26;
11
+ function resolveAnalyticsChartGranularity(period, dayCount) {
12
+ if ('number' == typeof period || 'all' === period && dayCount <= 90) return 'day';
13
+ if ('ytd' === period || dayCount <= 732) return 'seven-day';
14
+ return 'month';
15
+ }
16
+ function bucketAnalyticsTimeseries(days, granularity) {
17
+ if ('day' === granularity) return days.map((day)=>({
18
+ from: day.day,
19
+ to: day.day,
20
+ granularity,
21
+ dayCount: 1,
22
+ views: day.views,
23
+ visitors: day.visitors,
24
+ downloads: day.downloads
25
+ }));
26
+ if ('seven-day' === granularity) return bucketBySevenDays(days);
27
+ return bucketByUtcMonth(days);
28
+ }
29
+ function buildAnalyticsColumns(buckets) {
30
+ if (0 === buckets.length) return [];
31
+ const ceiling = Math.max(1, ...buckets.map((bucket)=>bucket.views));
32
+ const step = VIEWBOX_WIDTH / buckets.length;
33
+ const width = Math.max(2, Math.min(step * (1 - COLUMN_GAP_RATIO), MAX_COLUMN_WIDTH));
34
+ return buckets.map((bucket, index)=>{
35
+ const centre = index * step + step / 2;
36
+ const height = bucket.views / ceiling * VIEWBOX_HEIGHT;
37
+ const insetHeight = bucket.visitors / ceiling * VIEWBOX_HEIGHT;
38
+ const insetWidth = Math.max(1, width / 2);
39
+ return {
40
+ index,
41
+ x: centre - width / 2,
42
+ width,
43
+ y: VIEWBOX_HEIGHT - height,
44
+ height,
45
+ insetX: centre - insetWidth / 2,
46
+ insetWidth,
47
+ insetY: VIEWBOX_HEIGHT - insetHeight,
48
+ insetHeight,
49
+ hitX: index * step,
50
+ hitWidth: step
51
+ };
52
+ });
53
+ }
54
+ function AnalyticsTimeseries({ days, granularity, locale }) {
55
+ const { t } = useTranslation('byline-admin');
56
+ const [hovered, setHovered] = useState(null);
57
+ const plot = useRef(null);
58
+ const buckets = useMemo(()=>bucketAnalyticsTimeseries(days, granularity), [
59
+ days,
60
+ granularity
61
+ ]);
62
+ const columns = useMemo(()=>buildAnalyticsColumns(buckets), [
63
+ buckets
64
+ ]);
65
+ const numbers = useMemo(()=>new Intl.NumberFormat(locale), [
66
+ locale
67
+ ]);
68
+ const dayLabel = useMemo(()=>new Intl.DateTimeFormat(locale, {
69
+ month: 'short',
70
+ day: 'numeric',
71
+ timeZone: 'UTC'
72
+ }), [
73
+ locale
74
+ ]);
75
+ const formatDay = (day)=>dayLabel.format(new Date(`${day}T00:00:00.000Z`));
76
+ const formatBucket = (bucket)=>1 === bucket.dayCount ? formatDay(bucket.from) : `${formatDay(bucket.from)} – ${formatDay(bucket.to)}`;
77
+ const visitorLabel = (bucket)=>'day' === bucket.granularity ? t('analytics.stats.dailyUniques') : t('analytics.stats.summedDailyUniques');
78
+ const active = null == hovered ? void 0 : buckets[hovered];
79
+ const readBucket = (bucket)=>[
80
+ formatBucket(bucket),
81
+ `${t('analytics.stats.views')} ${numbers.format(bucket.views)}`,
82
+ `${visitorLabel(bucket)} ${numbers.format(bucket.visitors)}`,
83
+ `${t('analytics.stats.downloads')} ${numbers.format(bucket.downloads)}`
84
+ ].join(', ');
85
+ const trackPointer = (event)=>{
86
+ if (0 === buckets.length || null == plot.current) return;
87
+ const bounds = plot.current.getBoundingClientRect();
88
+ if (0 === bounds.width) return;
89
+ const ratio = (event.clientX - bounds.left) / bounds.width;
90
+ setHovered(Math.min(buckets.length - 1, Math.max(0, Math.floor(ratio * buckets.length))));
91
+ };
92
+ const trackKey = (event)=>{
93
+ if (0 === buckets.length) return;
94
+ const step = 'ArrowRight' === event.key ? 1 : 'ArrowLeft' === event.key ? -1 : 0;
95
+ if (0 === step && 'Home' !== event.key && 'End' !== event.key && 'Escape' !== event.key) return;
96
+ event.preventDefault();
97
+ if ('Escape' === event.key) return setHovered(null);
98
+ if ('Home' === event.key) return setHovered(0);
99
+ if ('End' === event.key) return setHovered(buckets.length - 1);
100
+ setHovered((current)=>{
101
+ const next = (current ?? (step > 0 ? -1 : buckets.length)) + step;
102
+ return Math.min(buckets.length - 1, Math.max(0, next));
103
+ });
104
+ };
105
+ return /*#__PURE__*/ jsxs("div", {
106
+ className: clsx('byline-analytics-timeseries', timeseries_module.root),
107
+ role: "slider",
108
+ tabIndex: 0,
109
+ "aria-label": t('analytics.chart.views'),
110
+ "aria-valuemin": 0,
111
+ "aria-valuemax": Math.max(0, buckets.length - 1),
112
+ "aria-valuenow": hovered ?? 0,
113
+ "aria-valuetext": null == active ? t('analytics.chart.hint') : readBucket(active),
114
+ onPointerMove: trackPointer,
115
+ onPointerLeave: ()=>setHovered(null),
116
+ onKeyDown: trackKey,
117
+ onBlur: ()=>setHovered(null),
118
+ children: [
119
+ /*#__PURE__*/ jsxs("svg", {
120
+ ref: plot,
121
+ className: clsx('byline-analytics-chart', timeseries_module.chart),
122
+ viewBox: `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`,
123
+ preserveAspectRatio: "none",
124
+ "aria-hidden": "true",
125
+ children: [
126
+ /*#__PURE__*/ jsx("title", {
127
+ children: t('analytics.chart.views')
128
+ }),
129
+ [
130
+ 0.5,
131
+ 1
132
+ ].map((fraction)=>/*#__PURE__*/ jsx("line", {
133
+ className: timeseries_module.gridline,
134
+ x1: 0,
135
+ x2: VIEWBOX_WIDTH,
136
+ y1: VIEWBOX_HEIGHT - VIEWBOX_HEIGHT * fraction,
137
+ y2: VIEWBOX_HEIGHT - VIEWBOX_HEIGHT * fraction,
138
+ vectorEffect: "non-scaling-stroke"
139
+ }, fraction)),
140
+ columns.map((column)=>/*#__PURE__*/ jsxs("g", {
141
+ className: clsx(timeseries_module.column, hovered === column.index && timeseries_module.columnActive),
142
+ children: [
143
+ /*#__PURE__*/ jsx("rect", {
144
+ className: timeseries_module.views,
145
+ x: column.x,
146
+ width: column.width,
147
+ y: column.y,
148
+ height: column.height
149
+ }),
150
+ /*#__PURE__*/ jsx("rect", {
151
+ className: timeseries_module.visitors,
152
+ x: column.insetX,
153
+ width: column.insetWidth,
154
+ y: column.insetY,
155
+ height: column.insetHeight
156
+ })
157
+ ]
158
+ }, buckets[column.index]?.from))
159
+ ]
160
+ }),
161
+ /*#__PURE__*/ jsxs("div", {
162
+ className: clsx('byline-analytics-axis', timeseries_module.axis),
163
+ "aria-hidden": "true",
164
+ children: [
165
+ /*#__PURE__*/ jsx("span", {
166
+ children: buckets.length > 0 && null != buckets[0] ? formatDay(buckets[0].from) : ''
167
+ }),
168
+ /*#__PURE__*/ jsx("span", {
169
+ children: buckets.length > 0 && null != buckets[buckets.length - 1] ? formatDay(buckets[buckets.length - 1]?.to ?? '') : ''
170
+ })
171
+ ]
172
+ }),
173
+ /*#__PURE__*/ jsx("p", {
174
+ className: clsx('byline-analytics-readout', timeseries_module.readout),
175
+ "aria-live": "polite",
176
+ children: null == active ? t('analytics.chart.hint') : /*#__PURE__*/ jsxs(Fragment, {
177
+ children: [
178
+ /*#__PURE__*/ jsx("strong", {
179
+ children: formatBucket(active)
180
+ }),
181
+ /*#__PURE__*/ jsx("span", {
182
+ className: timeseries_module.swatchViews
183
+ }),
184
+ t('analytics.stats.views'),
185
+ " ",
186
+ numbers.format(active.views),
187
+ /*#__PURE__*/ jsx("span", {
188
+ className: timeseries_module.swatchVisitors
189
+ }),
190
+ visitorLabel(active),
191
+ " ",
192
+ numbers.format(active.visitors),
193
+ /*#__PURE__*/ jsx("span", {
194
+ className: timeseries_module.swatchDownloads
195
+ }),
196
+ t('analytics.stats.downloads'),
197
+ " ",
198
+ numbers.format(active.downloads)
199
+ ]
200
+ })
201
+ })
202
+ ]
203
+ });
204
+ }
205
+ function bucketBySevenDays(days) {
206
+ const buckets = [];
207
+ for(let index = 0; index < days.length; index += 7){
208
+ const rows = days.slice(index, index + 7);
209
+ const first = rows[0];
210
+ const last = rows[rows.length - 1];
211
+ if (null != first && null != last) buckets.push(combineRows(first.day, last.day, 'seven-day', rows));
212
+ }
213
+ return buckets;
214
+ }
215
+ function bucketByUtcMonth(days) {
216
+ const buckets = [];
217
+ let rows = [];
218
+ let month;
219
+ const flush = ()=>{
220
+ const first = rows[0];
221
+ const last = rows[rows.length - 1];
222
+ if (null != first && null != last) buckets.push(combineRows(first.day, last.day, 'month', rows));
223
+ rows = [];
224
+ };
225
+ for (const day of days){
226
+ const nextMonth = day.day.slice(0, 7);
227
+ if (null != month && nextMonth !== month) flush();
228
+ month = nextMonth;
229
+ rows.push(day);
230
+ }
231
+ flush();
232
+ return buckets;
233
+ }
234
+ function combineRows(from, to, granularity, rows) {
235
+ let views = 0;
236
+ let visitors = 0;
237
+ let downloads = 0;
238
+ for (const row of rows){
239
+ views += row.views;
240
+ visitors += row.visitors;
241
+ downloads += row.downloads;
242
+ }
243
+ return {
244
+ from,
245
+ to,
246
+ granularity,
247
+ dayCount: rows.length,
248
+ views,
249
+ visitors,
250
+ downloads
251
+ };
252
+ }
253
+ export { AnalyticsTimeseries, bucketAnalyticsTimeseries, buildAnalyticsColumns, resolveAnalyticsChartGranularity };
@@ -0,0 +1,17 @@
1
+ import "./timeseries_module.css";
2
+ const timeseries_module = {
3
+ root: "root-L2qL4D",
4
+ chart: "chart-giYTrx",
5
+ gridline: "gridline-R_wAwP",
6
+ column: "column-_Cw2Ua",
7
+ views: "views-WHbm44",
8
+ columnActive: "columnActive-CwHwbo",
9
+ visitors: "visitors-n6IL4B",
10
+ hit: "hit-ZxDHvk",
11
+ axis: "axis-fBshTw",
12
+ readout: "readout-L9jEpz",
13
+ swatchViews: "swatchViews-OCeKNe",
14
+ swatchVisitors: "swatchVisitors-O3BWz6",
15
+ swatchDownloads: "swatchDownloads-SVo9mN"
16
+ };
17
+ export default timeseries_module;
@@ -0,0 +1,126 @@
1
+ .root-L2qL4D {
2
+ border-radius: .25rem;
3
+ flex-direction: column;
4
+ gap: .5rem;
5
+ display: flex;
6
+ }
7
+
8
+ .root-L2qL4D:focus-visible {
9
+ outline: 2px solid var(--primary-500);
10
+ outline-offset: 4px;
11
+ }
12
+
13
+ :is(.chart-giYTrx, .byline-analytics-chart) {
14
+ width: 100%;
15
+ height: 11rem;
16
+ display: block;
17
+ }
18
+
19
+ .gridline-R_wAwP {
20
+ stroke: var(--gray-200);
21
+ stroke-width: 1px;
22
+ }
23
+
24
+ .column-_Cw2Ua {
25
+ transition: opacity .12s;
26
+ }
27
+
28
+ .views-WHbm44 {
29
+ fill: #38bdf8;
30
+ opacity: .38;
31
+ transition: opacity .12s;
32
+ }
33
+
34
+ .columnActive-CwHwbo .views-WHbm44 {
35
+ opacity: .62;
36
+ }
37
+
38
+ .visitors-n6IL4B {
39
+ fill: #7c3aed;
40
+ opacity: .95;
41
+ }
42
+
43
+ .hit-ZxDHvk {
44
+ fill: #0000;
45
+ cursor: crosshair;
46
+ }
47
+
48
+ :is(.axis-fBshTw, .byline-analytics-axis) {
49
+ font-variant-numeric: tabular-nums;
50
+ letter-spacing: .04em;
51
+ color: var(--gray-500);
52
+ justify-content: space-between;
53
+ font-size: .6875rem;
54
+ display: flex;
55
+ }
56
+
57
+ :is(.readout-L9jEpz, .byline-analytics-readout) {
58
+ font-variant-numeric: tabular-nums;
59
+ min-height: 1.25rem;
60
+ color: var(--gray-500);
61
+ flex-wrap: wrap;
62
+ align-items: center;
63
+ gap: .35rem .75rem;
64
+ margin: 0;
65
+ font-size: .75rem;
66
+ display: flex;
67
+ }
68
+
69
+ .readout-L9jEpz strong {
70
+ color: inherit;
71
+ font-weight: 600;
72
+ }
73
+
74
+ .swatchViews-OCeKNe, .swatchVisitors-O3BWz6, .swatchDownloads-SVo9mN {
75
+ border-radius: 1px;
76
+ width: .5rem;
77
+ height: .5rem;
78
+ margin-inline: .4rem -.4rem;
79
+ display: inline-block;
80
+ }
81
+
82
+ .swatchViews-OCeKNe {
83
+ opacity: .55;
84
+ background-color: #38bdf8;
85
+ }
86
+
87
+ .swatchVisitors-O3BWz6 {
88
+ background-color: #7c3aed;
89
+ }
90
+
91
+ .swatchDownloads-SVo9mN {
92
+ background-color: #10b981;
93
+ }
94
+
95
+ :is([data-theme="dark"], .dark) .gridline-R_wAwP {
96
+ stroke: var(--gray-700);
97
+ }
98
+
99
+ :is(:is([data-theme="dark"], .dark) .axis-fBshTw, :is([data-theme="dark"], .dark) .byline-analytics-axis, :is([data-theme="dark"], .dark) .readout-L9jEpz, :is([data-theme="dark"], .dark) .byline-analytics-readout) {
100
+ color: var(--gray-400);
101
+ }
102
+
103
+ :is([data-theme="dark"], .dark) .visitors-n6IL4B {
104
+ fill: #a78bfa;
105
+ }
106
+
107
+ :is([data-theme="dark"], .dark) .swatchVisitors-O3BWz6 {
108
+ background-color: #a78bfa;
109
+ }
110
+
111
+ :is([data-theme="dark"], .dark) .swatchDownloads-SVo9mN {
112
+ background-color: #34d399;
113
+ }
114
+
115
+ @media (prefers-reduced-motion: reduce) {
116
+ .column-_Cw2Ua, .views-WHbm44 {
117
+ transition: none;
118
+ }
119
+ }
120
+
121
+ @media (max-width: 720px) {
122
+ :is(.chart-giYTrx, .byline-analytics-chart) {
123
+ height: 9rem;
124
+ }
125
+ }
126
+
@@ -0,0 +1,9 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export { ANALYTICS_ABILITIES, type AnalyticsAbilityKey, registerAnalyticsAbilities, } from './abilities.js';
9
+ export type { AnalyticsDashboardData, AnalyticsDashboardPeriod } from './types.js';
@@ -0,0 +1 @@
1
+ export { ANALYTICS_ABILITIES, registerAnalyticsAbilities } from "./abilities.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { AnalyticsCountryTotal, AnalyticsDateRange, AnalyticsPathTotal, AnalyticsRankedTotals, AnalyticsReferrerTotal, AnalyticsReportCoverage, AnalyticsSummary } from '@byline/analytics';
9
+ import type { AnalyticsDashboardPeriod as PortableAnalyticsDashboardPeriod } from '@byline/analytics/config';
10
+ export type AnalyticsDashboardPeriod = PortableAnalyticsDashboardPeriod;
11
+ export interface AnalyticsDashboardData {
12
+ range: AnalyticsDateRange;
13
+ coverage: AnalyticsReportCoverage;
14
+ summary: AnalyticsSummary;
15
+ pages: AnalyticsRankedTotals<AnalyticsPathTotal>;
16
+ downloads: AnalyticsRankedTotals<AnalyticsPathTotal>;
17
+ referrers: AnalyticsRankedTotals<AnalyticsReferrerTotal>;
18
+ /** Countries are unbounded by a top-N limit, so there is nothing to truncate. */
19
+ countries: AnalyticsCountryTotal[];
20
+ }
@@ -0,0 +1 @@
1
+ export { };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/admin",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "4.15.0",
5
+ "version": "4.17.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -77,6 +77,16 @@
77
77
  "import": "./dist/modules/admin-preferences/index.js",
78
78
  "require": "./dist/modules/admin-preferences/index.js"
79
79
  },
80
+ "./analytics": {
81
+ "types": "./dist/modules/analytics/index.d.ts",
82
+ "import": "./dist/modules/analytics/index.js",
83
+ "require": "./dist/modules/analytics/index.js"
84
+ },
85
+ "./analytics/components/dashboard": {
86
+ "types": "./dist/modules/analytics/components/dashboard.d.ts",
87
+ "import": "./dist/modules/analytics/components/dashboard.js",
88
+ "require": "./dist/modules/analytics/components/dashboard.js"
89
+ },
80
90
  "./services": {
81
91
  "types": "./dist/services/admin-services-context.d.ts",
82
92
  "import": "./dist/services/admin-services-context.js",
@@ -169,32 +179,32 @@
169
179
  "@lezer/highlight": "^1.2.3",
170
180
  "@tanstack/react-form-start": "^1.33.5",
171
181
  "clsx": "^2.1.1",
172
- "jose": "^6.2.9",
182
+ "jose": "^6.2.10",
173
183
  "react-diff-viewer-continued": "^4.4.0",
174
184
  "uuid": "^14.0.2",
175
185
  "zod": "^4.4.3",
176
- "@byline/auth": "4.15.0",
177
- "@byline/core": "4.15.0",
178
- "@byline/ui": "4.15.0",
179
- "@byline/i18n": "4.15.0"
186
+ "@byline/auth": "4.17.0",
187
+ "@byline/i18n": "4.17.0",
188
+ "@byline/analytics": "4.17.0",
189
+ "@byline/analytics-agent": "4.17.0",
190
+ "@byline/core": "4.17.0",
191
+ "@byline/ui": "4.17.0"
180
192
  },
181
193
  "peerDependencies": {
182
194
  "react": "^19.0.0",
183
195
  "react-dom": "^19.0.0"
184
196
  },
185
197
  "devDependencies": {
186
- "@biomejs/biome": "2.5.9",
198
+ "@biomejs/biome": "2.5.10",
187
199
  "@rsbuild/plugin-react": "^2.1.0",
188
200
  "@rslib/core": "^0.23.2",
189
- "@types/node": "^26.2.0",
201
+ "@types/node": "^26.4.0",
190
202
  "@types/react": "19.2.18",
191
- "@types/react-dom": "19.2.4",
203
+ "@types/react-dom": "19.2.5",
192
204
  "@vitejs/plugin-react": "^6.1.0",
193
205
  "jsdom": "^30.0.1",
194
206
  "react": "^19.2.8",
195
207
  "react-dom": "^19.2.8",
196
- "rimraf": "^6.1.3",
197
- "tsx": "^4.23.12",
198
208
  "typescript": "^7.0.2",
199
209
  "typescript-plugin-css-modules": "^5.2.0",
200
210
  "vitest": "^4.1.11"
package/src/abilities.ts CHANGED
@@ -12,6 +12,7 @@ import { registerAdminActivityAbilities } from './modules/admin-activity/abiliti
12
12
  import { registerAdminPermissionsAbilities } from './modules/admin-permissions/abilities.js'
13
13
  import { registerAdminRolesAbilities } from './modules/admin-roles/abilities.js'
14
14
  import { registerAdminUsersAbilities } from './modules/admin-users/abilities.js'
15
+ import { registerAnalyticsAbilities } from './modules/analytics/abilities.js'
15
16
 
16
17
  /**
17
18
  * Register every ability contributed by the admin subsystem.
@@ -30,5 +31,6 @@ export function registerAdminAbilities(registry: AbilityRegistry): void {
30
31
  registerAdminRolesAbilities(registry)
31
32
  registerAdminPermissionsAbilities(registry)
32
33
  registerAdminActivityAbilities(registry)
34
+ registerAnalyticsAbilities(registry)
33
35
  // registerAccountAbilities(registry) — added when that module lands
34
36
  }
@@ -10,11 +10,11 @@ import { useState } from 'react'
10
10
 
11
11
  import type {
12
12
  CollectionAdminConfig,
13
- CollectionDefinition,
14
13
  RelationField as FieldType,
14
+ MultiCollectionDefinition,
15
15
  RelatedDocumentValue,
16
16
  } from '@byline/core'
17
- import { getCollectionAdminConfig, getCollectionDefinition } from '@byline/core'
17
+ import { getCollectionAdminConfig, getCollectionDefinition, isSingleton } from '@byline/core'
18
18
  import { useTranslation } from '@byline/i18n/react'
19
19
  import {
20
20
  Button,
@@ -80,7 +80,9 @@ export const RelationField = ({
80
80
  // config drives the picker-column rendering inside RelationSummary so
81
81
  // the selected tile matches the picker row exactly. Missing target →
82
82
  // render an inline error and disable the picker.
83
- const targetDef: CollectionDefinition | null = getCollectionDefinition(field.targetCollection)
83
+ const targetDefinition = getCollectionDefinition(field.targetCollection)
84
+ const targetDef: MultiCollectionDefinition | null =
85
+ targetDefinition != null && !isSingleton(targetDefinition) ? targetDefinition : null
84
86
  const targetAdminConfig: CollectionAdminConfig | null = getCollectionAdminConfig(
85
87
  field.targetCollection
86
88
  )
@@ -10,11 +10,11 @@ import { type ReactNode, useState } from 'react'
10
10
 
11
11
  import type {
12
12
  CollectionAdminConfig,
13
- CollectionDefinition,
14
13
  RelationField as FieldType,
14
+ MultiCollectionDefinition,
15
15
  RelatedDocumentValue,
16
16
  } from '@byline/core'
17
- import { getCollectionAdminConfig, getCollectionDefinition } from '@byline/core'
17
+ import { getCollectionAdminConfig, getCollectionDefinition, isSingleton } from '@byline/core'
18
18
  import { useTranslation } from '@byline/i18n/react'
19
19
  import {
20
20
  Button,
@@ -122,7 +122,9 @@ export const RelationManyField = ({ field, defaultValue, id, path }: RelationMan
122
122
  const fieldError = useFieldError(fieldPath)
123
123
  const { getFieldValue, setFieldValue } = useFormContext()
124
124
 
125
- const targetDef: CollectionDefinition | null = getCollectionDefinition(field.targetCollection)
125
+ const targetDefinition = getCollectionDefinition(field.targetCollection)
126
+ const targetDef: MultiCollectionDefinition | null =
127
+ targetDefinition != null && !isSingleton(targetDefinition) ? targetDefinition : null
126
128
  const targetAdminConfig: CollectionAdminConfig | null = getCollectionAdminConfig(
127
129
  field.targetCollection
128
130
  )
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { useCallback, useEffect, useState } from 'react'
10
10
 
11
- import type { CollectionAdminConfig, CollectionDefinition } from '@byline/core'
11
+ import type { CollectionAdminConfig, MultiCollectionDefinition } from '@byline/core'
12
12
  import { getCollectionAdminConfig } from '@byline/core'
13
13
  import { useTranslation } from '@byline/i18n/react'
14
14
  import { Button, CheckIcon, LoaderRing, Modal, Search } from '@byline/ui/react'
@@ -57,7 +57,7 @@ interface RelationPickerBaseProps {
57
57
  /** The target collection path (e.g. `'media'`). */
58
58
  targetCollectionPath: string
59
59
  /** The target collection definition (used for labels + displayField fallback). */
60
- targetDefinition?: CollectionDefinition | null
60
+ targetDefinition?: MultiCollectionDefinition | null
61
61
  /** Explicit display field to render as row label. */
62
62
  displayField?: string
63
63
  /**
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
 
9
- import type { CollectionAdminConfig, CollectionDefinition } from '@byline/core'
9
+ import type { CollectionAdminConfig, MultiCollectionDefinition } from '@byline/core'
10
10
  import cx from 'clsx'
11
11
 
12
12
  import { PickerCell, resolveFallbackDisplayField, resolveRowLabel } from './relation-display'
@@ -34,7 +34,7 @@ import styles from './relation-summary.module.css'
34
34
  // ---------------------------------------------------------------------------
35
35
 
36
36
  interface RelationSummaryProps {
37
- targetDefinition: CollectionDefinition
37
+ targetDefinition: MultiCollectionDefinition
38
38
  targetAdminConfig: CollectionAdminConfig | null
39
39
  displayField?: string
40
40
  /** The raw relation value from the form. May be a plain ref or a populated envelope. */