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