@richardmcquiston01/dashboard-widgets-toolkit 0.14.0 → 0.16.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/core.cjs +2232 -1587
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +49 -3
- package/dist/core.d.ts +49 -3
- package/dist/core.js +2210 -1588
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +2232 -1587
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2210 -1588
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +600 -59
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +25 -2
- package/dist/react.d.ts +25 -2
- package/dist/react.js +600 -59
- package/dist/react.js.map +1 -1
- package/dist/{storage-_Eb8Y98-.d.cts → storage-oivwB7al.d.cts} +247 -56
- package/dist/{storage-_Eb8Y98-.d.ts → storage-oivwB7al.d.ts} +247 -56
- package/package.json +1 -1
package/dist/core.cjs
CHANGED
|
@@ -1,88 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// src/core/payload.ts
|
|
4
|
-
var WIDGET_KINDS = [
|
|
5
|
-
"TEXT",
|
|
6
|
-
"KPI",
|
|
7
|
-
"GAUGE",
|
|
8
|
-
"TABLE",
|
|
9
|
-
"BAR_LIST",
|
|
10
|
-
"ALERT_LIST",
|
|
11
|
-
"GRAPH"
|
|
12
|
-
];
|
|
13
|
-
function isWidgetKind(value) {
|
|
14
|
-
return typeof value === "string" && WIDGET_KINDS.includes(value);
|
|
15
|
-
}
|
|
16
|
-
var VALUE_FORMATS = ["number", "currency", "percent"];
|
|
17
|
-
var MAX_GRAPH_SERIES = 8;
|
|
18
|
-
var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
|
|
19
|
-
function emptyWidget(text = DEFAULT_EMPTY_TEXT) {
|
|
20
|
-
return { empty: true, text };
|
|
21
|
-
}
|
|
22
|
-
function isWidgetEmptyState(payload) {
|
|
23
|
-
return "empty" in payload && payload.empty === true;
|
|
24
|
-
}
|
|
25
|
-
function emptyTextFor(payload) {
|
|
26
|
-
if (isWidgetEmptyState(payload)) {
|
|
27
|
-
return payload.text;
|
|
28
|
-
}
|
|
29
|
-
switch (payload.kind) {
|
|
30
|
-
case "TABLE":
|
|
31
|
-
return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
|
|
32
|
-
case "BAR_LIST":
|
|
33
|
-
return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
|
|
34
|
-
case "ALERT_LIST":
|
|
35
|
-
return payload.items.length === 0 ? payload.emptyText : null;
|
|
36
|
-
case "GRAPH":
|
|
37
|
-
return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
|
|
38
|
-
case "TEXT":
|
|
39
|
-
case "KPI":
|
|
40
|
-
case "GAUGE":
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// src/core/compat.ts
|
|
46
|
-
function isStringArray(value) {
|
|
47
|
-
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
48
|
-
}
|
|
49
|
-
function upgradeLegacyWidgetData(value) {
|
|
50
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
51
|
-
return value;
|
|
52
|
-
}
|
|
53
|
-
const record = value;
|
|
54
|
-
if (record["kind"] === "TABLE" && isStringArray(record["columns"]) && Array.isArray(record["rows"]) && record["rows"].every(isStringArray)) {
|
|
55
|
-
const rows = record["rows"];
|
|
56
|
-
return {
|
|
57
|
-
...record,
|
|
58
|
-
columns: record["columns"].map((label) => ({ label })),
|
|
59
|
-
rows: rows.map((row) => row.map((text) => ({ text })))
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
if (record["kind"] === "GRAPH" && Array.isArray(record["points"]) && record["series"] === void 0) {
|
|
63
|
-
const { points, ...rest } = record;
|
|
64
|
-
return {
|
|
65
|
-
...rest,
|
|
66
|
-
series: [{ name: "Value", points }],
|
|
67
|
-
chartType: "bar",
|
|
68
|
-
valueFormat: "number"
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
return value;
|
|
72
|
-
}
|
|
73
|
-
function fromLegacyDefinition(legacy) {
|
|
74
|
-
if (!isWidgetKind(legacy.viewType)) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
key: legacy.widgetKey,
|
|
79
|
-
title: legacy.title,
|
|
80
|
-
...legacy.description === null ? {} : { description: legacy.description },
|
|
81
|
-
kind: legacy.viewType,
|
|
82
|
-
sortOrder: legacy.sortOrder
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
3
|
// src/core/definition.ts
|
|
87
4
|
var WIDGET_SIZES = ["small", "medium", "large", "full"];
|
|
88
5
|
var WIDGET_FILLS = ["height", "width", "both"];
|
|
@@ -90,13 +7,15 @@ var NOT_LOCKED = Object.freeze({
|
|
|
90
7
|
move: false,
|
|
91
8
|
hide: false,
|
|
92
9
|
minimize: false,
|
|
93
|
-
options: false
|
|
10
|
+
options: false,
|
|
11
|
+
clone: false
|
|
94
12
|
});
|
|
95
13
|
var FULLY_LOCKED = Object.freeze({
|
|
96
14
|
move: true,
|
|
97
15
|
hide: true,
|
|
98
16
|
minimize: true,
|
|
99
|
-
options: true
|
|
17
|
+
options: true,
|
|
18
|
+
clone: true
|
|
100
19
|
});
|
|
101
20
|
function resolveWidgetLock(definition) {
|
|
102
21
|
const setting = definition.locked;
|
|
@@ -106,7 +25,8 @@ function resolveWidgetLock(definition) {
|
|
|
106
25
|
move: setting.move === true,
|
|
107
26
|
hide: setting.hide === true,
|
|
108
27
|
minimize: setting.minimize === true,
|
|
109
|
-
options: setting.options === true
|
|
28
|
+
options: setting.options === true,
|
|
29
|
+
clone: setting.clone === true
|
|
110
30
|
};
|
|
111
31
|
}
|
|
112
32
|
function defineWidget(definition) {
|
|
@@ -130,6 +50,103 @@ function isWidgetVisibleTo(definition, roles) {
|
|
|
130
50
|
return definition.roles.some((role) => roles.includes(role));
|
|
131
51
|
}
|
|
132
52
|
|
|
53
|
+
// src/core/grid.ts
|
|
54
|
+
var WIDTH_COLUMNS = 12;
|
|
55
|
+
var MIN_WIDGET_WIDTH = 2;
|
|
56
|
+
var MAX_WIDGET_WIDTH = 12;
|
|
57
|
+
function isWidgetWidth(value) {
|
|
58
|
+
return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
|
|
59
|
+
}
|
|
60
|
+
function widthForSize(size) {
|
|
61
|
+
switch (size) {
|
|
62
|
+
case "small":
|
|
63
|
+
return 3;
|
|
64
|
+
case "large":
|
|
65
|
+
return 6;
|
|
66
|
+
case "full":
|
|
67
|
+
return 12;
|
|
68
|
+
default:
|
|
69
|
+
return 4;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function itemWidth(item) {
|
|
73
|
+
return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
|
|
74
|
+
}
|
|
75
|
+
function usesWidthColumns(items) {
|
|
76
|
+
return items.some((item) => item.width !== void 0);
|
|
77
|
+
}
|
|
78
|
+
function fillsWidth(fill) {
|
|
79
|
+
return fill === "width" || fill === "both";
|
|
80
|
+
}
|
|
81
|
+
function fillsHeight(fill) {
|
|
82
|
+
return fill === "height" || fill === "both";
|
|
83
|
+
}
|
|
84
|
+
function baseColumnSpan(size, columns, largeSpan = 2) {
|
|
85
|
+
if (size === "full") return columns;
|
|
86
|
+
if (size === "large") return Math.min(largeSpan, columns);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
function fillColumnSpans(items, columns, largeSpan = 2) {
|
|
90
|
+
return placeAndFill(
|
|
91
|
+
items,
|
|
92
|
+
columns,
|
|
93
|
+
(item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
function fillWidthSpans(items) {
|
|
97
|
+
return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
|
|
98
|
+
}
|
|
99
|
+
function placeRows(items) {
|
|
100
|
+
return placeAndFillRows(items, WIDTH_COLUMNS, itemWidth).rows;
|
|
101
|
+
}
|
|
102
|
+
function fitCount(items, maxRows) {
|
|
103
|
+
const rows = placeRows(items);
|
|
104
|
+
let count = 0;
|
|
105
|
+
for (const row of rows.slice(0, Math.max(0, Math.floor(maxRows)))) {
|
|
106
|
+
count += row.keys.length;
|
|
107
|
+
}
|
|
108
|
+
return count;
|
|
109
|
+
}
|
|
110
|
+
function placeAndFill(items, columns, spanOf) {
|
|
111
|
+
return placeAndFillRows(items, columns, spanOf).spans;
|
|
112
|
+
}
|
|
113
|
+
function placeAndFillRows(items, columns, spanOf) {
|
|
114
|
+
const trackCount = Math.max(1, Math.floor(columns));
|
|
115
|
+
const spans = /* @__PURE__ */ new Map();
|
|
116
|
+
const rows = [];
|
|
117
|
+
let row = [];
|
|
118
|
+
let used = 0;
|
|
119
|
+
function closeRow() {
|
|
120
|
+
if (row.length === 0) return;
|
|
121
|
+
const fillers = row.filter(({ item }) => fillsWidth(item.fill));
|
|
122
|
+
const leftover = trackCount - used;
|
|
123
|
+
const finalSpans = /* @__PURE__ */ new Map();
|
|
124
|
+
if (fillers.length > 0) {
|
|
125
|
+
const share = Math.floor(leftover / fillers.length);
|
|
126
|
+
const remainder = leftover % fillers.length;
|
|
127
|
+
fillers.forEach(({ item, span }, index) => {
|
|
128
|
+
const grown = span + share + (index < remainder ? 1 : 0);
|
|
129
|
+
spans.set(item.key, grown);
|
|
130
|
+
finalSpans.set(item.key, grown);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
rows.push({
|
|
134
|
+
keys: row.map(({ item }) => item.key),
|
|
135
|
+
spans: row.map(({ item, span }) => finalSpans.get(item.key) ?? span)
|
|
136
|
+
});
|
|
137
|
+
row = [];
|
|
138
|
+
used = 0;
|
|
139
|
+
}
|
|
140
|
+
for (const item of items) {
|
|
141
|
+
const span = Math.min(spanOf(item, trackCount), trackCount);
|
|
142
|
+
if (used + span > trackCount && row.length > 0) closeRow();
|
|
143
|
+
row.push({ item, span });
|
|
144
|
+
used += span;
|
|
145
|
+
}
|
|
146
|
+
closeRow();
|
|
147
|
+
return { spans, rows };
|
|
148
|
+
}
|
|
149
|
+
|
|
133
150
|
// src/core/format.ts
|
|
134
151
|
var DEFAULT_CURRENCY = "USD";
|
|
135
152
|
function localeArgument(locale) {
|
|
@@ -215,6 +232,48 @@ function kpiDelta(current, previous, options = {}) {
|
|
|
215
232
|
};
|
|
216
233
|
}
|
|
217
234
|
|
|
235
|
+
// src/core/payload.ts
|
|
236
|
+
var WIDGET_KINDS = [
|
|
237
|
+
"TEXT",
|
|
238
|
+
"KPI",
|
|
239
|
+
"GAUGE",
|
|
240
|
+
"TABLE",
|
|
241
|
+
"BAR_LIST",
|
|
242
|
+
"ALERT_LIST",
|
|
243
|
+
"GRAPH"
|
|
244
|
+
];
|
|
245
|
+
function isWidgetKind(value) {
|
|
246
|
+
return typeof value === "string" && WIDGET_KINDS.includes(value);
|
|
247
|
+
}
|
|
248
|
+
var VALUE_FORMATS = ["number", "currency", "percent"];
|
|
249
|
+
var MAX_GRAPH_SERIES = 8;
|
|
250
|
+
var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
|
|
251
|
+
function emptyWidget(text = DEFAULT_EMPTY_TEXT) {
|
|
252
|
+
return { empty: true, text };
|
|
253
|
+
}
|
|
254
|
+
function isWidgetEmptyState(payload) {
|
|
255
|
+
return "empty" in payload && payload.empty === true;
|
|
256
|
+
}
|
|
257
|
+
function emptyTextFor(payload) {
|
|
258
|
+
if (isWidgetEmptyState(payload)) {
|
|
259
|
+
return payload.text;
|
|
260
|
+
}
|
|
261
|
+
switch (payload.kind) {
|
|
262
|
+
case "TABLE":
|
|
263
|
+
return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
|
|
264
|
+
case "BAR_LIST":
|
|
265
|
+
return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
|
|
266
|
+
case "ALERT_LIST":
|
|
267
|
+
return payload.items.length === 0 ? payload.emptyText : null;
|
|
268
|
+
case "GRAPH":
|
|
269
|
+
return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
|
|
270
|
+
case "TEXT":
|
|
271
|
+
case "KPI":
|
|
272
|
+
case "GAUGE":
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
218
277
|
// src/core/result.ts
|
|
219
278
|
function ok(value) {
|
|
220
279
|
return { ok: true, value };
|
|
@@ -237,41 +296,298 @@ function describeUnknownError(cause) {
|
|
|
237
296
|
}
|
|
238
297
|
}
|
|
239
298
|
|
|
240
|
-
// src/core/
|
|
241
|
-
var
|
|
242
|
-
var
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
299
|
+
// src/core/views.ts
|
|
300
|
+
var BASE_VIEW_KEY = "base";
|
|
301
|
+
var MAX_WIDGET_VIEWS = 8;
|
|
302
|
+
var VIEW_CONVERSIONS = {
|
|
303
|
+
tableToBarList: { from: "TABLE", to: "BAR_LIST" },
|
|
304
|
+
tableToGraph: { from: "TABLE", to: "GRAPH" },
|
|
305
|
+
barListToTable: { from: "BAR_LIST", to: "TABLE" },
|
|
306
|
+
graphToTable: { from: "GRAPH", to: "TABLE" },
|
|
307
|
+
barListToGraph: { from: "BAR_LIST", to: "GRAPH" }
|
|
308
|
+
};
|
|
309
|
+
var KIND_LABELS = {
|
|
310
|
+
TEXT: "Text",
|
|
311
|
+
KPI: "Number",
|
|
312
|
+
GAUGE: "Gauge",
|
|
313
|
+
TABLE: "Table",
|
|
314
|
+
BAR_LIST: "Bars",
|
|
315
|
+
ALERT_LIST: "List",
|
|
316
|
+
GRAPH: "Chart"
|
|
317
|
+
};
|
|
318
|
+
function kindLabel(kind) {
|
|
319
|
+
return KIND_LABELS[kind];
|
|
248
320
|
}
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
return false;
|
|
252
|
-
}
|
|
253
|
-
const scheme = schemeOf(url);
|
|
254
|
-
return scheme === null || scheme === "http" || scheme === "https" || scheme === "mailto";
|
|
321
|
+
function hasViews(item) {
|
|
322
|
+
return item.views !== void 0 && item.views.length > 0;
|
|
255
323
|
}
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
const scheme = schemeOf(url);
|
|
261
|
-
return scheme === null || scheme === "http" || scheme === "https";
|
|
324
|
+
function findView(item, key) {
|
|
325
|
+
return item.views?.find((view) => view.key === key);
|
|
262
326
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
327
|
+
function viewChoices(item) {
|
|
328
|
+
if (!hasViews(item)) return [];
|
|
329
|
+
return [
|
|
330
|
+
{
|
|
331
|
+
key: BASE_VIEW_KEY,
|
|
332
|
+
label: item.baseViewLabel ?? kindLabel(item.kind),
|
|
333
|
+
kind: item.kind
|
|
334
|
+
},
|
|
335
|
+
...(item.views ?? []).map(({ key, label, kind }) => ({
|
|
336
|
+
key,
|
|
337
|
+
label,
|
|
338
|
+
kind
|
|
339
|
+
}))
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
function isKnownView(item, key) {
|
|
343
|
+
return key !== void 0 && (key === BASE_VIEW_KEY || findView(item, key) !== void 0);
|
|
344
|
+
}
|
|
345
|
+
function effectiveViewKey(item, chosen) {
|
|
346
|
+
if (!hasViews(item)) return BASE_VIEW_KEY;
|
|
347
|
+
if (isKnownView(item, chosen)) return chosen;
|
|
348
|
+
if (isKnownView(item, item.defaultView)) return item.defaultView;
|
|
349
|
+
return BASE_VIEW_KEY;
|
|
350
|
+
}
|
|
351
|
+
function viewKind(item, chosen) {
|
|
352
|
+
return findView(item, effectiveViewKey(item, chosen))?.kind ?? item.kind;
|
|
353
|
+
}
|
|
354
|
+
function cellValue(cell) {
|
|
355
|
+
const value = cell?.value;
|
|
356
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
357
|
+
}
|
|
358
|
+
function columnName(table, index) {
|
|
359
|
+
return table.columns[index]?.label ?? `#${index + 1}`;
|
|
360
|
+
}
|
|
361
|
+
function checkColumn(table, index, field, context) {
|
|
362
|
+
if (!Number.isInteger(index) || index < 0 || index >= table.columns.length) {
|
|
363
|
+
return `${context.where}: ${field} column ${index} does not exist (the table has ${table.columns.length} columns, numbered from 0).`;
|
|
364
|
+
}
|
|
365
|
+
return void 0;
|
|
366
|
+
}
|
|
367
|
+
function numericColumn(table, index, context) {
|
|
368
|
+
const numbers = [];
|
|
369
|
+
for (const [rowIndex, row] of table.rows.entries()) {
|
|
370
|
+
const value = cellValue(row[index]);
|
|
371
|
+
if (value === void 0) {
|
|
372
|
+
return err(
|
|
373
|
+
`${context.where}: row ${rowIndex + 1}, column ${index + 1} ("${columnName(table, index)}") has no numeric value. Add value to the cells.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
numbers.push(value);
|
|
377
|
+
}
|
|
378
|
+
return ok(numbers);
|
|
379
|
+
}
|
|
380
|
+
function tableToBarList(table, conversion, context) {
|
|
381
|
+
const bad = checkColumn(table, conversion.label, "label", context) ?? checkColumn(table, conversion.value, "value", context);
|
|
382
|
+
if (bad !== void 0) return err(bad);
|
|
383
|
+
const numbers = numericColumn(table, conversion.value, context);
|
|
384
|
+
if (!numbers.ok) return numbers;
|
|
385
|
+
const items = table.rows.map((row, index) => ({
|
|
386
|
+
label: row[conversion.label]?.text ?? "",
|
|
387
|
+
value: numbers.value[index],
|
|
388
|
+
display: row[conversion.value]?.text ?? String(numbers.value[index])
|
|
389
|
+
}));
|
|
390
|
+
return ok({ kind: "BAR_LIST", items });
|
|
391
|
+
}
|
|
392
|
+
function tableToGraph(table, conversion, context) {
|
|
393
|
+
if (conversion.values.length === 0 || conversion.values.length > MAX_GRAPH_SERIES) {
|
|
394
|
+
return err(
|
|
395
|
+
`${context.where}: a chart needs 1 to ${MAX_GRAPH_SERIES} series columns, got ${conversion.values.length}.`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
let bad = checkColumn(
|
|
399
|
+
table,
|
|
400
|
+
conversion.label,
|
|
401
|
+
"label",
|
|
402
|
+
context
|
|
403
|
+
);
|
|
404
|
+
for (const index of conversion.values) {
|
|
405
|
+
bad ??= checkColumn(table, index, "series", context);
|
|
406
|
+
}
|
|
407
|
+
if (bad !== void 0) return err(bad);
|
|
408
|
+
const series = [];
|
|
409
|
+
for (const index of conversion.values) {
|
|
410
|
+
const numbers = numericColumn(table, index, context);
|
|
411
|
+
if (!numbers.ok) return numbers;
|
|
412
|
+
series.push({
|
|
413
|
+
name: columnName(table, index),
|
|
414
|
+
points: table.rows.map((row, rowIndex) => ({
|
|
415
|
+
label: row[conversion.label]?.text ?? "",
|
|
416
|
+
value: numbers.value[rowIndex]
|
|
417
|
+
}))
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return ok({
|
|
421
|
+
kind: "GRAPH",
|
|
422
|
+
series,
|
|
423
|
+
chartType: conversion.chartType,
|
|
424
|
+
valueFormat: conversion.valueFormat ?? "number",
|
|
425
|
+
...conversion.currency === void 0 ? {} : { currency: conversion.currency },
|
|
426
|
+
xLabel: columnName(table, conversion.label)
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
function barListToTable(list, conversion) {
|
|
430
|
+
return ok({
|
|
431
|
+
kind: "TABLE",
|
|
432
|
+
columns: [
|
|
433
|
+
{ label: conversion.labelHeading ?? "Name" },
|
|
434
|
+
{ label: conversion.valueHeading ?? "Value", numeric: true }
|
|
435
|
+
],
|
|
436
|
+
rows: list.items.map((item) => [
|
|
437
|
+
{ text: item.label },
|
|
438
|
+
{ text: item.display ?? String(item.value), value: item.value }
|
|
439
|
+
])
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function graphToTable(graph, context) {
|
|
443
|
+
const first = graph.series[0];
|
|
444
|
+
if (first === void 0)
|
|
445
|
+
return err(`${context.where}: the chart has no series.`);
|
|
446
|
+
const format = {
|
|
447
|
+
...graph.currency === void 0 ? {} : { currency: graph.currency },
|
|
448
|
+
...context.locale === void 0 ? {} : { locale: context.locale }
|
|
449
|
+
};
|
|
450
|
+
return ok({
|
|
451
|
+
kind: "TABLE",
|
|
452
|
+
columns: [
|
|
453
|
+
{ label: graph.xLabel ?? "Label" },
|
|
454
|
+
...graph.series.map((series) => ({ label: series.name, numeric: true }))
|
|
455
|
+
],
|
|
456
|
+
rows: first.points.map((point, index) => [
|
|
457
|
+
{ text: point.label },
|
|
458
|
+
...graph.series.map((series) => {
|
|
459
|
+
const value = series.points[index]?.value;
|
|
460
|
+
return value === void 0 ? { text: "\u2014" } : { text: formatValue(value, graph.valueFormat, format), value };
|
|
461
|
+
})
|
|
462
|
+
])
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
function barListToGraph(list, conversion) {
|
|
466
|
+
return ok({
|
|
467
|
+
kind: "GRAPH",
|
|
468
|
+
series: [
|
|
469
|
+
{
|
|
470
|
+
name: conversion.seriesName ?? "Value",
|
|
471
|
+
points: list.items.map((item) => ({
|
|
472
|
+
label: item.label,
|
|
473
|
+
value: item.value
|
|
474
|
+
}))
|
|
475
|
+
}
|
|
476
|
+
],
|
|
477
|
+
chartType: conversion.chartType,
|
|
478
|
+
valueFormat: conversion.valueFormat ?? "number",
|
|
479
|
+
...conversion.currency === void 0 ? {} : { currency: conversion.currency }
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function convertView(widgetKey, view, data, options = {}) {
|
|
483
|
+
const conversion = view.convert;
|
|
484
|
+
const context = {
|
|
485
|
+
where: `Widget ${JSON.stringify(widgetKey)} view ${JSON.stringify(view.key)}`,
|
|
486
|
+
...options.locale === void 0 ? {} : { locale: options.locale }
|
|
487
|
+
};
|
|
488
|
+
const expected = VIEW_CONVERSIONS[conversion.type]?.from;
|
|
489
|
+
if (expected === void 0) {
|
|
490
|
+
return err(
|
|
491
|
+
`${context.where}: unknown conversion type ${JSON.stringify(conversion.type)}.`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
if (data.kind !== expected) {
|
|
495
|
+
return err(
|
|
496
|
+
`${context.where}: ${conversion.type} reads a ${expected} payload, but the widget returned ${data.kind}.`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
switch (conversion.type) {
|
|
500
|
+
case "tableToBarList":
|
|
501
|
+
return tableToBarList(data, conversion, context);
|
|
502
|
+
case "tableToGraph":
|
|
503
|
+
return tableToGraph(data, conversion, context);
|
|
504
|
+
case "barListToTable":
|
|
505
|
+
return barListToTable(data, conversion);
|
|
506
|
+
case "graphToTable":
|
|
507
|
+
return graphToTable(data, context);
|
|
508
|
+
case "barListToGraph":
|
|
509
|
+
return barListToGraph(data, conversion);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function viewNote(view, data) {
|
|
513
|
+
if (data?.kind !== "TABLE") return "";
|
|
514
|
+
const conversion = view.convert;
|
|
515
|
+
let indexes;
|
|
516
|
+
if (conversion.type === "tableToBarList") {
|
|
517
|
+
indexes = [conversion.label, conversion.value];
|
|
518
|
+
} else if (conversion.type === "tableToGraph") {
|
|
519
|
+
indexes = [conversion.label, ...conversion.values];
|
|
520
|
+
} else {
|
|
521
|
+
return "";
|
|
522
|
+
}
|
|
523
|
+
const names = indexes.map(
|
|
524
|
+
(index) => data.columns[index]?.label ?? `#${index + 1}`
|
|
525
|
+
);
|
|
526
|
+
return `Shows ${names.join(", ")} only.`;
|
|
527
|
+
}
|
|
528
|
+
function applyView(widget, chosen, options = {}) {
|
|
529
|
+
const key = effectiveViewKey(widget.definition, chosen);
|
|
530
|
+
const view = findView(widget.definition, key);
|
|
531
|
+
if (view === void 0) return widget;
|
|
532
|
+
const definition = {
|
|
533
|
+
...widget.definition,
|
|
534
|
+
kind: view.kind
|
|
535
|
+
};
|
|
536
|
+
if (widget.status === "loading") return { ...widget, definition };
|
|
537
|
+
if (widget.status === "empty") return { ...widget, definition };
|
|
538
|
+
if (widget.status === "error") return { ...widget, definition };
|
|
539
|
+
const converted = convertView(
|
|
540
|
+
widget.definition.key,
|
|
541
|
+
view,
|
|
542
|
+
widget.data,
|
|
543
|
+
options
|
|
544
|
+
);
|
|
545
|
+
if (!converted.ok) {
|
|
546
|
+
return {
|
|
547
|
+
definition,
|
|
548
|
+
status: "error",
|
|
549
|
+
error: converted.error,
|
|
550
|
+
...widget.options === void 0 ? {} : { options: widget.options }
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return { ...widget, definition, data: converted.value };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/core/url.ts
|
|
557
|
+
var SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
|
|
558
|
+
var IGNORED_IN_SCHEME = /[\u0000- \u007f]/g;
|
|
559
|
+
function schemeOf(url) {
|
|
560
|
+
const match = SCHEME_PATTERN.exec(
|
|
561
|
+
url.replace(IGNORED_IN_SCHEME, "")
|
|
562
|
+
);
|
|
563
|
+
return match === null ? null : (match[1] ?? "").toLowerCase();
|
|
564
|
+
}
|
|
565
|
+
function isSafeHref(url) {
|
|
566
|
+
if (url.trim() === "") {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
const scheme = schemeOf(url);
|
|
570
|
+
return scheme === null || scheme === "http" || scheme === "https" || scheme === "mailto";
|
|
571
|
+
}
|
|
572
|
+
function isSafeImageUrl(url) {
|
|
573
|
+
if (url.trim() === "") {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
const scheme = schemeOf(url);
|
|
577
|
+
return scheme === null || scheme === "http" || scheme === "https";
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/core/detail.ts
|
|
581
|
+
var DETAIL_MODES = ["client", "server"];
|
|
582
|
+
var DEFAULT_DETAIL_PAGE_SIZE = 25;
|
|
583
|
+
var MAX_DETAIL_PAGE_SIZE = 200;
|
|
584
|
+
function resolveDetailOptions(definition) {
|
|
585
|
+
const detail = definition.detail;
|
|
586
|
+
if (detail === void 0 || detail === false) return void 0;
|
|
587
|
+
const options = detail === true ? {} : detail;
|
|
588
|
+
return {
|
|
589
|
+
title: options.title ?? definition.title,
|
|
590
|
+
pageSize: options.pageSize ?? DEFAULT_DETAIL_PAGE_SIZE,
|
|
275
591
|
mode: options.mode ?? "client"
|
|
276
592
|
};
|
|
277
593
|
}
|
|
@@ -291,15 +607,15 @@ function defaultDetailQuery(pageSize = DEFAULT_DETAIL_PAGE_SIZE) {
|
|
|
291
607
|
function fold(text) {
|
|
292
608
|
return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
293
609
|
}
|
|
294
|
-
function
|
|
610
|
+
function cellValue2(cell) {
|
|
295
611
|
return cell.value ?? cell.text;
|
|
296
612
|
}
|
|
297
613
|
function cellMatches(cell, needle) {
|
|
298
|
-
return fold(String(
|
|
614
|
+
return fold(String(cellValue2(cell))).includes(needle);
|
|
299
615
|
}
|
|
300
616
|
function compareCells(a, b, collator) {
|
|
301
|
-
const left =
|
|
302
|
-
const right =
|
|
617
|
+
const left = cellValue2(a);
|
|
618
|
+
const right = cellValue2(b);
|
|
303
619
|
if (typeof left === "number" && typeof right === "number") {
|
|
304
620
|
return left - right;
|
|
305
621
|
}
|
|
@@ -591,103 +907,6 @@ function deriveDetailData(data, locale) {
|
|
|
591
907
|
return void 0;
|
|
592
908
|
}
|
|
593
909
|
|
|
594
|
-
// src/core/grid.ts
|
|
595
|
-
var WIDTH_COLUMNS = 12;
|
|
596
|
-
var MIN_WIDGET_WIDTH = 2;
|
|
597
|
-
var MAX_WIDGET_WIDTH = 12;
|
|
598
|
-
function isWidgetWidth(value) {
|
|
599
|
-
return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
|
|
600
|
-
}
|
|
601
|
-
function widthForSize(size) {
|
|
602
|
-
switch (size) {
|
|
603
|
-
case "small":
|
|
604
|
-
return 3;
|
|
605
|
-
case "large":
|
|
606
|
-
return 6;
|
|
607
|
-
case "full":
|
|
608
|
-
return 12;
|
|
609
|
-
default:
|
|
610
|
-
return 4;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
function itemWidth(item) {
|
|
614
|
-
return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
|
|
615
|
-
}
|
|
616
|
-
function usesWidthColumns(items) {
|
|
617
|
-
return items.some((item) => item.width !== void 0);
|
|
618
|
-
}
|
|
619
|
-
function fillsWidth(fill) {
|
|
620
|
-
return fill === "width" || fill === "both";
|
|
621
|
-
}
|
|
622
|
-
function fillsHeight(fill) {
|
|
623
|
-
return fill === "height" || fill === "both";
|
|
624
|
-
}
|
|
625
|
-
function baseColumnSpan(size, columns, largeSpan = 2) {
|
|
626
|
-
if (size === "full") return columns;
|
|
627
|
-
if (size === "large") return Math.min(largeSpan, columns);
|
|
628
|
-
return 1;
|
|
629
|
-
}
|
|
630
|
-
function fillColumnSpans(items, columns, largeSpan = 2) {
|
|
631
|
-
return placeAndFill(
|
|
632
|
-
items,
|
|
633
|
-
columns,
|
|
634
|
-
(item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
function fillWidthSpans(items) {
|
|
638
|
-
return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
|
|
639
|
-
}
|
|
640
|
-
function placeRows(items) {
|
|
641
|
-
return placeAndFillRows(items, WIDTH_COLUMNS, itemWidth).rows;
|
|
642
|
-
}
|
|
643
|
-
function fitCount(items, maxRows) {
|
|
644
|
-
const rows = placeRows(items);
|
|
645
|
-
let count = 0;
|
|
646
|
-
for (const row of rows.slice(0, Math.max(0, Math.floor(maxRows)))) {
|
|
647
|
-
count += row.keys.length;
|
|
648
|
-
}
|
|
649
|
-
return count;
|
|
650
|
-
}
|
|
651
|
-
function placeAndFill(items, columns, spanOf) {
|
|
652
|
-
return placeAndFillRows(items, columns, spanOf).spans;
|
|
653
|
-
}
|
|
654
|
-
function placeAndFillRows(items, columns, spanOf) {
|
|
655
|
-
const trackCount = Math.max(1, Math.floor(columns));
|
|
656
|
-
const spans = /* @__PURE__ */ new Map();
|
|
657
|
-
const rows = [];
|
|
658
|
-
let row = [];
|
|
659
|
-
let used = 0;
|
|
660
|
-
function closeRow() {
|
|
661
|
-
if (row.length === 0) return;
|
|
662
|
-
const fillers = row.filter(({ item }) => fillsWidth(item.fill));
|
|
663
|
-
const leftover = trackCount - used;
|
|
664
|
-
const finalSpans = /* @__PURE__ */ new Map();
|
|
665
|
-
if (fillers.length > 0) {
|
|
666
|
-
const share = Math.floor(leftover / fillers.length);
|
|
667
|
-
const remainder = leftover % fillers.length;
|
|
668
|
-
fillers.forEach(({ item, span }, index) => {
|
|
669
|
-
const grown = span + share + (index < remainder ? 1 : 0);
|
|
670
|
-
spans.set(item.key, grown);
|
|
671
|
-
finalSpans.set(item.key, grown);
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
rows.push({
|
|
675
|
-
keys: row.map(({ item }) => item.key),
|
|
676
|
-
spans: row.map(({ item, span }) => finalSpans.get(item.key) ?? span)
|
|
677
|
-
});
|
|
678
|
-
row = [];
|
|
679
|
-
used = 0;
|
|
680
|
-
}
|
|
681
|
-
for (const item of items) {
|
|
682
|
-
const span = Math.min(spanOf(item, trackCount), trackCount);
|
|
683
|
-
if (used + span > trackCount && row.length > 0) closeRow();
|
|
684
|
-
row.push({ item, span });
|
|
685
|
-
used += span;
|
|
686
|
-
}
|
|
687
|
-
closeRow();
|
|
688
|
-
return { spans, rows };
|
|
689
|
-
}
|
|
690
|
-
|
|
691
910
|
// src/core/options.ts
|
|
692
911
|
var DATE_RANGE_PRESETS = [
|
|
693
912
|
"today",
|
|
@@ -993,6 +1212,7 @@ function parseOne(raw) {
|
|
|
993
1212
|
const width = raw["width"];
|
|
994
1213
|
const flexible = raw["flexible"];
|
|
995
1214
|
const options = raw["options"];
|
|
1215
|
+
const view = raw["view"];
|
|
996
1216
|
const cleanTitle = typeof title === "string" ? title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH) : "";
|
|
997
1217
|
const chosen = {};
|
|
998
1218
|
if (isRecord(options)) {
|
|
@@ -1005,7 +1225,8 @@ function parseOne(raw) {
|
|
|
1005
1225
|
...cleanTitle === "" ? {} : { title: cleanTitle },
|
|
1006
1226
|
...isWidgetWidth(width) ? { width } : {},
|
|
1007
1227
|
...typeof flexible === "boolean" ? { flexible } : {},
|
|
1008
|
-
...Object.keys(chosen).length === 0 ? {} : { options: chosen }
|
|
1228
|
+
...Object.keys(chosen).length === 0 ? {} : { options: chosen },
|
|
1229
|
+
...typeof view === "string" && view !== "" && view.length <= 80 ? { view } : {}
|
|
1009
1230
|
};
|
|
1010
1231
|
return Object.keys(settings).length === 0 ? void 0 : settings;
|
|
1011
1232
|
}
|
|
@@ -1032,7 +1253,8 @@ function serializeSettings(settings) {
|
|
|
1032
1253
|
...one.title === void 0 ? {} : { title: one.title },
|
|
1033
1254
|
...one.width === void 0 ? {} : { width: one.width },
|
|
1034
1255
|
...one.flexible === void 0 ? {} : { flexible: one.flexible },
|
|
1035
|
-
...one.options === void 0 || Object.keys(one.options).length === 0 ? {} : { options: { ...one.options } }
|
|
1256
|
+
...one.options === void 0 || Object.keys(one.options).length === 0 ? {} : { options: { ...one.options } },
|
|
1257
|
+
...one.view === void 0 ? {} : { view: one.view }
|
|
1036
1258
|
};
|
|
1037
1259
|
if (Object.keys(piece).length > 0) out[key] = piece;
|
|
1038
1260
|
}
|
|
@@ -1124,12 +1346,18 @@ function optionValuesFromLayout(definitions, layout, options = {}) {
|
|
|
1124
1346
|
}
|
|
1125
1347
|
function sameSettings(a, b) {
|
|
1126
1348
|
if (a === b) return true;
|
|
1127
|
-
return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && sameOptionValues(a?.options ?? {}, b?.options ?? {});
|
|
1349
|
+
return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && a?.view === b?.view && sameOptionValues(a?.options ?? {}, b?.options ?? {});
|
|
1128
1350
|
}
|
|
1129
1351
|
function setWidgetSettings(layout, key, patch) {
|
|
1130
1352
|
const before = layout.settings?.[key];
|
|
1131
1353
|
const draft = { ...before };
|
|
1132
|
-
for (const field of [
|
|
1354
|
+
for (const field of [
|
|
1355
|
+
"title",
|
|
1356
|
+
"width",
|
|
1357
|
+
"flexible",
|
|
1358
|
+
"options",
|
|
1359
|
+
"view"
|
|
1360
|
+
]) {
|
|
1133
1361
|
if (!(field in patch)) continue;
|
|
1134
1362
|
const value = patch[field];
|
|
1135
1363
|
if (value === void 0) {
|
|
@@ -1155,7 +1383,8 @@ function clearWidgetSettings(layout, key) {
|
|
|
1155
1383
|
title: void 0,
|
|
1156
1384
|
width: void 0,
|
|
1157
1385
|
flexible: void 0,
|
|
1158
|
-
options: void 0
|
|
1386
|
+
options: void 0,
|
|
1387
|
+
view: void 0
|
|
1159
1388
|
});
|
|
1160
1389
|
}
|
|
1161
1390
|
function normalizeTitle(title) {
|
|
@@ -1215,7 +1444,8 @@ function normalizeSettings(definitions, layout, options = {}) {
|
|
|
1215
1444
|
...title === void 0 ? {} : { title },
|
|
1216
1445
|
...one.width === void 0 ? {} : { width: Math.min(range.max, Math.max(range.min, one.width)) },
|
|
1217
1446
|
...one.flexible === void 0 ? {} : { flexible: one.flexible },
|
|
1218
|
-
...Object.keys(chosen).length === 0 ? {} : { options: chosen }
|
|
1447
|
+
...Object.keys(chosen).length === 0 ? {} : { options: chosen },
|
|
1448
|
+
...one.view !== void 0 && (one.view === BASE_VIEW_KEY || findView(definition, one.view) !== void 0) ? { view: one.view } : {}
|
|
1219
1449
|
};
|
|
1220
1450
|
if (Object.keys(next).length > 0) {
|
|
1221
1451
|
cleaned[definition.key] = sameSettings(one, next) ? one : next;
|
|
@@ -1228,57 +1458,184 @@ function normalizeSettings(definitions, layout, options = {}) {
|
|
|
1228
1458
|
return keys.length === 0 ? others : { ...others, settings: cleaned };
|
|
1229
1459
|
}
|
|
1230
1460
|
|
|
1231
|
-
// src/core/
|
|
1232
|
-
var
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
minimized: Object.freeze([])
|
|
1239
|
-
});
|
|
1240
|
-
function uniqueStrings(value) {
|
|
1241
|
-
if (!Array.isArray(value)) return [];
|
|
1461
|
+
// src/core/clones.ts
|
|
1462
|
+
var MAX_PARSED_CLONES = 500;
|
|
1463
|
+
function isRecord2(value) {
|
|
1464
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1465
|
+
}
|
|
1466
|
+
function parseClones(raw) {
|
|
1467
|
+
if (!Array.isArray(raw)) return void 0;
|
|
1242
1468
|
const seen = /* @__PURE__ */ new Set();
|
|
1243
|
-
|
|
1244
|
-
|
|
1469
|
+
const clones = [];
|
|
1470
|
+
for (const entry of raw.slice(0, MAX_PARSED_CLONES)) {
|
|
1471
|
+
if (!isRecord2(entry)) continue;
|
|
1472
|
+
const key = entry["key"];
|
|
1473
|
+
const from = entry["from"];
|
|
1474
|
+
if (typeof key !== "string" || typeof from !== "string" || key === "" || from === "" || key === from || seen.has(key)) {
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
seen.add(key);
|
|
1478
|
+
clones.push({ key, from });
|
|
1245
1479
|
}
|
|
1246
|
-
return
|
|
1480
|
+
return clones.length === 0 ? void 0 : clones;
|
|
1247
1481
|
}
|
|
1248
|
-
function
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1482
|
+
function serializeClones(clones) {
|
|
1483
|
+
if (clones === void 0 || clones.length === 0) return void 0;
|
|
1484
|
+
return clones.map(({ key, from }) => ({ key, from }));
|
|
1485
|
+
}
|
|
1486
|
+
function cloneNumber(key) {
|
|
1487
|
+
const match = /#(\d+)$/.exec(key);
|
|
1488
|
+
return match === null ? void 0 : Number(match[1]);
|
|
1489
|
+
}
|
|
1490
|
+
function cleanClones(definitions, clones, options = {}) {
|
|
1491
|
+
if (clones === void 0) return void 0;
|
|
1492
|
+
const byKey = new Map(definitions.map((d) => [d.key, d]));
|
|
1493
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1494
|
+
const kept = [];
|
|
1495
|
+
for (const clone of clones) {
|
|
1496
|
+
if (seen.has(clone.key)) continue;
|
|
1497
|
+
const original = byKey.get(clone.from);
|
|
1498
|
+
if (original === void 0 || original.sourceKey !== void 0) continue;
|
|
1499
|
+
const existing = byKey.get(clone.key);
|
|
1500
|
+
if (existing !== void 0 && existing.sourceKey !== clone.from) continue;
|
|
1501
|
+
if (options.overrideLocks !== true && resolveWidgetLock(original).clone) {
|
|
1502
|
+
continue;
|
|
1255
1503
|
}
|
|
1504
|
+
seen.add(clone.key);
|
|
1505
|
+
kept.push(clone);
|
|
1506
|
+
}
|
|
1507
|
+
if (kept.length === 0) return void 0;
|
|
1508
|
+
return kept.length === clones.length && kept.every((c, i) => c === clones[i]) ? clones : kept;
|
|
1509
|
+
}
|
|
1510
|
+
function defaultCloneTitle(original, clone, index) {
|
|
1511
|
+
const base = original.title ?? original.key;
|
|
1512
|
+
const suffix = ` (#${cloneNumber(clone.key) ?? index + 1})`;
|
|
1513
|
+
return `${base.slice(0, MAX_WIDGET_TITLE_LENGTH - suffix.length)}${suffix}`;
|
|
1514
|
+
}
|
|
1515
|
+
function withClones(definitions, layout, options = {}) {
|
|
1516
|
+
const valid = cleanClones(
|
|
1517
|
+
definitions,
|
|
1518
|
+
layout.clones,
|
|
1519
|
+
options
|
|
1520
|
+
);
|
|
1521
|
+
if (valid === void 0) return definitions;
|
|
1522
|
+
const present = new Set(definitions.map((d) => d.key));
|
|
1523
|
+
const toAdd = valid.filter((c) => !present.has(c.key));
|
|
1524
|
+
if (toAdd.length === 0) return definitions;
|
|
1525
|
+
const out = [];
|
|
1526
|
+
for (const definition of definitions) {
|
|
1527
|
+
out.push(definition);
|
|
1528
|
+
toAdd.forEach((clone, index) => {
|
|
1529
|
+
if (clone.from !== definition.key) return;
|
|
1530
|
+
const copy = { ...definition };
|
|
1531
|
+
copy.key = clone.key;
|
|
1532
|
+
copy.sourceKey = definition.key;
|
|
1533
|
+
copy.title = defaultCloneTitle(definition, clone, index);
|
|
1534
|
+
delete copy.locked;
|
|
1535
|
+
out.push(copy);
|
|
1536
|
+
});
|
|
1256
1537
|
}
|
|
1538
|
+
return out;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/core/compat.ts
|
|
1542
|
+
function isStringArray(value) {
|
|
1543
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
1544
|
+
}
|
|
1545
|
+
function upgradeLegacyWidgetData(value) {
|
|
1257
1546
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1258
|
-
return
|
|
1547
|
+
return value;
|
|
1259
1548
|
}
|
|
1260
1549
|
const record = value;
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
if (
|
|
1550
|
+
if (record["kind"] === "TABLE" && isStringArray(record["columns"]) && Array.isArray(record["rows"]) && record["rows"].every(isStringArray)) {
|
|
1551
|
+
const rows = record["rows"];
|
|
1552
|
+
return {
|
|
1553
|
+
...record,
|
|
1554
|
+
columns: record["columns"].map((label) => ({ label })),
|
|
1555
|
+
rows: rows.map((row) => row.map((text) => ({ text })))
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
if (record["kind"] === "GRAPH" && Array.isArray(record["points"]) && record["series"] === void 0) {
|
|
1559
|
+
const { points, ...rest } = record;
|
|
1560
|
+
return {
|
|
1561
|
+
...rest,
|
|
1562
|
+
series: [{ name: "Value", points }],
|
|
1563
|
+
chartType: "bar",
|
|
1564
|
+
valueFormat: "number"
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
return value;
|
|
1568
|
+
}
|
|
1569
|
+
function fromLegacyDefinition(legacy) {
|
|
1570
|
+
if (!isWidgetKind(legacy.viewType)) {
|
|
1571
|
+
return null;
|
|
1572
|
+
}
|
|
1573
|
+
return {
|
|
1574
|
+
key: legacy.widgetKey,
|
|
1575
|
+
title: legacy.title,
|
|
1576
|
+
...legacy.description === null ? {} : { description: legacy.description },
|
|
1577
|
+
kind: legacy.viewType,
|
|
1578
|
+
sortOrder: legacy.sortOrder
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// src/core/layout.ts
|
|
1583
|
+
var MAX_PAGE_TITLE_LENGTH = 30;
|
|
1584
|
+
var MAX_PARSED_PAGES = 50;
|
|
1585
|
+
var MAX_PAGE_ROWS = 20;
|
|
1586
|
+
var EMPTY_LAYOUT = Object.freeze({
|
|
1587
|
+
order: Object.freeze([]),
|
|
1588
|
+
hidden: Object.freeze([]),
|
|
1589
|
+
minimized: Object.freeze([])
|
|
1590
|
+
});
|
|
1591
|
+
function uniqueStrings(value) {
|
|
1592
|
+
if (!Array.isArray(value)) return [];
|
|
1593
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1594
|
+
for (const entry of value) {
|
|
1595
|
+
if (typeof entry === "string" && entry !== "") seen.add(entry);
|
|
1596
|
+
}
|
|
1597
|
+
return [...seen];
|
|
1598
|
+
}
|
|
1599
|
+
function parseLayout(raw) {
|
|
1600
|
+
let value = raw;
|
|
1601
|
+
if (typeof raw === "string") {
|
|
1602
|
+
try {
|
|
1603
|
+
value = JSON.parse(raw);
|
|
1604
|
+
} catch {
|
|
1605
|
+
return EMPTY_LAYOUT;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1609
|
+
return EMPTY_LAYOUT;
|
|
1610
|
+
}
|
|
1611
|
+
const record = value;
|
|
1612
|
+
const order = uniqueStrings(record["order"]);
|
|
1613
|
+
const hidden = uniqueStrings(record["hidden"]);
|
|
1614
|
+
const minimized = uniqueStrings(record["minimized"]);
|
|
1615
|
+
const pages = parsePages(record["pages"]);
|
|
1616
|
+
const settings = parseSettings(
|
|
1617
|
+
record["settings"]
|
|
1618
|
+
);
|
|
1619
|
+
const clones = parseClones(
|
|
1620
|
+
record["clones"]
|
|
1621
|
+
);
|
|
1622
|
+
const extras = {
|
|
1623
|
+
...settings === void 0 ? {} : { settings },
|
|
1624
|
+
...clones === void 0 ? {} : { clones }
|
|
1625
|
+
};
|
|
1626
|
+
if (pages.length > 0) {
|
|
1270
1627
|
return {
|
|
1271
1628
|
order: EMPTY_LAYOUT.order,
|
|
1272
1629
|
hidden: EMPTY_LAYOUT.hidden,
|
|
1273
1630
|
minimized: EMPTY_LAYOUT.minimized,
|
|
1274
1631
|
pages,
|
|
1275
|
-
...
|
|
1632
|
+
...extras
|
|
1276
1633
|
};
|
|
1277
1634
|
}
|
|
1278
1635
|
if (order.length === 0 && hidden.length === 0 && minimized.length === 0) {
|
|
1279
|
-
return
|
|
1636
|
+
return Object.keys(extras).length === 0 ? EMPTY_LAYOUT : { ...EMPTY_LAYOUT, ...extras };
|
|
1280
1637
|
}
|
|
1281
|
-
return { order, hidden, minimized, ...
|
|
1638
|
+
return { order, hidden, minimized, ...extras };
|
|
1282
1639
|
}
|
|
1283
1640
|
function parsePages(value) {
|
|
1284
1641
|
if (!Array.isArray(value)) return [];
|
|
@@ -1326,7 +1683,8 @@ function serializeLayout(layout) {
|
|
|
1326
1683
|
...page.maxRows === void 0 ? {} : { maxRows: page.maxRows }
|
|
1327
1684
|
}))
|
|
1328
1685
|
},
|
|
1329
|
-
...serializeSettings(layout.settings) === void 0 ? {} : { settings: serializeSettings(layout.settings) }
|
|
1686
|
+
...serializeSettings(layout.settings) === void 0 ? {} : { settings: serializeSettings(layout.settings) },
|
|
1687
|
+
...serializeClones(layout.clones) === void 0 ? {} : { clones: serializeClones(layout.clones) }
|
|
1330
1688
|
});
|
|
1331
1689
|
}
|
|
1332
1690
|
function bySortOrder(items) {
|
|
@@ -1491,1482 +1849,1746 @@ function enforceLocks(definitions, layout, options = {}) {
|
|
|
1491
1849
|
}));
|
|
1492
1850
|
}
|
|
1493
1851
|
|
|
1494
|
-
// src/core/
|
|
1495
|
-
var
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
return String(value);
|
|
1501
|
-
}
|
|
1502
|
-
return typeof value;
|
|
1503
|
-
}
|
|
1504
|
-
function isRecord2(value) {
|
|
1505
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1852
|
+
// src/core/pages.ts
|
|
1853
|
+
var DEFAULT_PAGE_KEY = "default";
|
|
1854
|
+
var DEFAULT_PAGE_ROWS = 4;
|
|
1855
|
+
function rowsAllowed(page, options) {
|
|
1856
|
+
const rows = page.maxRows ?? options.maxRows ?? DEFAULT_PAGE_ROWS;
|
|
1857
|
+
return Math.max(1, Math.min(MAX_PAGE_ROWS, Math.floor(rows)));
|
|
1506
1858
|
}
|
|
1507
|
-
function
|
|
1508
|
-
return
|
|
1859
|
+
function hasPages(layout) {
|
|
1860
|
+
return layout.pages !== void 0 && layout.pages.length > 0;
|
|
1509
1861
|
}
|
|
1510
|
-
function
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
problems.push(
|
|
1514
|
-
`${join(path, field)} must be a string, got ${describeType2(value)}.`
|
|
1515
|
-
);
|
|
1862
|
+
function pageList(layout) {
|
|
1863
|
+
if (layout.pages !== void 0 && layout.pages.length > 0) {
|
|
1864
|
+
return layout.pages;
|
|
1516
1865
|
}
|
|
1866
|
+
return [
|
|
1867
|
+
{
|
|
1868
|
+
key: DEFAULT_PAGE_KEY,
|
|
1869
|
+
title: "Page 1",
|
|
1870
|
+
order: layout.order,
|
|
1871
|
+
hidden: layout.hidden,
|
|
1872
|
+
minimized: layout.minimized
|
|
1873
|
+
}
|
|
1874
|
+
];
|
|
1517
1875
|
}
|
|
1518
|
-
function
|
|
1519
|
-
if (
|
|
1520
|
-
|
|
1521
|
-
|
|
1876
|
+
function withExplicitPages(layout) {
|
|
1877
|
+
if (hasPages(layout)) return layout;
|
|
1878
|
+
return {
|
|
1879
|
+
...layout,
|
|
1880
|
+
order: [],
|
|
1881
|
+
hidden: [],
|
|
1882
|
+
minimized: [],
|
|
1883
|
+
pages: pageList(layout)
|
|
1884
|
+
};
|
|
1522
1885
|
}
|
|
1523
|
-
function
|
|
1524
|
-
|
|
1525
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1526
|
-
problems.push(
|
|
1527
|
-
`${join(path, field)} must be a finite number, got ${describeType2(value)}.`
|
|
1528
|
-
);
|
|
1529
|
-
}
|
|
1886
|
+
function sameKeys(a, b) {
|
|
1887
|
+
return a === b || a.length === b.length && a.every((k, i) => k === b[i]);
|
|
1530
1888
|
}
|
|
1531
|
-
function
|
|
1532
|
-
if (
|
|
1533
|
-
|
|
1534
|
-
|
|
1889
|
+
function pageLayout(layout, pageKey) {
|
|
1890
|
+
if (!hasPages(layout))
|
|
1891
|
+
return pageKey === DEFAULT_PAGE_KEY ? layout : void 0;
|
|
1892
|
+
const page = layout.pages?.find(
|
|
1893
|
+
(p) => p.key === pageKey
|
|
1894
|
+
);
|
|
1895
|
+
if (page === void 0) return void 0;
|
|
1896
|
+
const { pages, ...rest } = layout;
|
|
1897
|
+
return {
|
|
1898
|
+
...rest,
|
|
1899
|
+
order: page.order,
|
|
1900
|
+
hidden: page.hidden,
|
|
1901
|
+
minimized: page.minimized
|
|
1902
|
+
};
|
|
1535
1903
|
}
|
|
1536
|
-
function
|
|
1537
|
-
const
|
|
1538
|
-
if (
|
|
1539
|
-
|
|
1540
|
-
`${path}.value must be a string or a finite number, got ${describeType2(value)}.`
|
|
1541
|
-
);
|
|
1904
|
+
function withPageLayout(layout, pageKey, next) {
|
|
1905
|
+
const current = pageLayout(layout, pageKey);
|
|
1906
|
+
if (current === void 0 || sameKeys(current.order, next.order) && sameKeys(current.hidden, next.hidden) && sameKeys(current.minimized, next.minimized)) {
|
|
1907
|
+
return layout;
|
|
1542
1908
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1909
|
+
if (!hasPages(layout)) {
|
|
1910
|
+
return {
|
|
1911
|
+
...layout,
|
|
1912
|
+
order: next.order,
|
|
1913
|
+
hidden: next.hidden,
|
|
1914
|
+
minimized: next.minimized
|
|
1915
|
+
};
|
|
1550
1916
|
}
|
|
1917
|
+
return {
|
|
1918
|
+
...layout,
|
|
1919
|
+
pages: (layout.pages ?? []).map(
|
|
1920
|
+
(page) => page.key === pageKey ? {
|
|
1921
|
+
...page,
|
|
1922
|
+
order: next.order,
|
|
1923
|
+
hidden: next.hidden,
|
|
1924
|
+
minimized: next.minimized
|
|
1925
|
+
} : page
|
|
1926
|
+
)
|
|
1927
|
+
};
|
|
1551
1928
|
}
|
|
1552
|
-
function
|
|
1553
|
-
|
|
1554
|
-
if (value === void 0) return;
|
|
1555
|
-
if (typeof value !== "string") {
|
|
1556
|
-
problems.push(
|
|
1557
|
-
`${join(path, field)} must be a string, got ${describeType2(value)}.`
|
|
1558
|
-
);
|
|
1559
|
-
} else if (!isSafe(value)) {
|
|
1560
|
-
problems.push(
|
|
1561
|
-
`${join(path, field)} must be ${allowed}, got ${JSON.stringify(value.slice(0, 80))}.`
|
|
1562
|
-
);
|
|
1563
|
-
}
|
|
1929
|
+
function normalizedTitle(title) {
|
|
1930
|
+
return title.trim().normalize("NFKC").toLowerCase();
|
|
1564
1931
|
}
|
|
1565
|
-
function
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1932
|
+
function resolvePageKey(layout, reference) {
|
|
1933
|
+
if (reference === void 0 || reference === "") return void 0;
|
|
1934
|
+
const pages = pageList(layout);
|
|
1935
|
+
const byKey = pages.find((p) => p.key === reference);
|
|
1936
|
+
if (byKey !== void 0) return byKey.key;
|
|
1937
|
+
const wanted = normalizedTitle(reference);
|
|
1938
|
+
return pages.find((p) => normalizedTitle(p.title) === wanted)?.key;
|
|
1939
|
+
}
|
|
1940
|
+
function homePageKey(layout, item) {
|
|
1941
|
+
return resolvePageKey(layout, item.page) ?? pageList(layout)[0].key;
|
|
1942
|
+
}
|
|
1943
|
+
function assignPages(definitions, layout) {
|
|
1944
|
+
const pages = pageList(layout);
|
|
1945
|
+
const assigned = new Map(pages.map((p) => [p.key, []]));
|
|
1946
|
+
for (const definition of definitions) {
|
|
1947
|
+
let pageKey;
|
|
1948
|
+
if (!resolveWidgetLock(definition).move) {
|
|
1949
|
+
pageKey = pages.find(
|
|
1950
|
+
(p) => p.order.includes(definition.key) || p.hidden.includes(definition.key)
|
|
1951
|
+
)?.key;
|
|
1952
|
+
}
|
|
1953
|
+
assigned.get(pageKey ?? homePageKey(layout, definition))?.push(definition);
|
|
1572
1954
|
}
|
|
1573
|
-
return
|
|
1955
|
+
return assigned;
|
|
1574
1956
|
}
|
|
1575
|
-
function
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
problems.push(
|
|
1579
|
-
`${join(path, field)} must be one of ${options.map((o) => `"${o}"`).join(", ")}, got ${typeof value === "string" ? JSON.stringify(value) : describeType2(value)}.`
|
|
1580
|
-
);
|
|
1957
|
+
function pageOf(definitions, layout, key) {
|
|
1958
|
+
for (const [pageKey, widgets] of assignPages(definitions, layout)) {
|
|
1959
|
+
if (widgets.some((w) => w.key === key)) return pageKey;
|
|
1581
1960
|
}
|
|
1961
|
+
return void 0;
|
|
1582
1962
|
}
|
|
1583
|
-
function
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
check(item, itemPath);
|
|
1588
|
-
} else {
|
|
1589
|
-
problems.push(
|
|
1590
|
-
`${itemPath} must be an object, got ${describeType2(item)}.`
|
|
1591
|
-
);
|
|
1592
|
-
}
|
|
1593
|
-
});
|
|
1963
|
+
function pageWidgets(definitions, layout, pageKey) {
|
|
1964
|
+
const onPage = assignPages(definitions, layout).get(pageKey) ?? [];
|
|
1965
|
+
const view = pageLayout(layout, pageKey);
|
|
1966
|
+
return view === void 0 ? [] : visibleWidgets(onPage, view);
|
|
1594
1967
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
if (data["previous"] !== null) {
|
|
1606
|
-
optionalNumber(data, "previous", "", problems);
|
|
1607
|
-
}
|
|
1608
|
-
requireOneOf(data, "format", VALUE_FORMATS, "", problems);
|
|
1609
|
-
optionalString(data, "currency", "", problems);
|
|
1610
|
-
requireString(data, "label", "", problems);
|
|
1611
|
-
optionalString(data, "hint", "", problems);
|
|
1612
|
-
optionalBoolean(data, "higherIsBetter", "", problems);
|
|
1613
|
-
return;
|
|
1614
|
-
case "GAUGE":
|
|
1615
|
-
requireNumber(data, "value", "", problems);
|
|
1616
|
-
requireNumber(data, "max", "", problems);
|
|
1617
|
-
requireString(data, "label", "", problems);
|
|
1618
|
-
if (typeof data["max"] === "number" && data["max"] <= 0) {
|
|
1619
|
-
problems.push(`max must be greater than 0, got ${data["max"]}.`);
|
|
1620
|
-
}
|
|
1621
|
-
return;
|
|
1622
|
-
case "TABLE": {
|
|
1623
|
-
const columns = requireArray(data, "columns", "", problems);
|
|
1624
|
-
if (columns !== null) {
|
|
1625
|
-
eachRecord(columns, "columns", problems, (column, path) => {
|
|
1626
|
-
requireString(column, "label", path, problems);
|
|
1627
|
-
optionalBoolean(column, "numeric", path, problems);
|
|
1628
|
-
});
|
|
1629
|
-
}
|
|
1630
|
-
const rows = requireArray(data, "rows", "", problems);
|
|
1631
|
-
if (rows !== null) {
|
|
1632
|
-
rows.forEach((row, rowIndex) => {
|
|
1633
|
-
const rowPath = `rows[${rowIndex}]`;
|
|
1634
|
-
if (!Array.isArray(row)) {
|
|
1635
|
-
problems.push(
|
|
1636
|
-
`${rowPath} must be an array of cells, got ${describeType2(row)}.`
|
|
1637
|
-
);
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
if (columns !== null && row.length !== columns.length) {
|
|
1641
|
-
problems.push(
|
|
1642
|
-
`${rowPath} has ${row.length} cell(s) but there are ${columns.length} column(s).`
|
|
1643
|
-
);
|
|
1644
|
-
}
|
|
1645
|
-
eachRecord(row, rowPath, problems, (cell, cellPath) => {
|
|
1646
|
-
requireString(cell, "text", cellPath, problems);
|
|
1647
|
-
optionalSortValue(cell, cellPath, problems);
|
|
1648
|
-
optionalUrl(
|
|
1649
|
-
cell,
|
|
1650
|
-
"href",
|
|
1651
|
-
cellPath,
|
|
1652
|
-
problems,
|
|
1653
|
-
isSafeHref,
|
|
1654
|
-
HREF_RULE
|
|
1655
|
-
);
|
|
1656
|
-
});
|
|
1657
|
-
});
|
|
1658
|
-
}
|
|
1659
|
-
optionalString(data, "footer", "", problems);
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
case "BAR_LIST": {
|
|
1663
|
-
const items = requireArray(data, "items", "", problems);
|
|
1664
|
-
if (items !== null) {
|
|
1665
|
-
eachRecord(items, "items", problems, (item, path) => {
|
|
1666
|
-
requireString(item, "label", path, problems);
|
|
1667
|
-
requireNumber(item, "value", path, problems);
|
|
1668
|
-
optionalString(item, "display", path, problems);
|
|
1669
|
-
});
|
|
1670
|
-
}
|
|
1671
|
-
optionalNumber(data, "total", "", problems);
|
|
1672
|
-
return;
|
|
1673
|
-
}
|
|
1674
|
-
case "ALERT_LIST": {
|
|
1675
|
-
const items = requireArray(data, "items", "", problems);
|
|
1676
|
-
if (items !== null) {
|
|
1677
|
-
eachRecord(items, "items", problems, (item, path) => {
|
|
1678
|
-
requireString(item, "title", path, problems);
|
|
1679
|
-
optionalUrl(item, "href", path, problems, isSafeHref, HREF_RULE);
|
|
1680
|
-
optionalUrl(
|
|
1681
|
-
item,
|
|
1682
|
-
"thumbnailUrl",
|
|
1683
|
-
path,
|
|
1684
|
-
problems,
|
|
1685
|
-
isSafeImageUrl,
|
|
1686
|
-
IMAGE_RULE
|
|
1687
|
-
);
|
|
1688
|
-
requireString(item, "valueLabel", path, problems);
|
|
1689
|
-
optionalString(item, "detail", path, problems);
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
requireNumber(data, "total", "", problems);
|
|
1693
|
-
requireString(data, "emptyText", "", problems);
|
|
1694
|
-
if (items !== null && typeof data["total"] === "number" && data["total"] < items.length) {
|
|
1695
|
-
problems.push(
|
|
1696
|
-
`total (${data["total"]}) must be at least the number of items (${items.length}).`
|
|
1697
|
-
);
|
|
1698
|
-
}
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
|
-
case "GRAPH": {
|
|
1702
|
-
const series = requireArray(data, "series", "", problems);
|
|
1703
|
-
if (series !== null) {
|
|
1704
|
-
if (series.length > MAX_GRAPH_SERIES) {
|
|
1705
|
-
problems.push(
|
|
1706
|
-
`series has ${series.length} entries; at most ${MAX_GRAPH_SERIES} are allowed (fold the rest into "Other").`
|
|
1707
|
-
);
|
|
1708
|
-
}
|
|
1709
|
-
eachRecord(series, "series", problems, (entry, path) => {
|
|
1710
|
-
requireString(entry, "name", path, problems);
|
|
1711
|
-
const points = requireArray(entry, "points", path, problems);
|
|
1712
|
-
if (points !== null) {
|
|
1713
|
-
eachRecord(
|
|
1714
|
-
points,
|
|
1715
|
-
`${path}.points`,
|
|
1716
|
-
problems,
|
|
1717
|
-
(point, pointPath) => {
|
|
1718
|
-
requireString(point, "label", pointPath, problems);
|
|
1719
|
-
requireNumber(point, "value", pointPath, problems);
|
|
1720
|
-
}
|
|
1721
|
-
);
|
|
1722
|
-
}
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
|
-
requireOneOf(data, "chartType", ["bar", "line"], "", problems);
|
|
1726
|
-
requireOneOf(data, "valueFormat", VALUE_FORMATS, "", problems);
|
|
1727
|
-
optionalString(data, "currency", "", problems);
|
|
1728
|
-
optionalString(data, "xLabel", "", problems);
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1968
|
+
function pageRoom(definitions, layout, pageKey, options = {}) {
|
|
1969
|
+
const page = pageList(layout).find(
|
|
1970
|
+
(p) => p.key === pageKey
|
|
1971
|
+
);
|
|
1972
|
+
if (page === void 0) return void 0;
|
|
1973
|
+
const maxRows = rowsAllowed(page, options);
|
|
1974
|
+
const rowsUsed = placeRows(
|
|
1975
|
+
pageWidgets(definitions, layout, pageKey)
|
|
1976
|
+
).length;
|
|
1977
|
+
return { maxRows, rowsUsed, rowsFree: Math.max(0, maxRows - rowsUsed) };
|
|
1732
1978
|
}
|
|
1733
|
-
function
|
|
1734
|
-
|
|
1735
|
-
if (!isRecord2(detail)) {
|
|
1736
|
-
problems.push(
|
|
1737
|
-
`detail must be true, false or an options object, got ${describeType2(detail)}.`
|
|
1738
|
-
);
|
|
1739
|
-
return;
|
|
1740
|
-
}
|
|
1741
|
-
optionalString(detail, "title", "detail", problems);
|
|
1742
|
-
const pageSize = detail["pageSize"];
|
|
1743
|
-
if (pageSize !== void 0 && !(typeof pageSize === "number" && Number.isInteger(pageSize) && pageSize >= 1 && pageSize <= MAX_DETAIL_PAGE_SIZE)) {
|
|
1744
|
-
problems.push(
|
|
1745
|
-
`detail.pageSize must be a whole number from 1 to ${MAX_DETAIL_PAGE_SIZE}, got ${typeof pageSize === "number" ? String(pageSize) : describeType2(pageSize)}.`
|
|
1746
|
-
);
|
|
1747
|
-
}
|
|
1748
|
-
if (detail["mode"] !== void 0) {
|
|
1749
|
-
requireOneOf(detail, "mode", DETAIL_MODES, "detail", problems);
|
|
1750
|
-
}
|
|
1979
|
+
function describePage(page) {
|
|
1980
|
+
return `Page "${page.title}"`;
|
|
1751
1981
|
}
|
|
1752
|
-
function
|
|
1753
|
-
|
|
1754
|
-
if (typeof controls !== "boolean" && !isRecord2(controls)) {
|
|
1755
|
-
problems.push(
|
|
1756
|
-
`tableControls must be true, false or an options object, got ${describeType2(controls)}.`
|
|
1757
|
-
);
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
if (isRecord2(controls)) {
|
|
1761
|
-
optionalBoolean(controls, "search", "tableControls", problems);
|
|
1762
|
-
optionalBoolean(controls, "sort", "tableControls", problems);
|
|
1763
|
-
}
|
|
1764
|
-
if (typeof kind === "string" && kind !== "TABLE") {
|
|
1765
|
-
problems.push(
|
|
1766
|
-
`tableControls only applies to TABLE widgets, but kind is "${kind}".`
|
|
1767
|
-
);
|
|
1768
|
-
}
|
|
1982
|
+
function titleOf(item) {
|
|
1983
|
+
return item.title ?? item.key;
|
|
1769
1984
|
}
|
|
1770
|
-
function
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
);
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
const { minWidth, maxWidth, width } = value;
|
|
1781
|
-
if (isWidgetWidth(minWidth) && isWidgetWidth(maxWidth) && minWidth > maxWidth) {
|
|
1782
|
-
problems.push(
|
|
1783
|
-
`minWidth (${minWidth}) must not be more than maxWidth (${maxWidth}).`
|
|
1985
|
+
function checkTitle(layout, title, exceptKey) {
|
|
1986
|
+
const trimmed = title.trim();
|
|
1987
|
+
if (trimmed === "") return err("A page title must not be empty.");
|
|
1988
|
+
if (trimmed.length > MAX_PAGE_TITLE_LENGTH) {
|
|
1989
|
+
return err(
|
|
1990
|
+
`Page title "${trimmed}" is ${trimmed.length} characters; the most is ${MAX_PAGE_TITLE_LENGTH}.`
|
|
1784
1991
|
);
|
|
1785
|
-
return;
|
|
1786
1992
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
}
|
|
1793
|
-
if (isWidgetWidth(maxWidth) && width > maxWidth) {
|
|
1794
|
-
problems.push(
|
|
1795
|
-
`width (${width}) must not be more than maxWidth (${maxWidth}).`
|
|
1796
|
-
);
|
|
1797
|
-
}
|
|
1993
|
+
const wanted = normalizedTitle(trimmed);
|
|
1994
|
+
const clash = pageList(layout).find(
|
|
1995
|
+
(p) => p.key !== exceptKey && normalizedTitle(p.title) === wanted
|
|
1996
|
+
);
|
|
1997
|
+
if (clash !== void 0) {
|
|
1998
|
+
return err(`Another page is already called "${clash.title}".`);
|
|
1798
1999
|
}
|
|
2000
|
+
return ok(trimmed);
|
|
1799
2001
|
}
|
|
1800
|
-
function
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
);
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
|
-
optionalBoolean(locked, "move", "locked", problems);
|
|
1809
|
-
optionalBoolean(locked, "hide", "locked", problems);
|
|
1810
|
-
optionalBoolean(locked, "minimize", "locked", problems);
|
|
1811
|
-
optionalBoolean(locked, "options", "locked", problems);
|
|
2002
|
+
function nextPageKey(layout) {
|
|
2003
|
+
const keys = new Set(pageList(layout).map((p) => p.key));
|
|
2004
|
+
let n = 1;
|
|
2005
|
+
while (keys.has(`page-${n}`)) n += 1;
|
|
2006
|
+
return `page-${n}`;
|
|
1812
2007
|
}
|
|
1813
|
-
function
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
2008
|
+
function nextPageTitle(layout) {
|
|
2009
|
+
const used = new Set(
|
|
2010
|
+
pageList(layout).map((p) => normalizedTitle(p.title))
|
|
2011
|
+
);
|
|
2012
|
+
let n = pageList(layout).length + 1;
|
|
2013
|
+
while (used.has(normalizedTitle(`Page ${n}`))) n += 1;
|
|
2014
|
+
return `Page ${n}`;
|
|
2015
|
+
}
|
|
2016
|
+
function addPage(layout, options = {}) {
|
|
2017
|
+
if (pageList(layout).length >= MAX_PARSED_PAGES) {
|
|
2018
|
+
return err(
|
|
2019
|
+
`The layout already has ${MAX_PARSED_PAGES} pages, the most it can keep.`
|
|
1818
2020
|
);
|
|
1819
|
-
return [];
|
|
1820
2021
|
}
|
|
1821
|
-
const
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2022
|
+
const key = options.key ?? nextPageKey(layout);
|
|
2023
|
+
if (pageList(layout).some((p) => p.key === key)) {
|
|
2024
|
+
return err(`A page with the key "${key}" already exists.`);
|
|
2025
|
+
}
|
|
2026
|
+
const title = checkTitle(
|
|
2027
|
+
layout,
|
|
2028
|
+
options.title ?? nextPageTitle(layout)
|
|
2029
|
+
);
|
|
2030
|
+
if (!title.ok) return title;
|
|
2031
|
+
const base = withExplicitPages(layout);
|
|
2032
|
+
const page = {
|
|
2033
|
+
key,
|
|
2034
|
+
title: title.value,
|
|
2035
|
+
order: [],
|
|
2036
|
+
hidden: [],
|
|
2037
|
+
minimized: []
|
|
2038
|
+
};
|
|
2039
|
+
return ok({ ...base, pages: [...base.pages ?? [], page] });
|
|
2040
|
+
}
|
|
2041
|
+
function renamePage(layout, pageKey, title) {
|
|
2042
|
+
const page = pageList(layout).find(
|
|
2043
|
+
(p) => p.key === pageKey
|
|
2044
|
+
);
|
|
2045
|
+
if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
|
|
2046
|
+
const checked = checkTitle(layout, title, pageKey);
|
|
2047
|
+
if (!checked.ok) return checked;
|
|
2048
|
+
if (checked.value === page.title) return ok(layout);
|
|
2049
|
+
const base = withExplicitPages(layout);
|
|
2050
|
+
return ok({
|
|
2051
|
+
...base,
|
|
2052
|
+
pages: (base.pages ?? []).map(
|
|
2053
|
+
(p) => p.key === pageKey ? { ...p, title: checked.value } : p
|
|
2054
|
+
)
|
|
1845
2055
|
});
|
|
1846
|
-
return keys;
|
|
1847
2056
|
}
|
|
1848
|
-
function
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2057
|
+
function movePage(layout, pageKey, offset) {
|
|
2058
|
+
const pages = layout.pages ?? [];
|
|
2059
|
+
const from = pages.findIndex((p) => p.key === pageKey);
|
|
2060
|
+
if (from === -1) return layout;
|
|
2061
|
+
const to = Math.max(
|
|
2062
|
+
0,
|
|
2063
|
+
Math.min(pages.length - 1, from + Math.trunc(offset))
|
|
2064
|
+
);
|
|
2065
|
+
if (to === from) return layout;
|
|
2066
|
+
const next = [...pages];
|
|
2067
|
+
const [moved] = next.splice(from, 1);
|
|
2068
|
+
next.splice(to, 0, moved);
|
|
2069
|
+
return { ...layout, pages: next };
|
|
2070
|
+
}
|
|
2071
|
+
function overflowing(definitions, layout, options) {
|
|
2072
|
+
return pageList(layout).find(
|
|
2073
|
+
(page) => placeRows(pageWidgets(definitions, layout, page.key)).length > rowsAllowed(page, options)
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
function removePage(definitions, layout, pageKey, options = {}) {
|
|
2077
|
+
const pages = pageList(layout);
|
|
2078
|
+
const index = pages.findIndex((p) => p.key === pageKey);
|
|
2079
|
+
const page = pages[index];
|
|
2080
|
+
if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
|
|
2081
|
+
if (pages.length === 1) {
|
|
2082
|
+
return err(
|
|
2083
|
+
`${describePage(page)} is the only page, so it can't be removed.`
|
|
2084
|
+
);
|
|
1852
2085
|
}
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2086
|
+
const target = pages[index > 0 ? index - 1 : 1];
|
|
2087
|
+
const merged = {
|
|
2088
|
+
...layout,
|
|
2089
|
+
pages: pages.filter((p) => p.key !== pageKey).map(
|
|
2090
|
+
(p) => p.key === target.key ? {
|
|
2091
|
+
...p,
|
|
2092
|
+
order: index > 0 ? [...p.order, ...page.order] : [...page.order, ...p.order],
|
|
2093
|
+
hidden: [...p.hidden, ...page.hidden],
|
|
2094
|
+
minimized: [...p.minimized, ...page.minimized]
|
|
2095
|
+
} : p
|
|
2096
|
+
)
|
|
2097
|
+
};
|
|
2098
|
+
const full = overflowing(
|
|
2099
|
+
definitions,
|
|
2100
|
+
merged,
|
|
2101
|
+
options
|
|
2102
|
+
);
|
|
2103
|
+
if (full !== void 0) {
|
|
2104
|
+
return err(
|
|
2105
|
+
`${describePage(page)} has widgets that don't fit on the other pages (${describePage(full)} would need more than ${rowsAllowed(full, options)} rows). Move or hide some first.`
|
|
2106
|
+
);
|
|
1857
2107
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2108
|
+
return ok(merged);
|
|
2109
|
+
}
|
|
2110
|
+
function without(keys, key) {
|
|
2111
|
+
return keys.includes(key) ? keys.filter((k) => k !== key) : keys;
|
|
2112
|
+
}
|
|
2113
|
+
function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {}) {
|
|
2114
|
+
const widget = definitions.find((d) => d.key === widgetKey);
|
|
2115
|
+
if (widget === void 0) {
|
|
2116
|
+
return err(
|
|
2117
|
+
`Widget "${widgetKey}" isn't one of the widgets, so it can't be moved to a page.`
|
|
1862
2118
|
);
|
|
1863
|
-
} else if (seenKeys.has(key)) {
|
|
1864
|
-
problems.push(`${path}.key "${key}" is used by another option.`);
|
|
1865
|
-
} else {
|
|
1866
|
-
seenKeys.add(key);
|
|
1867
2119
|
}
|
|
1868
|
-
|
|
1869
|
-
const
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
if (values.length > 0)
|
|
1901
|
-
requireOneOf(option, "default", values, path, problems);
|
|
1902
|
-
break;
|
|
1903
|
-
}
|
|
1904
|
-
case "number": {
|
|
1905
|
-
const { min, max, step } = option;
|
|
1906
|
-
requireNumber(option, "min", path, problems);
|
|
1907
|
-
requireNumber(option, "max", path, problems);
|
|
1908
|
-
if (typeof min === "number" && typeof max === "number" && min > max) {
|
|
1909
|
-
problems.push(
|
|
1910
|
-
`${path}.min (${min}) must not be more than max (${max}).`
|
|
1911
|
-
);
|
|
1912
|
-
}
|
|
1913
|
-
if (step !== void 0 && !(typeof step === "number" && Number.isFinite(step) && step > 0)) {
|
|
1914
|
-
problems.push(
|
|
1915
|
-
`${path}.step must be a number above 0, got ${describeType2(step)}.`
|
|
1916
|
-
);
|
|
2120
|
+
const pages = pageList(layout);
|
|
2121
|
+
const target = pages.find((p) => p.key === targetKey);
|
|
2122
|
+
if (target === void 0) return err(`Page "${targetKey}" doesn't exist.`);
|
|
2123
|
+
const sourceKey = pageOf(definitions, layout, widgetKey);
|
|
2124
|
+
if (sourceKey === targetKey) return ok(layout);
|
|
2125
|
+
if (resolveWidgetLock(widget).move) {
|
|
2126
|
+
return err(
|
|
2127
|
+
`Widget "${titleOf(widget)}" is locked against moving, so it can't be moved to ${describePage(target)}.`
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
const base = withExplicitPages(layout);
|
|
2131
|
+
const source = (base.pages ?? []).find(
|
|
2132
|
+
(p) => p.key === sourceKey
|
|
2133
|
+
);
|
|
2134
|
+
const hidden = source.hidden.includes(widgetKey);
|
|
2135
|
+
const minimized = source.minimized.includes(widgetKey);
|
|
2136
|
+
const targetOrder = pageWidgets(
|
|
2137
|
+
definitions,
|
|
2138
|
+
base,
|
|
2139
|
+
targetKey
|
|
2140
|
+
).map((d) => d.key);
|
|
2141
|
+
const moved = {
|
|
2142
|
+
...base,
|
|
2143
|
+
pages: (base.pages ?? []).map((p) => {
|
|
2144
|
+
if (p.key === sourceKey) {
|
|
2145
|
+
return {
|
|
2146
|
+
...p,
|
|
2147
|
+
order: without(p.order, widgetKey),
|
|
2148
|
+
hidden: without(p.hidden, widgetKey),
|
|
2149
|
+
minimized: without(p.minimized, widgetKey)
|
|
2150
|
+
};
|
|
1917
2151
|
}
|
|
1918
|
-
if (
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
);
|
|
1926
|
-
} else if (typeof min === "number" && typeof step === "number" && step > 0 && Math.abs(
|
|
1927
|
-
(fallback - min) / step - Math.round((fallback - min) / step)
|
|
1928
|
-
) > 1e-9) {
|
|
1929
|
-
problems.push(
|
|
1930
|
-
`${path}.default (${fallback}) must be ${min} plus a whole number of steps of ${step}.`
|
|
1931
|
-
);
|
|
2152
|
+
if (p.key === targetKey) {
|
|
2153
|
+
return {
|
|
2154
|
+
...p,
|
|
2155
|
+
order: hidden ? p.order : [...targetOrder, widgetKey],
|
|
2156
|
+
hidden: hidden ? [...p.hidden, widgetKey] : p.hidden,
|
|
2157
|
+
minimized: minimized ? [...p.minimized, widgetKey] : p.minimized
|
|
2158
|
+
};
|
|
1932
2159
|
}
|
|
1933
|
-
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
problems.push(
|
|
1946
|
-
`${path}.maxLength must be a whole number from 1 to ${MAX_TEXT_OPTION_LENGTH}, got ${typeof maxLength === "number" ? maxLength : describeType2(maxLength)}.`
|
|
1947
|
-
);
|
|
1948
|
-
}
|
|
1949
|
-
if (typeof fallback !== "string") {
|
|
1950
|
-
problems.push(
|
|
1951
|
-
`${path}.default must be a string, got ${describeType2(fallback)}.`
|
|
1952
|
-
);
|
|
1953
|
-
} else if (typeof maxLength === "number" && fallback.length > maxLength) {
|
|
1954
|
-
problems.push(
|
|
1955
|
-
`${path}.default is ${fallback.length} characters; maxLength is ${maxLength}.`
|
|
1956
|
-
);
|
|
1957
|
-
}
|
|
1958
|
-
break;
|
|
1959
|
-
}
|
|
1960
|
-
case "dateRange": {
|
|
1961
|
-
const presets = option["presets"];
|
|
1962
|
-
const offered = [];
|
|
1963
|
-
if (presets !== void 0) {
|
|
1964
|
-
if (!Array.isArray(presets) || presets.length === 0) {
|
|
1965
|
-
problems.push(
|
|
1966
|
-
`${path}.presets must be a non-empty array of { value, label }, got ${describeType2(presets)}.`
|
|
1967
|
-
);
|
|
1968
|
-
} else {
|
|
1969
|
-
presets.forEach((preset, index) => {
|
|
1970
|
-
const presetPath = `${path}.presets[${index}]`;
|
|
1971
|
-
if (!isRecord2(preset)) {
|
|
1972
|
-
problems.push(
|
|
1973
|
-
`${presetPath} must be an object, got ${describeType2(preset)}.`
|
|
1974
|
-
);
|
|
1975
|
-
return;
|
|
1976
|
-
}
|
|
1977
|
-
requireString(preset, "label", presetPath, problems);
|
|
1978
|
-
requireOneOf(
|
|
1979
|
-
preset,
|
|
1980
|
-
"value",
|
|
1981
|
-
DATE_RANGE_PRESETS,
|
|
1982
|
-
presetPath,
|
|
1983
|
-
problems
|
|
1984
|
-
);
|
|
1985
|
-
if (typeof preset["value"] === "string")
|
|
1986
|
-
offered.push(preset["value"]);
|
|
1987
|
-
});
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
const allowed = offered.length > 0 ? offered : DATE_RANGE_PRESETS;
|
|
1991
|
-
if (typeof fallback !== "string" || !(allowed.includes(fallback) || isIsoInterval(fallback))) {
|
|
1992
|
-
problems.push(
|
|
1993
|
-
`${path}.default must be one of ${allowed.map((p) => `"${p}"`).join(", ")} or an interval like "2026-01-01/2026-01-31", got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
|
|
1994
|
-
);
|
|
1995
|
-
}
|
|
1996
|
-
break;
|
|
1997
|
-
}
|
|
1998
|
-
case "color":
|
|
1999
|
-
if (typeof fallback !== "string" || !/^#[0-9a-fA-F]{6}$/.test(fallback)) {
|
|
2000
|
-
problems.push(
|
|
2001
|
-
`${path}.default must be a color like "#1c5cab", got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
|
|
2002
|
-
);
|
|
2003
|
-
}
|
|
2004
|
-
break;
|
|
2005
|
-
case "sort":
|
|
2006
|
-
case "columns": {
|
|
2007
|
-
const supported = type === "sort" ? kind === "TABLE" || kind === "BAR_LIST" : kind === "TABLE";
|
|
2008
|
-
if (!supported && typeof kind === "string") {
|
|
2009
|
-
problems.push(
|
|
2010
|
-
`${path} is a ${type} option, which only applies to ${type === "sort" ? "TABLE and BAR_LIST" : "TABLE"} widgets, but kind is "${kind}".`
|
|
2011
|
-
);
|
|
2012
|
-
}
|
|
2013
|
-
if (seenTypes.has(type)) {
|
|
2014
|
-
problems.push(
|
|
2015
|
-
`${path} is a second ${type} option; a widget has at most one.`
|
|
2016
|
-
);
|
|
2017
|
-
}
|
|
2018
|
-
seenTypes.add(type);
|
|
2019
|
-
const columnKeys = checkOptionColumns(
|
|
2020
|
-
option,
|
|
2021
|
-
path,
|
|
2022
|
-
supported ? kind : void 0,
|
|
2023
|
-
problems
|
|
2024
|
-
);
|
|
2025
|
-
if (option["apply"] !== void 0) {
|
|
2026
|
-
requireOneOf(option, "apply", ["provider", "client"], path, problems);
|
|
2027
|
-
}
|
|
2028
|
-
if (type === "sort") {
|
|
2029
|
-
if (fallback !== void 0) {
|
|
2030
|
-
const sort = typeof fallback === "string" ? parseSortValue(fallback) : void 0;
|
|
2031
|
-
if (sort === void 0) {
|
|
2032
|
-
problems.push(
|
|
2033
|
-
`${path}.default must look like "c1:desc" ("column:asc" or "column:desc"), got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
|
|
2034
|
-
);
|
|
2035
|
-
} else if (columnKeys.length > 0 && !columnKeys.includes(sort.column)) {
|
|
2036
|
-
requireOneOf(
|
|
2037
|
-
{ default: sort.column },
|
|
2038
|
-
"default",
|
|
2039
|
-
columnKeys,
|
|
2040
|
-
path,
|
|
2041
|
-
problems
|
|
2042
|
-
);
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
} else if (!Array.isArray(fallback) || fallback.length === 0 || fallback.some((k) => typeof k !== "string")) {
|
|
2046
|
-
problems.push(
|
|
2047
|
-
`${path}.default must be a non-empty array of column keys, got ${describeType2(fallback)}.`
|
|
2048
|
-
);
|
|
2049
|
-
} else {
|
|
2050
|
-
const shown = /* @__PURE__ */ new Set();
|
|
2051
|
-
for (const columnKey of fallback) {
|
|
2052
|
-
if (columnKeys.length > 0 && !columnKeys.includes(columnKey)) {
|
|
2053
|
-
problems.push(
|
|
2054
|
-
`${path}.default has "${columnKey}", which is not one of the columns (${columnKeys.map((c) => `"${c}"`).join(", ")}).`
|
|
2055
|
-
);
|
|
2056
|
-
} else if (shown.has(columnKey)) {
|
|
2057
|
-
problems.push(`${path}.default lists "${columnKey}" twice.`);
|
|
2058
|
-
}
|
|
2059
|
-
shown.add(columnKey);
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
break;
|
|
2160
|
+
return p;
|
|
2161
|
+
})
|
|
2162
|
+
};
|
|
2163
|
+
if (!hidden) {
|
|
2164
|
+
const allowed = rowsAllowed(target, options);
|
|
2165
|
+
const needed = placeRows(
|
|
2166
|
+
pageWidgets(definitions, moved, targetKey)
|
|
2167
|
+
).length;
|
|
2168
|
+
if (needed > allowed) {
|
|
2169
|
+
return err(
|
|
2170
|
+
`${describePage(target)} has no room for "${titleOf(widget)}": it would need row ${needed}, and the page allows ${allowed}.`
|
|
2171
|
+
);
|
|
2063
2172
|
}
|
|
2064
2173
|
}
|
|
2174
|
+
return ok(moved);
|
|
2065
2175
|
}
|
|
2066
|
-
function
|
|
2067
|
-
if (
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2176
|
+
function withoutLockField(definition) {
|
|
2177
|
+
if (definition.locked === void 0) return definition;
|
|
2178
|
+
const copy = { ...definition };
|
|
2179
|
+
delete copy.locked;
|
|
2180
|
+
return copy;
|
|
2181
|
+
}
|
|
2182
|
+
function uniqueTitle2(taken, title, index) {
|
|
2183
|
+
let base = title.trim().slice(0, MAX_PAGE_TITLE_LENGTH);
|
|
2184
|
+
if (base === "") base = `Page ${index + 1}`;
|
|
2185
|
+
let candidate = base;
|
|
2186
|
+
let n = 1;
|
|
2187
|
+
while (taken.has(normalizedTitle(candidate))) {
|
|
2188
|
+
const suffix = ` (#${n})`;
|
|
2189
|
+
candidate = `${base.slice(0, MAX_PAGE_TITLE_LENGTH - suffix.length)}${suffix}`;
|
|
2190
|
+
n += 1;
|
|
2077
2191
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
seenKeys,
|
|
2086
|
-
seenTypes,
|
|
2087
|
-
problems
|
|
2088
|
-
);
|
|
2192
|
+
taken.add(normalizedTitle(candidate));
|
|
2193
|
+
return candidate;
|
|
2194
|
+
}
|
|
2195
|
+
function samePages(a, b) {
|
|
2196
|
+
return a.length === b.length && a.every((p, i) => {
|
|
2197
|
+
const q = b[i];
|
|
2198
|
+
return p.key === q.key && p.title === q.title && p.maxRows === q.maxRows && sameKeys(p.order, q.order) && sameKeys(p.hidden, q.hidden) && sameKeys(p.minimized, q.minimized);
|
|
2089
2199
|
});
|
|
2090
2200
|
}
|
|
2091
|
-
function
|
|
2092
|
-
const
|
|
2093
|
-
|
|
2094
|
-
|
|
2201
|
+
function normalizeLayout(definitions, layout, options = {}) {
|
|
2202
|
+
const clones = cleanClones(definitions, layout.clones, options);
|
|
2203
|
+
let base = layout;
|
|
2204
|
+
if (clones !== layout.clones) {
|
|
2205
|
+
const { clones: _dropped, ...others } = layout;
|
|
2206
|
+
base = clones === void 0 ? others : { ...others, clones };
|
|
2207
|
+
}
|
|
2208
|
+
return normalizeWidgets(
|
|
2209
|
+
withClones(definitions, base, options),
|
|
2210
|
+
base,
|
|
2211
|
+
options
|
|
2212
|
+
);
|
|
2095
2213
|
}
|
|
2096
|
-
function
|
|
2097
|
-
const
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2214
|
+
function normalizeWidgets(definitions, layout, options) {
|
|
2215
|
+
const known = pruneLayout(definitions, layout);
|
|
2216
|
+
const enforced = enforceLocks(definitions, known, options);
|
|
2217
|
+
const locked = normalizeSettings(
|
|
2218
|
+
definitions,
|
|
2219
|
+
enforced,
|
|
2220
|
+
options
|
|
2221
|
+
);
|
|
2222
|
+
if (!hasPages(locked)) return locked === layout ? layout : locked;
|
|
2223
|
+
const defs = effectiveDefinitions(
|
|
2224
|
+
options.overrideLocks === true ? definitions.map(withoutLockField) : definitions,
|
|
2225
|
+
locked,
|
|
2226
|
+
options
|
|
2227
|
+
);
|
|
2228
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2229
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
2230
|
+
const held = /* @__PURE__ */ new Set();
|
|
2231
|
+
let pages = [];
|
|
2232
|
+
(locked.pages ?? []).forEach((page, index) => {
|
|
2233
|
+
if (seenKeys.has(page.key)) return;
|
|
2234
|
+
seenKeys.add(page.key);
|
|
2235
|
+
const keep = (list) => [
|
|
2236
|
+
...new Set(list.filter((k) => !held.has(k)))
|
|
2237
|
+
];
|
|
2238
|
+
const next = {
|
|
2239
|
+
...page,
|
|
2240
|
+
title: uniqueTitle2(taken, page.title, index),
|
|
2241
|
+
order: keep(page.order),
|
|
2242
|
+
hidden: keep(page.hidden),
|
|
2243
|
+
minimized: keep(page.minimized)
|
|
2244
|
+
};
|
|
2245
|
+
for (const k of [...next.order, ...next.hidden]) held.add(k);
|
|
2246
|
+
pages.push(next);
|
|
2247
|
+
});
|
|
2248
|
+
const work = () => ({ ...locked, pages });
|
|
2249
|
+
const pinned = defs.filter((d) => resolveWidgetLock(d).move);
|
|
2250
|
+
for (const widget of pinned) {
|
|
2251
|
+
const home = homePageKey(work(), widget);
|
|
2252
|
+
const keysHere = (p, list) => p.key !== home && p[list].includes(widget.key);
|
|
2253
|
+
const wasHidden = pages.some((p) => keysHere(p, "hidden"));
|
|
2254
|
+
const wasMinimized = pages.some((p) => keysHere(p, "minimized"));
|
|
2255
|
+
if (!wasHidden && !wasMinimized) continue;
|
|
2256
|
+
pages = pages.map((p) => {
|
|
2257
|
+
let next = p;
|
|
2258
|
+
if (keysHere(p, "hidden")) {
|
|
2259
|
+
next = { ...next, hidden: without(next.hidden, widget.key) };
|
|
2260
|
+
}
|
|
2261
|
+
if (keysHere(p, "minimized")) {
|
|
2262
|
+
next = { ...next, minimized: without(next.minimized, widget.key) };
|
|
2263
|
+
}
|
|
2264
|
+
if (p.key === home) {
|
|
2265
|
+
next = {
|
|
2266
|
+
...next,
|
|
2267
|
+
hidden: wasHidden && !next.hidden.includes(widget.key) ? [...next.hidden, widget.key] : next.hidden,
|
|
2268
|
+
minimized: wasMinimized && !next.minimized.includes(widget.key) ? [...next.minimized, widget.key] : next.minimized
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
return next;
|
|
2272
|
+
});
|
|
2108
2273
|
}
|
|
2109
|
-
const
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
`${subject}: kind must be one of ${WIDGET_KINDS.join(", ")} (or the payload must be {"empty": true, "text": ...}), got ${typeof kind === "string" ? JSON.stringify(kind) : describeType2(kind)}.`
|
|
2113
|
-
);
|
|
2274
|
+
const placedKeys = /* @__PURE__ */ new Set();
|
|
2275
|
+
for (const p of pages) {
|
|
2276
|
+
for (const k of [...p.order, ...p.hidden]) placedKeys.add(k);
|
|
2114
2277
|
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2278
|
+
const pinnedKeys = new Set(pinned.map((d) => d.key));
|
|
2279
|
+
const unplaced = defs.filter((d) => !placedKeys.has(d.key) && !pinnedKeys.has(d.key)).map((definition, index) => ({ definition, index })).sort(
|
|
2280
|
+
(a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
|
|
2281
|
+
).map(({ definition }) => definition);
|
|
2282
|
+
for (const widget of unplaced) {
|
|
2283
|
+
const home = homePageKey(work(), widget);
|
|
2284
|
+
pages = pages.map(
|
|
2285
|
+
(p) => p.key === home ? { ...p, order: [...p.order, widget.key] } : p
|
|
2118
2286
|
);
|
|
2119
2287
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
`Widget definition: must be an object, got ${describeType2(value)}.`
|
|
2288
|
+
for (let i = 0; i < pages.length; i += 1) {
|
|
2289
|
+
const page = pages[i];
|
|
2290
|
+
const shown = pageWidgets(defs, work(), page.key);
|
|
2291
|
+
const fits = fitCount(shown, rowsAllowed(page, options));
|
|
2292
|
+
if (fits >= shown.length) continue;
|
|
2293
|
+
const moving = shown.slice(fits).filter((d) => !pinnedKeys.has(d.key)).map((d) => d.key);
|
|
2294
|
+
if (moving.length === 0) continue;
|
|
2295
|
+
const moveSet = new Set(moving);
|
|
2296
|
+
const movedMinimized = page.minimized.filter(
|
|
2297
|
+
(k) => moveSet.has(k)
|
|
2131
2298
|
);
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2299
|
+
pages = pages.map(
|
|
2300
|
+
(p) => p.key === page.key ? {
|
|
2301
|
+
...p,
|
|
2302
|
+
order: p.order.filter((k) => !moveSet.has(k)),
|
|
2303
|
+
minimized: p.minimized.filter((k) => !moveSet.has(k))
|
|
2304
|
+
} : p
|
|
2305
|
+
);
|
|
2306
|
+
let placed = false;
|
|
2307
|
+
for (let j = i + 1; j < pages.length && !placed; j += 1) {
|
|
2308
|
+
const candidate = pages[j];
|
|
2309
|
+
const trial = pages.map(
|
|
2310
|
+
(p) => p.key === candidate.key ? {
|
|
2311
|
+
...p,
|
|
2312
|
+
order: [...moving, ...p.order],
|
|
2313
|
+
minimized: [...p.minimized, ...movedMinimized]
|
|
2314
|
+
} : p
|
|
2315
|
+
);
|
|
2316
|
+
const items = pageWidgets(
|
|
2317
|
+
defs,
|
|
2318
|
+
{ ...locked, pages: trial },
|
|
2319
|
+
candidate.key
|
|
2320
|
+
);
|
|
2321
|
+
if (placeRows(items).length <= rowsAllowed(candidate, options)) {
|
|
2322
|
+
pages = trial;
|
|
2323
|
+
placed = true;
|
|
2150
2324
|
}
|
|
2151
|
-
}
|
|
2325
|
+
}
|
|
2326
|
+
if (!placed) {
|
|
2327
|
+
const key = nextPageKey(work());
|
|
2328
|
+
const title = uniqueTitle2(
|
|
2329
|
+
new Set(pages.map((p) => normalizedTitle(p.title))),
|
|
2330
|
+
`Page ${pages.length + 1}`,
|
|
2331
|
+
pages.length
|
|
2332
|
+
);
|
|
2333
|
+
pages = [
|
|
2334
|
+
...pages,
|
|
2335
|
+
{ key, title, order: moving, hidden: [], minimized: movedMinimized }
|
|
2336
|
+
];
|
|
2337
|
+
}
|
|
2152
2338
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2339
|
+
const result = work();
|
|
2340
|
+
return samePages(layout.pages ?? [], pages) && sameKeys(layout.order, result.order) && locked.settings === layout.settings ? layout : result;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// src/core/duplicate.ts
|
|
2344
|
+
function canDuplicate(definitions, key, options = {}) {
|
|
2345
|
+
const definition = definitions.find(
|
|
2346
|
+
(d) => d.key === key
|
|
2347
|
+
);
|
|
2348
|
+
if (definition === void 0) return false;
|
|
2349
|
+
if (options.overrideLocks === true) return true;
|
|
2350
|
+
const sourceKey = definition.sourceKey ?? definition.key;
|
|
2351
|
+
const original = definitions.find(
|
|
2352
|
+
(d) => d.key === sourceKey
|
|
2353
|
+
);
|
|
2354
|
+
return original !== void 0 && !resolveWidgetLock(original).clone;
|
|
2355
|
+
}
|
|
2356
|
+
function nextCloneKey(definitions, layout, fromKey) {
|
|
2357
|
+
const taken = /* @__PURE__ */ new Set([
|
|
2358
|
+
...definitions.map((d) => d.key),
|
|
2359
|
+
...(layout.clones ?? []).map((c) => c.key)
|
|
2360
|
+
]);
|
|
2361
|
+
let highest = 0;
|
|
2362
|
+
for (const clone of layout.clones ?? []) {
|
|
2363
|
+
if (clone.from === fromKey) {
|
|
2364
|
+
highest = Math.max(highest, cloneNumber(clone.key) ?? 0);
|
|
2365
|
+
}
|
|
2156
2366
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2367
|
+
let n = highest + 1;
|
|
2368
|
+
while (taken.has(`${fromKey}#${n}`)) n += 1;
|
|
2369
|
+
return `${fromKey}#${n}`;
|
|
2370
|
+
}
|
|
2371
|
+
function suggestedViewTitle(title, viewLabel) {
|
|
2372
|
+
return `${title} ${viewLabel}`.trim().slice(0, MAX_WIDGET_TITLE_LENGTH);
|
|
2373
|
+
}
|
|
2374
|
+
function duplicateWidget(definitions, layout, key, options = {}) {
|
|
2375
|
+
const widget = definitions.find((d) => d.key === key);
|
|
2376
|
+
if (widget === void 0) {
|
|
2377
|
+
return err(`Widget ${JSON.stringify(key)} isn't on this dashboard.`);
|
|
2378
|
+
}
|
|
2379
|
+
const originalKey = widget.sourceKey ?? widget.key;
|
|
2380
|
+
if (!canDuplicate(definitions, key, options)) {
|
|
2381
|
+
return err(
|
|
2382
|
+
`Widget ${JSON.stringify(originalKey)} is locked against duplicating.`
|
|
2160
2383
|
);
|
|
2161
2384
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2385
|
+
const cloneKey = nextCloneKey(definitions, layout, originalKey);
|
|
2386
|
+
const copiedSettings = (() => {
|
|
2387
|
+
const mine = layout.settings?.[key];
|
|
2388
|
+
if (mine === void 0) return void 0;
|
|
2389
|
+
const { title: _title, ...rest } = mine;
|
|
2390
|
+
return Object.keys(rest).length === 0 ? void 0 : rest;
|
|
2391
|
+
})();
|
|
2392
|
+
const settings = copiedSettings === void 0 ? layout.settings : { ...layout.settings, [cloneKey]: copiedSettings };
|
|
2393
|
+
let next = {
|
|
2394
|
+
...layout,
|
|
2395
|
+
clones: [...layout.clones ?? [], { key: cloneKey, from: originalKey }],
|
|
2396
|
+
...settings === void 0 ? {} : { settings }
|
|
2397
|
+
};
|
|
2398
|
+
const all = withClones(definitions, next, options);
|
|
2399
|
+
const assigned = assignPages(all, next);
|
|
2400
|
+
const pageKey = [...assigned].find(
|
|
2401
|
+
([, widgets]) => widgets.some((w) => w.key === key)
|
|
2402
|
+
)?.[0];
|
|
2403
|
+
if (pageKey !== void 0) {
|
|
2404
|
+
const view = pageLayout(next, pageKey);
|
|
2405
|
+
const onPage = assigned.get(pageKey) ?? [];
|
|
2406
|
+
if (view !== void 0) {
|
|
2407
|
+
const shown = visibleWidgets(onPage, view).filter((d) => !resolveWidgetLock(d).move && d.key !== cloneKey).map((d) => d.key);
|
|
2408
|
+
const at = shown.indexOf(key);
|
|
2409
|
+
const order = at === -1 ? [...shown, cloneKey] : [...shown.slice(0, at + 1), cloneKey, ...shown.slice(at + 1)];
|
|
2410
|
+
next = withPageLayout(next, pageKey, { ...view, order });
|
|
2172
2411
|
}
|
|
2173
2412
|
}
|
|
2174
|
-
if (
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2413
|
+
if (hasPages(next) && pageKey !== void 0) {
|
|
2414
|
+
const effective = effectiveDefinitions(all, next, options);
|
|
2415
|
+
const room = pageRoom(effective, next, pageKey, options);
|
|
2416
|
+
if (room !== void 0 && room.rowsUsed > room.maxRows) {
|
|
2417
|
+
const page = next.pages?.find(
|
|
2418
|
+
(p) => p.key === pageKey
|
|
2419
|
+
);
|
|
2420
|
+
const width = effective.find(
|
|
2421
|
+
(d) => d.key === cloneKey
|
|
2422
|
+
)?.width;
|
|
2423
|
+
return err(
|
|
2424
|
+
`Page "${page?.title ?? pageKey}" has no room for a widget${width === void 0 ? "" : ` ${width} wide`} (it holds ${room.maxRows} rows).`
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2179
2427
|
}
|
|
2180
|
-
return ok(
|
|
2428
|
+
return ok({ layout: next, key: cloneKey });
|
|
2429
|
+
}
|
|
2430
|
+
function deleteClone(layout, key) {
|
|
2431
|
+
if (layout.clones?.some((clone) => clone.key === key) !== true) return layout;
|
|
2432
|
+
const without2 = (keys) => keys.includes(key) ? keys.filter((k) => k !== key) : keys;
|
|
2433
|
+
const { clones, settings, ...rest } = layout;
|
|
2434
|
+
const remaining = (clones ?? []).filter((c) => c.key !== key);
|
|
2435
|
+
const { [key]: _gone, ...otherSettings } = settings ?? {};
|
|
2436
|
+
return {
|
|
2437
|
+
...rest,
|
|
2438
|
+
order: without2(layout.order),
|
|
2439
|
+
hidden: without2(layout.hidden),
|
|
2440
|
+
minimized: without2(layout.minimized),
|
|
2441
|
+
...layout.pages === void 0 ? {} : {
|
|
2442
|
+
pages: layout.pages.map((page) => ({
|
|
2443
|
+
...page,
|
|
2444
|
+
order: without2(page.order),
|
|
2445
|
+
hidden: without2(page.hidden),
|
|
2446
|
+
minimized: without2(page.minimized)
|
|
2447
|
+
}))
|
|
2448
|
+
},
|
|
2449
|
+
...Object.keys(otherSettings).length === 0 ? {} : { settings: otherSettings },
|
|
2450
|
+
...remaining.length === 0 ? {} : { clones: remaining }
|
|
2451
|
+
};
|
|
2181
2452
|
}
|
|
2182
2453
|
|
|
2183
|
-
// src/core/
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
if (
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
widgetKey: definition.key,
|
|
2191
|
-
expectedKind: definition.kind
|
|
2192
|
-
});
|
|
2193
|
-
if (!result.ok) {
|
|
2194
|
-
return { definition, status: "error", error: result.error };
|
|
2195
|
-
}
|
|
2196
|
-
checked = result.value;
|
|
2197
|
-
}
|
|
2198
|
-
const emptyText = emptyTextFor(checked);
|
|
2199
|
-
if (emptyText !== null) {
|
|
2200
|
-
return { definition, status: "empty", emptyText };
|
|
2454
|
+
// src/core/validate.ts
|
|
2455
|
+
var MAX_REPORTED_PROBLEMS = 10;
|
|
2456
|
+
function describeType2(value) {
|
|
2457
|
+
if (value === null) return "null";
|
|
2458
|
+
if (Array.isArray(value)) return "array";
|
|
2459
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
2460
|
+
return String(value);
|
|
2201
2461
|
}
|
|
2202
|
-
return
|
|
2462
|
+
return typeof value;
|
|
2203
2463
|
}
|
|
2204
|
-
function
|
|
2205
|
-
return
|
|
2464
|
+
function isRecord3(value) {
|
|
2465
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2206
2466
|
}
|
|
2207
|
-
function
|
|
2208
|
-
return
|
|
2467
|
+
function join(path, field) {
|
|
2468
|
+
return path === "" ? field : `${path}.${field}`;
|
|
2209
2469
|
}
|
|
2210
|
-
function
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2470
|
+
function requireString(record, field, path, problems) {
|
|
2471
|
+
const value = record[field];
|
|
2472
|
+
if (typeof value !== "string") {
|
|
2473
|
+
problems.push(
|
|
2474
|
+
`${join(path, field)} must be a string, got ${describeType2(value)}.`
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2213
2477
|
}
|
|
2214
|
-
function
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
options
|
|
2219
|
-
);
|
|
2478
|
+
function optionalString(record, field, path, problems) {
|
|
2479
|
+
if (record[field] !== void 0) {
|
|
2480
|
+
requireString(record, field, path, problems);
|
|
2481
|
+
}
|
|
2220
2482
|
}
|
|
2221
|
-
function
|
|
2222
|
-
|
|
2223
|
-
if (
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
widget.data,
|
|
2227
|
-
values
|
|
2483
|
+
function requireNumber(record, field, path, problems) {
|
|
2484
|
+
const value = record[field];
|
|
2485
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2486
|
+
problems.push(
|
|
2487
|
+
`${join(path, field)} must be a finite number, got ${describeType2(value)}.`
|
|
2228
2488
|
);
|
|
2229
|
-
return { ...widget, data, options: values };
|
|
2230
2489
|
}
|
|
2231
|
-
return { ...widget, options: values };
|
|
2232
2490
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
return finalizeWidget(
|
|
2238
|
-
await resolveWidgetUnapplied(definition, provider, context, options),
|
|
2239
|
-
values
|
|
2240
|
-
);
|
|
2491
|
+
function optionalNumber(record, field, path, problems) {
|
|
2492
|
+
if (record[field] !== void 0) {
|
|
2493
|
+
requireNumber(record, field, path, problems);
|
|
2494
|
+
}
|
|
2241
2495
|
}
|
|
2242
|
-
|
|
2243
|
-
const
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
definition,
|
|
2248
|
-
`Widget ${quotedKey}: no provider is registered for this key.`
|
|
2496
|
+
function optionalSortValue(record, path, problems) {
|
|
2497
|
+
const value = record["value"];
|
|
2498
|
+
if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
|
|
2499
|
+
problems.push(
|
|
2500
|
+
`${path}.value must be a string or a finite number, got ${describeType2(value)}.`
|
|
2249
2501
|
);
|
|
2250
2502
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2503
|
+
}
|
|
2504
|
+
function optionalBoolean(record, field, path, problems) {
|
|
2505
|
+
const value = record[field];
|
|
2506
|
+
if (value !== void 0 && typeof value !== "boolean") {
|
|
2507
|
+
problems.push(
|
|
2508
|
+
`${join(path, field)} must be a boolean, got ${describeType2(value)}.`
|
|
2255
2509
|
);
|
|
2256
2510
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
controller.abort(new Error(`Timed out after ${timeoutMs} ms.`));
|
|
2265
|
-
}, timeoutMs);
|
|
2266
|
-
const interrupted = new Promise((_resolve, reject) => {
|
|
2267
|
-
controller.signal.addEventListener(
|
|
2268
|
-
"abort",
|
|
2269
|
-
() => reject(new LoadInterruption()),
|
|
2270
|
-
{ once: true }
|
|
2511
|
+
}
|
|
2512
|
+
function optionalUrl(record, field, path, problems, isSafe, allowed) {
|
|
2513
|
+
const value = record[field];
|
|
2514
|
+
if (value === void 0) return;
|
|
2515
|
+
if (typeof value !== "string") {
|
|
2516
|
+
problems.push(
|
|
2517
|
+
`${join(path, field)} must be a string, got ${describeType2(value)}.`
|
|
2271
2518
|
);
|
|
2272
|
-
})
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
payload = await Promise.race([
|
|
2276
|
-
Promise.resolve().then(() => {
|
|
2277
|
-
if (controller.signal.aborted) throw new LoadInterruption();
|
|
2278
|
-
return provider(context, definition, {
|
|
2279
|
-
signal: controller.signal,
|
|
2280
|
-
options: values
|
|
2281
|
-
});
|
|
2282
|
-
}),
|
|
2283
|
-
interrupted
|
|
2284
|
-
]);
|
|
2285
|
-
} catch (cause) {
|
|
2286
|
-
if (cause instanceof LoadInterruption) {
|
|
2287
|
-
return failedWidget(
|
|
2288
|
-
definition,
|
|
2289
|
-
timedOut ? `Widget ${quotedKey}: provider timed out after ${timeoutMs} ms.` : `Widget ${quotedKey}: loading was canceled.`
|
|
2290
|
-
);
|
|
2291
|
-
}
|
|
2292
|
-
return failedWidget(
|
|
2293
|
-
definition,
|
|
2294
|
-
`Widget ${quotedKey}: provider failed: ${describeUnknownError(cause)}`
|
|
2519
|
+
} else if (!isSafe(value)) {
|
|
2520
|
+
problems.push(
|
|
2521
|
+
`${join(path, field)} must be ${allowed}, got ${JSON.stringify(value.slice(0, 80))}.`
|
|
2295
2522
|
);
|
|
2296
|
-
} finally {
|
|
2297
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
2298
|
-
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
2299
2523
|
}
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
const
|
|
2303
|
-
if (
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
});
|
|
2309
|
-
} catch {
|
|
2310
|
-
}
|
|
2524
|
+
}
|
|
2525
|
+
function requireArray(record, field, path, problems) {
|
|
2526
|
+
const value = record[field];
|
|
2527
|
+
if (!Array.isArray(value)) {
|
|
2528
|
+
problems.push(
|
|
2529
|
+
`${join(path, field)} must be an array, got ${describeType2(value)}.`
|
|
2530
|
+
);
|
|
2531
|
+
return null;
|
|
2311
2532
|
}
|
|
2312
|
-
return
|
|
2533
|
+
return value;
|
|
2313
2534
|
}
|
|
2314
|
-
function
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2535
|
+
function requireOneOf(record, field, options, path, problems) {
|
|
2536
|
+
const value = record[field];
|
|
2537
|
+
if (typeof value !== "string" || !options.includes(value)) {
|
|
2538
|
+
problems.push(
|
|
2539
|
+
`${join(path, field)} must be one of ${options.map((o) => `"${o}"`).join(", ")}, got ${typeof value === "string" ? JSON.stringify(value) : describeType2(value)}.`
|
|
2540
|
+
);
|
|
2541
|
+
}
|
|
2318
2542
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
(
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
);
|
|
2543
|
+
function eachRecord(items, path, problems, check) {
|
|
2544
|
+
items.forEach((item, index) => {
|
|
2545
|
+
const itemPath = `${path}[${index}]`;
|
|
2546
|
+
if (isRecord3(item)) {
|
|
2547
|
+
check(item, itemPath);
|
|
2548
|
+
} else {
|
|
2549
|
+
problems.push(
|
|
2550
|
+
`${itemPath} must be an object, got ${describeType2(item)}.`
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
2331
2554
|
}
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
function
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
(
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
}
|
|
2345
|
-
const unapplied = /* @__PURE__ */ new Map();
|
|
2346
|
-
const visible = widgetsFor(definitions, context.roles);
|
|
2347
|
-
let snapshot = loadingWidgets(visible);
|
|
2348
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
2349
|
-
const started = /* @__PURE__ */ new Set();
|
|
2350
|
-
const generations = /* @__PURE__ */ new Map();
|
|
2351
|
-
const inFlight = /* @__PURE__ */ new Map();
|
|
2352
|
-
let disposed = false;
|
|
2353
|
-
function publish(next) {
|
|
2354
|
-
snapshot = next;
|
|
2355
|
-
for (const listener of [...listeners]) {
|
|
2356
|
-
try {
|
|
2357
|
-
listener();
|
|
2358
|
-
} catch {
|
|
2555
|
+
var HREF_RULE = "an http(s), mailto or relative URL";
|
|
2556
|
+
var IMAGE_RULE = "an http(s) or relative URL";
|
|
2557
|
+
function checkPayloadFields(kind, data, problems) {
|
|
2558
|
+
switch (kind) {
|
|
2559
|
+
case "TEXT":
|
|
2560
|
+
requireString(data, "value", "", problems);
|
|
2561
|
+
requireString(data, "label", "", problems);
|
|
2562
|
+
return;
|
|
2563
|
+
case "KPI":
|
|
2564
|
+
requireNumber(data, "value", "", problems);
|
|
2565
|
+
if (data["previous"] !== null) {
|
|
2566
|
+
optionalNumber(data, "previous", "", problems);
|
|
2359
2567
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
"
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
cacheKeyFor(definition, runOptions, values)
|
|
2392
|
-
);
|
|
2393
|
-
if (entry !== void 0 && isCurrent()) {
|
|
2394
|
-
const cached = resolvePayload(definition, entry.payload, runOptions);
|
|
2395
|
-
if (cached.status === "ok") {
|
|
2396
|
-
const stale = { ...cached, updatedAt: entry.storedAt, stale: true };
|
|
2397
|
-
unapplied.set(key, stale);
|
|
2398
|
-
replace(key, finalizeWidget(stale, values));
|
|
2568
|
+
requireOneOf(data, "format", VALUE_FORMATS, "", problems);
|
|
2569
|
+
optionalString(data, "currency", "", problems);
|
|
2570
|
+
requireString(data, "label", "", problems);
|
|
2571
|
+
optionalString(data, "hint", "", problems);
|
|
2572
|
+
optionalBoolean(data, "higherIsBetter", "", problems);
|
|
2573
|
+
return;
|
|
2574
|
+
case "GAUGE":
|
|
2575
|
+
requireNumber(data, "value", "", problems);
|
|
2576
|
+
requireNumber(data, "max", "", problems);
|
|
2577
|
+
requireString(data, "label", "", problems);
|
|
2578
|
+
if (typeof data["max"] === "number" && data["max"] <= 0) {
|
|
2579
|
+
problems.push(`max must be greater than 0, got ${data["max"]}.`);
|
|
2580
|
+
}
|
|
2581
|
+
return;
|
|
2582
|
+
case "TABLE": {
|
|
2583
|
+
const columns = requireArray(data, "columns", "", problems);
|
|
2584
|
+
if (columns !== null) {
|
|
2585
|
+
eachRecord(columns, "columns", problems, (column, path) => {
|
|
2586
|
+
requireString(column, "label", path, problems);
|
|
2587
|
+
optionalBoolean(column, "numeric", path, problems);
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
const rows = requireArray(data, "rows", "", problems);
|
|
2591
|
+
if (rows !== null) {
|
|
2592
|
+
rows.forEach((row, rowIndex) => {
|
|
2593
|
+
const rowPath = `rows[${rowIndex}]`;
|
|
2594
|
+
if (!Array.isArray(row)) {
|
|
2595
|
+
problems.push(
|
|
2596
|
+
`${rowPath} must be an array of cells, got ${describeType2(row)}.`
|
|
2597
|
+
);
|
|
2598
|
+
return;
|
|
2399
2599
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2600
|
+
if (columns !== null && row.length !== columns.length) {
|
|
2601
|
+
problems.push(
|
|
2602
|
+
`${rowPath} has ${row.length} cell(s) but there are ${columns.length} column(s).`
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
eachRecord(row, rowPath, problems, (cell, cellPath) => {
|
|
2606
|
+
requireString(cell, "text", cellPath, problems);
|
|
2607
|
+
optionalSortValue(cell, cellPath, problems);
|
|
2608
|
+
optionalUrl(
|
|
2609
|
+
cell,
|
|
2610
|
+
"href",
|
|
2611
|
+
cellPath,
|
|
2612
|
+
problems,
|
|
2613
|
+
isSafeHref,
|
|
2614
|
+
HREF_RULE
|
|
2615
|
+
);
|
|
2616
|
+
});
|
|
2617
|
+
});
|
|
2402
2618
|
}
|
|
2619
|
+
optionalString(data, "footer", "", problems);
|
|
2620
|
+
return;
|
|
2403
2621
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
if (result.status === "ok") unapplied.set(key, result);
|
|
2413
|
-
else unapplied.delete(key);
|
|
2414
|
-
replace(key, finalizeWidget(result, values));
|
|
2415
|
-
}
|
|
2416
|
-
function start(definition, readCache) {
|
|
2417
|
-
started.add(definition.key);
|
|
2418
|
-
void run(definition, readCache);
|
|
2419
|
-
}
|
|
2420
|
-
function definitionFor(key) {
|
|
2421
|
-
return visible.find((definition) => definition.key === key);
|
|
2422
|
-
}
|
|
2423
|
-
function load(key) {
|
|
2424
|
-
if (disposed || started.has(key)) return;
|
|
2425
|
-
const definition = definitionFor(key);
|
|
2426
|
-
if (definition !== void 0) start(definition, true);
|
|
2427
|
-
}
|
|
2428
|
-
function loadAll() {
|
|
2429
|
-
for (const definition of visible) load(definition.key);
|
|
2430
|
-
}
|
|
2431
|
-
function refresh(key, refreshOptions) {
|
|
2432
|
-
if (disposed) return;
|
|
2433
|
-
const keys = key === void 0 ? [...started] : [key];
|
|
2434
|
-
for (const target of keys) {
|
|
2435
|
-
const definition = definitionFor(target);
|
|
2436
|
-
if (definition === void 0) continue;
|
|
2437
|
-
if (refreshOptions?.skipInFlight === true && inFlight.has(target)) {
|
|
2438
|
-
continue;
|
|
2439
|
-
}
|
|
2440
|
-
const current = snapshot.find(
|
|
2441
|
-
(widget) => widget.definition.key === target
|
|
2442
|
-
);
|
|
2443
|
-
if (current?.status === "error") {
|
|
2444
|
-
replace(target, { definition, status: "loading" });
|
|
2622
|
+
case "BAR_LIST": {
|
|
2623
|
+
const items = requireArray(data, "items", "", problems);
|
|
2624
|
+
if (items !== null) {
|
|
2625
|
+
eachRecord(items, "items", problems, (item, path) => {
|
|
2626
|
+
requireString(item, "label", path, problems);
|
|
2627
|
+
requireNumber(item, "value", path, problems);
|
|
2628
|
+
optionalString(item, "display", path, problems);
|
|
2629
|
+
});
|
|
2445
2630
|
}
|
|
2446
|
-
|
|
2631
|
+
optionalNumber(data, "total", "", problems);
|
|
2632
|
+
return;
|
|
2447
2633
|
}
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2634
|
+
case "ALERT_LIST": {
|
|
2635
|
+
const items = requireArray(data, "items", "", problems);
|
|
2636
|
+
if (items !== null) {
|
|
2637
|
+
eachRecord(items, "items", problems, (item, path) => {
|
|
2638
|
+
requireString(item, "title", path, problems);
|
|
2639
|
+
optionalUrl(item, "href", path, problems, isSafeHref, HREF_RULE);
|
|
2640
|
+
optionalUrl(
|
|
2641
|
+
item,
|
|
2642
|
+
"thumbnailUrl",
|
|
2643
|
+
path,
|
|
2644
|
+
problems,
|
|
2645
|
+
isSafeImageUrl,
|
|
2646
|
+
IMAGE_RULE
|
|
2647
|
+
);
|
|
2648
|
+
requireString(item, "valueLabel", path, problems);
|
|
2649
|
+
optionalString(item, "detail", path, problems);
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
requireNumber(data, "total", "", problems);
|
|
2653
|
+
requireString(data, "emptyText", "", problems);
|
|
2654
|
+
if (items !== null && typeof data["total"] === "number" && data["total"] < items.length) {
|
|
2655
|
+
problems.push(
|
|
2656
|
+
`total (${data["total"]}) must be at least the number of items (${items.length}).`
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2461
2659
|
return;
|
|
2462
2660
|
}
|
|
2463
|
-
|
|
2464
|
-
(
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2661
|
+
case "GRAPH": {
|
|
2662
|
+
const series = requireArray(data, "series", "", problems);
|
|
2663
|
+
if (series !== null) {
|
|
2664
|
+
if (series.length > MAX_GRAPH_SERIES) {
|
|
2665
|
+
problems.push(
|
|
2666
|
+
`series has ${series.length} entries; at most ${MAX_GRAPH_SERIES} are allowed (fold the rest into "Other").`
|
|
2667
|
+
);
|
|
2668
|
+
}
|
|
2669
|
+
eachRecord(series, "series", problems, (entry, path) => {
|
|
2670
|
+
requireString(entry, "name", path, problems);
|
|
2671
|
+
const points = requireArray(entry, "points", path, problems);
|
|
2672
|
+
if (points !== null) {
|
|
2673
|
+
eachRecord(
|
|
2674
|
+
points,
|
|
2675
|
+
`${path}.points`,
|
|
2676
|
+
problems,
|
|
2677
|
+
(point, pointPath) => {
|
|
2678
|
+
requireString(point, "label", pointPath, problems);
|
|
2679
|
+
requireNumber(point, "value", pointPath, problems);
|
|
2680
|
+
}
|
|
2681
|
+
);
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
requireOneOf(data, "chartType", ["bar", "line"], "", problems);
|
|
2686
|
+
requireOneOf(data, "valueFormat", VALUE_FORMATS, "", problems);
|
|
2687
|
+
optionalString(data, "currency", "", problems);
|
|
2688
|
+
optionalString(data, "xLabel", "", problems);
|
|
2689
|
+
return;
|
|
2469
2690
|
}
|
|
2470
|
-
start(definition, true);
|
|
2471
2691
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2692
|
+
}
|
|
2693
|
+
function checkDetailOption(detail, problems) {
|
|
2694
|
+
if (detail === void 0 || typeof detail === "boolean") return;
|
|
2695
|
+
if (!isRecord3(detail)) {
|
|
2696
|
+
problems.push(
|
|
2697
|
+
`detail must be true, false or an options object, got ${describeType2(detail)}.`
|
|
2698
|
+
);
|
|
2699
|
+
return;
|
|
2476
2700
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2701
|
+
optionalString(detail, "title", "detail", problems);
|
|
2702
|
+
const pageSize = detail["pageSize"];
|
|
2703
|
+
if (pageSize !== void 0 && !(typeof pageSize === "number" && Number.isInteger(pageSize) && pageSize >= 1 && pageSize <= MAX_DETAIL_PAGE_SIZE)) {
|
|
2704
|
+
problems.push(
|
|
2705
|
+
`detail.pageSize must be a whole number from 1 to ${MAX_DETAIL_PAGE_SIZE}, got ${typeof pageSize === "number" ? String(pageSize) : describeType2(pageSize)}.`
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
if (detail["mode"] !== void 0) {
|
|
2709
|
+
requireOneOf(detail, "mode", DETAIL_MODES, "detail", problems);
|
|
2482
2710
|
}
|
|
2483
|
-
return {
|
|
2484
|
-
getSnapshot: () => snapshot,
|
|
2485
|
-
subscribe,
|
|
2486
|
-
load,
|
|
2487
|
-
loadAll,
|
|
2488
|
-
refresh,
|
|
2489
|
-
setOptions,
|
|
2490
|
-
dispose
|
|
2491
|
-
};
|
|
2492
|
-
}
|
|
2493
|
-
|
|
2494
|
-
// src/core/pages.ts
|
|
2495
|
-
var DEFAULT_PAGE_KEY = "default";
|
|
2496
|
-
var DEFAULT_PAGE_ROWS = 4;
|
|
2497
|
-
function rowsAllowed(page, options) {
|
|
2498
|
-
const rows = page.maxRows ?? options.maxRows ?? DEFAULT_PAGE_ROWS;
|
|
2499
|
-
return Math.max(1, Math.min(MAX_PAGE_ROWS, Math.floor(rows)));
|
|
2500
2711
|
}
|
|
2501
|
-
function
|
|
2502
|
-
|
|
2712
|
+
function checkTableControlsOption(controls, kind, problems) {
|
|
2713
|
+
if (controls === void 0 || controls === false) return;
|
|
2714
|
+
if (typeof controls !== "boolean" && !isRecord3(controls)) {
|
|
2715
|
+
problems.push(
|
|
2716
|
+
`tableControls must be true, false or an options object, got ${describeType2(controls)}.`
|
|
2717
|
+
);
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
if (isRecord3(controls)) {
|
|
2721
|
+
optionalBoolean(controls, "search", "tableControls", problems);
|
|
2722
|
+
optionalBoolean(controls, "sort", "tableControls", problems);
|
|
2723
|
+
}
|
|
2724
|
+
if (typeof kind === "string" && kind !== "TABLE") {
|
|
2725
|
+
problems.push(
|
|
2726
|
+
`tableControls only applies to TABLE widgets, but kind is "${kind}".`
|
|
2727
|
+
);
|
|
2728
|
+
}
|
|
2503
2729
|
}
|
|
2504
|
-
function
|
|
2505
|
-
|
|
2506
|
-
|
|
2730
|
+
function checkWidthLimits(value, problems) {
|
|
2731
|
+
for (const field of ["minWidth", "maxWidth"]) {
|
|
2732
|
+
const limit = value[field];
|
|
2733
|
+
if (limit !== void 0 && !isWidgetWidth(limit)) {
|
|
2734
|
+
problems.push(
|
|
2735
|
+
`${field} must be an integer from ${MIN_WIDGET_WIDTH} to ${MAX_WIDGET_WIDTH} (twelfths of the row), got ${typeof limit === "number" ? String(limit) : describeType2(limit)}.`
|
|
2736
|
+
);
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2507
2739
|
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2740
|
+
const { minWidth, maxWidth, width } = value;
|
|
2741
|
+
if (isWidgetWidth(minWidth) && isWidgetWidth(maxWidth) && minWidth > maxWidth) {
|
|
2742
|
+
problems.push(
|
|
2743
|
+
`minWidth (${minWidth}) must not be more than maxWidth (${maxWidth}).`
|
|
2744
|
+
);
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
if (isWidgetWidth(width)) {
|
|
2748
|
+
if (isWidgetWidth(minWidth) && width < minWidth) {
|
|
2749
|
+
problems.push(
|
|
2750
|
+
`width (${width}) must not be less than minWidth (${minWidth}).`
|
|
2751
|
+
);
|
|
2515
2752
|
}
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
order: [],
|
|
2523
|
-
hidden: [],
|
|
2524
|
-
minimized: [],
|
|
2525
|
-
pages: pageList(layout)
|
|
2526
|
-
};
|
|
2753
|
+
if (isWidgetWidth(maxWidth) && width > maxWidth) {
|
|
2754
|
+
problems.push(
|
|
2755
|
+
`width (${width}) must not be more than maxWidth (${maxWidth}).`
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2527
2759
|
}
|
|
2528
|
-
function
|
|
2529
|
-
|
|
2760
|
+
function checkLockedOption(locked, problems) {
|
|
2761
|
+
if (locked === void 0 || typeof locked === "boolean") return;
|
|
2762
|
+
if (!isRecord3(locked)) {
|
|
2763
|
+
problems.push(
|
|
2764
|
+
`locked must be true, false or an object, got ${describeType2(locked)}.`
|
|
2765
|
+
);
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
optionalBoolean(locked, "move", "locked", problems);
|
|
2769
|
+
optionalBoolean(locked, "hide", "locked", problems);
|
|
2770
|
+
optionalBoolean(locked, "minimize", "locked", problems);
|
|
2771
|
+
optionalBoolean(locked, "options", "locked", problems);
|
|
2772
|
+
optionalBoolean(locked, "clone", "locked", problems);
|
|
2530
2773
|
}
|
|
2531
|
-
function
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2774
|
+
function checkOptionColumns(option, path, kind, problems) {
|
|
2775
|
+
const columns = option["columns"];
|
|
2776
|
+
if (!Array.isArray(columns) || columns.length === 0) {
|
|
2777
|
+
problems.push(
|
|
2778
|
+
`${join(path, "columns")} must be a non-empty array of { key, label }, got ${describeType2(columns)}.`
|
|
2779
|
+
);
|
|
2780
|
+
return [];
|
|
2781
|
+
}
|
|
2782
|
+
const keys = [];
|
|
2783
|
+
columns.forEach((entry, index) => {
|
|
2784
|
+
const entryPath = `${path}.columns[${index}]`;
|
|
2785
|
+
if (!isRecord3(entry)) {
|
|
2786
|
+
problems.push(
|
|
2787
|
+
`${entryPath} must be an object, got ${describeType2(entry)}.`
|
|
2788
|
+
);
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
requireString(entry, "label", entryPath, problems);
|
|
2792
|
+
const key = entry["key"];
|
|
2793
|
+
if (typeof key !== "string") {
|
|
2794
|
+
problems.push(
|
|
2795
|
+
`${entryPath}.key must be a string, got ${describeType2(key)}.`
|
|
2796
|
+
);
|
|
2797
|
+
} else if (typeof kind === "string" && !isColumnKeyFor(kind, key)) {
|
|
2798
|
+
problems.push(
|
|
2799
|
+
`${entryPath}.key "${key}" is not a column of a ${kind} widget (${kind === "TABLE" ? '"c0", "c1", ...' : '"label" or "value"'}).`
|
|
2800
|
+
);
|
|
2801
|
+
} else if (keys.includes(key)) {
|
|
2802
|
+
problems.push(`${entryPath}.key "${key}" is used twice.`);
|
|
2803
|
+
} else {
|
|
2804
|
+
keys.push(key);
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
return keys;
|
|
2545
2808
|
}
|
|
2546
|
-
function
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
return
|
|
2809
|
+
function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
|
|
2810
|
+
if (!isRecord3(option)) {
|
|
2811
|
+
problems.push(`${path} must be an object, got ${describeType2(option)}.`);
|
|
2812
|
+
return;
|
|
2550
2813
|
}
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
hidden: next.hidden,
|
|
2556
|
-
minimized: next.minimized
|
|
2557
|
-
};
|
|
2814
|
+
const type = option["type"];
|
|
2815
|
+
if (typeof type !== "string" || !WIDGET_OPTION_TYPES.includes(type)) {
|
|
2816
|
+
requireOneOf(option, "type", WIDGET_OPTION_TYPES, path, problems);
|
|
2817
|
+
return;
|
|
2558
2818
|
}
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
(
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2819
|
+
const key = option["key"];
|
|
2820
|
+
if (typeof key !== "string" || !OPTION_KEY_PATTERN.test(key)) {
|
|
2821
|
+
problems.push(
|
|
2822
|
+
`${path}.key must be letters, digits, "_" or "-", starting with a letter (40 characters at most), got ${typeof key === "string" ? JSON.stringify(key) : describeType2(key)}.`
|
|
2823
|
+
);
|
|
2824
|
+
} else if (seenKeys.has(key)) {
|
|
2825
|
+
problems.push(`${path}.key "${key}" is used by another option.`);
|
|
2826
|
+
} else {
|
|
2827
|
+
seenKeys.add(key);
|
|
2828
|
+
}
|
|
2829
|
+
requireString(option, "label", path, problems);
|
|
2830
|
+
const fallback = option["default"];
|
|
2831
|
+
switch (type) {
|
|
2832
|
+
case "choice": {
|
|
2833
|
+
const choices = option["choices"];
|
|
2834
|
+
const values = [];
|
|
2835
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
2836
|
+
problems.push(
|
|
2837
|
+
`${path}.choices must be a non-empty array of { value, label }, got ${describeType2(choices)}.`
|
|
2838
|
+
);
|
|
2839
|
+
} else {
|
|
2840
|
+
choices.forEach((choice, index) => {
|
|
2841
|
+
const choicePath = `${path}.choices[${index}]`;
|
|
2842
|
+
if (!isRecord3(choice)) {
|
|
2843
|
+
problems.push(
|
|
2844
|
+
`${choicePath} must be an object, got ${describeType2(choice)}.`
|
|
2845
|
+
);
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
requireString(choice, "label", choicePath, problems);
|
|
2849
|
+
const value = choice["value"];
|
|
2850
|
+
if (typeof value !== "string" || value === "") {
|
|
2851
|
+
problems.push(
|
|
2852
|
+
`${choicePath}.value must be a non-empty string, got ${describeType2(value)}.`
|
|
2853
|
+
);
|
|
2854
|
+
} else if (values.includes(value)) {
|
|
2855
|
+
problems.push(`${choicePath}.value "${value}" is used twice.`);
|
|
2856
|
+
} else {
|
|
2857
|
+
values.push(value);
|
|
2858
|
+
}
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
if (values.length > 0)
|
|
2862
|
+
requireOneOf(option, "default", values, path, problems);
|
|
2863
|
+
break;
|
|
2864
|
+
}
|
|
2865
|
+
case "number": {
|
|
2866
|
+
const { min, max, step } = option;
|
|
2867
|
+
requireNumber(option, "min", path, problems);
|
|
2868
|
+
requireNumber(option, "max", path, problems);
|
|
2869
|
+
if (typeof min === "number" && typeof max === "number" && min > max) {
|
|
2870
|
+
problems.push(
|
|
2871
|
+
`${path}.min (${min}) must not be more than max (${max}).`
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
if (step !== void 0 && !(typeof step === "number" && Number.isFinite(step) && step > 0)) {
|
|
2875
|
+
problems.push(
|
|
2876
|
+
`${path}.step must be a number above 0, got ${describeType2(step)}.`
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
if (typeof fallback !== "number" || !Number.isFinite(fallback)) {
|
|
2880
|
+
problems.push(
|
|
2881
|
+
`${path}.default must be a finite number, got ${describeType2(fallback)}.`
|
|
2882
|
+
);
|
|
2883
|
+
} else if (typeof min === "number" && typeof max === "number" && (fallback < min || fallback > max)) {
|
|
2884
|
+
problems.push(
|
|
2885
|
+
`${path}.default (${fallback}) must be between min (${min}) and max (${max}).`
|
|
2886
|
+
);
|
|
2887
|
+
} else if (typeof min === "number" && typeof step === "number" && step > 0 && Math.abs(
|
|
2888
|
+
(fallback - min) / step - Math.round((fallback - min) / step)
|
|
2889
|
+
) > 1e-9) {
|
|
2890
|
+
problems.push(
|
|
2891
|
+
`${path}.default (${fallback}) must be ${min} plus a whole number of steps of ${step}.`
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
break;
|
|
2895
|
+
}
|
|
2896
|
+
case "boolean":
|
|
2897
|
+
if (typeof fallback !== "boolean") {
|
|
2898
|
+
problems.push(
|
|
2899
|
+
`${path}.default must be a boolean, got ${describeType2(fallback)}.`
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2902
|
+
break;
|
|
2903
|
+
case "text": {
|
|
2904
|
+
const maxLength = option["maxLength"];
|
|
2905
|
+
if (typeof maxLength !== "number" || !Number.isInteger(maxLength) || maxLength < 1 || maxLength > MAX_TEXT_OPTION_LENGTH) {
|
|
2906
|
+
problems.push(
|
|
2907
|
+
`${path}.maxLength must be a whole number from 1 to ${MAX_TEXT_OPTION_LENGTH}, got ${typeof maxLength === "number" ? maxLength : describeType2(maxLength)}.`
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
if (typeof fallback !== "string") {
|
|
2911
|
+
problems.push(
|
|
2912
|
+
`${path}.default must be a string, got ${describeType2(fallback)}.`
|
|
2913
|
+
);
|
|
2914
|
+
} else if (typeof maxLength === "number" && fallback.length > maxLength) {
|
|
2915
|
+
problems.push(
|
|
2916
|
+
`${path}.default is ${fallback.length} characters; maxLength is ${maxLength}.`
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
break;
|
|
2920
|
+
}
|
|
2921
|
+
case "dateRange": {
|
|
2922
|
+
const presets = option["presets"];
|
|
2923
|
+
const offered = [];
|
|
2924
|
+
if (presets !== void 0) {
|
|
2925
|
+
if (!Array.isArray(presets) || presets.length === 0) {
|
|
2926
|
+
problems.push(
|
|
2927
|
+
`${path}.presets must be a non-empty array of { value, label }, got ${describeType2(presets)}.`
|
|
2928
|
+
);
|
|
2929
|
+
} else {
|
|
2930
|
+
presets.forEach((preset, index) => {
|
|
2931
|
+
const presetPath = `${path}.presets[${index}]`;
|
|
2932
|
+
if (!isRecord3(preset)) {
|
|
2933
|
+
problems.push(
|
|
2934
|
+
`${presetPath} must be an object, got ${describeType2(preset)}.`
|
|
2935
|
+
);
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
requireString(preset, "label", presetPath, problems);
|
|
2939
|
+
requireOneOf(
|
|
2940
|
+
preset,
|
|
2941
|
+
"value",
|
|
2942
|
+
DATE_RANGE_PRESETS,
|
|
2943
|
+
presetPath,
|
|
2944
|
+
problems
|
|
2945
|
+
);
|
|
2946
|
+
if (typeof preset["value"] === "string")
|
|
2947
|
+
offered.push(preset["value"]);
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
const allowed = offered.length > 0 ? offered : DATE_RANGE_PRESETS;
|
|
2952
|
+
if (typeof fallback !== "string" || !(allowed.includes(fallback) || isIsoInterval(fallback))) {
|
|
2953
|
+
problems.push(
|
|
2954
|
+
`${path}.default must be one of ${allowed.map((p) => `"${p}"`).join(", ")} or an interval like "2026-01-01/2026-01-31", got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
break;
|
|
2958
|
+
}
|
|
2959
|
+
case "color":
|
|
2960
|
+
if (typeof fallback !== "string" || !/^#[0-9a-fA-F]{6}$/.test(fallback)) {
|
|
2961
|
+
problems.push(
|
|
2962
|
+
`${path}.default must be a color like "#1c5cab", got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2965
|
+
break;
|
|
2966
|
+
case "sort":
|
|
2967
|
+
case "columns": {
|
|
2968
|
+
const supported = type === "sort" ? kind === "TABLE" || kind === "BAR_LIST" : kind === "TABLE";
|
|
2969
|
+
if (!supported && typeof kind === "string") {
|
|
2970
|
+
problems.push(
|
|
2971
|
+
`${path} is a ${type} option, which only applies to ${type === "sort" ? "TABLE and BAR_LIST" : "TABLE"} widgets, but kind is "${kind}".`
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
if (seenTypes.has(type)) {
|
|
2975
|
+
problems.push(
|
|
2976
|
+
`${path} is a second ${type} option; a widget has at most one.`
|
|
2977
|
+
);
|
|
2978
|
+
}
|
|
2979
|
+
seenTypes.add(type);
|
|
2980
|
+
const columnKeys = checkOptionColumns(
|
|
2981
|
+
option,
|
|
2982
|
+
path,
|
|
2983
|
+
supported ? kind : void 0,
|
|
2984
|
+
problems
|
|
2985
|
+
);
|
|
2986
|
+
if (option["apply"] !== void 0) {
|
|
2987
|
+
requireOneOf(option, "apply", ["provider", "client"], path, problems);
|
|
2988
|
+
}
|
|
2989
|
+
if (type === "sort") {
|
|
2990
|
+
if (fallback !== void 0) {
|
|
2991
|
+
const sort = typeof fallback === "string" ? parseSortValue(fallback) : void 0;
|
|
2992
|
+
if (sort === void 0) {
|
|
2993
|
+
problems.push(
|
|
2994
|
+
`${path}.default must look like "c1:desc" ("column:asc" or "column:desc"), got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
|
|
2995
|
+
);
|
|
2996
|
+
} else if (columnKeys.length > 0 && !columnKeys.includes(sort.column)) {
|
|
2997
|
+
requireOneOf(
|
|
2998
|
+
{ default: sort.column },
|
|
2999
|
+
"default",
|
|
3000
|
+
columnKeys,
|
|
3001
|
+
path,
|
|
3002
|
+
problems
|
|
3003
|
+
);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
} else if (!Array.isArray(fallback) || fallback.length === 0 || fallback.some((k) => typeof k !== "string")) {
|
|
3007
|
+
problems.push(
|
|
3008
|
+
`${path}.default must be a non-empty array of column keys, got ${describeType2(fallback)}.`
|
|
3009
|
+
);
|
|
3010
|
+
} else {
|
|
3011
|
+
const shown = /* @__PURE__ */ new Set();
|
|
3012
|
+
for (const columnKey of fallback) {
|
|
3013
|
+
if (columnKeys.length > 0 && !columnKeys.includes(columnKey)) {
|
|
3014
|
+
problems.push(
|
|
3015
|
+
`${path}.default has "${columnKey}", which is not one of the columns (${columnKeys.map((c) => `"${c}"`).join(", ")}).`
|
|
3016
|
+
);
|
|
3017
|
+
} else if (shown.has(columnKey)) {
|
|
3018
|
+
problems.push(`${path}.default lists "${columnKey}" twice.`);
|
|
3019
|
+
}
|
|
3020
|
+
shown.add(columnKey);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
break;
|
|
2594
3024
|
}
|
|
2595
|
-
assigned.get(pageKey ?? homePageKey(layout, definition))?.push(definition);
|
|
2596
3025
|
}
|
|
2597
|
-
return assigned;
|
|
2598
3026
|
}
|
|
2599
|
-
function
|
|
2600
|
-
|
|
2601
|
-
|
|
3027
|
+
function checkOptions(options, kind, problems) {
|
|
3028
|
+
if (options === void 0) return;
|
|
3029
|
+
if (!Array.isArray(options)) {
|
|
3030
|
+
problems.push(`options must be an array, got ${describeType2(options)}.`);
|
|
3031
|
+
return;
|
|
2602
3032
|
}
|
|
2603
|
-
|
|
3033
|
+
if (options.length > MAX_WIDGET_OPTIONS) {
|
|
3034
|
+
problems.push(
|
|
3035
|
+
`options lists ${options.length} options; the most a widget may declare is ${MAX_WIDGET_OPTIONS}.`
|
|
3036
|
+
);
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
3040
|
+
const seenTypes = /* @__PURE__ */ new Set();
|
|
3041
|
+
options.forEach((option, index) => {
|
|
3042
|
+
checkOption(
|
|
3043
|
+
option,
|
|
3044
|
+
`options[${index}]`,
|
|
3045
|
+
kind,
|
|
3046
|
+
seenKeys,
|
|
3047
|
+
seenTypes,
|
|
3048
|
+
problems
|
|
3049
|
+
);
|
|
3050
|
+
});
|
|
2604
3051
|
}
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
3052
|
+
var MAX_VIEW_COLUMN = 1e3;
|
|
3053
|
+
function checkColumnIndex(value, path, problems) {
|
|
3054
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > MAX_VIEW_COLUMN) {
|
|
3055
|
+
problems.push(
|
|
3056
|
+
`${path} must be a column index (a whole number from 0), got ${typeof value === "number" ? String(value) : describeType2(value)}.`
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
2609
3059
|
}
|
|
2610
|
-
function
|
|
2611
|
-
|
|
2612
|
-
(
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
)
|
|
2619
|
-
|
|
3060
|
+
function checkConversion(conversion, path, baseKind, viewKind2, problems) {
|
|
3061
|
+
if (!isRecord3(conversion)) {
|
|
3062
|
+
problems.push(
|
|
3063
|
+
`${path} must be an object with a type, got ${describeType2(conversion)}.`
|
|
3064
|
+
);
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
const type = conversion["type"];
|
|
3068
|
+
const known = typeof type === "string" && Object.hasOwn(VIEW_CONVERSIONS, type) ? VIEW_CONVERSIONS[type] : void 0;
|
|
3069
|
+
if (known === void 0) {
|
|
3070
|
+
problems.push(
|
|
3071
|
+
`${path}.type must be one of ${Object.keys(VIEW_CONVERSIONS).map((name) => `"${name}"`).join(", ")}, got ${describeType2(type)}.`
|
|
3072
|
+
);
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
if (baseKind !== known.from) {
|
|
3076
|
+
problems.push(
|
|
3077
|
+
`${path}.type "${String(type)}" reads a ${known.from} widget, but this one is ${String(baseKind)}.`
|
|
3078
|
+
);
|
|
3079
|
+
}
|
|
3080
|
+
if (viewKind2 !== known.to) {
|
|
3081
|
+
problems.push(
|
|
3082
|
+
`${path}.type "${String(type)}" produces ${known.to}, but the view's kind is ${String(viewKind2)}.`
|
|
3083
|
+
);
|
|
3084
|
+
}
|
|
3085
|
+
switch (type) {
|
|
3086
|
+
case "tableToBarList":
|
|
3087
|
+
checkColumnIndex(conversion["label"], `${path}.label`, problems);
|
|
3088
|
+
checkColumnIndex(conversion["value"], `${path}.value`, problems);
|
|
3089
|
+
break;
|
|
3090
|
+
case "tableToGraph": {
|
|
3091
|
+
checkColumnIndex(conversion["label"], `${path}.label`, problems);
|
|
3092
|
+
const values = conversion["values"];
|
|
3093
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
3094
|
+
problems.push(`${path}.values must list 1 or more column indexes.`);
|
|
3095
|
+
} else if (values.length > MAX_GRAPH_SERIES) {
|
|
3096
|
+
problems.push(
|
|
3097
|
+
`${path}.values lists ${values.length} series; a chart carries at most ${MAX_GRAPH_SERIES}.`
|
|
3098
|
+
);
|
|
3099
|
+
} else {
|
|
3100
|
+
values.forEach(
|
|
3101
|
+
(index, at) => checkColumnIndex(index, `${path}.values[${at}]`, problems)
|
|
3102
|
+
);
|
|
3103
|
+
}
|
|
3104
|
+
if (conversion["chartType"] !== "bar" && conversion["chartType"] !== "line") {
|
|
3105
|
+
problems.push(`${path}.chartType must be "bar" or "line".`);
|
|
3106
|
+
}
|
|
3107
|
+
break;
|
|
3108
|
+
}
|
|
3109
|
+
case "barListToGraph":
|
|
3110
|
+
if (conversion["chartType"] !== "bar" && conversion["chartType"] !== "line") {
|
|
3111
|
+
problems.push(`${path}.chartType must be "bar" or "line".`);
|
|
3112
|
+
}
|
|
3113
|
+
break;
|
|
3114
|
+
}
|
|
3115
|
+
if (conversion["valueFormat"] !== void 0 && !VALUE_FORMATS.includes(conversion["valueFormat"])) {
|
|
3116
|
+
problems.push(
|
|
3117
|
+
`${path}.valueFormat must be one of ${VALUE_FORMATS.map((f) => `"${f}"`).join(", ")}.`
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
2620
3120
|
}
|
|
2621
|
-
function
|
|
2622
|
-
|
|
3121
|
+
function checkViews(value, problems) {
|
|
3122
|
+
const views = value["views"];
|
|
3123
|
+
const defaultView = value["defaultView"];
|
|
3124
|
+
optionalString(value, "baseViewLabel", "", problems);
|
|
3125
|
+
if (value["baseViewLabel"] === "") {
|
|
3126
|
+
problems.push("baseViewLabel must not be empty.");
|
|
3127
|
+
}
|
|
3128
|
+
const declared = /* @__PURE__ */ new Set([BASE_VIEW_KEY]);
|
|
3129
|
+
if (views !== void 0) {
|
|
3130
|
+
if (!Array.isArray(views)) {
|
|
3131
|
+
problems.push(`views must be an array, got ${describeType2(views)}.`);
|
|
3132
|
+
} else if (views.length > MAX_WIDGET_VIEWS) {
|
|
3133
|
+
problems.push(
|
|
3134
|
+
`views lists ${views.length} views; the most a widget may declare is ${MAX_WIDGET_VIEWS}.`
|
|
3135
|
+
);
|
|
3136
|
+
} else {
|
|
3137
|
+
views.forEach((view, index) => {
|
|
3138
|
+
const path = `views[${index}]`;
|
|
3139
|
+
if (!isRecord3(view)) {
|
|
3140
|
+
problems.push(
|
|
3141
|
+
`${path} must be an object, got ${describeType2(view)}.`
|
|
3142
|
+
);
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
const key = view["key"];
|
|
3146
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
3147
|
+
problems.push(`${path}.key must be a non-empty string.`);
|
|
3148
|
+
} else if (declared.has(key)) {
|
|
3149
|
+
problems.push(
|
|
3150
|
+
key === BASE_VIEW_KEY ? `${path}.key "${BASE_VIEW_KEY}" is reserved for the widget's own kind.` : `${path}.key "${key}" is used twice.`
|
|
3151
|
+
);
|
|
3152
|
+
} else {
|
|
3153
|
+
declared.add(key);
|
|
3154
|
+
}
|
|
3155
|
+
if (typeof view["label"] !== "string" || view["label"].trim() === "") {
|
|
3156
|
+
problems.push(`${path}.label must be a non-empty string.`);
|
|
3157
|
+
}
|
|
3158
|
+
requireOneOf(view, "kind", WIDGET_KINDS, path, problems);
|
|
3159
|
+
if (view["convert"] === void 0) {
|
|
3160
|
+
problems.push(
|
|
3161
|
+
`${path}.convert is required: views supplied by a provider are not supported yet.`
|
|
3162
|
+
);
|
|
3163
|
+
} else {
|
|
3164
|
+
checkConversion(
|
|
3165
|
+
view["convert"],
|
|
3166
|
+
`${path}.convert`,
|
|
3167
|
+
value["kind"],
|
|
3168
|
+
view["kind"],
|
|
3169
|
+
problems
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
if (defaultView !== void 0) {
|
|
3176
|
+
if (typeof defaultView !== "string" || !declared.has(defaultView)) {
|
|
3177
|
+
problems.push(
|
|
3178
|
+
`defaultView must be "${BASE_VIEW_KEY}" or the key of a declared view, got ${typeof defaultView === "string" ? JSON.stringify(defaultView) : describeType2(defaultView)}.`
|
|
3179
|
+
);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
2623
3182
|
}
|
|
2624
|
-
function
|
|
2625
|
-
|
|
3183
|
+
function formatProblems(prefix, problems) {
|
|
3184
|
+
const shown = problems.slice(0, MAX_REPORTED_PROBLEMS);
|
|
3185
|
+
const more = problems.length - shown.length;
|
|
3186
|
+
return `${prefix}: ${shown.join(" ")}${more > 0 ? ` (and ${more} more problem(s))` : ""}`;
|
|
2626
3187
|
}
|
|
2627
|
-
function
|
|
2628
|
-
const
|
|
2629
|
-
if (
|
|
2630
|
-
|
|
3188
|
+
function validateWidgetData(value, options = {}) {
|
|
3189
|
+
const subject = options.widgetKey === void 0 ? "Widget data" : `Widget ${JSON.stringify(options.widgetKey)}`;
|
|
3190
|
+
if (!isRecord3(value)) {
|
|
3191
|
+
return err(`${subject}: must be an object, got ${describeType2(value)}.`);
|
|
3192
|
+
}
|
|
3193
|
+
if (value["empty"] === true) {
|
|
3194
|
+
if (typeof value["text"] !== "string") {
|
|
3195
|
+
return err(
|
|
3196
|
+
`${subject} (empty): text must be a string, got ${describeType2(value["text"])}.`
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
return ok(value);
|
|
3200
|
+
}
|
|
3201
|
+
const kind = value["kind"];
|
|
3202
|
+
if (!isWidgetKind(kind)) {
|
|
2631
3203
|
return err(
|
|
2632
|
-
|
|
3204
|
+
`${subject}: kind must be one of ${WIDGET_KINDS.join(", ")} (or the payload must be {"empty": true, "text": ...}), got ${typeof kind === "string" ? JSON.stringify(kind) : describeType2(kind)}.`
|
|
2633
3205
|
);
|
|
2634
3206
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
if (clash !== void 0) {
|
|
2640
|
-
return err(`Another page is already called "${clash.title}".`);
|
|
3207
|
+
if (options.expectedKind !== void 0 && kind !== options.expectedKind) {
|
|
3208
|
+
return err(
|
|
3209
|
+
`${subject}: expected kind ${options.expectedKind} from its definition, got ${kind}.`
|
|
3210
|
+
);
|
|
2641
3211
|
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
return `page-${n}`;
|
|
2649
|
-
}
|
|
2650
|
-
function nextPageTitle(layout) {
|
|
2651
|
-
const used = new Set(
|
|
2652
|
-
pageList(layout).map((p) => normalizedTitle(p.title))
|
|
2653
|
-
);
|
|
2654
|
-
let n = pageList(layout).length + 1;
|
|
2655
|
-
while (used.has(normalizedTitle(`Page ${n}`))) n += 1;
|
|
2656
|
-
return `Page ${n}`;
|
|
3212
|
+
const problems = [];
|
|
3213
|
+
checkPayloadFields(kind, value, problems);
|
|
3214
|
+
if (problems.length > 0) {
|
|
3215
|
+
return err(formatProblems(`${subject} (${kind})`, problems));
|
|
3216
|
+
}
|
|
3217
|
+
return ok(value);
|
|
2657
3218
|
}
|
|
2658
|
-
function
|
|
2659
|
-
if (
|
|
3219
|
+
function validateWidgetDefinition(value) {
|
|
3220
|
+
if (!isRecord3(value)) {
|
|
2660
3221
|
return err(
|
|
2661
|
-
`
|
|
3222
|
+
`Widget definition: must be an object, got ${describeType2(value)}.`
|
|
2662
3223
|
);
|
|
2663
3224
|
}
|
|
2664
|
-
const
|
|
2665
|
-
|
|
2666
|
-
|
|
3225
|
+
const subject = typeof value["key"] === "string" ? `Widget definition ${JSON.stringify(value["key"])}` : "Widget definition";
|
|
3226
|
+
const problems = [];
|
|
3227
|
+
requireString(value, "key", "", problems);
|
|
3228
|
+
if (value["key"] === "") {
|
|
3229
|
+
problems.push("key must not be empty.");
|
|
2667
3230
|
}
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
);
|
|
2672
|
-
if (
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
3231
|
+
requireString(value, "title", "", problems);
|
|
3232
|
+
optionalString(value, "description", "", problems);
|
|
3233
|
+
requireOneOf(value, "kind", WIDGET_KINDS, "", problems);
|
|
3234
|
+
optionalNumber(value, "sortOrder", "", problems);
|
|
3235
|
+
if (value["roles"] !== void 0) {
|
|
3236
|
+
const roles = requireArray(value, "roles", "", problems);
|
|
3237
|
+
roles?.forEach((role, index) => {
|
|
3238
|
+
if (typeof role !== "string") {
|
|
3239
|
+
problems.push(
|
|
3240
|
+
`roles[${index}] must be a string, got ${describeType2(role)}.`
|
|
3241
|
+
);
|
|
3242
|
+
}
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
optionalBoolean(value, "active", "", problems);
|
|
3246
|
+
if (value["defaultSize"] !== void 0) {
|
|
3247
|
+
requireOneOf(value, "defaultSize", WIDGET_SIZES, "", problems);
|
|
3248
|
+
}
|
|
3249
|
+
if (value["width"] !== void 0 && !isWidgetWidth(value["width"])) {
|
|
3250
|
+
problems.push(
|
|
3251
|
+
`width must be an integer from ${MIN_WIDGET_WIDTH} to ${MAX_WIDGET_WIDTH} (twelfths of the row), got ${typeof value["width"] === "number" ? String(value["width"]) : describeType2(value["width"])}.`
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
checkWidthLimits(value, problems);
|
|
3255
|
+
checkDetailOption(value["detail"], problems);
|
|
3256
|
+
checkTableControlsOption(value["tableControls"], value["kind"], problems);
|
|
3257
|
+
checkLockedOption(value["locked"], problems);
|
|
3258
|
+
checkOptions(value["options"], value["kind"], problems);
|
|
3259
|
+
checkViews(value, problems);
|
|
3260
|
+
if (value["page"] !== void 0) {
|
|
3261
|
+
if (typeof value["page"] !== "string" || value["page"].trim() === "") {
|
|
3262
|
+
problems.push(
|
|
3263
|
+
`page must be a non-empty page key or title, got ${describeType2(value["page"])}.`
|
|
3264
|
+
);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
if (value["fill"] !== void 0) {
|
|
3268
|
+
requireOneOf(value, "fill", WIDGET_FILLS, "", problems);
|
|
3269
|
+
}
|
|
3270
|
+
if (problems.length > 0) {
|
|
3271
|
+
return err(formatProblems(subject, problems));
|
|
3272
|
+
}
|
|
3273
|
+
return ok(value);
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
// src/core/resolve.ts
|
|
3277
|
+
function resolvePayload(definition, payload, options = {}) {
|
|
3278
|
+
let checked;
|
|
3279
|
+
if (options.validate === false) {
|
|
3280
|
+
checked = payload;
|
|
3281
|
+
} else {
|
|
3282
|
+
const result = validateWidgetData(payload, {
|
|
3283
|
+
widgetKey: definition.key,
|
|
3284
|
+
expectedKind: definition.kind
|
|
3285
|
+
});
|
|
3286
|
+
if (!result.ok) {
|
|
3287
|
+
return { definition, status: "error", error: result.error };
|
|
3288
|
+
}
|
|
3289
|
+
checked = result.value;
|
|
3290
|
+
}
|
|
3291
|
+
const emptyText = emptyTextFor(checked);
|
|
3292
|
+
if (emptyText !== null) {
|
|
3293
|
+
return { definition, status: "empty", emptyText };
|
|
3294
|
+
}
|
|
3295
|
+
return { definition, status: "ok", data: checked };
|
|
2682
3296
|
}
|
|
2683
|
-
function
|
|
2684
|
-
|
|
2685
|
-
(p) => p.key === pageKey
|
|
2686
|
-
);
|
|
2687
|
-
if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
|
|
2688
|
-
const checked = checkTitle(layout, title, pageKey);
|
|
2689
|
-
if (!checked.ok) return checked;
|
|
2690
|
-
if (checked.value === page.title) return ok(layout);
|
|
2691
|
-
const base = withExplicitPages(layout);
|
|
2692
|
-
return ok({
|
|
2693
|
-
...base,
|
|
2694
|
-
pages: (base.pages ?? []).map(
|
|
2695
|
-
(p) => p.key === pageKey ? { ...p, title: checked.value } : p
|
|
2696
|
-
)
|
|
2697
|
-
});
|
|
3297
|
+
function failedWidget(definition, error) {
|
|
3298
|
+
return { definition, status: "error", error };
|
|
2698
3299
|
}
|
|
2699
|
-
function
|
|
2700
|
-
|
|
2701
|
-
const from = pages.findIndex((p) => p.key === pageKey);
|
|
2702
|
-
if (from === -1) return layout;
|
|
2703
|
-
const to = Math.max(
|
|
2704
|
-
0,
|
|
2705
|
-
Math.min(pages.length - 1, from + Math.trunc(offset))
|
|
2706
|
-
);
|
|
2707
|
-
if (to === from) return layout;
|
|
2708
|
-
const next = [...pages];
|
|
2709
|
-
const [moved] = next.splice(from, 1);
|
|
2710
|
-
next.splice(to, 0, moved);
|
|
2711
|
-
return { ...layout, pages: next };
|
|
3300
|
+
function loadingWidgets(definitions) {
|
|
3301
|
+
return definitions.map((definition) => ({ definition, status: "loading" }));
|
|
2712
3302
|
}
|
|
2713
|
-
function
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
);
|
|
3303
|
+
function cacheKeyFor(definition, options, values) {
|
|
3304
|
+
const base = options.cacheKey === void 0 ? definition.sourceKey ?? definition.key : options.cacheKey(definition);
|
|
3305
|
+
return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
|
|
2717
3306
|
}
|
|
2718
|
-
function
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
|
|
2723
|
-
if (pages.length === 1) {
|
|
2724
|
-
return err(
|
|
2725
|
-
`${describePage(page)} is the only page, so it can't be removed.`
|
|
2726
|
-
);
|
|
2727
|
-
}
|
|
2728
|
-
const target = pages[index > 0 ? index - 1 : 1];
|
|
2729
|
-
const merged = {
|
|
2730
|
-
...layout,
|
|
2731
|
-
pages: pages.filter((p) => p.key !== pageKey).map(
|
|
2732
|
-
(p) => p.key === target.key ? {
|
|
2733
|
-
...p,
|
|
2734
|
-
order: index > 0 ? [...p.order, ...page.order] : [...page.order, ...p.order],
|
|
2735
|
-
hidden: [...p.hidden, ...page.hidden],
|
|
2736
|
-
minimized: [...p.minimized, ...page.minimized]
|
|
2737
|
-
} : p
|
|
2738
|
-
)
|
|
2739
|
-
};
|
|
2740
|
-
const full = overflowing(
|
|
2741
|
-
definitions,
|
|
2742
|
-
merged,
|
|
3307
|
+
function optionValuesFor(definition, options) {
|
|
3308
|
+
return resolveOptionValues(
|
|
3309
|
+
definition,
|
|
3310
|
+
options.optionValues?.[definition.key],
|
|
2743
3311
|
options
|
|
2744
3312
|
);
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
3313
|
+
}
|
|
3314
|
+
function finalizeWidget(widget, values) {
|
|
3315
|
+
if (Object.keys(values).length === 0) return widget;
|
|
3316
|
+
if (widget.status === "ok") {
|
|
3317
|
+
const data = applyClientOptions(
|
|
3318
|
+
widget.definition,
|
|
3319
|
+
widget.data,
|
|
3320
|
+
values
|
|
2748
3321
|
);
|
|
3322
|
+
return { ...widget, data, options: values };
|
|
2749
3323
|
}
|
|
2750
|
-
return
|
|
3324
|
+
return { ...widget, options: values };
|
|
2751
3325
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
3326
|
+
var LoadInterruption = class extends Error {
|
|
3327
|
+
};
|
|
3328
|
+
async function resolveWidget(definition, provider, context, options = {}) {
|
|
3329
|
+
const values = optionValuesFor(definition, options);
|
|
3330
|
+
return finalizeWidget(
|
|
3331
|
+
await resolveWidgetUnapplied(definition, provider, context, options),
|
|
3332
|
+
values
|
|
3333
|
+
);
|
|
2754
3334
|
}
|
|
2755
|
-
function
|
|
2756
|
-
const
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
3335
|
+
async function resolveWidgetUnapplied(definition, provider, context, options = {}) {
|
|
3336
|
+
const values = optionValuesFor(definition, options);
|
|
3337
|
+
const quotedKey = JSON.stringify(definition.key);
|
|
3338
|
+
if (provider === void 0) {
|
|
3339
|
+
return failedWidget(
|
|
3340
|
+
definition,
|
|
3341
|
+
`Widget ${quotedKey}: no provider is registered for this key.`
|
|
2760
3342
|
);
|
|
2761
3343
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
if (sourceKey === targetKey) return ok(layout);
|
|
2767
|
-
if (resolveWidgetLock(widget).move) {
|
|
2768
|
-
return err(
|
|
2769
|
-
`Widget "${titleOf(widget)}" is locked against moving, so it can't be moved to ${describePage(target)}.`
|
|
3344
|
+
if (options.signal?.aborted === true) {
|
|
3345
|
+
return failedWidget(
|
|
3346
|
+
definition,
|
|
3347
|
+
`Widget ${quotedKey}: loading was canceled before it started.`
|
|
2770
3348
|
);
|
|
2771
3349
|
}
|
|
2772
|
-
const
|
|
2773
|
-
const
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
const
|
|
2777
|
-
const
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
return
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
if (!hidden) {
|
|
2806
|
-
const allowed = rowsAllowed(target, options);
|
|
2807
|
-
const needed = placeRows(
|
|
2808
|
-
pageWidgets(definitions, moved, targetKey)
|
|
2809
|
-
).length;
|
|
2810
|
-
if (needed > allowed) {
|
|
2811
|
-
return err(
|
|
2812
|
-
`${describePage(target)} has no room for "${titleOf(widget)}": it would need row ${needed}, and the page allows ${allowed}.`
|
|
3350
|
+
const controller = new AbortController();
|
|
3351
|
+
const onExternalAbort = () => controller.abort(options.signal?.reason);
|
|
3352
|
+
options.signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
3353
|
+
let timedOut = false;
|
|
3354
|
+
const timeoutMs = options.timeoutMs !== void 0 && options.timeoutMs > 0 ? options.timeoutMs : void 0;
|
|
3355
|
+
const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
|
|
3356
|
+
timedOut = true;
|
|
3357
|
+
controller.abort(new Error(`Timed out after ${timeoutMs} ms.`));
|
|
3358
|
+
}, timeoutMs);
|
|
3359
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
3360
|
+
controller.signal.addEventListener(
|
|
3361
|
+
"abort",
|
|
3362
|
+
() => reject(new LoadInterruption()),
|
|
3363
|
+
{ once: true }
|
|
3364
|
+
);
|
|
3365
|
+
});
|
|
3366
|
+
let payload;
|
|
3367
|
+
try {
|
|
3368
|
+
payload = await Promise.race([
|
|
3369
|
+
Promise.resolve().then(() => {
|
|
3370
|
+
if (controller.signal.aborted) throw new LoadInterruption();
|
|
3371
|
+
return provider(context, definition, {
|
|
3372
|
+
signal: controller.signal,
|
|
3373
|
+
options: values
|
|
3374
|
+
});
|
|
3375
|
+
}),
|
|
3376
|
+
interrupted
|
|
3377
|
+
]);
|
|
3378
|
+
} catch (cause) {
|
|
3379
|
+
if (cause instanceof LoadInterruption) {
|
|
3380
|
+
return failedWidget(
|
|
3381
|
+
definition,
|
|
3382
|
+
timedOut ? `Widget ${quotedKey}: provider timed out after ${timeoutMs} ms.` : `Widget ${quotedKey}: loading was canceled.`
|
|
2813
3383
|
);
|
|
2814
3384
|
}
|
|
3385
|
+
return failedWidget(
|
|
3386
|
+
definition,
|
|
3387
|
+
`Widget ${quotedKey}: provider failed: ${describeUnknownError(cause)}`
|
|
3388
|
+
);
|
|
3389
|
+
} finally {
|
|
3390
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
3391
|
+
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
2815
3392
|
}
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
if (
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
let candidate = base;
|
|
2828
|
-
let n = 1;
|
|
2829
|
-
while (taken.has(normalizedTitle(candidate))) {
|
|
2830
|
-
const suffix = ` (#${n})`;
|
|
2831
|
-
candidate = `${base.slice(0, MAX_PAGE_TITLE_LENGTH - suffix.length)}${suffix}`;
|
|
2832
|
-
n += 1;
|
|
3393
|
+
const resolved = resolvePayload(definition, payload, options);
|
|
3394
|
+
if (resolved.status !== "ok") return resolved;
|
|
3395
|
+
const storedAt = Date.now();
|
|
3396
|
+
if (options.cache !== void 0) {
|
|
3397
|
+
try {
|
|
3398
|
+
await options.cache.set(cacheKeyFor(definition, options, values), {
|
|
3399
|
+
payload: resolved.data,
|
|
3400
|
+
storedAt
|
|
3401
|
+
});
|
|
3402
|
+
} catch {
|
|
3403
|
+
}
|
|
2833
3404
|
}
|
|
2834
|
-
|
|
2835
|
-
return candidate;
|
|
3405
|
+
return { ...resolved, updatedAt: storedAt };
|
|
2836
3406
|
}
|
|
2837
|
-
function
|
|
2838
|
-
return
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
});
|
|
3407
|
+
function widgetsFor(definitions, roles) {
|
|
3408
|
+
return sortDefinitions(
|
|
3409
|
+
definitions.filter((definition) => isWidgetVisibleTo(definition, roles))
|
|
3410
|
+
);
|
|
2842
3411
|
}
|
|
2843
|
-
function
|
|
2844
|
-
const
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
3412
|
+
async function resolveWidgets(definitions, providers, context, options = {}) {
|
|
3413
|
+
const visible = widgetsFor(definitions, context.roles);
|
|
3414
|
+
return Promise.all(
|
|
3415
|
+
visible.map(
|
|
3416
|
+
(definition) => resolveWidget(
|
|
3417
|
+
definition,
|
|
3418
|
+
Object.prototype.hasOwnProperty.call(
|
|
3419
|
+
providers,
|
|
3420
|
+
definition.sourceKey ?? definition.key
|
|
3421
|
+
) ? providers[definition.sourceKey ?? definition.key] : void 0,
|
|
3422
|
+
context,
|
|
3423
|
+
options
|
|
3424
|
+
)
|
|
3425
|
+
)
|
|
2850
3426
|
);
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
// src/core/loader.ts
|
|
3430
|
+
function createWidgetLoader(options) {
|
|
3431
|
+
const { definitions, providers, context, ...baseOptions } = options;
|
|
3432
|
+
const chosen = new Map(
|
|
3433
|
+
Object.entries(baseOptions.optionValues ?? {}).flatMap(
|
|
3434
|
+
([key, values]) => values === void 0 ? [] : [[key, values]]
|
|
3435
|
+
)
|
|
2856
3436
|
);
|
|
2857
|
-
const
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
pages.push(next);
|
|
2876
|
-
});
|
|
2877
|
-
const work = () => ({ ...locked, pages });
|
|
2878
|
-
const pinned = defs.filter((d) => resolveWidgetLock(d).move);
|
|
2879
|
-
for (const widget of pinned) {
|
|
2880
|
-
const home = homePageKey(work(), widget);
|
|
2881
|
-
const keysHere = (p, list) => p.key !== home && p[list].includes(widget.key);
|
|
2882
|
-
const wasHidden = pages.some((p) => keysHere(p, "hidden"));
|
|
2883
|
-
const wasMinimized = pages.some((p) => keysHere(p, "minimized"));
|
|
2884
|
-
if (!wasHidden && !wasMinimized) continue;
|
|
2885
|
-
pages = pages.map((p) => {
|
|
2886
|
-
let next = p;
|
|
2887
|
-
if (keysHere(p, "hidden")) {
|
|
2888
|
-
next = { ...next, hidden: without(next.hidden, widget.key) };
|
|
2889
|
-
}
|
|
2890
|
-
if (keysHere(p, "minimized")) {
|
|
2891
|
-
next = { ...next, minimized: without(next.minimized, widget.key) };
|
|
3437
|
+
const resolveOptions = baseOptions;
|
|
3438
|
+
function currentOptions() {
|
|
3439
|
+
return { ...resolveOptions, optionValues: Object.fromEntries(chosen) };
|
|
3440
|
+
}
|
|
3441
|
+
const unapplied = /* @__PURE__ */ new Map();
|
|
3442
|
+
const visible = widgetsFor(definitions, context.roles);
|
|
3443
|
+
let snapshot = loadingWidgets(visible);
|
|
3444
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3445
|
+
const started = /* @__PURE__ */ new Set();
|
|
3446
|
+
const generations = /* @__PURE__ */ new Map();
|
|
3447
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
3448
|
+
let disposed = false;
|
|
3449
|
+
function publish(next) {
|
|
3450
|
+
snapshot = next;
|
|
3451
|
+
for (const listener of [...listeners]) {
|
|
3452
|
+
try {
|
|
3453
|
+
listener();
|
|
3454
|
+
} catch {
|
|
2892
3455
|
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
function replace(key, widget) {
|
|
3459
|
+
publish(
|
|
3460
|
+
snapshot.map(
|
|
3461
|
+
(current) => current.definition.key === key ? widget : current
|
|
3462
|
+
)
|
|
3463
|
+
);
|
|
3464
|
+
}
|
|
3465
|
+
function abortAll(reason) {
|
|
3466
|
+
for (const controller of inFlight.values()) controller.abort(reason);
|
|
3467
|
+
inFlight.clear();
|
|
3468
|
+
}
|
|
3469
|
+
resolveOptions.signal?.addEventListener(
|
|
3470
|
+
"abort",
|
|
3471
|
+
() => abortAll(resolveOptions.signal?.reason),
|
|
3472
|
+
{ once: true }
|
|
3473
|
+
);
|
|
3474
|
+
async function run(definition, readCache) {
|
|
3475
|
+
const key = definition.key;
|
|
3476
|
+
const generation = (generations.get(key) ?? 0) + 1;
|
|
3477
|
+
generations.set(key, generation);
|
|
3478
|
+
inFlight.get(key)?.abort(new Error("Superseded by a newer load."));
|
|
3479
|
+
const controller = new AbortController();
|
|
3480
|
+
inFlight.set(key, controller);
|
|
3481
|
+
const isCurrent = () => !disposed && generations.get(key) === generation;
|
|
3482
|
+
const runOptions = currentOptions();
|
|
3483
|
+
const values = optionValuesFor(definition, runOptions);
|
|
3484
|
+
if (readCache && runOptions.cache !== void 0) {
|
|
3485
|
+
try {
|
|
3486
|
+
const entry = await runOptions.cache.get(
|
|
3487
|
+
cacheKeyFor(definition, runOptions, values)
|
|
3488
|
+
);
|
|
3489
|
+
if (entry !== void 0 && isCurrent()) {
|
|
3490
|
+
const cached = resolvePayload(definition, entry.payload, runOptions);
|
|
3491
|
+
if (cached.status === "ok") {
|
|
3492
|
+
const stale = { ...cached, updatedAt: entry.storedAt, stale: true };
|
|
3493
|
+
unapplied.set(key, stale);
|
|
3494
|
+
replace(key, finalizeWidget(stale, values));
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
} catch {
|
|
2899
3498
|
}
|
|
2900
|
-
|
|
3499
|
+
}
|
|
3500
|
+
if (!isCurrent()) return;
|
|
3501
|
+
const providerKey = definition.sourceKey ?? key;
|
|
3502
|
+
const provider = Object.prototype.hasOwnProperty.call(
|
|
3503
|
+
providers,
|
|
3504
|
+
providerKey
|
|
3505
|
+
) ? providers[providerKey] : void 0;
|
|
3506
|
+
const result = await resolveWidgetUnapplied(definition, provider, context, {
|
|
3507
|
+
...runOptions,
|
|
3508
|
+
signal: controller.signal
|
|
2901
3509
|
});
|
|
3510
|
+
if (!isCurrent()) return;
|
|
3511
|
+
inFlight.delete(key);
|
|
3512
|
+
if (result.status === "ok") unapplied.set(key, result);
|
|
3513
|
+
else unapplied.delete(key);
|
|
3514
|
+
replace(key, finalizeWidget(result, values));
|
|
2902
3515
|
}
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
3516
|
+
function start(definition, readCache) {
|
|
3517
|
+
started.add(definition.key);
|
|
3518
|
+
void run(definition, readCache);
|
|
2906
3519
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
(a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
|
|
2910
|
-
).map(({ definition }) => definition);
|
|
2911
|
-
for (const widget of unplaced) {
|
|
2912
|
-
const home = homePageKey(work(), widget);
|
|
2913
|
-
pages = pages.map(
|
|
2914
|
-
(p) => p.key === home ? { ...p, order: [...p.order, widget.key] } : p
|
|
2915
|
-
);
|
|
3520
|
+
function definitionFor(key) {
|
|
3521
|
+
return visible.find((definition) => definition.key === key);
|
|
2916
3522
|
}
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
const
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
}
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
for (let j = i + 1; j < pages.length && !placed; j += 1) {
|
|
2937
|
-
const candidate = pages[j];
|
|
2938
|
-
const trial = pages.map(
|
|
2939
|
-
(p) => p.key === candidate.key ? {
|
|
2940
|
-
...p,
|
|
2941
|
-
order: [...moving, ...p.order],
|
|
2942
|
-
minimized: [...p.minimized, ...movedMinimized]
|
|
2943
|
-
} : p
|
|
2944
|
-
);
|
|
2945
|
-
const items = pageWidgets(
|
|
2946
|
-
defs,
|
|
2947
|
-
{ ...locked, pages: trial },
|
|
2948
|
-
candidate.key
|
|
3523
|
+
function load(key) {
|
|
3524
|
+
if (disposed || started.has(key)) return;
|
|
3525
|
+
const definition = definitionFor(key);
|
|
3526
|
+
if (definition !== void 0) start(definition, true);
|
|
3527
|
+
}
|
|
3528
|
+
function loadAll() {
|
|
3529
|
+
for (const definition of visible) load(definition.key);
|
|
3530
|
+
}
|
|
3531
|
+
function refresh(key, refreshOptions) {
|
|
3532
|
+
if (disposed) return;
|
|
3533
|
+
const keys = key === void 0 ? [...started] : [key];
|
|
3534
|
+
for (const target of keys) {
|
|
3535
|
+
const definition = definitionFor(target);
|
|
3536
|
+
if (definition === void 0) continue;
|
|
3537
|
+
if (refreshOptions?.skipInFlight === true && inFlight.has(target)) {
|
|
3538
|
+
continue;
|
|
3539
|
+
}
|
|
3540
|
+
const current = snapshot.find(
|
|
3541
|
+
(widget) => widget.definition.key === target
|
|
2949
3542
|
);
|
|
2950
|
-
if (
|
|
2951
|
-
|
|
2952
|
-
placed = true;
|
|
3543
|
+
if (current?.status === "error") {
|
|
3544
|
+
replace(target, { definition, status: "loading" });
|
|
2953
3545
|
}
|
|
3546
|
+
start(definition, false);
|
|
2954
3547
|
}
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3548
|
+
}
|
|
3549
|
+
function setOptions(key, values) {
|
|
3550
|
+
if (disposed) return;
|
|
3551
|
+
const definition = definitionFor(key);
|
|
3552
|
+
if (definition === void 0) return;
|
|
3553
|
+
const before = optionValuesFor(definition, currentOptions());
|
|
3554
|
+
chosen.set(key, { ...values });
|
|
3555
|
+
const runOptions = currentOptions();
|
|
3556
|
+
const after = optionValuesFor(definition, runOptions);
|
|
3557
|
+
if (sameOptionValues(before, after) || !started.has(key)) return;
|
|
3558
|
+
const kept = unapplied.get(key);
|
|
3559
|
+
if (kept !== void 0 && optionsCacheSuffix(definition, before, runOptions) === optionsCacheSuffix(definition, after, runOptions)) {
|
|
3560
|
+
replace(key, finalizeWidget(kept, after));
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
const current = snapshot.find(
|
|
3564
|
+
(widget) => widget.definition.key === key
|
|
3565
|
+
);
|
|
3566
|
+
if (current?.status === "ok") replace(key, { ...current, stale: true });
|
|
3567
|
+
else if (current?.status === "error") {
|
|
3568
|
+
replace(key, { definition, status: "loading" });
|
|
2966
3569
|
}
|
|
3570
|
+
start(definition, true);
|
|
2967
3571
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
3572
|
+
function dispose() {
|
|
3573
|
+
disposed = true;
|
|
3574
|
+
abortAll(new Error("The widget loader was disposed."));
|
|
3575
|
+
listeners.clear();
|
|
3576
|
+
}
|
|
3577
|
+
function subscribe(listener) {
|
|
3578
|
+
listeners.add(listener);
|
|
3579
|
+
return () => {
|
|
3580
|
+
listeners.delete(listener);
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
return {
|
|
3584
|
+
getSnapshot: () => snapshot,
|
|
3585
|
+
subscribe,
|
|
3586
|
+
load,
|
|
3587
|
+
loadAll,
|
|
3588
|
+
refresh,
|
|
3589
|
+
setOptions,
|
|
3590
|
+
dispose
|
|
3591
|
+
};
|
|
2970
3592
|
}
|
|
2971
3593
|
|
|
2972
3594
|
// src/core/scale.ts
|
|
@@ -3798,6 +4420,7 @@ function indent(block) {
|
|
|
3798
4420
|
return block.split("\n").map((line) => ` ${line}`).join("\n");
|
|
3799
4421
|
}
|
|
3800
4422
|
|
|
4423
|
+
exports.BASE_VIEW_KEY = BASE_VIEW_KEY;
|
|
3801
4424
|
exports.DATE_RANGE_PRESETS = DATE_RANGE_PRESETS;
|
|
3802
4425
|
exports.DEFAULT_COMPARISON_LABEL = DEFAULT_COMPARISON_LABEL;
|
|
3803
4426
|
exports.DEFAULT_CURRENCY = DEFAULT_CURRENCY;
|
|
@@ -3813,17 +4436,20 @@ exports.MAX_DETAIL_PAGE_SIZE = MAX_DETAIL_PAGE_SIZE;
|
|
|
3813
4436
|
exports.MAX_GRAPH_SERIES = MAX_GRAPH_SERIES;
|
|
3814
4437
|
exports.MAX_PAGE_ROWS = MAX_PAGE_ROWS;
|
|
3815
4438
|
exports.MAX_PAGE_TITLE_LENGTH = MAX_PAGE_TITLE_LENGTH;
|
|
4439
|
+
exports.MAX_PARSED_CLONES = MAX_PARSED_CLONES;
|
|
3816
4440
|
exports.MAX_PARSED_PAGES = MAX_PARSED_PAGES;
|
|
3817
4441
|
exports.MAX_PARSED_SETTINGS = MAX_PARSED_SETTINGS;
|
|
3818
4442
|
exports.MAX_REPORTED_PROBLEMS = MAX_REPORTED_PROBLEMS;
|
|
3819
4443
|
exports.MAX_TEXT_OPTION_LENGTH = MAX_TEXT_OPTION_LENGTH;
|
|
3820
4444
|
exports.MAX_WIDGET_OPTIONS = MAX_WIDGET_OPTIONS;
|
|
3821
4445
|
exports.MAX_WIDGET_TITLE_LENGTH = MAX_WIDGET_TITLE_LENGTH;
|
|
4446
|
+
exports.MAX_WIDGET_VIEWS = MAX_WIDGET_VIEWS;
|
|
3822
4447
|
exports.MAX_WIDGET_WIDTH = MAX_WIDGET_WIDTH;
|
|
3823
4448
|
exports.MIN_WIDGET_WIDTH = MIN_WIDGET_WIDTH;
|
|
3824
4449
|
exports.OPTION_KEY_PATTERN = OPTION_KEY_PATTERN;
|
|
3825
4450
|
exports.THEME_TOKEN_VARIABLES = THEME_TOKEN_VARIABLES;
|
|
3826
4451
|
exports.VALUE_FORMATS = VALUE_FORMATS;
|
|
4452
|
+
exports.VIEW_CONVERSIONS = VIEW_CONVERSIONS;
|
|
3827
4453
|
exports.WIDGET_FILLS = WIDGET_FILLS;
|
|
3828
4454
|
exports.WIDGET_KINDS = WIDGET_KINDS;
|
|
3829
4455
|
exports.WIDGET_OPTION_TYPES = WIDGET_OPTION_TYPES;
|
|
@@ -3832,20 +4458,28 @@ exports.WIDTH_COLUMNS = WIDTH_COLUMNS;
|
|
|
3832
4458
|
exports.addPage = addPage;
|
|
3833
4459
|
exports.applyClientOptions = applyClientOptions;
|
|
3834
4460
|
exports.applySettings = applySettings;
|
|
4461
|
+
exports.applyView = applyView;
|
|
3835
4462
|
exports.assignPages = assignPages;
|
|
3836
4463
|
exports.baseColumnSpan = baseColumnSpan;
|
|
3837
4464
|
exports.cacheKeyFor = cacheKeyFor;
|
|
4465
|
+
exports.canDuplicate = canDuplicate;
|
|
4466
|
+
exports.cleanClones = cleanClones;
|
|
3838
4467
|
exports.clearWidgetSettings = clearWidgetSettings;
|
|
4468
|
+
exports.cloneNumber = cloneNumber;
|
|
3839
4469
|
exports.coerceOptionValue = coerceOptionValue;
|
|
4470
|
+
exports.convertView = convertView;
|
|
3840
4471
|
exports.createLayoutPersistence = createLayoutPersistence;
|
|
3841
4472
|
exports.createTheme = createTheme;
|
|
3842
4473
|
exports.createWidgetLoader = createWidgetLoader;
|
|
3843
4474
|
exports.defaultDetailQuery = defaultDetailQuery;
|
|
3844
4475
|
exports.defaultOptionValue = defaultOptionValue;
|
|
3845
4476
|
exports.defineWidget = defineWidget;
|
|
4477
|
+
exports.deleteClone = deleteClone;
|
|
3846
4478
|
exports.deriveDetailData = deriveDetailData;
|
|
3847
4479
|
exports.describeUnknownError = describeUnknownError;
|
|
4480
|
+
exports.duplicateWidget = duplicateWidget;
|
|
3848
4481
|
exports.effectiveDefinitions = effectiveDefinitions;
|
|
4482
|
+
exports.effectiveViewKey = effectiveViewKey;
|
|
3849
4483
|
exports.emptyTextFor = emptyTextFor;
|
|
3850
4484
|
exports.emptyWidget = emptyWidget;
|
|
3851
4485
|
exports.enforceLocks = enforceLocks;
|
|
@@ -3856,12 +4490,14 @@ exports.fillWidthSpans = fillWidthSpans;
|
|
|
3856
4490
|
exports.fillsHeight = fillsHeight;
|
|
3857
4491
|
exports.fillsWidth = fillsWidth;
|
|
3858
4492
|
exports.finalizeWidget = finalizeWidget;
|
|
4493
|
+
exports.findView = findView;
|
|
3859
4494
|
exports.fitCount = fitCount;
|
|
3860
4495
|
exports.formatCount = formatCount;
|
|
3861
4496
|
exports.formatPercentChange = formatPercentChange;
|
|
3862
4497
|
exports.formatValue = formatValue;
|
|
3863
4498
|
exports.fromLegacyDefinition = fromLegacyDefinition;
|
|
3864
4499
|
exports.hasPages = hasPages;
|
|
4500
|
+
exports.hasViews = hasViews;
|
|
3865
4501
|
exports.hiddenWidgets = hiddenWidgets;
|
|
3866
4502
|
exports.hideWidget = hideWidget;
|
|
3867
4503
|
exports.isClientApplied = isClientApplied;
|
|
@@ -3879,6 +4515,7 @@ exports.isWidgetKind = isWidgetKind;
|
|
|
3879
4515
|
exports.isWidgetVisibleTo = isWidgetVisibleTo;
|
|
3880
4516
|
exports.isWidgetWidth = isWidgetWidth;
|
|
3881
4517
|
exports.itemWidth = itemWidth;
|
|
4518
|
+
exports.kindLabel = kindLabel;
|
|
3882
4519
|
exports.kpiDelta = kpiDelta;
|
|
3883
4520
|
exports.labelStride = labelStride;
|
|
3884
4521
|
exports.loadingWidgets = loadingWidgets;
|
|
@@ -3890,6 +4527,7 @@ exports.moveWidgetBy = moveWidgetBy;
|
|
|
3890
4527
|
exports.moveWidgetDown = moveWidgetDown;
|
|
3891
4528
|
exports.moveWidgetToPage = moveWidgetToPage;
|
|
3892
4529
|
exports.moveWidgetUp = moveWidgetUp;
|
|
4530
|
+
exports.nextCloneKey = nextCloneKey;
|
|
3893
4531
|
exports.nextPageKey = nextPageKey;
|
|
3894
4532
|
exports.niceDomain = niceDomain;
|
|
3895
4533
|
exports.niceStep = niceStep;
|
|
@@ -3905,6 +4543,7 @@ exports.pageList = pageList;
|
|
|
3905
4543
|
exports.pageOf = pageOf;
|
|
3906
4544
|
exports.pageRoom = pageRoom;
|
|
3907
4545
|
exports.pageWidgets = pageWidgets;
|
|
4546
|
+
exports.parseClones = parseClones;
|
|
3908
4547
|
exports.parseDetailQuery = parseDetailQuery;
|
|
3909
4548
|
exports.parseLayout = parseLayout;
|
|
3910
4549
|
exports.parseSettings = parseSettings;
|
|
@@ -3928,6 +4567,7 @@ exports.resolveWidgetUnapplied = resolveWidgetUnapplied;
|
|
|
3928
4567
|
exports.resolveWidgets = resolveWidgets;
|
|
3929
4568
|
exports.restoreWidget = restoreWidget;
|
|
3930
4569
|
exports.sameOptionValues = sameOptionValues;
|
|
4570
|
+
exports.serializeClones = serializeClones;
|
|
3931
4571
|
exports.serializeDetailQuery = serializeDetailQuery;
|
|
3932
4572
|
exports.serializeLayout = serializeLayout;
|
|
3933
4573
|
exports.serializeSettings = serializeSettings;
|
|
@@ -3936,6 +4576,7 @@ exports.shareOf = shareOf;
|
|
|
3936
4576
|
exports.showWidget = showWidget;
|
|
3937
4577
|
exports.sortDefinitions = sortDefinitions;
|
|
3938
4578
|
exports.sortFromOptions = sortFromOptions;
|
|
4579
|
+
exports.suggestedViewTitle = suggestedViewTitle;
|
|
3939
4580
|
exports.tableDetailData = tableDetailData;
|
|
3940
4581
|
exports.toggleHidden = toggleHidden;
|
|
3941
4582
|
exports.toggleMinimized = toggleMinimized;
|
|
@@ -3945,12 +4586,16 @@ exports.utf8Length = utf8Length;
|
|
|
3945
4586
|
exports.validateDetailData = validateDetailData;
|
|
3946
4587
|
exports.validateWidgetData = validateWidgetData;
|
|
3947
4588
|
exports.validateWidgetDefinition = validateWidgetDefinition;
|
|
4589
|
+
exports.viewChoices = viewChoices;
|
|
4590
|
+
exports.viewKind = viewKind;
|
|
4591
|
+
exports.viewNote = viewNote;
|
|
3948
4592
|
exports.visibleWidgets = visibleWidgets;
|
|
3949
4593
|
exports.widgetOptionValues = widgetOptionValues;
|
|
3950
4594
|
exports.widgetsFor = widgetsFor;
|
|
3951
4595
|
exports.widthForSize = widthForSize;
|
|
3952
4596
|
exports.widthRange = widthRange;
|
|
3953
4597
|
exports.widthShareLabel = widthShareLabel;
|
|
4598
|
+
exports.withClones = withClones;
|
|
3954
4599
|
exports.withEncoding = withEncoding;
|
|
3955
4600
|
exports.withFallback = withFallback;
|
|
3956
4601
|
exports.withLogging = withLogging;
|