@richardmcquiston01/dashboard-widgets-toolkit 0.10.0 → 0.11.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/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useId, useState, useRef, useEffect, useMemo, useCallback } from 'react';
1
+ import { createContext, useContext, useId, useState, useRef, useEffect, useCallback, useMemo } from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
 
4
4
  // src/react/card.tsx
@@ -470,6 +470,265 @@ function placeAndFillRows(items, columns, spanOf) {
470
470
  return { spans, rows };
471
471
  }
472
472
 
473
+ // src/core/options.ts
474
+ var DATE_RANGE_PRESETS = [
475
+ "today",
476
+ "yesterday",
477
+ "last7",
478
+ "last30",
479
+ "last90",
480
+ "thisMonth",
481
+ "lastMonth",
482
+ "thisYear",
483
+ "lastYear"
484
+ ];
485
+ var COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
486
+ var INTERVAL_PATTERN = /^(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})$/;
487
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
488
+ function isClientApplied(option) {
489
+ return (option.type === "sort" || option.type === "columns") && option.apply === "client";
490
+ }
491
+ function civilToday(context) {
492
+ const now = context.now ?? Date.now();
493
+ let timeZone = context.timeZone ?? "UTC";
494
+ try {
495
+ new Intl.DateTimeFormat("en-US", { timeZone });
496
+ } catch {
497
+ timeZone = "UTC";
498
+ }
499
+ const parts = new Intl.DateTimeFormat("en-US", {
500
+ timeZone,
501
+ year: "numeric",
502
+ month: "2-digit",
503
+ day: "2-digit"
504
+ }).formatToParts(new Date(now));
505
+ const pick = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
506
+ return { year: pick("year"), month: pick("month"), day: pick("day") };
507
+ }
508
+ function toIso(epochMs) {
509
+ return new Date(epochMs).toISOString().slice(0, 10);
510
+ }
511
+ var DAY_MS = 864e5;
512
+ function parseIsoDay(text) {
513
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
514
+ if (match === null) return void 0;
515
+ const epoch = Date.UTC(
516
+ Number(match[1]),
517
+ Number(match[2]) - 1,
518
+ Number(match[3])
519
+ );
520
+ return toIso(epoch) === text ? epoch : void 0;
521
+ }
522
+ function isIsoInterval(value) {
523
+ const match = INTERVAL_PATTERN.exec(value);
524
+ if (match === null) return false;
525
+ const start = parseIsoDay(match[1]);
526
+ const end = parseIsoDay(match[2]);
527
+ return start !== void 0 && end !== void 0 && start <= end;
528
+ }
529
+ function isPreset(value) {
530
+ return DATE_RANGE_PRESETS.includes(value);
531
+ }
532
+ function resolveDateRange(value, context = {}) {
533
+ if (isIsoInterval(value)) return value;
534
+ if (!isPreset(value)) return void 0;
535
+ const today = civilToday(context);
536
+ const todayMs = Date.UTC(today.year, today.month - 1, today.day);
537
+ const range = (startMs, endMs) => `${toIso(startMs)}/${toIso(endMs)}`;
538
+ switch (value) {
539
+ case "today":
540
+ return range(todayMs, todayMs);
541
+ case "yesterday":
542
+ return range(todayMs - DAY_MS, todayMs - DAY_MS);
543
+ case "last7":
544
+ return range(todayMs - 6 * DAY_MS, todayMs);
545
+ case "last30":
546
+ return range(todayMs - 29 * DAY_MS, todayMs);
547
+ case "last90":
548
+ return range(todayMs - 89 * DAY_MS, todayMs);
549
+ case "thisMonth":
550
+ return range(Date.UTC(today.year, today.month - 1, 1), todayMs);
551
+ case "lastMonth":
552
+ return range(
553
+ Date.UTC(today.year, today.month - 2, 1),
554
+ Date.UTC(today.year, today.month - 1, 1) - DAY_MS
555
+ );
556
+ case "thisYear":
557
+ return range(Date.UTC(today.year, 0, 1), todayMs);
558
+ case "lastYear":
559
+ return range(
560
+ Date.UTC(today.year - 1, 0, 1),
561
+ Date.UTC(today.year, 0, 1) - DAY_MS
562
+ );
563
+ }
564
+ }
565
+ function onStepGrid(option, value) {
566
+ if (option.step === void 0) return true;
567
+ const steps = (value - option.min) / option.step;
568
+ return Math.abs(steps - Math.round(steps)) < 1e-9;
569
+ }
570
+ function parseSortValue(value) {
571
+ const separator = value.lastIndexOf(":");
572
+ if (separator <= 0) return void 0;
573
+ const column = value.slice(0, separator);
574
+ const direction = value.slice(separator + 1);
575
+ if (direction !== "asc" && direction !== "desc") return void 0;
576
+ return { column, direction };
577
+ }
578
+ function sortFromOptions(definition, values) {
579
+ if (values === void 0) return void 0;
580
+ const sortOption = definition.options?.find(
581
+ (option) => option.type === "sort"
582
+ );
583
+ if (sortOption === void 0) return void 0;
584
+ const chosen = values[sortOption.key];
585
+ return typeof chosen === "string" ? parseSortValue(chosen) : void 0;
586
+ }
587
+ function coerceOptionValue(option, value, context = {}) {
588
+ switch (option.type) {
589
+ case "choice":
590
+ return typeof value === "string" && option.choices.some((choice) => choice.value === value) ? value : void 0;
591
+ case "number":
592
+ return typeof value === "number" && Number.isFinite(value) && value >= option.min && value <= option.max && onStepGrid(option, value) ? value : void 0;
593
+ case "boolean":
594
+ return typeof value === "boolean" ? value : void 0;
595
+ case "text": {
596
+ if (typeof value !== "string") return void 0;
597
+ const trimmed = value.trim();
598
+ return trimmed.length <= option.maxLength && !CONTROL_CHARACTERS.test(trimmed) ? trimmed : void 0;
599
+ }
600
+ case "dateRange": {
601
+ if (typeof value !== "string") return void 0;
602
+ if (isPreset(value) && option.presets !== void 0 && !option.presets.some((preset) => preset.value === value)) {
603
+ return void 0;
604
+ }
605
+ return resolveDateRange(value, context);
606
+ }
607
+ case "color":
608
+ return typeof value === "string" && COLOR_PATTERN.test(value) ? value.toLowerCase() : void 0;
609
+ case "sort": {
610
+ if (typeof value !== "string") return void 0;
611
+ const sort = parseSortValue(value);
612
+ return sort !== void 0 && option.columns.some((column) => column.key === sort.column) ? value : void 0;
613
+ }
614
+ case "columns": {
615
+ if (!Array.isArray(value) || value.length === 0) return void 0;
616
+ const keys = value;
617
+ const allowed = new Set(option.columns.map((c) => c.key));
618
+ const seen = /* @__PURE__ */ new Set();
619
+ for (const key of keys) {
620
+ if (typeof key !== "string" || !allowed.has(key) || seen.has(key)) {
621
+ return void 0;
622
+ }
623
+ seen.add(key);
624
+ }
625
+ return [...seen];
626
+ }
627
+ }
628
+ }
629
+ function defaultOptionValue(option, context = {}) {
630
+ switch (option.type) {
631
+ case "dateRange":
632
+ return resolveDateRange(option.default, context);
633
+ case "sort":
634
+ return option.default;
635
+ case "columns":
636
+ return [...option.default];
637
+ default:
638
+ return option.default;
639
+ }
640
+ }
641
+ var NO_VALUES = Object.freeze({});
642
+ function resolveOptionValues(definition, chosen, context = {}) {
643
+ const options = definition.options;
644
+ if (options === void 0 || options.length === 0) return NO_VALUES;
645
+ const values = {};
646
+ for (const option of options) {
647
+ const picked = chosen?.[option.key];
648
+ const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked, context)) ?? defaultOptionValue(option, context);
649
+ if (value !== void 0) values[option.key] = value;
650
+ }
651
+ return values;
652
+ }
653
+ function sameValue(a, b) {
654
+ if (Array.isArray(a) && Array.isArray(b)) {
655
+ return a.length === b.length && a.every((item, i) => item === b[i]);
656
+ }
657
+ return a === b;
658
+ }
659
+ function sameOptionValues(a, b) {
660
+ const keys = Object.keys({ ...a, ...b });
661
+ return keys.every((key) => sameValue(a[key], b[key]));
662
+ }
663
+ function optionsCacheSuffix(definition, values, context = {}) {
664
+ const parts = [];
665
+ for (const option of definition.options ?? []) {
666
+ if (isClientApplied(option)) continue;
667
+ const value = values[option.key];
668
+ if (sameValue(value, defaultOptionValue(option, context))) continue;
669
+ parts.push(
670
+ `${option.key}=${encodeURIComponent(JSON.stringify(value ?? null))}`
671
+ );
672
+ }
673
+ parts.sort();
674
+ return parts.length === 0 ? "" : `?${parts.join("&")}`;
675
+ }
676
+ function tableColumnIndex(key) {
677
+ const match = /^c(\d+)$/.exec(key);
678
+ return match === null ? void 0 : Number(match[1]);
679
+ }
680
+ function sortBarItems(items, sort) {
681
+ const sign = sort.direction === "desc" ? -1 : 1;
682
+ return items.map((item, index) => ({ item, index })).sort((a, b) => {
683
+ const order = sort.column === "value" ? a.item.value - b.item.value : a.item.label.localeCompare(b.item.label);
684
+ return order * sign || a.index - b.index;
685
+ }).map(({ item }) => item);
686
+ }
687
+ function applyClientOptions(definition, data, values) {
688
+ const options = definition.options ?? [];
689
+ const sortOption = options.find(
690
+ (o) => o.type === "sort" && o.apply === "client"
691
+ );
692
+ const columnsOption = options.find(
693
+ (o) => o.type === "columns" && o.apply === "client"
694
+ );
695
+ const sortValue = sortOption === void 0 ? void 0 : values[sortOption.key];
696
+ const columnsValue = columnsOption === void 0 ? void 0 : values[columnsOption.key];
697
+ let next = data;
698
+ if (typeof sortValue === "string") {
699
+ const sort = parseSortValue(sortValue);
700
+ if (sort !== void 0 && next.kind === "BAR_LIST") {
701
+ if (sort.column === "label" || sort.column === "value") {
702
+ next = { ...next, items: sortBarItems(next.items, sort) };
703
+ }
704
+ } else if (sort !== void 0 && next.kind === "TABLE") {
705
+ const index = tableColumnIndex(sort.column);
706
+ if (index !== void 0 && index < next.columns.length) {
707
+ const sorted = queryRows(tableDetailData(next), {
708
+ page: 1,
709
+ pageSize: Math.max(1, next.rows.length),
710
+ sort
711
+ });
712
+ if (sorted.ok) next = { ...next, rows: sorted.value.rows };
713
+ }
714
+ }
715
+ }
716
+ if (Array.isArray(columnsValue) && next.kind === "TABLE") {
717
+ const table = next;
718
+ const indexes = columnsValue.map(tableColumnIndex).filter(
719
+ (index) => index !== void 0 && index < table.columns.length
720
+ );
721
+ if (indexes.length > 0) {
722
+ next = {
723
+ ...table,
724
+ columns: indexes.map((i) => table.columns[i]),
725
+ rows: table.rows.map((row) => indexes.map((i) => row[i]))
726
+ };
727
+ }
728
+ }
729
+ return next;
730
+ }
731
+
473
732
  // src/core/scale.ts
474
733
  function niceStep(value) {
475
734
  if (!(value > 0) || !Number.isFinite(value)) return 1;
@@ -1236,7 +1495,8 @@ function GaugeWidget({
1236
1495
  }
1237
1496
  function TableWidget({
1238
1497
  data,
1239
- controls
1498
+ controls,
1499
+ initialSort
1240
1500
  }) {
1241
1501
  if (controls === void 0) {
1242
1502
  return /* @__PURE__ */ jsxs("div", { className: "dwt-table-wrap", children: [
@@ -1244,7 +1504,14 @@ function TableWidget({
1244
1504
  data.footer !== void 0 && /* @__PURE__ */ jsx("p", { className: "dwt-note", children: data.footer })
1245
1505
  ] });
1246
1506
  }
1247
- return /* @__PURE__ */ jsx(InteractiveTable, { data, controls });
1507
+ return /* @__PURE__ */ jsx(
1508
+ InteractiveTable,
1509
+ {
1510
+ data,
1511
+ controls,
1512
+ initialSort
1513
+ }
1514
+ );
1248
1515
  }
1249
1516
  function TableView({
1250
1517
  data,
@@ -1297,13 +1564,20 @@ function nextTableSort(current, column) {
1297
1564
  }
1298
1565
  function InteractiveTable({
1299
1566
  data,
1300
- controls
1567
+ controls,
1568
+ initialSort
1301
1569
  }) {
1302
1570
  const slot = useSlotClassName();
1303
1571
  const { labels, locale } = useWidgetSettings();
1304
1572
  const searchId = useId();
1305
1573
  const [search, setSearch] = useState("");
1306
- const [sort, setSort] = useState(void 0);
1574
+ const [sort, setSort] = useState(initialSort);
1575
+ const seed = initialSort === void 0 ? "" : `${initialSort.column}:${initialSort.direction}`;
1576
+ const [seededWith, setSeededWith] = useState(seed);
1577
+ if (seededWith !== seed) {
1578
+ setSeededWith(seed);
1579
+ setSort(initialSort);
1580
+ }
1307
1581
  const localeTag = typeof locale === "string" ? locale : locale?.[0];
1308
1582
  const query = {
1309
1583
  page: 1,
@@ -1405,7 +1679,8 @@ function AlertListWidget({
1405
1679
  }
1406
1680
  function WidgetContent({
1407
1681
  data,
1408
- tableControls
1682
+ tableControls,
1683
+ initialSort
1409
1684
  }) {
1410
1685
  switch (data.kind) {
1411
1686
  case "TEXT":
@@ -1415,7 +1690,14 @@ function WidgetContent({
1415
1690
  case "GAUGE":
1416
1691
  return /* @__PURE__ */ jsx(GaugeWidget, { data });
1417
1692
  case "TABLE":
1418
- return /* @__PURE__ */ jsx(TableWidget, { data, controls: tableControls });
1693
+ return /* @__PURE__ */ jsx(
1694
+ TableWidget,
1695
+ {
1696
+ data,
1697
+ controls: tableControls,
1698
+ initialSort
1699
+ }
1700
+ );
1419
1701
  case "BAR_LIST":
1420
1702
  return /* @__PURE__ */ jsx(BarListWidget, { data });
1421
1703
  case "ALERT_LIST":
@@ -1612,7 +1894,8 @@ function ResolvedWidgetCard({
1612
1894
  WidgetContent,
1613
1895
  {
1614
1896
  data: widget.data,
1615
- tableControls: resolveTableControls(definition)
1897
+ tableControls: resolveTableControls(definition),
1898
+ initialSort: sortFromOptions(definition, widget.options)
1616
1899
  }
1617
1900
  ) : null
1618
1901
  }
@@ -2374,7 +2657,8 @@ function WidgetDetailDialog({
2374
2657
  }
2375
2658
  );
2376
2659
  }
2377
- function useDetailData(definition, load, enabled, fallback) {
2660
+ var NO_OPTION_VALUES = Object.freeze({});
2661
+ function useDetailData(definition, load, enabled, fallback, optionValues = NO_OPTION_VALUES) {
2378
2662
  const [state, setState] = useState({ status: "loading" });
2379
2663
  const [attempt, setAttempt] = useState(0);
2380
2664
  useEffect(() => {
@@ -2384,6 +2668,7 @@ function useDetailData(definition, load, enabled, fallback) {
2384
2668
  Promise.resolve().then(
2385
2669
  () => load === void 0 ? void 0 : load(definition, {
2386
2670
  signal: controller.signal,
2671
+ options: optionValues,
2387
2672
  query: defaultDetailQuery()
2388
2673
  })
2389
2674
  ).then((raw) => {
@@ -2411,7 +2696,7 @@ function useDetailData(definition, load, enabled, fallback) {
2411
2696
  });
2412
2697
  });
2413
2698
  return () => controller.abort();
2414
- }, [enabled, load, fallback, definition, attempt]);
2699
+ }, [enabled, load, fallback, definition, optionValues, attempt]);
2415
2700
  return { state, reload: () => setAttempt((count) => count + 1) };
2416
2701
  }
2417
2702
  var NEW_PAGE_VALUE = "__new-page__";
@@ -2527,7 +2812,14 @@ function DetailHost({
2527
2812
  const { definition } = widget;
2528
2813
  const options = resolveDetailOptions(definition);
2529
2814
  const pageSize = options?.pageSize ?? 25;
2530
- const [query, setQuery] = useState(defaultDetailQuery(pageSize));
2815
+ const optionSort = sortFromOptions(
2816
+ definition,
2817
+ widget.options
2818
+ );
2819
+ const [query, setQuery] = useState({
2820
+ ...defaultDetailQuery(pageSize),
2821
+ ...optionSort === void 0 ? {} : { sort: optionSort }
2822
+ });
2531
2823
  const derived = useMemo(
2532
2824
  () => widget.status === "ok" ? deriveDetailData(widget.data, locale) : void 0,
2533
2825
  [widget, locale]
@@ -2536,7 +2828,8 @@ function DetailHost({
2536
2828
  definition,
2537
2829
  loadDetail,
2538
2830
  true,
2539
- derived
2831
+ derived,
2832
+ widget.options
2540
2833
  );
2541
2834
  const title = options?.title ?? definition.title;
2542
2835
  return /* @__PURE__ */ jsx(WidgetDetailDialog, { title, open: true, onClose, children: /* @__PURE__ */ jsx(
@@ -3390,6 +3683,196 @@ function Dashboard({
3390
3683
  }
3391
3684
  );
3392
3685
  }
3686
+ function useStoredLayout(options) {
3687
+ const {
3688
+ persistence,
3689
+ scope,
3690
+ definitions,
3691
+ defaultLayout,
3692
+ saveDelayMs = 500,
3693
+ initialLayout,
3694
+ normalizeOptions
3695
+ } = options;
3696
+ const scopeKey = persistence.keyFor(scope);
3697
+ const [layout, setLayoutState] = useState(
3698
+ initialLayout ?? defaultLayout
3699
+ );
3700
+ const [backup, setBackupState] = useState(null);
3701
+ const [status, setStatus] = useState("loading");
3702
+ const [error, setError] = useState(null);
3703
+ const [conflict, setConflict] = useState(null);
3704
+ const [remoteLayout, setRemoteLayout] = useState(
3705
+ null
3706
+ );
3707
+ const latest = useRef(layout);
3708
+ const scopeRef = useRef(scope);
3709
+ scopeRef.current = scope;
3710
+ const normalizeRef = useRef(normalizeOptions);
3711
+ normalizeRef.current = normalizeOptions;
3712
+ const timer = useRef(void 0);
3713
+ const dirty = useRef(false);
3714
+ const apply = useCallback((next) => {
3715
+ latest.current = next;
3716
+ setLayoutState(next);
3717
+ }, []);
3718
+ const finishSave = useCallback(
3719
+ (result, mine) => {
3720
+ if (!result.ok) {
3721
+ setError(result.error);
3722
+ setStatus("error");
3723
+ return;
3724
+ }
3725
+ const saved = result.value;
3726
+ if (saved.outcome === "saved") {
3727
+ dirty.current = latest.current !== mine;
3728
+ setError(null);
3729
+ setStatus(dirty.current ? "saving" : "ready");
3730
+ return;
3731
+ }
3732
+ if (saved.outcome === "kept-theirs") {
3733
+ dirty.current = false;
3734
+ apply(saved.layout);
3735
+ setError(null);
3736
+ setStatus("ready");
3737
+ return;
3738
+ }
3739
+ setConflict({ mine, theirs: saved.layout });
3740
+ setStatus("conflict");
3741
+ },
3742
+ [apply]
3743
+ );
3744
+ const flush = useCallback(() => {
3745
+ if (timer.current !== void 0) {
3746
+ clearTimeout(timer.current);
3747
+ timer.current = void 0;
3748
+ }
3749
+ if (!dirty.current) return;
3750
+ const mine = latest.current;
3751
+ setStatus("saving");
3752
+ void persistence.save(scopeRef.current, mine).then((result) => finishSave(result, mine));
3753
+ }, [persistence, finishSave]);
3754
+ useEffect(() => {
3755
+ let current = true;
3756
+ setStatus("loading");
3757
+ setConflict(null);
3758
+ setRemoteLayout(null);
3759
+ void persistence.load(scopeRef.current, definitions, normalizeRef.current).then((result) => {
3760
+ if (!current) return;
3761
+ if (!result.ok) {
3762
+ setError(result.error);
3763
+ setStatus("error");
3764
+ return;
3765
+ }
3766
+ setError(null);
3767
+ if (!dirty.current && result.value.layout !== null) {
3768
+ apply(result.value.layout);
3769
+ }
3770
+ setStatus(dirty.current ? "saving" : "ready");
3771
+ });
3772
+ void persistence.loadBackup(scopeRef.current).then((result) => {
3773
+ if (current && result.ok) setBackupState(result.value);
3774
+ });
3775
+ return () => {
3776
+ current = false;
3777
+ };
3778
+ }, [persistence, scopeKey, definitions, apply]);
3779
+ useEffect(() => {
3780
+ return persistence.watch(
3781
+ scopeRef.current,
3782
+ definitions,
3783
+ (loaded) => {
3784
+ if (loaded.layout === null) return;
3785
+ if (dirty.current) {
3786
+ setRemoteLayout(loaded.layout);
3787
+ return;
3788
+ }
3789
+ apply(loaded.layout);
3790
+ },
3791
+ normalizeRef.current
3792
+ );
3793
+ }, [persistence, scopeKey, definitions, apply]);
3794
+ useEffect(() => {
3795
+ const onHide = () => {
3796
+ if (typeof document === "undefined" || document.visibilityState !== "hidden") {
3797
+ return;
3798
+ }
3799
+ flush();
3800
+ };
3801
+ if (typeof window !== "undefined") {
3802
+ window.addEventListener("pagehide", flush);
3803
+ document.addEventListener("visibilitychange", onHide);
3804
+ }
3805
+ return () => {
3806
+ if (typeof window !== "undefined") {
3807
+ window.removeEventListener("pagehide", flush);
3808
+ document.removeEventListener("visibilitychange", onHide);
3809
+ }
3810
+ flush();
3811
+ };
3812
+ }, [flush]);
3813
+ const setLayout = useCallback(
3814
+ (next) => {
3815
+ apply(next);
3816
+ dirty.current = true;
3817
+ if (timer.current !== void 0) clearTimeout(timer.current);
3818
+ timer.current = setTimeout(flush, Math.max(0, saveDelayMs));
3819
+ },
3820
+ [apply, flush, saveDelayMs]
3821
+ );
3822
+ const setBackup = useCallback(
3823
+ (next) => {
3824
+ setBackupState(next);
3825
+ if (next === null) return;
3826
+ void persistence.saveBackup(scopeRef.current, next).then((result) => {
3827
+ if (!result.ok) setError(result.error);
3828
+ });
3829
+ },
3830
+ [persistence]
3831
+ );
3832
+ const resolveConflict = useCallback(
3833
+ (choice) => {
3834
+ const pending = conflict;
3835
+ if (pending === null) return;
3836
+ setConflict(null);
3837
+ if (choice === "theirs") {
3838
+ dirty.current = false;
3839
+ apply(pending.theirs);
3840
+ setStatus("ready");
3841
+ return;
3842
+ }
3843
+ setStatus("saving");
3844
+ void persistence.save(scopeRef.current, pending.mine, { force: true }).then((result) => finishSave(result, pending.mine));
3845
+ },
3846
+ [conflict, persistence, apply, finishSave]
3847
+ );
3848
+ const resolveRemote = useCallback(
3849
+ (choice) => {
3850
+ const remote = remoteLayout;
3851
+ setRemoteLayout(null);
3852
+ if (choice === "use" && remote !== null) {
3853
+ if (timer.current !== void 0) {
3854
+ clearTimeout(timer.current);
3855
+ timer.current = void 0;
3856
+ }
3857
+ dirty.current = false;
3858
+ apply(remote);
3859
+ }
3860
+ },
3861
+ [remoteLayout, apply]
3862
+ );
3863
+ return {
3864
+ layout,
3865
+ setLayout,
3866
+ backup,
3867
+ setBackup,
3868
+ status,
3869
+ error,
3870
+ conflict,
3871
+ resolveConflict,
3872
+ remoteLayout,
3873
+ resolveRemote
3874
+ };
3875
+ }
3393
3876
 
3394
3877
  // src/core/payload.ts
3395
3878
  var WIDGET_KINDS = [
@@ -3733,12 +4216,33 @@ function failedWidget(definition, error) {
3733
4216
  function loadingWidgets(definitions) {
3734
4217
  return definitions.map((definition) => ({ definition, status: "loading" }));
3735
4218
  }
3736
- function cacheKeyFor(definition, options) {
3737
- return options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
4219
+ function cacheKeyFor(definition, options, values) {
4220
+ const base = options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
4221
+ return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
4222
+ }
4223
+ function optionValuesFor(definition, options) {
4224
+ return resolveOptionValues(
4225
+ definition,
4226
+ options.optionValues?.[definition.key],
4227
+ options
4228
+ );
4229
+ }
4230
+ function finalizeWidget(widget, values) {
4231
+ if (Object.keys(values).length === 0) return widget;
4232
+ if (widget.status === "ok") {
4233
+ const data = applyClientOptions(
4234
+ widget.definition,
4235
+ widget.data,
4236
+ values
4237
+ );
4238
+ return { ...widget, data, options: values };
4239
+ }
4240
+ return { ...widget, options: values };
3738
4241
  }
3739
4242
  var LoadInterruption = class extends Error {
3740
4243
  };
3741
- async function resolveWidget(definition, provider, context, options = {}) {
4244
+ async function resolveWidgetUnapplied(definition, provider, context, options = {}) {
4245
+ const values = optionValuesFor(definition, options);
3742
4246
  const quotedKey = JSON.stringify(definition.key);
3743
4247
  if (provider === void 0) {
3744
4248
  return failedWidget(
@@ -3773,7 +4277,10 @@ async function resolveWidget(definition, provider, context, options = {}) {
3773
4277
  payload = await Promise.race([
3774
4278
  Promise.resolve().then(() => {
3775
4279
  if (controller.signal.aborted) throw new LoadInterruption();
3776
- return provider(context, definition, { signal: controller.signal });
4280
+ return provider(context, definition, {
4281
+ signal: controller.signal,
4282
+ options: values
4283
+ });
3777
4284
  }),
3778
4285
  interrupted
3779
4286
  ]);
@@ -3797,7 +4304,7 @@ async function resolveWidget(definition, provider, context, options = {}) {
3797
4304
  const storedAt = Date.now();
3798
4305
  if (options.cache !== void 0) {
3799
4306
  try {
3800
- await options.cache.set(cacheKeyFor(definition, options), {
4307
+ await options.cache.set(cacheKeyFor(definition, options, values), {
3801
4308
  payload: resolved.data,
3802
4309
  storedAt
3803
4310
  });
@@ -3814,7 +4321,17 @@ function widgetsFor(definitions, roles) {
3814
4321
 
3815
4322
  // src/core/loader.ts
3816
4323
  function createWidgetLoader(options) {
3817
- const { definitions, providers, context, ...resolveOptions } = options;
4324
+ const { definitions, providers, context, ...baseOptions } = options;
4325
+ const chosen = new Map(
4326
+ Object.entries(baseOptions.optionValues ?? {}).flatMap(
4327
+ ([key, values]) => values === void 0 ? [] : [[key, values]]
4328
+ )
4329
+ );
4330
+ const resolveOptions = baseOptions;
4331
+ function currentOptions() {
4332
+ return { ...resolveOptions, optionValues: Object.fromEntries(chosen) };
4333
+ }
4334
+ const unapplied = /* @__PURE__ */ new Map();
3818
4335
  const visible = widgetsFor(definitions, context.roles);
3819
4336
  let snapshot = loadingWidgets(visible);
3820
4337
  const listeners = /* @__PURE__ */ new Set();
@@ -3855,19 +4372,19 @@ function createWidgetLoader(options) {
3855
4372
  const controller = new AbortController();
3856
4373
  inFlight.set(key, controller);
3857
4374
  const isCurrent = () => !disposed && generations.get(key) === generation;
3858
- if (readCache && resolveOptions.cache !== void 0) {
4375
+ const runOptions = currentOptions();
4376
+ const values = optionValuesFor(definition, runOptions);
4377
+ if (readCache && runOptions.cache !== void 0) {
3859
4378
  try {
3860
- const entry = await resolveOptions.cache.get(
3861
- cacheKeyFor(definition, resolveOptions)
4379
+ const entry = await runOptions.cache.get(
4380
+ cacheKeyFor(definition, runOptions, values)
3862
4381
  );
3863
4382
  if (entry !== void 0 && isCurrent()) {
3864
- const cached = resolvePayload(
3865
- definition,
3866
- entry.payload,
3867
- resolveOptions
3868
- );
4383
+ const cached = resolvePayload(definition, entry.payload, runOptions);
3869
4384
  if (cached.status === "ok") {
3870
- replace(key, { ...cached, updatedAt: entry.storedAt, stale: true });
4385
+ const stale = { ...cached, updatedAt: entry.storedAt, stale: true };
4386
+ unapplied.set(key, stale);
4387
+ replace(key, finalizeWidget(stale, values));
3871
4388
  }
3872
4389
  }
3873
4390
  } catch {
@@ -3875,13 +4392,15 @@ function createWidgetLoader(options) {
3875
4392
  }
3876
4393
  if (!isCurrent()) return;
3877
4394
  const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
3878
- const result = await resolveWidget(definition, provider, context, {
3879
- ...resolveOptions,
4395
+ const result = await resolveWidgetUnapplied(definition, provider, context, {
4396
+ ...runOptions,
3880
4397
  signal: controller.signal
3881
4398
  });
3882
4399
  if (!isCurrent()) return;
3883
4400
  inFlight.delete(key);
3884
- replace(key, result);
4401
+ if (result.status === "ok") unapplied.set(key, result);
4402
+ else unapplied.delete(key);
4403
+ replace(key, finalizeWidget(result, values));
3885
4404
  }
3886
4405
  function start(definition, readCache) {
3887
4406
  started.add(definition.key);
@@ -3916,6 +4435,29 @@ function createWidgetLoader(options) {
3916
4435
  start(definition, false);
3917
4436
  }
3918
4437
  }
4438
+ function setOptions(key, values) {
4439
+ if (disposed) return;
4440
+ const definition = definitionFor(key);
4441
+ if (definition === void 0) return;
4442
+ const before = optionValuesFor(definition, currentOptions());
4443
+ chosen.set(key, { ...values });
4444
+ const runOptions = currentOptions();
4445
+ const after = optionValuesFor(definition, runOptions);
4446
+ if (sameOptionValues(before, after) || !started.has(key)) return;
4447
+ const kept = unapplied.get(key);
4448
+ if (kept !== void 0 && optionsCacheSuffix(definition, before, runOptions) === optionsCacheSuffix(definition, after, runOptions)) {
4449
+ replace(key, finalizeWidget(kept, after));
4450
+ return;
4451
+ }
4452
+ const current = snapshot.find(
4453
+ (widget) => widget.definition.key === key
4454
+ );
4455
+ if (current?.status === "ok") replace(key, { ...current, stale: true });
4456
+ else if (current?.status === "error") {
4457
+ replace(key, { definition, status: "loading" });
4458
+ }
4459
+ start(definition, true);
4460
+ }
3919
4461
  function dispose() {
3920
4462
  disposed = true;
3921
4463
  abortAll(new Error("The widget loader was disposed."));
@@ -3933,6 +4475,7 @@ function createWidgetLoader(options) {
3933
4475
  load,
3934
4476
  loadAll,
3935
4477
  refresh,
4478
+ setOptions,
3936
4479
  dispose
3937
4480
  };
3938
4481
  }
@@ -3947,7 +4490,8 @@ function useWidgets(definitions, providers, context, options = {}) {
3947
4490
  timeoutMs,
3948
4491
  signal,
3949
4492
  cache,
3950
- cacheKey
4493
+ cacheKey,
4494
+ optionValues
3951
4495
  } = options;
3952
4496
  const placeholders = useMemo(
3953
4497
  () => loadingWidgets(widgetsFor(definitions, context.roles)),
@@ -3957,6 +4501,8 @@ function useWidgets(definitions, providers, context, options = {}) {
3957
4501
  const [loader, setLoader] = useState(null);
3958
4502
  const loaderRef = useRef(null);
3959
4503
  const gridRef = useRef(null);
4504
+ const optionValuesRef = useRef(optionValues);
4505
+ optionValuesRef.current = optionValues;
3960
4506
  useEffect(() => {
3961
4507
  const created = createWidgetLoader({
3962
4508
  definitions,
@@ -3966,7 +4512,8 @@ function useWidgets(definitions, providers, context, options = {}) {
3966
4512
  ...timeoutMs === void 0 ? {} : { timeoutMs },
3967
4513
  ...signal === void 0 ? {} : { signal },
3968
4514
  ...cache === void 0 ? {} : { cache },
3969
- ...cacheKey === void 0 ? {} : { cacheKey }
4515
+ ...cacheKey === void 0 ? {} : { cacheKey },
4516
+ ...optionValuesRef.current === void 0 ? {} : { optionValues: optionValuesRef.current }
3970
4517
  });
3971
4518
  loaderRef.current = created;
3972
4519
  setWidgets(created.getSnapshot());
@@ -4038,12 +4585,25 @@ function useWidgets(definitions, providers, context, options = {}) {
4038
4585
  }, refreshMs);
4039
4586
  return () => clearInterval(timer);
4040
4587
  }, [loader, refreshMs]);
4588
+ const optionSignature = JSON.stringify(optionValues ?? {});
4589
+ useEffect(() => {
4590
+ if (loader === null) return;
4591
+ for (const [key, chosen] of Object.entries(optionValuesRef.current ?? {})) {
4592
+ if (chosen !== void 0) loader.setOptions(key, chosen);
4593
+ }
4594
+ }, [loader, optionSignature]);
4595
+ const setOptions = useCallback(
4596
+ (key, chosen) => {
4597
+ loaderRef.current?.setOptions(key, chosen);
4598
+ },
4599
+ []
4600
+ );
4041
4601
  const refresh = useCallback((key) => {
4042
4602
  loaderRef.current?.refresh(key);
4043
4603
  }, []);
4044
- return { widgets, refresh, gridRef };
4604
+ return { widgets, refresh, setOptions, gridRef };
4045
4605
  }
4046
4606
 
4047
- export { AlertListWidget, BarListWidget, DEFAULT_LABELS, DEFAULT_SERIES_COLORS, Dashboard, GaugeWidget, GraphWidget, KpiWidget, ResolvedWidgetCard, TableWidget, TextWidget, Thumbnail, WidgetCard, WidgetContent, WidgetDetail, WidgetDetailDialog, WidgetGrid, WidgetLink, WidgetSettingsProvider, barPath, buildChartModel, describeChart, joinClassNames, seriesColor, useDetailData, useWidgetSettings, useWidgets, widgetTitle };
4607
+ export { AlertListWidget, BarListWidget, DEFAULT_LABELS, DEFAULT_SERIES_COLORS, Dashboard, GaugeWidget, GraphWidget, KpiWidget, ResolvedWidgetCard, TableWidget, TextWidget, Thumbnail, WidgetCard, WidgetContent, WidgetDetail, WidgetDetailDialog, WidgetGrid, WidgetLink, WidgetSettingsProvider, barPath, buildChartModel, describeChart, joinClassNames, seriesColor, useDetailData, useStoredLayout, useWidgetSettings, useWidgets, widgetTitle };
4048
4608
  //# sourceMappingURL=react.js.map
4049
4609
  //# sourceMappingURL=react.js.map