@richardmcquiston01/dashboard-widgets-toolkit 0.4.1 → 0.6.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/README.md +3 -370
- package/dist/core.cjs +42 -9
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +41 -10
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +42 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +41 -10
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +589 -470
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +19 -9
- package/dist/react.d.ts +19 -9
- package/dist/react.js +589 -470
- package/dist/react.js.map +1 -1
- package/dist/{scale-dWDGE1zW.d.cts → scale-D51ZGIF4.d.cts} +30 -1
- package/dist/{scale-dWDGE1zW.d.ts → scale-D51ZGIF4.d.ts} +30 -1
- package/dist/styles.css +16 -0
- package/package.json +1 -1
package/dist/react.cjs
CHANGED
|
@@ -5,80 +5,6 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
5
5
|
|
|
6
6
|
// src/react/card.tsx
|
|
7
7
|
|
|
8
|
-
// src/core/grid.ts
|
|
9
|
-
var WIDTH_COLUMNS = 12;
|
|
10
|
-
var MIN_WIDGET_WIDTH = 2;
|
|
11
|
-
var MAX_WIDGET_WIDTH = 12;
|
|
12
|
-
function isWidgetWidth(value) {
|
|
13
|
-
return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
|
|
14
|
-
}
|
|
15
|
-
function widthForSize(size) {
|
|
16
|
-
switch (size) {
|
|
17
|
-
case "small":
|
|
18
|
-
return 3;
|
|
19
|
-
case "large":
|
|
20
|
-
return 6;
|
|
21
|
-
case "full":
|
|
22
|
-
return 12;
|
|
23
|
-
default:
|
|
24
|
-
return 4;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function itemWidth(item) {
|
|
28
|
-
return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
|
|
29
|
-
}
|
|
30
|
-
function usesWidthColumns(items) {
|
|
31
|
-
return items.some((item) => item.width !== void 0);
|
|
32
|
-
}
|
|
33
|
-
function fillsWidth(fill) {
|
|
34
|
-
return fill === "width" || fill === "both";
|
|
35
|
-
}
|
|
36
|
-
function fillsHeight(fill) {
|
|
37
|
-
return fill === "height" || fill === "both";
|
|
38
|
-
}
|
|
39
|
-
function baseColumnSpan(size, columns, largeSpan = 2) {
|
|
40
|
-
if (size === "full") return columns;
|
|
41
|
-
if (size === "large") return Math.min(largeSpan, columns);
|
|
42
|
-
return 1;
|
|
43
|
-
}
|
|
44
|
-
function fillColumnSpans(items, columns, largeSpan = 2) {
|
|
45
|
-
return placeAndFill(
|
|
46
|
-
items,
|
|
47
|
-
columns,
|
|
48
|
-
(item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
function fillWidthSpans(items) {
|
|
52
|
-
return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
|
|
53
|
-
}
|
|
54
|
-
function placeAndFill(items, columns, spanOf) {
|
|
55
|
-
const trackCount = Math.max(1, Math.floor(columns));
|
|
56
|
-
const spans = /* @__PURE__ */ new Map();
|
|
57
|
-
let row = [];
|
|
58
|
-
let used = 0;
|
|
59
|
-
function closeRow() {
|
|
60
|
-
const fillers = row.filter(({ item }) => fillsWidth(item.fill));
|
|
61
|
-
const leftover = trackCount - used;
|
|
62
|
-
if (fillers.length > 0) {
|
|
63
|
-
const share = Math.floor(leftover / fillers.length);
|
|
64
|
-
const remainder = leftover % fillers.length;
|
|
65
|
-
fillers.forEach(({ item, span }, index) => {
|
|
66
|
-
spans.set(item.key, span + share + (index < remainder ? 1 : 0));
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
row = [];
|
|
70
|
-
used = 0;
|
|
71
|
-
}
|
|
72
|
-
for (const item of items) {
|
|
73
|
-
const span = Math.min(spanOf(item, trackCount), trackCount);
|
|
74
|
-
if (used + span > trackCount && row.length > 0) closeRow();
|
|
75
|
-
row.push({ item, span });
|
|
76
|
-
used += span;
|
|
77
|
-
}
|
|
78
|
-
closeRow();
|
|
79
|
-
return spans;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
8
|
// src/core/format.ts
|
|
83
9
|
var DEFAULT_CURRENCY = "USD";
|
|
84
10
|
function localeArgument(locale) {
|
|
@@ -142,23 +68,393 @@ function formatPercentChange(change, locale) {
|
|
|
142
68
|
signDisplay: "exceptZero"
|
|
143
69
|
}).format(rounded / 100);
|
|
144
70
|
}
|
|
145
|
-
var DEFAULT_COMPARISON_LABEL = "previous period";
|
|
146
|
-
function kpiDelta(current, previous, options = {}) {
|
|
147
|
-
const percent = percentChange(current, previous);
|
|
148
|
-
if (percent === null) {
|
|
149
|
-
return null;
|
|
71
|
+
var DEFAULT_COMPARISON_LABEL = "previous period";
|
|
72
|
+
function kpiDelta(current, previous, options = {}) {
|
|
73
|
+
const percent = percentChange(current, previous);
|
|
74
|
+
if (percent === null) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const text = formatPercentChange(percent, options.locale);
|
|
78
|
+
const direction = text === "\xB10%" ? "flat" : percent > 0 ? "up" : "down";
|
|
79
|
+
const higherIsBetter = options.higherIsBetter ?? true;
|
|
80
|
+
const sentiment = direction === "flat" ? "neutral" : direction === "up" === higherIsBetter ? "good" : "bad";
|
|
81
|
+
return {
|
|
82
|
+
percent,
|
|
83
|
+
direction,
|
|
84
|
+
sentiment,
|
|
85
|
+
text,
|
|
86
|
+
description: `${text} vs ${options.comparisonLabel ?? DEFAULT_COMPARISON_LABEL}`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/core/result.ts
|
|
91
|
+
function ok(value) {
|
|
92
|
+
return { ok: true, value };
|
|
93
|
+
}
|
|
94
|
+
function err(error) {
|
|
95
|
+
return { ok: false, error };
|
|
96
|
+
}
|
|
97
|
+
function describeUnknownError(cause) {
|
|
98
|
+
if (cause instanceof Error) {
|
|
99
|
+
return cause.message === "" ? cause.name : cause.message;
|
|
100
|
+
}
|
|
101
|
+
if (typeof cause === "string") {
|
|
102
|
+
return cause;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const json = JSON.stringify(cause);
|
|
106
|
+
return json ?? String(cause);
|
|
107
|
+
} catch {
|
|
108
|
+
return String(cause);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/core/url.ts
|
|
113
|
+
var SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
|
|
114
|
+
var IGNORED_IN_SCHEME = /[\u0000- \u007f]/g;
|
|
115
|
+
function schemeOf(url) {
|
|
116
|
+
const match = SCHEME_PATTERN.exec(
|
|
117
|
+
url.replace(IGNORED_IN_SCHEME, "")
|
|
118
|
+
);
|
|
119
|
+
return match === null ? null : (match[1] ?? "").toLowerCase();
|
|
120
|
+
}
|
|
121
|
+
function isSafeHref(url) {
|
|
122
|
+
if (url.trim() === "") {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const scheme = schemeOf(url);
|
|
126
|
+
return scheme === null || scheme === "http" || scheme === "https" || scheme === "mailto";
|
|
127
|
+
}
|
|
128
|
+
function isSafeImageUrl(url) {
|
|
129
|
+
if (url.trim() === "") {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const scheme = schemeOf(url);
|
|
133
|
+
return scheme === null || scheme === "http" || scheme === "https";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/core/detail.ts
|
|
137
|
+
var DEFAULT_DETAIL_PAGE_SIZE = 25;
|
|
138
|
+
function resolveDetailOptions(definition) {
|
|
139
|
+
const detail = definition.detail;
|
|
140
|
+
if (detail === void 0 || detail === false) return void 0;
|
|
141
|
+
const options = detail === true ? {} : detail;
|
|
142
|
+
return {
|
|
143
|
+
title: options.title ?? definition.title,
|
|
144
|
+
pageSize: options.pageSize ?? DEFAULT_DETAIL_PAGE_SIZE,
|
|
145
|
+
mode: options.mode ?? "client"
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function resolveTableControls(definition) {
|
|
149
|
+
const setting = definition.tableControls;
|
|
150
|
+
if (setting === void 0 || setting === false) return void 0;
|
|
151
|
+
const options = setting === true ? {} : setting;
|
|
152
|
+
const resolved = {
|
|
153
|
+
search: options.search ?? true,
|
|
154
|
+
sort: options.sort ?? true
|
|
155
|
+
};
|
|
156
|
+
return resolved.search || resolved.sort ? resolved : void 0;
|
|
157
|
+
}
|
|
158
|
+
function defaultDetailQuery(pageSize = DEFAULT_DETAIL_PAGE_SIZE) {
|
|
159
|
+
return { page: 1, pageSize };
|
|
160
|
+
}
|
|
161
|
+
function fold(text) {
|
|
162
|
+
return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
163
|
+
}
|
|
164
|
+
function cellValue(cell) {
|
|
165
|
+
return cell.value ?? cell.text;
|
|
166
|
+
}
|
|
167
|
+
function cellMatches(cell, needle) {
|
|
168
|
+
return fold(String(cellValue(cell))).includes(needle);
|
|
169
|
+
}
|
|
170
|
+
function compareCells(a, b, collator) {
|
|
171
|
+
const left = cellValue(a);
|
|
172
|
+
const right = cellValue(b);
|
|
173
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
174
|
+
return left - right;
|
|
175
|
+
}
|
|
176
|
+
if (typeof left === "number") return -1;
|
|
177
|
+
if (typeof right === "number") return 1;
|
|
178
|
+
return collator.compare(left, right);
|
|
179
|
+
}
|
|
180
|
+
function queryRows(data, query, locale) {
|
|
181
|
+
const indexOf = new Map(
|
|
182
|
+
data.columns.map((column, index) => [column.key, index])
|
|
183
|
+
);
|
|
184
|
+
const filterEntries = [];
|
|
185
|
+
for (const [key, text] of Object.entries(query.filters ?? {})) {
|
|
186
|
+
const index = indexOf.get(key);
|
|
187
|
+
if (index === void 0) {
|
|
188
|
+
return err(
|
|
189
|
+
`Detail query: filter column "${key}" does not exist. Columns: ${listKeys(data)}.`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const needle = fold(text.trim());
|
|
193
|
+
if (needle !== "") filterEntries.push({ index, needle });
|
|
194
|
+
}
|
|
195
|
+
let sortIndex = -1;
|
|
196
|
+
if (query.sort !== void 0) {
|
|
197
|
+
const found = indexOf.get(query.sort.column);
|
|
198
|
+
const column = found === void 0 ? void 0 : data.columns[found];
|
|
199
|
+
if (found === void 0 || column === void 0) {
|
|
200
|
+
return err(
|
|
201
|
+
`Detail query: sort column "${query.sort.column}" does not exist. Columns: ${listKeys(data)}.`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (column.sortable === false) {
|
|
205
|
+
return err(
|
|
206
|
+
`Detail query: column "${column.key}" is not sortable (sortable: false).`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
sortIndex = found;
|
|
210
|
+
}
|
|
211
|
+
const search = fold((query.search ?? "").trim());
|
|
212
|
+
let rows = data.rows.filter(
|
|
213
|
+
(row) => filterEntries.every(({ index, needle }) => {
|
|
214
|
+
const cell = row[index];
|
|
215
|
+
return cell !== void 0 && cellMatches(cell, needle);
|
|
216
|
+
}) && (search === "" || row.some((cell) => cellMatches(cell, search)))
|
|
217
|
+
);
|
|
218
|
+
if (query.sort !== void 0 && sortIndex >= 0) {
|
|
219
|
+
const collator = new Intl.Collator(locale, {
|
|
220
|
+
numeric: true,
|
|
221
|
+
sensitivity: "base"
|
|
222
|
+
});
|
|
223
|
+
const sign = query.sort.direction === "desc" ? -1 : 1;
|
|
224
|
+
rows = rows.map((row, position) => ({ row, position })).sort((a, b) => {
|
|
225
|
+
const left = a.row[sortIndex];
|
|
226
|
+
const right = b.row[sortIndex];
|
|
227
|
+
const order = left === void 0 || right === void 0 ? 0 : sign * compareCells(left, right, collator);
|
|
228
|
+
return order || a.position - b.position;
|
|
229
|
+
}).map(({ row }) => row);
|
|
230
|
+
}
|
|
231
|
+
const pageSize = Math.max(1, Math.floor(query.pageSize));
|
|
232
|
+
const pageCount = Math.max(1, Math.ceil(rows.length / pageSize));
|
|
233
|
+
const page = Math.min(Math.max(1, Math.floor(query.page)), pageCount);
|
|
234
|
+
return ok({
|
|
235
|
+
rows: rows.slice((page - 1) * pageSize, page * pageSize),
|
|
236
|
+
totalRows: rows.length,
|
|
237
|
+
page,
|
|
238
|
+
pageCount
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function listKeys(data) {
|
|
242
|
+
return data.columns.map((column) => `"${column.key}"`).join(", ");
|
|
243
|
+
}
|
|
244
|
+
function describeType(value) {
|
|
245
|
+
if (value === null) return "null";
|
|
246
|
+
if (Array.isArray(value)) return "array";
|
|
247
|
+
return typeof value;
|
|
248
|
+
}
|
|
249
|
+
function validateDetailData(data, subject = "Detail data") {
|
|
250
|
+
const problems = [];
|
|
251
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
252
|
+
return err(`${subject}: must be an object, got ${describeType(data)}.`);
|
|
253
|
+
}
|
|
254
|
+
const record = data;
|
|
255
|
+
const columns = record["columns"];
|
|
256
|
+
const rows = record["rows"];
|
|
257
|
+
const keys = /* @__PURE__ */ new Set();
|
|
258
|
+
let columnCount = 0;
|
|
259
|
+
if (!Array.isArray(columns) || columns.length === 0) {
|
|
260
|
+
problems.push(
|
|
261
|
+
`columns must be a non-empty array, got ${describeType(columns)}.`
|
|
262
|
+
);
|
|
263
|
+
} else {
|
|
264
|
+
columnCount = columns.length;
|
|
265
|
+
columns.forEach((column, index) => {
|
|
266
|
+
const at = `columns[${index}]`;
|
|
267
|
+
if (typeof column !== "object" || column === null) {
|
|
268
|
+
problems.push(`${at} must be an object, got ${describeType(column)}.`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const fields = column;
|
|
272
|
+
const key = fields["key"];
|
|
273
|
+
if (typeof key !== "string" || key === "") {
|
|
274
|
+
problems.push(`${at}.key must be a non-empty string.`);
|
|
275
|
+
} else if (keys.has(key)) {
|
|
276
|
+
problems.push(`${at}.key "${key}" is used by an earlier column.`);
|
|
277
|
+
} else {
|
|
278
|
+
keys.add(key);
|
|
279
|
+
}
|
|
280
|
+
if (typeof fields["label"] !== "string") {
|
|
281
|
+
problems.push(
|
|
282
|
+
`${at}.label must be a string, got ${describeType(fields["label"])}.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
for (const flag of ["numeric", "sortable", "filterable"]) {
|
|
286
|
+
const flagValue = fields[flag];
|
|
287
|
+
if (flagValue !== void 0 && typeof flagValue !== "boolean") {
|
|
288
|
+
problems.push(
|
|
289
|
+
`${at}.${flag} must be a boolean, got ${describeType(flagValue)}.`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (!Array.isArray(rows)) {
|
|
296
|
+
problems.push(`rows must be an array, got ${describeType(rows)}.`);
|
|
297
|
+
} else {
|
|
298
|
+
rows.forEach((row, rowIndex) => {
|
|
299
|
+
if (!Array.isArray(row)) {
|
|
300
|
+
problems.push(
|
|
301
|
+
`rows[${rowIndex}] must be an array of cells, got ${describeType(row)}.`
|
|
302
|
+
);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (columnCount > 0 && row.length !== columnCount) {
|
|
306
|
+
problems.push(
|
|
307
|
+
`rows[${rowIndex}] has ${row.length} cells but there are ${columnCount} columns.`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
row.forEach((cell, cellIndex) => {
|
|
311
|
+
const at = `rows[${rowIndex}][${cellIndex}]`;
|
|
312
|
+
if (typeof cell !== "object" || cell === null) {
|
|
313
|
+
problems.push(`${at} must be an object, got ${describeType(cell)}.`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const fields = cell;
|
|
317
|
+
if (typeof fields["text"] !== "string") {
|
|
318
|
+
problems.push(
|
|
319
|
+
`${at}.text must be a string, got ${describeType(fields["text"])}.`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const value = fields["value"];
|
|
323
|
+
if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
|
|
324
|
+
problems.push(
|
|
325
|
+
`${at}.value must be a string or a finite number, got ${describeType(value)}.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const href = fields["href"];
|
|
329
|
+
if (href !== void 0) {
|
|
330
|
+
if (typeof href !== "string") {
|
|
331
|
+
problems.push(
|
|
332
|
+
`${at}.href must be a string, got ${describeType(href)}.`
|
|
333
|
+
);
|
|
334
|
+
} else if (!isSafeHref(href)) {
|
|
335
|
+
problems.push(
|
|
336
|
+
`${at}.href "${href}" is not allowed (only http, https, mailto and relative URLs).`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const totalRows = record["totalRows"];
|
|
344
|
+
if (totalRows !== void 0 && !(typeof totalRows === "number" && Number.isInteger(totalRows) && totalRows >= 0)) {
|
|
345
|
+
problems.push(
|
|
346
|
+
`totalRows must be a non-negative whole number, got ${describeType(totalRows)}.`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (problems.length > 0) {
|
|
350
|
+
return err(`${subject}: ${problems.join(" ")}`);
|
|
351
|
+
}
|
|
352
|
+
return ok(data);
|
|
353
|
+
}
|
|
354
|
+
function tableDetailData(data) {
|
|
355
|
+
return {
|
|
356
|
+
columns: data.columns.map((column, index) => ({
|
|
357
|
+
key: `c${index}`,
|
|
358
|
+
label: column.label,
|
|
359
|
+
...column.numeric === void 0 ? {} : { numeric: column.numeric }
|
|
360
|
+
})),
|
|
361
|
+
rows: data.rows
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function deriveDetailData(data, locale) {
|
|
365
|
+
if (data.kind === "TABLE") {
|
|
366
|
+
return data.footer === void 0 ? tableDetailData(data) : void 0;
|
|
367
|
+
}
|
|
368
|
+
if (data.kind === "BAR_LIST") {
|
|
369
|
+
return {
|
|
370
|
+
columns: [
|
|
371
|
+
{ key: "label", label: "Name" },
|
|
372
|
+
{ key: "value", label: "Value", numeric: true }
|
|
373
|
+
],
|
|
374
|
+
rows: data.items.map((item) => [
|
|
375
|
+
{ text: item.label },
|
|
376
|
+
{
|
|
377
|
+
text: item.display ?? formatValue(item.value, "number", { locale }),
|
|
378
|
+
value: item.value
|
|
379
|
+
}
|
|
380
|
+
])
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return void 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/core/grid.ts
|
|
387
|
+
var WIDTH_COLUMNS = 12;
|
|
388
|
+
var MIN_WIDGET_WIDTH = 2;
|
|
389
|
+
var MAX_WIDGET_WIDTH = 12;
|
|
390
|
+
function isWidgetWidth(value) {
|
|
391
|
+
return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
|
|
392
|
+
}
|
|
393
|
+
function widthForSize(size) {
|
|
394
|
+
switch (size) {
|
|
395
|
+
case "small":
|
|
396
|
+
return 3;
|
|
397
|
+
case "large":
|
|
398
|
+
return 6;
|
|
399
|
+
case "full":
|
|
400
|
+
return 12;
|
|
401
|
+
default:
|
|
402
|
+
return 4;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function itemWidth(item) {
|
|
406
|
+
return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
|
|
407
|
+
}
|
|
408
|
+
function usesWidthColumns(items) {
|
|
409
|
+
return items.some((item) => item.width !== void 0);
|
|
410
|
+
}
|
|
411
|
+
function fillsWidth(fill) {
|
|
412
|
+
return fill === "width" || fill === "both";
|
|
413
|
+
}
|
|
414
|
+
function fillsHeight(fill) {
|
|
415
|
+
return fill === "height" || fill === "both";
|
|
416
|
+
}
|
|
417
|
+
function baseColumnSpan(size, columns, largeSpan = 2) {
|
|
418
|
+
if (size === "full") return columns;
|
|
419
|
+
if (size === "large") return Math.min(largeSpan, columns);
|
|
420
|
+
return 1;
|
|
421
|
+
}
|
|
422
|
+
function fillColumnSpans(items, columns, largeSpan = 2) {
|
|
423
|
+
return placeAndFill(
|
|
424
|
+
items,
|
|
425
|
+
columns,
|
|
426
|
+
(item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
function fillWidthSpans(items) {
|
|
430
|
+
return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
|
|
431
|
+
}
|
|
432
|
+
function placeAndFill(items, columns, spanOf) {
|
|
433
|
+
const trackCount = Math.max(1, Math.floor(columns));
|
|
434
|
+
const spans = /* @__PURE__ */ new Map();
|
|
435
|
+
let row = [];
|
|
436
|
+
let used = 0;
|
|
437
|
+
function closeRow() {
|
|
438
|
+
const fillers = row.filter(({ item }) => fillsWidth(item.fill));
|
|
439
|
+
const leftover = trackCount - used;
|
|
440
|
+
if (fillers.length > 0) {
|
|
441
|
+
const share = Math.floor(leftover / fillers.length);
|
|
442
|
+
const remainder = leftover % fillers.length;
|
|
443
|
+
fillers.forEach(({ item, span }, index) => {
|
|
444
|
+
spans.set(item.key, span + share + (index < remainder ? 1 : 0));
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
row = [];
|
|
448
|
+
used = 0;
|
|
150
449
|
}
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
text,
|
|
160
|
-
description: `${text} vs ${options.comparisonLabel ?? DEFAULT_COMPARISON_LABEL}`
|
|
161
|
-
};
|
|
450
|
+
for (const item of items) {
|
|
451
|
+
const span = Math.min(spanOf(item, trackCount), trackCount);
|
|
452
|
+
if (used + span > trackCount && row.length > 0) closeRow();
|
|
453
|
+
row.push({ item, span });
|
|
454
|
+
used += span;
|
|
455
|
+
}
|
|
456
|
+
closeRow();
|
|
457
|
+
return spans;
|
|
162
458
|
}
|
|
163
459
|
|
|
164
460
|
// src/core/scale.ts
|
|
@@ -730,30 +1026,6 @@ function GraphWidget({
|
|
|
730
1026
|
}
|
|
731
1027
|
);
|
|
732
1028
|
}
|
|
733
|
-
|
|
734
|
-
// src/core/url.ts
|
|
735
|
-
var SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
|
|
736
|
-
var IGNORED_IN_SCHEME = /[\u0000- \u007f]/g;
|
|
737
|
-
function schemeOf(url) {
|
|
738
|
-
const match = SCHEME_PATTERN.exec(
|
|
739
|
-
url.replace(IGNORED_IN_SCHEME, "")
|
|
740
|
-
);
|
|
741
|
-
return match === null ? null : (match[1] ?? "").toLowerCase();
|
|
742
|
-
}
|
|
743
|
-
function isSafeHref(url) {
|
|
744
|
-
if (url.trim() === "") {
|
|
745
|
-
return false;
|
|
746
|
-
}
|
|
747
|
-
const scheme = schemeOf(url);
|
|
748
|
-
return scheme === null || scheme === "http" || scheme === "https" || scheme === "mailto";
|
|
749
|
-
}
|
|
750
|
-
function isSafeImageUrl(url) {
|
|
751
|
-
if (url.trim() === "") {
|
|
752
|
-
return false;
|
|
753
|
-
}
|
|
754
|
-
const scheme = schemeOf(url);
|
|
755
|
-
return scheme === null || scheme === "http" || scheme === "https";
|
|
756
|
-
}
|
|
757
1029
|
function isNewTabTarget(target) {
|
|
758
1030
|
return target !== void 0 && target !== "" && target !== "_self";
|
|
759
1031
|
}
|
|
@@ -916,29 +1188,120 @@ function GaugeWidget({
|
|
|
916
1188
|
] });
|
|
917
1189
|
}
|
|
918
1190
|
function TableWidget({
|
|
919
|
-
data
|
|
1191
|
+
data,
|
|
1192
|
+
controls
|
|
1193
|
+
}) {
|
|
1194
|
+
if (controls === void 0) {
|
|
1195
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-table-wrap", children: [
|
|
1196
|
+
/* @__PURE__ */ jsxRuntime.jsx(TableView, { data }),
|
|
1197
|
+
data.footer !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "dwt-note", children: data.footer })
|
|
1198
|
+
] });
|
|
1199
|
+
}
|
|
1200
|
+
return /* @__PURE__ */ jsxRuntime.jsx(InteractiveTable, { data, controls });
|
|
1201
|
+
}
|
|
1202
|
+
function TableView({
|
|
1203
|
+
data,
|
|
1204
|
+
rows = data.rows,
|
|
1205
|
+
sort,
|
|
1206
|
+
onSort
|
|
920
1207
|
}) {
|
|
921
1208
|
const slot = useSlotClassName();
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1209
|
+
const { labels } = useWidgetSettings();
|
|
1210
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: slot("table", "dwt-table"), children: [
|
|
1211
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: data.columns.map((column, index) => {
|
|
1212
|
+
const key = `c${index}`;
|
|
1213
|
+
const sorted = sort?.column === key;
|
|
1214
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
925
1215
|
"th",
|
|
926
1216
|
{
|
|
927
1217
|
scope: "col",
|
|
928
1218
|
className: column.numeric === true ? "dwt-numeric" : void 0,
|
|
929
|
-
|
|
1219
|
+
"aria-sort": sorted ? sort?.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
1220
|
+
children: onSort === void 0 ? column.label : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1221
|
+
"button",
|
|
1222
|
+
{
|
|
1223
|
+
type: "button",
|
|
1224
|
+
className: "dwt-detail-sort",
|
|
1225
|
+
"aria-label": labels.sortBy(column.label),
|
|
1226
|
+
onClick: () => onSort(key),
|
|
1227
|
+
children: [
|
|
1228
|
+
column.label,
|
|
1229
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: !sorted ? " \u2195" : sort?.direction === "asc" ? " \u2191" : " \u2193" })
|
|
1230
|
+
]
|
|
1231
|
+
}
|
|
1232
|
+
)
|
|
930
1233
|
},
|
|
931
1234
|
index
|
|
932
|
-
)
|
|
933
|
-
|
|
934
|
-
|
|
1235
|
+
);
|
|
1236
|
+
}) }) }),
|
|
1237
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: rows.map((row, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx("tr", { children: row.map((cell, cellIndex) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1238
|
+
"td",
|
|
1239
|
+
{
|
|
1240
|
+
className: data.columns[cellIndex]?.numeric === true ? "dwt-numeric" : void 0,
|
|
1241
|
+
children: cell.href === void 0 ? cell.text : /* @__PURE__ */ jsxRuntime.jsx(WidgetLink, { href: cell.href, className: "dwt-link", children: cell.text })
|
|
1242
|
+
},
|
|
1243
|
+
cellIndex
|
|
1244
|
+
)) }, rowIndex)) })
|
|
1245
|
+
] }) });
|
|
1246
|
+
}
|
|
1247
|
+
function nextTableSort(current, column) {
|
|
1248
|
+
if (current?.column !== column) return { column, direction: "asc" };
|
|
1249
|
+
return current.direction === "asc" ? { column, direction: "desc" } : void 0;
|
|
1250
|
+
}
|
|
1251
|
+
function InteractiveTable({
|
|
1252
|
+
data,
|
|
1253
|
+
controls
|
|
1254
|
+
}) {
|
|
1255
|
+
const slot = useSlotClassName();
|
|
1256
|
+
const { labels, locale } = useWidgetSettings();
|
|
1257
|
+
const searchId = react.useId();
|
|
1258
|
+
const [search, setSearch] = react.useState("");
|
|
1259
|
+
const [sort, setSort] = react.useState(void 0);
|
|
1260
|
+
const localeTag = typeof locale === "string" ? locale : locale?.[0];
|
|
1261
|
+
const query = {
|
|
1262
|
+
page: 1,
|
|
1263
|
+
pageSize: Math.max(1, data.rows.length),
|
|
1264
|
+
...controls.search && search !== "" ? { search } : {},
|
|
1265
|
+
...controls.sort && sort !== void 0 ? { sort } : {}
|
|
1266
|
+
};
|
|
1267
|
+
const result = queryRows(tableDetailData(data), query, localeTag);
|
|
1268
|
+
const page = result.ok ? result.value : void 0;
|
|
1269
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-table-wrap", children: [
|
|
1270
|
+
controls.search && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-table-search", children: [
|
|
1271
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: searchId, className: "dwt-visually-hidden", children: labels.search }),
|
|
1272
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1273
|
+
"input",
|
|
935
1274
|
{
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1275
|
+
id: searchId,
|
|
1276
|
+
type: "search",
|
|
1277
|
+
placeholder: labels.search,
|
|
1278
|
+
value: search,
|
|
1279
|
+
onChange: (event) => setSearch(event.target.value)
|
|
1280
|
+
}
|
|
1281
|
+
)
|
|
941
1282
|
] }),
|
|
1283
|
+
result.ok ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1284
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1285
|
+
"p",
|
|
1286
|
+
{
|
|
1287
|
+
className: "dwt-visually-hidden",
|
|
1288
|
+
role: "status",
|
|
1289
|
+
"aria-live": "polite",
|
|
1290
|
+
"aria-atomic": "true",
|
|
1291
|
+
children: page !== void 0 && page.totalRows === 0 ? labels.noResults : page === void 0 ? "" : labels.showingRows(1, page.rows.length, page.totalRows)
|
|
1292
|
+
}
|
|
1293
|
+
),
|
|
1294
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1295
|
+
TableView,
|
|
1296
|
+
{
|
|
1297
|
+
data,
|
|
1298
|
+
rows: page?.rows ?? data.rows,
|
|
1299
|
+
sort,
|
|
1300
|
+
onSort: controls.sort ? (column) => setSort(nextTableSort(sort, column)) : void 0
|
|
1301
|
+
}
|
|
1302
|
+
),
|
|
1303
|
+
page !== void 0 && page.totalRows === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "dwt-note", children: labels.noResults })
|
|
1304
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: slot("error", "dwt-error"), children: result.error }),
|
|
942
1305
|
data.footer !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "dwt-note", children: data.footer })
|
|
943
1306
|
] });
|
|
944
1307
|
}
|
|
@@ -994,7 +1357,8 @@ function AlertListWidget({
|
|
|
994
1357
|
] });
|
|
995
1358
|
}
|
|
996
1359
|
function WidgetContent({
|
|
997
|
-
data
|
|
1360
|
+
data,
|
|
1361
|
+
tableControls
|
|
998
1362
|
}) {
|
|
999
1363
|
switch (data.kind) {
|
|
1000
1364
|
case "TEXT":
|
|
@@ -1004,7 +1368,7 @@ function WidgetContent({
|
|
|
1004
1368
|
case "GAUGE":
|
|
1005
1369
|
return /* @__PURE__ */ jsxRuntime.jsx(GaugeWidget, { data });
|
|
1006
1370
|
case "TABLE":
|
|
1007
|
-
return /* @__PURE__ */ jsxRuntime.jsx(TableWidget, { data });
|
|
1371
|
+
return /* @__PURE__ */ jsxRuntime.jsx(TableWidget, { data, controls: tableControls });
|
|
1008
1372
|
case "BAR_LIST":
|
|
1009
1373
|
return /* @__PURE__ */ jsxRuntime.jsx(BarListWidget, { data });
|
|
1010
1374
|
case "ALERT_LIST":
|
|
@@ -1195,7 +1559,13 @@ function ResolvedWidgetCard({
|
|
|
1195
1559
|
...definition.fill === void 0 ? {} : { fill: definition.fill },
|
|
1196
1560
|
...widget.status === "empty" ? { emptyText: widget.emptyText } : {},
|
|
1197
1561
|
...widget.status === "error" ? { error: widget.error } : {},
|
|
1198
|
-
children: widget.status === "ok" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1562
|
+
children: widget.status === "ok" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1563
|
+
WidgetContent,
|
|
1564
|
+
{
|
|
1565
|
+
data: widget.data,
|
|
1566
|
+
tableControls: resolveTableControls(definition)
|
|
1567
|
+
}
|
|
1568
|
+
) : null
|
|
1199
1569
|
}
|
|
1200
1570
|
);
|
|
1201
1571
|
}
|
|
@@ -1209,342 +1579,82 @@ var EMPTY_LAYOUT = Object.freeze({
|
|
|
1209
1579
|
function bySortOrder(items) {
|
|
1210
1580
|
return items.map((item, index) => ({ item, index })).sort(
|
|
1211
1581
|
(a, b) => (a.item.sortOrder ?? 0) - (b.item.sortOrder ?? 0) || a.index - b.index
|
|
1212
|
-
).map(({ item }) => item);
|
|
1213
|
-
}
|
|
1214
|
-
function visibleWidgets(definitions, layout) {
|
|
1215
|
-
const hidden = new Set(layout.hidden);
|
|
1216
|
-
const visible = definitions.filter((d) => !hidden.has(d.key));
|
|
1217
|
-
const byKey = new Map(visible.map((d) => [d.key, d]));
|
|
1218
|
-
const ordered = [];
|
|
1219
|
-
for (const key of layout.order) {
|
|
1220
|
-
const definition = byKey.get(key);
|
|
1221
|
-
if (definition !== void 0) {
|
|
1222
|
-
ordered.push(definition);
|
|
1223
|
-
byKey.delete(key);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
for (const definition of bySortOrder(visible)) {
|
|
1227
|
-
if (byKey.has(definition.key)) ordered.push(definition);
|
|
1228
|
-
}
|
|
1229
|
-
return ordered;
|
|
1230
|
-
}
|
|
1231
|
-
function hiddenWidgets(definitions, layout) {
|
|
1232
|
-
const hidden = new Set(layout.hidden);
|
|
1233
|
-
return bySortOrder(definitions.filter((d) => hidden.has(d.key)));
|
|
1234
|
-
}
|
|
1235
|
-
function moveWidget(definitions, layout, key, toIndex) {
|
|
1236
|
-
const current = visibleWidgets(definitions, layout).map(
|
|
1237
|
-
(d) => d.key
|
|
1238
|
-
);
|
|
1239
|
-
const fromIndex = current.indexOf(key);
|
|
1240
|
-
if (fromIndex === -1 || !Number.isInteger(toIndex) || toIndex < 0 || toIndex >= current.length || toIndex === fromIndex) {
|
|
1241
|
-
return layout;
|
|
1242
|
-
}
|
|
1243
|
-
const next = [...current];
|
|
1244
|
-
next.splice(fromIndex, 1);
|
|
1245
|
-
next.splice(toIndex, 0, key);
|
|
1246
|
-
return { ...layout, order: next };
|
|
1247
|
-
}
|
|
1248
|
-
function moveWidgetBy(definitions, layout, key, offset) {
|
|
1249
|
-
const current = visibleWidgets(definitions, layout).map(
|
|
1250
|
-
(d) => d.key
|
|
1251
|
-
);
|
|
1252
|
-
const fromIndex = current.indexOf(key);
|
|
1253
|
-
if (fromIndex === -1) return layout;
|
|
1254
|
-
const toIndex = Math.max(
|
|
1255
|
-
0,
|
|
1256
|
-
Math.min(current.length - 1, fromIndex + Math.trunc(offset))
|
|
1257
|
-
);
|
|
1258
|
-
return moveWidget(definitions, layout, key, toIndex);
|
|
1259
|
-
}
|
|
1260
|
-
function isHidden(layout, key) {
|
|
1261
|
-
return layout.hidden.includes(key);
|
|
1262
|
-
}
|
|
1263
|
-
function isMinimized(layout, key) {
|
|
1264
|
-
return layout.minimized.includes(key);
|
|
1265
|
-
}
|
|
1266
|
-
function hideWidget(layout, key) {
|
|
1267
|
-
if (isHidden(layout, key)) return layout;
|
|
1268
|
-
return { ...layout, hidden: [...layout.hidden, key] };
|
|
1269
|
-
}
|
|
1270
|
-
function showWidget(layout, key) {
|
|
1271
|
-
if (!isHidden(layout, key)) return layout;
|
|
1272
|
-
return {
|
|
1273
|
-
...layout,
|
|
1274
|
-
hidden: layout.hidden.filter((k) => k !== key),
|
|
1275
|
-
minimized: layout.minimized.filter((k) => k !== key)
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
function minimizeWidget(layout, key) {
|
|
1279
|
-
if (isMinimized(layout, key)) return layout;
|
|
1280
|
-
return { ...layout, minimized: [...layout.minimized, key] };
|
|
1281
|
-
}
|
|
1282
|
-
function restoreWidget(layout, key) {
|
|
1283
|
-
if (!isMinimized(layout, key)) return layout;
|
|
1284
|
-
return { ...layout, minimized: layout.minimized.filter((k) => k !== key) };
|
|
1285
|
-
}
|
|
1286
|
-
function toggleMinimized(layout, key) {
|
|
1287
|
-
return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
// src/core/result.ts
|
|
1291
|
-
function ok(value) {
|
|
1292
|
-
return { ok: true, value };
|
|
1293
|
-
}
|
|
1294
|
-
function err(error) {
|
|
1295
|
-
return { ok: false, error };
|
|
1296
|
-
}
|
|
1297
|
-
function describeUnknownError(cause) {
|
|
1298
|
-
if (cause instanceof Error) {
|
|
1299
|
-
return cause.message === "" ? cause.name : cause.message;
|
|
1300
|
-
}
|
|
1301
|
-
if (typeof cause === "string") {
|
|
1302
|
-
return cause;
|
|
1303
|
-
}
|
|
1304
|
-
try {
|
|
1305
|
-
const json = JSON.stringify(cause);
|
|
1306
|
-
return json ?? String(cause);
|
|
1307
|
-
} catch {
|
|
1308
|
-
return String(cause);
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// src/core/detail.ts
|
|
1313
|
-
var DEFAULT_DETAIL_PAGE_SIZE = 25;
|
|
1314
|
-
function resolveDetailOptions(definition) {
|
|
1315
|
-
const detail = definition.detail;
|
|
1316
|
-
if (detail === void 0 || detail === false) return void 0;
|
|
1317
|
-
const options = detail === true ? {} : detail;
|
|
1318
|
-
return {
|
|
1319
|
-
title: options.title ?? definition.title,
|
|
1320
|
-
pageSize: options.pageSize ?? DEFAULT_DETAIL_PAGE_SIZE,
|
|
1321
|
-
mode: options.mode ?? "client"
|
|
1322
|
-
};
|
|
1323
|
-
}
|
|
1324
|
-
function defaultDetailQuery(pageSize = DEFAULT_DETAIL_PAGE_SIZE) {
|
|
1325
|
-
return { page: 1, pageSize };
|
|
1326
|
-
}
|
|
1327
|
-
function fold(text) {
|
|
1328
|
-
return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
1329
|
-
}
|
|
1330
|
-
function cellValue(cell) {
|
|
1331
|
-
return cell.value ?? cell.text;
|
|
1332
|
-
}
|
|
1333
|
-
function cellMatches(cell, needle) {
|
|
1334
|
-
return fold(String(cellValue(cell))).includes(needle);
|
|
1335
|
-
}
|
|
1336
|
-
function compareCells(a, b, collator) {
|
|
1337
|
-
const left = cellValue(a);
|
|
1338
|
-
const right = cellValue(b);
|
|
1339
|
-
if (typeof left === "number" && typeof right === "number") {
|
|
1340
|
-
return left - right;
|
|
1341
|
-
}
|
|
1342
|
-
if (typeof left === "number") return -1;
|
|
1343
|
-
if (typeof right === "number") return 1;
|
|
1344
|
-
return collator.compare(left, right);
|
|
1582
|
+
).map(({ item }) => item);
|
|
1345
1583
|
}
|
|
1346
|
-
function
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
);
|
|
1350
|
-
const
|
|
1351
|
-
for (const
|
|
1352
|
-
const
|
|
1353
|
-
if (
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
);
|
|
1584
|
+
function visibleWidgets(definitions, layout) {
|
|
1585
|
+
const hidden = new Set(layout.hidden);
|
|
1586
|
+
const visible = definitions.filter((d) => !hidden.has(d.key));
|
|
1587
|
+
const byKey = new Map(visible.map((d) => [d.key, d]));
|
|
1588
|
+
const ordered = [];
|
|
1589
|
+
for (const key of layout.order) {
|
|
1590
|
+
const definition = byKey.get(key);
|
|
1591
|
+
if (definition !== void 0) {
|
|
1592
|
+
ordered.push(definition);
|
|
1593
|
+
byKey.delete(key);
|
|
1357
1594
|
}
|
|
1358
|
-
const needle = fold(text.trim());
|
|
1359
|
-
if (needle !== "") filterEntries.push({ index, needle });
|
|
1360
1595
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
const found = indexOf.get(query.sort.column);
|
|
1364
|
-
const column = found === void 0 ? void 0 : data.columns[found];
|
|
1365
|
-
if (found === void 0 || column === void 0) {
|
|
1366
|
-
return err(
|
|
1367
|
-
`Detail query: sort column "${query.sort.column}" does not exist. Columns: ${listKeys(data)}.`
|
|
1368
|
-
);
|
|
1369
|
-
}
|
|
1370
|
-
if (column.sortable === false) {
|
|
1371
|
-
return err(
|
|
1372
|
-
`Detail query: column "${column.key}" is not sortable (sortable: false).`
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
sortIndex = found;
|
|
1596
|
+
for (const definition of bySortOrder(visible)) {
|
|
1597
|
+
if (byKey.has(definition.key)) ordered.push(definition);
|
|
1376
1598
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1599
|
+
return ordered;
|
|
1600
|
+
}
|
|
1601
|
+
function hiddenWidgets(definitions, layout) {
|
|
1602
|
+
const hidden = new Set(layout.hidden);
|
|
1603
|
+
return bySortOrder(definitions.filter((d) => hidden.has(d.key)));
|
|
1604
|
+
}
|
|
1605
|
+
function moveWidget(definitions, layout, key, toIndex) {
|
|
1606
|
+
const current = visibleWidgets(definitions, layout).map(
|
|
1607
|
+
(d) => d.key
|
|
1383
1608
|
);
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
sensitivity: "base"
|
|
1388
|
-
});
|
|
1389
|
-
const sign = query.sort.direction === "desc" ? -1 : 1;
|
|
1390
|
-
rows = rows.map((row, position) => ({ row, position })).sort((a, b) => {
|
|
1391
|
-
const left = a.row[sortIndex];
|
|
1392
|
-
const right = b.row[sortIndex];
|
|
1393
|
-
const order = left === void 0 || right === void 0 ? 0 : sign * compareCells(left, right, collator);
|
|
1394
|
-
return order || a.position - b.position;
|
|
1395
|
-
}).map(({ row }) => row);
|
|
1609
|
+
const fromIndex = current.indexOf(key);
|
|
1610
|
+
if (fromIndex === -1 || !Number.isInteger(toIndex) || toIndex < 0 || toIndex >= current.length || toIndex === fromIndex) {
|
|
1611
|
+
return layout;
|
|
1396
1612
|
}
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
return
|
|
1401
|
-
rows: rows.slice((page - 1) * pageSize, page * pageSize),
|
|
1402
|
-
totalRows: rows.length,
|
|
1403
|
-
page,
|
|
1404
|
-
pageCount
|
|
1405
|
-
});
|
|
1613
|
+
const next = [...current];
|
|
1614
|
+
next.splice(fromIndex, 1);
|
|
1615
|
+
next.splice(toIndex, 0, key);
|
|
1616
|
+
return { ...layout, order: next };
|
|
1406
1617
|
}
|
|
1407
|
-
function
|
|
1408
|
-
|
|
1618
|
+
function moveWidgetBy(definitions, layout, key, offset) {
|
|
1619
|
+
const current = visibleWidgets(definitions, layout).map(
|
|
1620
|
+
(d) => d.key
|
|
1621
|
+
);
|
|
1622
|
+
const fromIndex = current.indexOf(key);
|
|
1623
|
+
if (fromIndex === -1) return layout;
|
|
1624
|
+
const toIndex = Math.max(
|
|
1625
|
+
0,
|
|
1626
|
+
Math.min(current.length - 1, fromIndex + Math.trunc(offset))
|
|
1627
|
+
);
|
|
1628
|
+
return moveWidget(definitions, layout, key, toIndex);
|
|
1409
1629
|
}
|
|
1410
|
-
function
|
|
1411
|
-
|
|
1412
|
-
if (Array.isArray(value)) return "array";
|
|
1413
|
-
return typeof value;
|
|
1630
|
+
function isHidden(layout, key) {
|
|
1631
|
+
return layout.hidden.includes(key);
|
|
1414
1632
|
}
|
|
1415
|
-
function
|
|
1416
|
-
|
|
1417
|
-
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
1418
|
-
return err(`${subject}: must be an object, got ${describeType(data)}.`);
|
|
1419
|
-
}
|
|
1420
|
-
const record = data;
|
|
1421
|
-
const columns = record["columns"];
|
|
1422
|
-
const rows = record["rows"];
|
|
1423
|
-
const keys = /* @__PURE__ */ new Set();
|
|
1424
|
-
let columnCount = 0;
|
|
1425
|
-
if (!Array.isArray(columns) || columns.length === 0) {
|
|
1426
|
-
problems.push(
|
|
1427
|
-
`columns must be a non-empty array, got ${describeType(columns)}.`
|
|
1428
|
-
);
|
|
1429
|
-
} else {
|
|
1430
|
-
columnCount = columns.length;
|
|
1431
|
-
columns.forEach((column, index) => {
|
|
1432
|
-
const at = `columns[${index}]`;
|
|
1433
|
-
if (typeof column !== "object" || column === null) {
|
|
1434
|
-
problems.push(`${at} must be an object, got ${describeType(column)}.`);
|
|
1435
|
-
return;
|
|
1436
|
-
}
|
|
1437
|
-
const fields = column;
|
|
1438
|
-
const key = fields["key"];
|
|
1439
|
-
if (typeof key !== "string" || key === "") {
|
|
1440
|
-
problems.push(`${at}.key must be a non-empty string.`);
|
|
1441
|
-
} else if (keys.has(key)) {
|
|
1442
|
-
problems.push(`${at}.key "${key}" is used by an earlier column.`);
|
|
1443
|
-
} else {
|
|
1444
|
-
keys.add(key);
|
|
1445
|
-
}
|
|
1446
|
-
if (typeof fields["label"] !== "string") {
|
|
1447
|
-
problems.push(
|
|
1448
|
-
`${at}.label must be a string, got ${describeType(fields["label"])}.`
|
|
1449
|
-
);
|
|
1450
|
-
}
|
|
1451
|
-
for (const flag of ["numeric", "sortable", "filterable"]) {
|
|
1452
|
-
const flagValue = fields[flag];
|
|
1453
|
-
if (flagValue !== void 0 && typeof flagValue !== "boolean") {
|
|
1454
|
-
problems.push(
|
|
1455
|
-
`${at}.${flag} must be a boolean, got ${describeType(flagValue)}.`
|
|
1456
|
-
);
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
});
|
|
1460
|
-
}
|
|
1461
|
-
if (!Array.isArray(rows)) {
|
|
1462
|
-
problems.push(`rows must be an array, got ${describeType(rows)}.`);
|
|
1463
|
-
} else {
|
|
1464
|
-
rows.forEach((row, rowIndex) => {
|
|
1465
|
-
if (!Array.isArray(row)) {
|
|
1466
|
-
problems.push(
|
|
1467
|
-
`rows[${rowIndex}] must be an array of cells, got ${describeType(row)}.`
|
|
1468
|
-
);
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
if (columnCount > 0 && row.length !== columnCount) {
|
|
1472
|
-
problems.push(
|
|
1473
|
-
`rows[${rowIndex}] has ${row.length} cells but there are ${columnCount} columns.`
|
|
1474
|
-
);
|
|
1475
|
-
}
|
|
1476
|
-
row.forEach((cell, cellIndex) => {
|
|
1477
|
-
const at = `rows[${rowIndex}][${cellIndex}]`;
|
|
1478
|
-
if (typeof cell !== "object" || cell === null) {
|
|
1479
|
-
problems.push(`${at} must be an object, got ${describeType(cell)}.`);
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1482
|
-
const fields = cell;
|
|
1483
|
-
if (typeof fields["text"] !== "string") {
|
|
1484
|
-
problems.push(
|
|
1485
|
-
`${at}.text must be a string, got ${describeType(fields["text"])}.`
|
|
1486
|
-
);
|
|
1487
|
-
}
|
|
1488
|
-
const value = fields["value"];
|
|
1489
|
-
if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
|
|
1490
|
-
problems.push(
|
|
1491
|
-
`${at}.value must be a string or a finite number, got ${describeType(value)}.`
|
|
1492
|
-
);
|
|
1493
|
-
}
|
|
1494
|
-
const href = fields["href"];
|
|
1495
|
-
if (href !== void 0) {
|
|
1496
|
-
if (typeof href !== "string") {
|
|
1497
|
-
problems.push(
|
|
1498
|
-
`${at}.href must be a string, got ${describeType(href)}.`
|
|
1499
|
-
);
|
|
1500
|
-
} else if (!isSafeHref(href)) {
|
|
1501
|
-
problems.push(
|
|
1502
|
-
`${at}.href "${href}" is not allowed (only http, https, mailto and relative URLs).`
|
|
1503
|
-
);
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
});
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
const totalRows = record["totalRows"];
|
|
1510
|
-
if (totalRows !== void 0 && !(typeof totalRows === "number" && Number.isInteger(totalRows) && totalRows >= 0)) {
|
|
1511
|
-
problems.push(
|
|
1512
|
-
`totalRows must be a non-negative whole number, got ${describeType(totalRows)}.`
|
|
1513
|
-
);
|
|
1514
|
-
}
|
|
1515
|
-
if (problems.length > 0) {
|
|
1516
|
-
return err(`${subject}: ${problems.join(" ")}`);
|
|
1517
|
-
}
|
|
1518
|
-
return ok(data);
|
|
1633
|
+
function isMinimized(layout, key) {
|
|
1634
|
+
return layout.minimized.includes(key);
|
|
1519
1635
|
}
|
|
1520
|
-
function
|
|
1521
|
-
if (
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
value: item.value
|
|
1543
|
-
}
|
|
1544
|
-
])
|
|
1545
|
-
};
|
|
1546
|
-
}
|
|
1547
|
-
return void 0;
|
|
1636
|
+
function hideWidget(layout, key) {
|
|
1637
|
+
if (isHidden(layout, key)) return layout;
|
|
1638
|
+
return { ...layout, hidden: [...layout.hidden, key] };
|
|
1639
|
+
}
|
|
1640
|
+
function showWidget(layout, key) {
|
|
1641
|
+
if (!isHidden(layout, key)) return layout;
|
|
1642
|
+
return {
|
|
1643
|
+
...layout,
|
|
1644
|
+
hidden: layout.hidden.filter((k) => k !== key),
|
|
1645
|
+
minimized: layout.minimized.filter((k) => k !== key)
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
function minimizeWidget(layout, key) {
|
|
1649
|
+
if (isMinimized(layout, key)) return layout;
|
|
1650
|
+
return { ...layout, minimized: [...layout.minimized, key] };
|
|
1651
|
+
}
|
|
1652
|
+
function restoreWidget(layout, key) {
|
|
1653
|
+
if (!isMinimized(layout, key)) return layout;
|
|
1654
|
+
return { ...layout, minimized: layout.minimized.filter((k) => k !== key) };
|
|
1655
|
+
}
|
|
1656
|
+
function toggleMinimized(layout, key) {
|
|
1657
|
+
return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
|
|
1548
1658
|
}
|
|
1549
1659
|
function nextSort(current, column) {
|
|
1550
1660
|
if (current?.column !== column) return { column, direction: "asc" };
|
|
@@ -1785,22 +1895,30 @@ function WidgetDetailDialog({
|
|
|
1785
1895
|
}
|
|
1786
1896
|
);
|
|
1787
1897
|
}
|
|
1788
|
-
function useDetailData(definition, load, enabled) {
|
|
1898
|
+
function useDetailData(definition, load, enabled, fallback) {
|
|
1789
1899
|
const [state, setState] = react.useState({ status: "loading" });
|
|
1790
1900
|
const [attempt, setAttempt] = react.useState(0);
|
|
1791
1901
|
react.useEffect(() => {
|
|
1792
|
-
if (!enabled
|
|
1902
|
+
if (!enabled) return void 0;
|
|
1793
1903
|
const controller = new AbortController();
|
|
1794
1904
|
setState({ status: "loading" });
|
|
1795
1905
|
Promise.resolve().then(
|
|
1796
|
-
() => load(definition, {
|
|
1906
|
+
() => load === void 0 ? void 0 : load(definition, {
|
|
1797
1907
|
signal: controller.signal,
|
|
1798
1908
|
query: defaultDetailQuery()
|
|
1799
1909
|
})
|
|
1800
1910
|
).then((raw) => {
|
|
1801
1911
|
if (controller.signal.aborted) return;
|
|
1912
|
+
const data = raw ?? fallback;
|
|
1913
|
+
if (data === void 0) {
|
|
1914
|
+
setState({
|
|
1915
|
+
status: "error",
|
|
1916
|
+
error: `Widget "${definition.key}" detail: no data. The loader returned nothing for this widget and its card does not hold the full data (use a loader that returns rows, or a TABLE without a footer or a BAR_LIST).`
|
|
1917
|
+
});
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1802
1920
|
const checked = validateDetailData(
|
|
1803
|
-
|
|
1921
|
+
data,
|
|
1804
1922
|
`Widget "${definition.key}" detail`
|
|
1805
1923
|
);
|
|
1806
1924
|
setState(
|
|
@@ -1814,7 +1932,7 @@ function useDetailData(definition, load, enabled) {
|
|
|
1814
1932
|
});
|
|
1815
1933
|
});
|
|
1816
1934
|
return () => controller.abort();
|
|
1817
|
-
}, [enabled, load, definition, attempt]);
|
|
1935
|
+
}, [enabled, load, fallback, definition, attempt]);
|
|
1818
1936
|
return { state, reload: () => setAttempt((count) => count + 1) };
|
|
1819
1937
|
}
|
|
1820
1938
|
var NARROW_VIEWPORT_QUERY = "(max-width: 640px)";
|
|
@@ -1912,12 +2030,16 @@ function DetailHost({
|
|
|
1912
2030
|
const options = resolveDetailOptions(definition);
|
|
1913
2031
|
const pageSize = options?.pageSize ?? 25;
|
|
1914
2032
|
const [query, setQuery] = react.useState(defaultDetailQuery(pageSize));
|
|
1915
|
-
const derived =
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2033
|
+
const derived = react.useMemo(
|
|
2034
|
+
() => widget.status === "ok" ? deriveDetailData(widget.data, locale) : void 0,
|
|
2035
|
+
[widget, locale]
|
|
2036
|
+
);
|
|
2037
|
+
const { state, reload } = useDetailData(
|
|
2038
|
+
definition,
|
|
2039
|
+
loadDetail,
|
|
2040
|
+
true,
|
|
2041
|
+
derived
|
|
1919
2042
|
);
|
|
1920
|
-
const { state, reload } = useDetailData(definition, load, true);
|
|
1921
2043
|
const title = options?.title ?? definition.title;
|
|
1922
2044
|
return /* @__PURE__ */ jsxRuntime.jsx(WidgetDetailDialog, { title, open: true, onClose, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1923
2045
|
WidgetDetail,
|
|
@@ -1925,10 +2047,7 @@ function DetailHost({
|
|
|
1925
2047
|
title,
|
|
1926
2048
|
query,
|
|
1927
2049
|
onQueryChange: setQuery,
|
|
1928
|
-
status:
|
|
1929
|
-
...load === void 0 ? {
|
|
1930
|
-
error: `Widget "${definition.key}" has no detail data: pass loadDetail, or use a TABLE without a footer or a BAR_LIST.`
|
|
1931
|
-
} : {},
|
|
2050
|
+
status: state.status,
|
|
1932
2051
|
...state.data === void 0 ? {} : { data: state.data },
|
|
1933
2052
|
...state.error === void 0 ? {} : { error: state.error },
|
|
1934
2053
|
onRetry: reload
|