flexdesk 0.16.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/css/flexdesk.css +153 -0
- package/dist/chunk-B5Q53LAJ.js +111 -0
- package/dist/chunk-B5Q53LAJ.js.map +7 -0
- package/dist/{chunk-CWPMLX67.js → chunk-W6FBJLPW.js} +2 -110
- package/dist/{chunk-CWPMLX67.js.map → chunk-W6FBJLPW.js.map} +3 -3
- package/dist/flexdesk.css +153 -0
- package/dist/tiles.js +471 -16
- package/dist/tiles.js.map +4 -4
- package/dist/widgets.js +23 -9
- package/dist/widgets.js.map +2 -2
- package/dist/wm.js +4 -2
- package/dist/wm.js.map +1 -1
- package/package.json +1 -1
- package/src/tiles/list_tiles.js +422 -0
- package/src/tiles/scope_picker.js +96 -0
- package/src/tiles/tile_base.js +11 -0
- package/src/tiles/tile_grid.js +36 -1
- package/src/ui/components/sortable_list.js +26 -4
package/dist/tiles.js
CHANGED
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
ActionDropdown
|
|
16
16
|
} from "./chunk-O5OHMWBB.js";
|
|
17
|
+
import {
|
|
18
|
+
showContextMenu
|
|
19
|
+
} from "./chunk-B5Q53LAJ.js";
|
|
20
|
+
import "./chunk-NNFTETK3.js";
|
|
17
21
|
import {
|
|
18
22
|
buildHistogramTrace,
|
|
19
23
|
buildLegendLayout,
|
|
@@ -234,6 +238,16 @@ var TileBase = class {
|
|
|
234
238
|
this._lastLoadedAt = null;
|
|
235
239
|
this._loadSeq = 0;
|
|
236
240
|
this.comparisonData = null;
|
|
241
|
+
this.scope = null;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Called when the board's scope changes (`TileGrid.setScope`), after
|
|
245
|
+
* `this.scope` is set. A no-op here: a tile whose rows the grid's data
|
|
246
|
+
* source loads is reloaded by the grid, and one fed through `setData` is
|
|
247
|
+
* fed again by whoever owns its data.
|
|
248
|
+
* @param {*} _scope
|
|
249
|
+
*/
|
|
250
|
+
onScopeChange(_scope) {
|
|
237
251
|
}
|
|
238
252
|
/**
|
|
239
253
|
* Set comparison data for overlay rendering.
|
|
@@ -696,7 +710,7 @@ var TileBase = class {
|
|
|
696
710
|
* @private
|
|
697
711
|
*/
|
|
698
712
|
_onConfigClick() {
|
|
699
|
-
this.element?.closest(".tile-grid")?.querySelectorAll(".tile.tile--selected").forEach((
|
|
713
|
+
this.element?.closest(".tile-grid")?.querySelectorAll(".tile.tile--selected").forEach((el2) => el2.classList.remove("tile--selected"));
|
|
700
714
|
this.element?.classList.add("tile--selected");
|
|
701
715
|
this.eventBus?.emit?.("tile:config-request", {
|
|
702
716
|
tileId: this.id,
|
|
@@ -1133,10 +1147,10 @@ var ChartTile = class extends TileBase {
|
|
|
1133
1147
|
* not awaited: a rejected purge (Plotly absent, already purged) must not
|
|
1134
1148
|
* surface as an unhandled rejection. */
|
|
1135
1149
|
_releasePlot() {
|
|
1136
|
-
const
|
|
1150
|
+
const el2 = this._plotEl;
|
|
1137
1151
|
this._plotEl = null;
|
|
1138
1152
|
this._drawn = false;
|
|
1139
|
-
if (
|
|
1153
|
+
if (el2) purge(el2).catch(() => {
|
|
1140
1154
|
});
|
|
1141
1155
|
}
|
|
1142
1156
|
// ── the draw ────────────────────────────────────────────────────────
|
|
@@ -1487,11 +1501,11 @@ var KpiTile = class extends TileBase {
|
|
|
1487
1501
|
if (delta) box.append(delta);
|
|
1488
1502
|
const foot = [this.config.foot, d.note].filter(Boolean).join(" \xB7 ");
|
|
1489
1503
|
if (foot) {
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
box.append(
|
|
1504
|
+
const el2 = document.createElement("div");
|
|
1505
|
+
el2.className = "fd-kpi__foot";
|
|
1506
|
+
el2.textContent = foot;
|
|
1507
|
+
el2.title = foot;
|
|
1508
|
+
box.append(el2);
|
|
1495
1509
|
}
|
|
1496
1510
|
content.append(box);
|
|
1497
1511
|
}
|
|
@@ -1503,19 +1517,19 @@ var KpiTile = class extends TileBase {
|
|
|
1503
1517
|
const reference = Number(rawReference);
|
|
1504
1518
|
if (!Number.isFinite(value) || !Number.isFinite(reference)) return null;
|
|
1505
1519
|
const change = value - reference;
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
if (change > 0)
|
|
1509
|
-
else if (change < 0)
|
|
1520
|
+
const el2 = document.createElement("div");
|
|
1521
|
+
el2.className = "fd-kpi__delta";
|
|
1522
|
+
if (change > 0) el2.classList.add(this.config.invert_delta ? "fd-kpi__delta--down" : "fd-kpi__delta--up");
|
|
1523
|
+
else if (change < 0) el2.classList.add(this.config.invert_delta ? "fd-kpi__delta--up" : "fd-kpi__delta--down");
|
|
1510
1524
|
const arrow = change > 0 ? "\u25B2" : change < 0 ? "\u25BC" : "\u2013";
|
|
1511
1525
|
const magnitude = this.config.delta_relative ? reference === 0 ? null : formatValue(Math.abs(change / reference), {
|
|
1512
1526
|
format: "percent",
|
|
1513
1527
|
decimals: this.config.decimals ?? 1
|
|
1514
1528
|
}) : this._format(Math.abs(change), false);
|
|
1515
1529
|
const sign = change > 0 ? "+" : change < 0 ? "-" : "";
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
return
|
|
1530
|
+
el2.textContent = magnitude === null ? `${arrow} \u2014` : `${arrow} ${sign}${magnitude}`;
|
|
1531
|
+
el2.title = `Compared with ${this._format(reference, false)}`;
|
|
1532
|
+
return el2;
|
|
1519
1533
|
}
|
|
1520
1534
|
_format(value, withAffixes) {
|
|
1521
1535
|
const format = this.config.format || "number";
|
|
@@ -2139,6 +2153,399 @@ function getLayoutPersistence() {
|
|
|
2139
2153
|
return instance;
|
|
2140
2154
|
}
|
|
2141
2155
|
|
|
2156
|
+
// src/tiles/list_tiles.js
|
|
2157
|
+
init_tile_registry();
|
|
2158
|
+
var TONES = /* @__PURE__ */ new Set(["danger", "warning", "ok", "muted"]);
|
|
2159
|
+
function normalize(data, key) {
|
|
2160
|
+
if (data == null) return null;
|
|
2161
|
+
const d = Array.isArray(data) ? { [key]: data } : data;
|
|
2162
|
+
if (typeof d !== "object" || !Array.isArray(d[key])) return null;
|
|
2163
|
+
const items = d[key].filter((r) => r && typeof r === "object" && r.key != null);
|
|
2164
|
+
const total = Number.isFinite(Number(d.total)) && d.total !== null && d.total !== "" ? Number(d.total) : items.length;
|
|
2165
|
+
return {
|
|
2166
|
+
items,
|
|
2167
|
+
total: Math.max(total, items.length),
|
|
2168
|
+
note: typeof d.note === "string" ? d.note : "",
|
|
2169
|
+
empty: typeof d.empty === "string" ? d.empty : "",
|
|
2170
|
+
action: d.action && typeof d.action === "object" && d.action.label ? d.action : null
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
var normalizeListData = (data) => normalize(data, "rows");
|
|
2174
|
+
var normalizePeopleData = (data) => normalize(data, "people");
|
|
2175
|
+
function el(tag, cls, text) {
|
|
2176
|
+
const e = document.createElement(tag);
|
|
2177
|
+
if (cls) e.className = cls;
|
|
2178
|
+
if (text != null) e.textContent = String(text);
|
|
2179
|
+
return e;
|
|
2180
|
+
}
|
|
2181
|
+
function glyph(name, cls = "") {
|
|
2182
|
+
const g = el("span", `material-symbols-outlined${cls ? ` ${cls}` : ""}`, name);
|
|
2183
|
+
g.setAttribute("aria-hidden", "true");
|
|
2184
|
+
return g;
|
|
2185
|
+
}
|
|
2186
|
+
var toneClass = (base, tone) => TONES.has(tone) ? ` ${base}--${tone}` : "";
|
|
2187
|
+
var RowsTile = class extends TileBase {
|
|
2188
|
+
static _KEY = "rows";
|
|
2189
|
+
constructor(options = {}) {
|
|
2190
|
+
super(options);
|
|
2191
|
+
this.onRowActivate = typeof options.onRowActivate === "function" ? options.onRowActivate : null;
|
|
2192
|
+
this.rowMenu = typeof options.rowMenu === "function" ? options.rowMenu : null;
|
|
2193
|
+
this.onRowMenuAction = typeof options.onRowMenuAction === "function" ? options.onRowMenuAction : null;
|
|
2194
|
+
this.onAction = typeof options.onAction === "function" ? options.onAction : null;
|
|
2195
|
+
this.rowsDraggable = options.rowsDraggable === true;
|
|
2196
|
+
this._items = /* @__PURE__ */ new Map();
|
|
2197
|
+
this._onClick = (e) => this._click(e);
|
|
2198
|
+
this._onKey = (e) => this._key(e);
|
|
2199
|
+
this._onMenu = (e) => this._menu(e);
|
|
2200
|
+
}
|
|
2201
|
+
/** Pure and static-only — it runs inside `TileBase`'s constructor. */
|
|
2202
|
+
getDefaultConfig() {
|
|
2203
|
+
return { title: "", empty_text: "", max_rows: 0 };
|
|
2204
|
+
}
|
|
2205
|
+
getConfigSchema() {
|
|
2206
|
+
return {
|
|
2207
|
+
fields: [
|
|
2208
|
+
{ name: "title", type: "text", label: "Tile title" },
|
|
2209
|
+
{
|
|
2210
|
+
name: "empty_text",
|
|
2211
|
+
type: "text",
|
|
2212
|
+
label: "When there is nothing",
|
|
2213
|
+
help: 'What the tile says when the list is empty, e.g. "Nothing is late."'
|
|
2214
|
+
},
|
|
2215
|
+
{
|
|
2216
|
+
name: "max_rows",
|
|
2217
|
+
type: "number",
|
|
2218
|
+
label: "Rows at most",
|
|
2219
|
+
min: 0,
|
|
2220
|
+
help: "0 shows them all; the tile scrolls."
|
|
2221
|
+
}
|
|
2222
|
+
]
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
mount(container) {
|
|
2226
|
+
super.mount(container);
|
|
2227
|
+
applyTitle2(this);
|
|
2228
|
+
const c = this.contentElement;
|
|
2229
|
+
if (!c) return;
|
|
2230
|
+
c.addEventListener("click", this._onClick);
|
|
2231
|
+
c.addEventListener("keydown", this._onKey);
|
|
2232
|
+
c.addEventListener("contextmenu", this._onMenu);
|
|
2233
|
+
}
|
|
2234
|
+
/** `null` is "the grid has no data" — see `ChartTile.update`. */
|
|
2235
|
+
update(data, config) {
|
|
2236
|
+
if (this._disposed) return;
|
|
2237
|
+
super.update(data === null ? void 0 : data, config);
|
|
2238
|
+
if (config) applyTitle2(this);
|
|
2239
|
+
}
|
|
2240
|
+
/** Hand this tile its rows; null is the empty state. */
|
|
2241
|
+
setData(data) {
|
|
2242
|
+
if (this._disposed) return;
|
|
2243
|
+
this.data = data ?? null;
|
|
2244
|
+
if (this.contentElement) this.render(this.data);
|
|
2245
|
+
}
|
|
2246
|
+
/** The item drawn under `key`, or null. */
|
|
2247
|
+
itemFor(key) {
|
|
2248
|
+
return this._items.get(String(key)) ?? null;
|
|
2249
|
+
}
|
|
2250
|
+
/** The element of the item drawn under `key`, or null. */
|
|
2251
|
+
rowElement(key) {
|
|
2252
|
+
const want = String(key);
|
|
2253
|
+
return [...this.contentElement?.querySelectorAll("[data-row-key]") || []].find((r) => r.dataset.rowKey === want) || null;
|
|
2254
|
+
}
|
|
2255
|
+
render(data) {
|
|
2256
|
+
const content = this.contentElement;
|
|
2257
|
+
if (!content) return;
|
|
2258
|
+
const norm = normalize(data, this.constructor._KEY);
|
|
2259
|
+
this._items.clear();
|
|
2260
|
+
if (!norm) {
|
|
2261
|
+
this.showEmpty("Nothing to show.");
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
const max = Math.max(0, Math.floor(Number(this.config.max_rows) || 0));
|
|
2265
|
+
const shown = max ? norm.items.slice(0, max) : norm.items;
|
|
2266
|
+
const box = el("div", `fd-list-tile fd-list-tile--${this.constructor._KEY}`);
|
|
2267
|
+
const bar = el("div", "fd-list-tile__bar");
|
|
2268
|
+
const count = el(
|
|
2269
|
+
"span",
|
|
2270
|
+
"fd-list-tile__count",
|
|
2271
|
+
shown.length < norm.total ? `${shown.length} of ${norm.total}` : String(norm.total)
|
|
2272
|
+
);
|
|
2273
|
+
count.title = `${norm.total} in all`;
|
|
2274
|
+
bar.append(count, el("span", "fd-list-tile__spacer"));
|
|
2275
|
+
if (norm.action) {
|
|
2276
|
+
const b = el("button", "fd-list-tile__action");
|
|
2277
|
+
b.type = "button";
|
|
2278
|
+
b.dataset.listAction = "action";
|
|
2279
|
+
if (norm.action.icon) b.append(glyph(norm.action.icon));
|
|
2280
|
+
b.append(el("span", null, norm.action.label));
|
|
2281
|
+
bar.append(b);
|
|
2282
|
+
}
|
|
2283
|
+
box.append(bar);
|
|
2284
|
+
const list = el("div", "fd-list-tile__rows");
|
|
2285
|
+
list.setAttribute("role", "list");
|
|
2286
|
+
if (!shown.length) {
|
|
2287
|
+
list.append(el("div", "fd-list-tile__empty", norm.empty || this.config.empty_text || "Nothing here."));
|
|
2288
|
+
}
|
|
2289
|
+
for (const item of shown) {
|
|
2290
|
+
const key = String(item.key);
|
|
2291
|
+
if (this._items.has(key)) continue;
|
|
2292
|
+
this._items.set(key, item);
|
|
2293
|
+
const row = this._renderItem(item);
|
|
2294
|
+
row.classList.add("fd-list-tile__row");
|
|
2295
|
+
row.dataset.rowKey = key;
|
|
2296
|
+
row.setAttribute("role", "button");
|
|
2297
|
+
row.tabIndex = 0;
|
|
2298
|
+
if (this.rowsDraggable) row.draggable = true;
|
|
2299
|
+
const wrap = el("div", "fd-list-tile__item");
|
|
2300
|
+
wrap.setAttribute("role", "listitem");
|
|
2301
|
+
wrap.append(row);
|
|
2302
|
+
list.append(wrap);
|
|
2303
|
+
}
|
|
2304
|
+
box.append(list);
|
|
2305
|
+
if (norm.note) {
|
|
2306
|
+
const note = el("div", "fd-list-tile__note", norm.note);
|
|
2307
|
+
note.setAttribute("role", "note");
|
|
2308
|
+
note.title = norm.note;
|
|
2309
|
+
box.append(note);
|
|
2310
|
+
}
|
|
2311
|
+
content.replaceChildren(box);
|
|
2312
|
+
}
|
|
2313
|
+
/** One item's row element. @abstract */
|
|
2314
|
+
_renderItem(_item) {
|
|
2315
|
+
return el("div");
|
|
2316
|
+
}
|
|
2317
|
+
// ── input ───────────────────────────────────────────────────────────
|
|
2318
|
+
_rowOf(target) {
|
|
2319
|
+
const row = target?.closest?.("[data-row-key]");
|
|
2320
|
+
return row && this.contentElement?.contains(row) ? row : null;
|
|
2321
|
+
}
|
|
2322
|
+
_click(e) {
|
|
2323
|
+
if (e.target?.closest?.("[data-list-action]")) {
|
|
2324
|
+
this._call(this.onAction, { tile: this, event: e });
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
const row = this._rowOf(e.target);
|
|
2328
|
+
if (row) this._activate(row, e);
|
|
2329
|
+
}
|
|
2330
|
+
_activate(rowEl, event) {
|
|
2331
|
+
const row = this.itemFor(rowEl.dataset.rowKey);
|
|
2332
|
+
if (row) this._call(this.onRowActivate, { row, event, tile: this });
|
|
2333
|
+
}
|
|
2334
|
+
_key(e) {
|
|
2335
|
+
const rowEl = this._rowOf(e.target);
|
|
2336
|
+
if (!rowEl) return;
|
|
2337
|
+
if (e.key === "ContextMenu" || e.key === "F10" && e.shiftKey) {
|
|
2338
|
+
const r = rowEl.getBoundingClientRect();
|
|
2339
|
+
if (this._openMenu(rowEl, e, r.left + 16, r.bottom)) e.preventDefault();
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
if ((e.key === "Enter" || e.key === " ") && !e.altKey) {
|
|
2343
|
+
e.preventDefault();
|
|
2344
|
+
this._activate(rowEl, e);
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
const rows = [...this.contentElement.querySelectorAll("[data-row-key]")];
|
|
2348
|
+
const i = rows.indexOf(rowEl);
|
|
2349
|
+
const to = { ArrowDown: i + 1, ArrowUp: i - 1, Home: 0, End: rows.length - 1 }[e.key];
|
|
2350
|
+
if (to === void 0 || !rows[to]) return;
|
|
2351
|
+
e.preventDefault();
|
|
2352
|
+
rows[to].focus();
|
|
2353
|
+
}
|
|
2354
|
+
_menu(e) {
|
|
2355
|
+
const rowEl = this._rowOf(e.target);
|
|
2356
|
+
if (rowEl && this._openMenu(rowEl, e, e.clientX, e.clientY)) e.preventDefault();
|
|
2357
|
+
}
|
|
2358
|
+
/** Show the row's menu, if it has one. → whether it did. */
|
|
2359
|
+
_openMenu(rowEl, event, x, y) {
|
|
2360
|
+
const row = this.itemFor(rowEl.dataset.rowKey);
|
|
2361
|
+
if (!row || typeof this.rowMenu !== "function") return false;
|
|
2362
|
+
let items = null;
|
|
2363
|
+
try {
|
|
2364
|
+
items = this.rowMenu({ row, event, tile: this });
|
|
2365
|
+
} catch (err) {
|
|
2366
|
+
console.error(`[${this.constructor.name}] rowMenu threw:`, err);
|
|
2367
|
+
return false;
|
|
2368
|
+
}
|
|
2369
|
+
if (!Array.isArray(items) || !items.length) return false;
|
|
2370
|
+
showContextMenu(x, y, items, (action) => this._call(this.onRowMenuAction, { action, row, tile: this }));
|
|
2371
|
+
return true;
|
|
2372
|
+
}
|
|
2373
|
+
/** A hook, called so that one that throws is reported and the tile goes on. */
|
|
2374
|
+
_call(fn, arg) {
|
|
2375
|
+
if (typeof fn !== "function") return;
|
|
2376
|
+
try {
|
|
2377
|
+
fn(arg);
|
|
2378
|
+
} catch (err) {
|
|
2379
|
+
console.error(`[${this.constructor.name}] a callback threw:`, err);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
dispose() {
|
|
2383
|
+
const c = this.contentElement;
|
|
2384
|
+
if (c) {
|
|
2385
|
+
c.removeEventListener("click", this._onClick);
|
|
2386
|
+
c.removeEventListener("keydown", this._onKey);
|
|
2387
|
+
c.removeEventListener("contextmenu", this._onMenu);
|
|
2388
|
+
}
|
|
2389
|
+
this._items.clear();
|
|
2390
|
+
super.dispose();
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
var ListTile = class extends RowsTile {
|
|
2394
|
+
static TYPE = "flexdesk.list";
|
|
2395
|
+
static TITLE = "List";
|
|
2396
|
+
static ICON = "list";
|
|
2397
|
+
static DESCRIPTION = "Rows you supply, with a count, a click and a right-click menu per row.";
|
|
2398
|
+
static DEFAULT_SIZE = { w: 6, h: 4 };
|
|
2399
|
+
static SIZE_CONSTRAINTS = { minW: 3, minH: 2, maxW: 12, maxH: 12 };
|
|
2400
|
+
static _KEY = "rows";
|
|
2401
|
+
_renderItem(r) {
|
|
2402
|
+
const row = el("div", r.className ? String(r.className) : null);
|
|
2403
|
+
if (r.icon) {
|
|
2404
|
+
const g = glyph(r.icon, "fd-list-tile__icon");
|
|
2405
|
+
if (r.iconTitle) {
|
|
2406
|
+
g.title = r.iconTitle;
|
|
2407
|
+
g.removeAttribute("aria-hidden");
|
|
2408
|
+
}
|
|
2409
|
+
row.append(g);
|
|
2410
|
+
}
|
|
2411
|
+
if (r.lead != null && r.lead !== "") row.append(el("span", "fd-list-tile__lead", r.lead));
|
|
2412
|
+
const title = el("span", "fd-list-tile__title", r.title ?? "");
|
|
2413
|
+
title.title = r.tip || String(r.title ?? "");
|
|
2414
|
+
row.append(title);
|
|
2415
|
+
for (const c of Array.isArray(r.cells) ? r.cells : []) {
|
|
2416
|
+
if (!c) continue;
|
|
2417
|
+
const cell = el("span", `fd-list-tile__cell${toneClass("fd-list-tile__cell", c.tone)}${c.className ? ` ${c.className}` : ""}`);
|
|
2418
|
+
if (c.icon) cell.append(glyph(c.icon));
|
|
2419
|
+
if (c.text != null && c.text !== "") cell.append(document.createTextNode(String(c.text)));
|
|
2420
|
+
if (c.title) cell.title = c.title;
|
|
2421
|
+
row.append(cell);
|
|
2422
|
+
}
|
|
2423
|
+
return row;
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
var PeopleTile = class extends RowsTile {
|
|
2427
|
+
static TYPE = "flexdesk.people";
|
|
2428
|
+
static TITLE = "People";
|
|
2429
|
+
static ICON = "groups";
|
|
2430
|
+
static DESCRIPTION = "One row per person, with how much is on them.";
|
|
2431
|
+
static DEFAULT_SIZE = { w: 6, h: 4 };
|
|
2432
|
+
static SIZE_CONSTRAINTS = { minW: 3, minH: 2, maxW: 12, maxH: 12 };
|
|
2433
|
+
static _KEY = "people";
|
|
2434
|
+
getDefaultConfig() {
|
|
2435
|
+
return { ...super.getDefaultConfig(), count_label: "open" };
|
|
2436
|
+
}
|
|
2437
|
+
getConfigSchema() {
|
|
2438
|
+
const { fields } = super.getConfigSchema();
|
|
2439
|
+
return { fields: [...fields, {
|
|
2440
|
+
name: "count_label",
|
|
2441
|
+
type: "text",
|
|
2442
|
+
label: "Count reads",
|
|
2443
|
+
help: `After each person's number, e.g. "open" for "3 open".`
|
|
2444
|
+
}] };
|
|
2445
|
+
}
|
|
2446
|
+
_renderItem(p) {
|
|
2447
|
+
const row = el("div", "fd-people-tile__row");
|
|
2448
|
+
row.append(glyph(p.icon || (p.muted ? "person_off" : "person"), "fd-list-tile__icon"));
|
|
2449
|
+
row.append(el("span", `fd-people-tile__name${p.muted ? " fd-people-tile__name--muted" : ""}`, p.name ?? p.key));
|
|
2450
|
+
const pills = el("span", "fd-people-tile__pills");
|
|
2451
|
+
for (const pill of Array.isArray(p.pills) ? p.pills : []) {
|
|
2452
|
+
if (!pill?.text) continue;
|
|
2453
|
+
const s = el("span", `fd-people-tile__pill${toneClass("fd-people-tile__pill", pill.tone)}`, pill.text);
|
|
2454
|
+
if (pill.title) s.title = pill.title;
|
|
2455
|
+
pills.append(s);
|
|
2456
|
+
}
|
|
2457
|
+
row.append(pills);
|
|
2458
|
+
const label = p.countLabel ?? this.config.count_label ?? "";
|
|
2459
|
+
row.append(el("span", "fd-people-tile__count", `${Number(p.count) || 0}${label ? ` ${label}` : ""}`));
|
|
2460
|
+
if (p.title) row.title = p.title;
|
|
2461
|
+
return row;
|
|
2462
|
+
}
|
|
2463
|
+
};
|
|
2464
|
+
function groupByPerson(items, personOf) {
|
|
2465
|
+
const by = /* @__PURE__ */ new Map();
|
|
2466
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
2467
|
+
const who = String(personOf(item) || "");
|
|
2468
|
+
if (!by.has(who)) by.set(who, { key: who, items: [] });
|
|
2469
|
+
by.get(who).items.push(item);
|
|
2470
|
+
}
|
|
2471
|
+
return [...by.values()].sort((a, b) => b.items.length - a.items.length || (a.key === "") - (b.key === "") || a.key.localeCompare(b.key));
|
|
2472
|
+
}
|
|
2473
|
+
function registerListTiles() {
|
|
2474
|
+
for (const Tile of [ListTile, PeopleTile]) {
|
|
2475
|
+
if (getWidget(Tile.TYPE) !== Tile) registerWidget(Tile);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
function applyTitle2(tile) {
|
|
2479
|
+
tile.setTitle(tile.config.title || tile.constructor.TITLE);
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// src/tiles/scope_picker.js
|
|
2483
|
+
function createScopePicker({ options = [], value, label = "Scope", onChange } = {}) {
|
|
2484
|
+
const opts = options.filter((o) => o && o.value != null).map((o) => ({ ...o, value: String(o.value) }));
|
|
2485
|
+
const known = (v) => opts.some((o) => o.value === String(v));
|
|
2486
|
+
let current = known(value) ? String(value) : opts[0]?.value ?? "";
|
|
2487
|
+
const element = document.createElement("span");
|
|
2488
|
+
element.className = "fd-scope-picker";
|
|
2489
|
+
element.setAttribute("role", "group");
|
|
2490
|
+
element.setAttribute("aria-label", label);
|
|
2491
|
+
const buttons = opts.map((o) => {
|
|
2492
|
+
const b = document.createElement("button");
|
|
2493
|
+
b.type = "button";
|
|
2494
|
+
b.className = "fd-scope-picker__btn";
|
|
2495
|
+
b.dataset.scope = o.value;
|
|
2496
|
+
b.textContent = o.label ?? o.value;
|
|
2497
|
+
if (o.hint) b.title = o.hint;
|
|
2498
|
+
element.append(b);
|
|
2499
|
+
return b;
|
|
2500
|
+
});
|
|
2501
|
+
const paint = () => {
|
|
2502
|
+
for (const b of buttons) {
|
|
2503
|
+
const on = b.dataset.scope === current;
|
|
2504
|
+
b.classList.toggle("fd-scope-picker__btn--on", on);
|
|
2505
|
+
b.setAttribute("aria-pressed", String(on));
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
paint();
|
|
2509
|
+
const onClick = (e) => {
|
|
2510
|
+
const b = e.target?.closest?.("[data-scope]");
|
|
2511
|
+
if (!b || !element.contains(b) || b.dataset.scope === current) return;
|
|
2512
|
+
current = b.dataset.scope;
|
|
2513
|
+
paint();
|
|
2514
|
+
try {
|
|
2515
|
+
onChange?.(current);
|
|
2516
|
+
} catch (err) {
|
|
2517
|
+
console.error("[ScopePicker] onChange threw:", err);
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
const onKey = (e) => {
|
|
2521
|
+
const i = buttons.indexOf(e.target);
|
|
2522
|
+
if (i < 0) return;
|
|
2523
|
+
const to = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: buttons.length - 1 }[e.key];
|
|
2524
|
+
if (to === void 0 || !buttons[to]) return;
|
|
2525
|
+
e.preventDefault();
|
|
2526
|
+
buttons[to].focus();
|
|
2527
|
+
};
|
|
2528
|
+
element.addEventListener("click", onClick);
|
|
2529
|
+
element.addEventListener("keydown", onKey);
|
|
2530
|
+
return {
|
|
2531
|
+
element,
|
|
2532
|
+
get value() {
|
|
2533
|
+
return current;
|
|
2534
|
+
},
|
|
2535
|
+
/** Press `v` without calling `onChange` (a value restored from elsewhere). */
|
|
2536
|
+
setValue(v) {
|
|
2537
|
+
if (!known(v)) return;
|
|
2538
|
+
current = String(v);
|
|
2539
|
+
paint();
|
|
2540
|
+
},
|
|
2541
|
+
dispose() {
|
|
2542
|
+
element.removeEventListener("click", onClick);
|
|
2543
|
+
element.removeEventListener("keydown", onKey);
|
|
2544
|
+
element.remove();
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2142
2549
|
// src/tiles/tile_grid.js
|
|
2143
2550
|
init_tile_registry();
|
|
2144
2551
|
var TileGrid = class {
|
|
@@ -2157,6 +2564,9 @@ var TileGrid = class {
|
|
|
2157
2564
|
* @param {{resolve: Function}} [options.dataSource] - Per-tile data provider,
|
|
2158
2565
|
* handed to every tile. With none, the grid is the broadcast-only grid
|
|
2159
2566
|
* it has always been and no tile is ever asked to load anything.
|
|
2567
|
+
* @param {*} [options.scope=null] - The board's scope, a filter every tile
|
|
2568
|
+
* follows (0.16.1); see `setScope`.
|
|
2569
|
+
* @param {Function} [options.onScopeChange] - `(scope)` after `setScope` changed it.
|
|
2160
2570
|
*/
|
|
2161
2571
|
constructor({
|
|
2162
2572
|
container,
|
|
@@ -2179,9 +2589,13 @@ var TileGrid = class {
|
|
|
2179
2589
|
stateGuard = null,
|
|
2180
2590
|
// Injected, never constructed here: the grid does not know what
|
|
2181
2591
|
// a binding is, only that a tile may have somewhere to ask.
|
|
2182
|
-
dataSource = null
|
|
2592
|
+
dataSource = null,
|
|
2593
|
+
scope = null,
|
|
2594
|
+
onScopeChange = null
|
|
2183
2595
|
}) {
|
|
2184
2596
|
this.host = host;
|
|
2597
|
+
this.scope = scope;
|
|
2598
|
+
this._onScopeChangeCallback = onScopeChange;
|
|
2185
2599
|
this.stateGuard = stateGuard;
|
|
2186
2600
|
this.dataSource = dataSource;
|
|
2187
2601
|
this.container = container;
|
|
@@ -2304,6 +2718,7 @@ var TileGrid = class {
|
|
|
2304
2718
|
dataSource: this.dataSource
|
|
2305
2719
|
});
|
|
2306
2720
|
if (!tile) return null;
|
|
2721
|
+
tile.scope = this.scope;
|
|
2307
2722
|
const layoutEntry = {
|
|
2308
2723
|
id: tileId,
|
|
2309
2724
|
type,
|
|
@@ -2374,6 +2789,39 @@ var TileGrid = class {
|
|
|
2374
2789
|
tile.loadData?.({ force: true });
|
|
2375
2790
|
});
|
|
2376
2791
|
}
|
|
2792
|
+
/**
|
|
2793
|
+
* THE BOARD'S SCOPE: one filter every tile follows ("Everyone", "Assigned
|
|
2794
|
+
* by me", …), set from a `createScopePicker` or from anything else.
|
|
2795
|
+
*
|
|
2796
|
+
* The grid only carries the value; what it means is the embedder's. Every
|
|
2797
|
+
* tile gets it as `tile.scope` and through `tile.onScopeChange(scope)`, the
|
|
2798
|
+
* grid's `onScopeChange(scope)` is called, and a grid with a data source
|
|
2799
|
+
* reloads its tiles, since their rows depend on it. The scope is not part
|
|
2800
|
+
* of the layout: it is how the board is being looked at, not what is on it.
|
|
2801
|
+
*
|
|
2802
|
+
* @param {*} scope - Compared by JSON: setting the same scope again does nothing.
|
|
2803
|
+
* @returns {boolean} whether it changed
|
|
2804
|
+
*/
|
|
2805
|
+
setScope(scope) {
|
|
2806
|
+
if (JSON.stringify(scope ?? null) === JSON.stringify(this.scope ?? null)) return false;
|
|
2807
|
+
this.scope = scope ?? null;
|
|
2808
|
+
this.tiles.forEach((tile) => {
|
|
2809
|
+
tile.scope = this.scope;
|
|
2810
|
+
try {
|
|
2811
|
+
tile.onScopeChange?.(this.scope);
|
|
2812
|
+
} catch (err) {
|
|
2813
|
+
console.error("[TileGrid] a tile's onScopeChange threw:", err);
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
try {
|
|
2817
|
+
this._onScopeChangeCallback?.(this.scope);
|
|
2818
|
+
} catch (err) {
|
|
2819
|
+
console.error("[TileGrid] onScopeChange threw:", err);
|
|
2820
|
+
}
|
|
2821
|
+
this.eventBus?.emit?.("grid:scope-changed", { scope: this.scope });
|
|
2822
|
+
if (this.dataSource) this.reloadAll();
|
|
2823
|
+
return true;
|
|
2824
|
+
}
|
|
2377
2825
|
/**
|
|
2378
2826
|
* Get current layout.
|
|
2379
2827
|
* @returns {Array} Layout array
|
|
@@ -2898,10 +3346,13 @@ export {
|
|
|
2898
3346
|
FieldTypes,
|
|
2899
3347
|
KpiTile,
|
|
2900
3348
|
LayoutPersistence,
|
|
3349
|
+
ListTile,
|
|
3350
|
+
PeopleTile,
|
|
2901
3351
|
TileBase,
|
|
2902
3352
|
TileGrid,
|
|
2903
3353
|
TileRegistry,
|
|
2904
3354
|
configureLayoutPersistence,
|
|
3355
|
+
createScopePicker,
|
|
2905
3356
|
createWidget,
|
|
2906
3357
|
formatVariableLabel,
|
|
2907
3358
|
getAllWidgetTypes,
|
|
@@ -2912,10 +3363,14 @@ export {
|
|
|
2912
3363
|
getWidget,
|
|
2913
3364
|
getWidgetCatalog,
|
|
2914
3365
|
getWidgetMetadata,
|
|
3366
|
+
groupByPerson,
|
|
2915
3367
|
hasWidget,
|
|
2916
3368
|
mergeWithDefaults,
|
|
2917
3369
|
normalizeChartData,
|
|
3370
|
+
normalizeListData,
|
|
3371
|
+
normalizePeopleData,
|
|
2918
3372
|
registerChartTiles,
|
|
3373
|
+
registerListTiles,
|
|
2919
3374
|
registerWidget,
|
|
2920
3375
|
setTileLogger,
|
|
2921
3376
|
validateConfig,
|