@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.
- package/dist/abilities.js +2 -0
- package/dist/fields/relation/relation-field.js +3 -2
- package/dist/fields/relation/relation-many-field.js +3 -2
- package/dist/fields/relation/relation-picker.d.ts +2 -2
- package/dist/fields/relation/relation-summary.d.ts +2 -2
- package/dist/forms/document-actions.js +30 -25
- package/dist/forms/form-renderer.d.ts +17 -3
- package/dist/forms/form-renderer.js +18 -9
- package/dist/forms/tree-placement-widget.js +3 -2
- package/dist/forms/use-form-layout.d.ts +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/modules/analytics/abilities.d.ts +14 -0
- package/dist/modules/analytics/abilities.js +21 -0
- package/dist/modules/analytics/components/dashboard.d.ts +20 -0
- package/dist/modules/analytics/components/dashboard.js +308 -0
- package/dist/modules/analytics/components/dashboard.module.js +29 -0
- package/dist/modules/analytics/components/dashboard.test.node.d.ts +8 -0
- package/dist/modules/analytics/components/dashboard_module.css +268 -0
- package/dist/modules/analytics/components/timeseries.d.ts +59 -0
- package/dist/modules/analytics/components/timeseries.js +253 -0
- package/dist/modules/analytics/components/timeseries.module.js +17 -0
- package/dist/modules/analytics/components/timeseries_module.css +126 -0
- package/dist/modules/analytics/index.d.ts +9 -0
- package/dist/modules/analytics/index.js +1 -0
- package/dist/modules/analytics/types.d.ts +20 -0
- package/dist/modules/analytics/types.js +1 -0
- package/package.json +21 -11
- package/src/abilities.ts +2 -0
- package/src/fields/relation/relation-field.tsx +5 -3
- package/src/fields/relation/relation-many-field.tsx +5 -3
- package/src/fields/relation/relation-picker.tsx +2 -2
- package/src/fields/relation/relation-summary.tsx +2 -2
- package/src/forms/document-actions.test.tsx +157 -0
- package/src/forms/document-actions.tsx +93 -74
- package/src/forms/form-renderer-capabilities.test.tsx +155 -0
- package/src/forms/form-renderer-submit.test.tsx +194 -0
- package/src/forms/form-renderer.tsx +59 -25
- package/src/forms/tree-placement-widget.tsx +3 -2
- package/src/forms/use-form-layout.ts +3 -3
- package/src/index.ts +1 -0
- package/src/modules/analytics/abilities.ts +33 -0
- package/src/modules/analytics/components/dashboard.module.css +330 -0
- package/src/modules/analytics/components/dashboard.test.node.ts +193 -0
- package/src/modules/analytics/components/dashboard.tsx +371 -0
- package/src/modules/analytics/components/timeseries.module.css +151 -0
- package/src/modules/analytics/components/timeseries.tsx +332 -0
- package/src/modules/analytics/index.ts +14 -0
- package/src/modules/analytics/types.ts +31 -0
- package/dist/forms/__probe-nested.test.node.d.ts +0 -1
- 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.
|
|
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.
|
|
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.
|
|
177
|
-
"@byline/
|
|
178
|
-
"@byline/
|
|
179
|
-
"@byline/
|
|
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.
|
|
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.
|
|
201
|
+
"@types/node": "^26.4.0",
|
|
190
202
|
"@types/react": "19.2.18",
|
|
191
|
-
"@types/react-dom": "19.2.
|
|
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
|
|
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
|
|
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,
|
|
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?:
|
|
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,
|
|
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:
|
|
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. */
|