@richardmcquiston01/dashboard-widgets-toolkit 0.6.0 → 0.8.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.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactNode, RefObject } from 'react';
2
- import { D as DashboardWidget, W as WidgetSize, a as WidgetFill, G as GraphWidgetData, N as NiceDomain, b as WidgetDefinition, c as DetailProviderOptions, d as DetailData, e as DetailQuery, L as Locale, f as DashboardLayout, A as AlertListWidgetData, B as BarListWidgetData, g as GaugeWidgetData, K as KpiWidgetData, T as TableWidgetData, R as ResolvedTableControls, h as TextWidgetData, i as WidgetData, j as ResolveOptions, k as WidgetContext, l as WidgetProviders } from './scale-D51ZGIF4.cjs';
2
+ import { D as DashboardWidget, W as WidgetSize, a as WidgetFill, G as GraphWidgetData, N as NiceDomain, b as WidgetDefinition, c as DetailProviderOptions, d as DetailData, e as DetailQuery, L as Locale, f as DashboardLayout, A as AlertListWidgetData, B as BarListWidgetData, g as GaugeWidgetData, K as KpiWidgetData, T as TableWidgetData, R as ResolvedTableControls, h as TextWidgetData, i as WidgetData, j as ResolveOptions, k as WidgetContext, l as WidgetProviders } from './scale-CrXxawQn.cjs';
3
3
 
4
4
  interface WidgetCardProps {
5
5
  readonly title: string;
@@ -230,6 +230,13 @@ interface DashboardProps extends WidgetSettings {
230
230
  * restore; persist it. Without it the layout is read-only (no controls).
231
231
  */
232
232
  readonly onLayoutChange?: (layout: DashboardLayout) => void;
233
+ /**
234
+ * Ignore widget `locked` settings, so locked widgets can be moved, hidden
235
+ * and minimized. For administrators editing the default layout: pass it
236
+ * only for roles you trust, and enforce locks again on the server for
237
+ * everyone else (`enforceLocks`). It never changes the locks themselves.
238
+ */
239
+ readonly overrideLocks?: boolean;
233
240
  readonly onRetry?: (key: string) => void;
234
241
  /**
235
242
  * Loads the full data for a widget's detail view (widgets whose definition
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactNode, RefObject } from 'react';
2
- import { D as DashboardWidget, W as WidgetSize, a as WidgetFill, G as GraphWidgetData, N as NiceDomain, b as WidgetDefinition, c as DetailProviderOptions, d as DetailData, e as DetailQuery, L as Locale, f as DashboardLayout, A as AlertListWidgetData, B as BarListWidgetData, g as GaugeWidgetData, K as KpiWidgetData, T as TableWidgetData, R as ResolvedTableControls, h as TextWidgetData, i as WidgetData, j as ResolveOptions, k as WidgetContext, l as WidgetProviders } from './scale-D51ZGIF4.js';
2
+ import { D as DashboardWidget, W as WidgetSize, a as WidgetFill, G as GraphWidgetData, N as NiceDomain, b as WidgetDefinition, c as DetailProviderOptions, d as DetailData, e as DetailQuery, L as Locale, f as DashboardLayout, A as AlertListWidgetData, B as BarListWidgetData, g as GaugeWidgetData, K as KpiWidgetData, T as TableWidgetData, R as ResolvedTableControls, h as TextWidgetData, i as WidgetData, j as ResolveOptions, k as WidgetContext, l as WidgetProviders } from './scale-CrXxawQn.js';
3
3
 
4
4
  interface WidgetCardProps {
5
5
  readonly title: string;
@@ -230,6 +230,13 @@ interface DashboardProps extends WidgetSettings {
230
230
  * restore; persist it. Without it the layout is read-only (no controls).
231
231
  */
232
232
  readonly onLayoutChange?: (layout: DashboardLayout) => void;
233
+ /**
234
+ * Ignore widget `locked` settings, so locked widgets can be moved, hidden
235
+ * and minimized. For administrators editing the default layout: pass it
236
+ * only for roles you trust, and enforce locks again on the server for
237
+ * everyone else (`enforceLocks`). It never changes the locks themselves.
238
+ */
239
+ readonly overrideLocks?: boolean;
233
240
  readonly onRetry?: (key: string) => void;
234
241
  /**
235
242
  * Loads the full data for a widget's detail view (widgets whose definition
package/dist/react.js CHANGED
@@ -1568,6 +1568,45 @@ function ResolvedWidgetCard({
1568
1568
  );
1569
1569
  }
1570
1570
 
1571
+ // src/core/definition.ts
1572
+ var NOT_LOCKED = Object.freeze({
1573
+ move: false,
1574
+ hide: false,
1575
+ minimize: false
1576
+ });
1577
+ var FULLY_LOCKED = Object.freeze({
1578
+ move: true,
1579
+ hide: true,
1580
+ minimize: true
1581
+ });
1582
+ function resolveWidgetLock(definition) {
1583
+ const setting = definition.locked;
1584
+ if (setting === true) return FULLY_LOCKED;
1585
+ if (setting === void 0 || setting === false) return NOT_LOCKED;
1586
+ return {
1587
+ move: setting.move === true,
1588
+ hide: setting.hide === true,
1589
+ minimize: setting.minimize === true
1590
+ };
1591
+ }
1592
+ function sortDefinitions(definitions) {
1593
+ return definitions.map((definition, index) => ({ definition, index })).sort(
1594
+ (a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
1595
+ ).map(({ definition }) => definition);
1596
+ }
1597
+ function isWidgetVisibleTo(definition, roles) {
1598
+ if (definition.active === false) {
1599
+ return false;
1600
+ }
1601
+ if (definition.roles === void 0 || definition.roles.length === 0) {
1602
+ return true;
1603
+ }
1604
+ if (roles === void 0) {
1605
+ return false;
1606
+ }
1607
+ return definition.roles.some((role) => roles.includes(role));
1608
+ }
1609
+
1571
1610
  // src/core/layout.ts
1572
1611
  var EMPTY_LAYOUT = Object.freeze({
1573
1612
  order: Object.freeze([]),
@@ -1579,9 +1618,10 @@ function bySortOrder(items) {
1579
1618
  (a, b) => (a.item.sortOrder ?? 0) - (b.item.sortOrder ?? 0) || a.index - b.index
1580
1619
  ).map(({ item }) => item);
1581
1620
  }
1582
- function visibleWidgets(definitions, layout) {
1583
- const hidden = new Set(layout.hidden);
1584
- const visible = definitions.filter((d) => !hidden.has(d.key));
1621
+ function isPinned(item) {
1622
+ return resolveWidgetLock(item).move;
1623
+ }
1624
+ function viewerOrder(visible, layout) {
1585
1625
  const byKey = new Map(visible.map((d) => [d.key, d]));
1586
1626
  const ordered = [];
1587
1627
  for (const key of layout.order) {
@@ -1596,34 +1636,62 @@ function visibleWidgets(definitions, layout) {
1596
1636
  }
1597
1637
  return ordered;
1598
1638
  }
1639
+ function placeAroundPinned(visible, free) {
1640
+ const slots = bySortOrder(visible).map(
1641
+ (item) => isPinned(item) ? item : void 0
1642
+ );
1643
+ let next = 0;
1644
+ return slots.map((pinned) => pinned ?? free[next++]);
1645
+ }
1646
+ function visibleWidgets(definitions, layout) {
1647
+ const hidden = new Set(layout.hidden);
1648
+ const visible = definitions.filter((d) => !hidden.has(d.key));
1649
+ const ordered = viewerOrder(visible, layout);
1650
+ if (!visible.some(isPinned)) return ordered;
1651
+ return placeAroundPinned(
1652
+ visible,
1653
+ ordered.filter((item) => !isPinned(item))
1654
+ );
1655
+ }
1599
1656
  function hiddenWidgets(definitions, layout) {
1600
1657
  const hidden = new Set(layout.hidden);
1601
1658
  return bySortOrder(definitions.filter((d) => hidden.has(d.key)));
1602
1659
  }
1603
1660
  function moveWidget(definitions, layout, key, toIndex) {
1604
- const current = visibleWidgets(definitions, layout).map(
1605
- (d) => d.key
1606
- );
1607
- const fromIndex = current.indexOf(key);
1608
- if (fromIndex === -1 || !Number.isInteger(toIndex) || toIndex < 0 || toIndex >= current.length || toIndex === fromIndex) {
1661
+ const current = visibleWidgets(definitions, layout);
1662
+ const fromIndex = current.findIndex((d) => d.key === key);
1663
+ const mover = current[fromIndex];
1664
+ if (mover === void 0 || !Number.isInteger(toIndex) || toIndex < 0 || toIndex >= current.length || toIndex === fromIndex || isPinned(mover) || isPinned(current[toIndex])) {
1609
1665
  return layout;
1610
1666
  }
1611
- const next = [...current];
1612
- next.splice(fromIndex, 1);
1613
- next.splice(toIndex, 0, key);
1614
- return { ...layout, order: next };
1667
+ const freeSlots = freeSlotIndexes(current);
1668
+ const free = current.filter((item) => !isPinned(item));
1669
+ const fromFree = freeSlots.indexOf(fromIndex);
1670
+ const toFree = freeSlots.indexOf(toIndex);
1671
+ free.splice(fromFree, 1);
1672
+ free.splice(toFree, 0, mover);
1673
+ const next = placeAroundPinned(current, free);
1674
+ return { ...layout, order: next.map((d) => d.key) };
1675
+ }
1676
+ function freeSlotIndexes(ordered) {
1677
+ const slots = [];
1678
+ ordered.forEach((item, index) => {
1679
+ if (!isPinned(item)) slots.push(index);
1680
+ });
1681
+ return slots;
1615
1682
  }
1616
1683
  function moveWidgetBy(definitions, layout, key, offset) {
1617
- const current = visibleWidgets(definitions, layout).map(
1618
- (d) => d.key
1619
- );
1620
- const fromIndex = current.indexOf(key);
1684
+ const current = visibleWidgets(definitions, layout);
1685
+ const fromIndex = current.findIndex((d) => d.key === key);
1621
1686
  if (fromIndex === -1) return layout;
1622
- const toIndex = Math.max(
1687
+ const freeSlots = freeSlotIndexes(current);
1688
+ const fromFree = freeSlots.indexOf(fromIndex);
1689
+ if (fromFree === -1) return layout;
1690
+ const toFree = Math.max(
1623
1691
  0,
1624
- Math.min(current.length - 1, fromIndex + Math.trunc(offset))
1692
+ Math.min(freeSlots.length - 1, fromFree + Math.trunc(offset))
1625
1693
  );
1626
- return moveWidget(definitions, layout, key, toIndex);
1694
+ return moveWidget(definitions, layout, key, freeSlots[toFree]);
1627
1695
  }
1628
1696
  function isHidden(layout, key) {
1629
1697
  return layout.hidden.includes(key);
@@ -1654,6 +1722,23 @@ function restoreWidget(layout, key) {
1654
1722
  function toggleMinimized(layout, key) {
1655
1723
  return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
1656
1724
  }
1725
+ function enforceLocks(definitions, layout, options = {}) {
1726
+ if (options.overrideLocks === true) return layout;
1727
+ const noMove = /* @__PURE__ */ new Set();
1728
+ const noHide = /* @__PURE__ */ new Set();
1729
+ const noMinimize = /* @__PURE__ */ new Set();
1730
+ for (const definition of definitions) {
1731
+ const lock = resolveWidgetLock(definition);
1732
+ if (lock.move) noMove.add(definition.key);
1733
+ if (lock.hide) noHide.add(definition.key);
1734
+ if (lock.minimize) noMinimize.add(definition.key);
1735
+ }
1736
+ const without = (keys, drop) => keys.some((key) => drop.has(key)) ? keys.filter((key) => !drop.has(key)) : keys;
1737
+ const order = without(layout.order, noMove);
1738
+ const hidden = without(layout.hidden, noHide);
1739
+ const minimized = without(layout.minimized, noMinimize);
1740
+ return order === layout.order && hidden === layout.hidden && minimized === layout.minimized ? layout : { ...layout, order, hidden, minimized };
1741
+ }
1657
1742
  function nextSort(current, column) {
1658
1743
  if (current?.column !== column) return { column, direction: "asc" };
1659
1744
  return current.direction === "asc" ? { column, direction: "desc" } : void 0;
@@ -2052,10 +2137,17 @@ function DetailHost({
2052
2137
  }
2053
2138
  ) });
2054
2139
  }
2140
+ function withoutLock(definition) {
2141
+ if (definition.locked === void 0) return definition;
2142
+ const copy = { ...definition };
2143
+ delete copy.locked;
2144
+ return copy;
2145
+ }
2055
2146
  function DashboardInner({
2056
2147
  widgets,
2057
- layout = EMPTY_LAYOUT,
2148
+ layout: savedLayout = EMPTY_LAYOUT,
2058
2149
  onLayoutChange,
2150
+ overrideLocks = false,
2059
2151
  onRetry,
2060
2152
  loadDetail,
2061
2153
  onOpenDetail,
@@ -2064,7 +2156,10 @@ function DashboardInner({
2064
2156
  }) {
2065
2157
  const slot = useSlotClassName();
2066
2158
  const { labels } = useWidgetSettings();
2067
- const definitions = widgets.map((w) => w.definition);
2159
+ const definitions = widgets.map(
2160
+ (w) => overrideLocks ? withoutLock(w.definition) : w.definition
2161
+ );
2162
+ const layout = enforceLocks(definitions, savedLayout);
2068
2163
  const byKey = new Map(
2069
2164
  widgets.map((w) => [w.definition.key, w])
2070
2165
  );
@@ -2137,32 +2232,45 @@ function DashboardInner({
2137
2232
  if (widget === void 0) return null;
2138
2233
  const key = definition.key;
2139
2234
  const title = definition.title;
2235
+ const lock = resolveWidgetLock(definition);
2236
+ const canMoveEarlier = !lock.move && moveWidgetBy(definitions, layout, key, -1) !== layout;
2237
+ const canMoveLater = !lock.move && moveWidgetBy(definitions, layout, key, 1) !== layout;
2140
2238
  const actions = editable ? /* @__PURE__ */ jsxs(Fragment, { children: [
2141
- /* @__PURE__ */ jsx(
2142
- "button",
2143
- {
2144
- type: "button",
2145
- className: slot("button", "dwt-button", "dwt-icon-button"),
2146
- "aria-label": labels.moveEarlier(title),
2147
- title: labels.moveEarlier(title),
2148
- disabled: index === 0,
2149
- onClick: () => change(moveWidgetBy(definitions, layout, key, -1)),
2150
- children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2191" })
2151
- }
2152
- ),
2153
- /* @__PURE__ */ jsx(
2154
- "button",
2155
- {
2156
- type: "button",
2157
- className: slot("button", "dwt-button", "dwt-icon-button"),
2158
- "aria-label": labels.moveLater(title),
2159
- title: labels.moveLater(title),
2160
- disabled: index === shown.length - 1,
2161
- onClick: () => change(moveWidgetBy(definitions, layout, key, 1)),
2162
- children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2193" })
2163
- }
2164
- ),
2165
- /* @__PURE__ */ jsx(
2239
+ !lock.move && /* @__PURE__ */ jsxs(Fragment, { children: [
2240
+ /* @__PURE__ */ jsx(
2241
+ "button",
2242
+ {
2243
+ type: "button",
2244
+ className: slot(
2245
+ "button",
2246
+ "dwt-button",
2247
+ "dwt-icon-button"
2248
+ ),
2249
+ "aria-label": labels.moveEarlier(title),
2250
+ title: labels.moveEarlier(title),
2251
+ disabled: index === 0 || !canMoveEarlier,
2252
+ onClick: () => change(moveWidgetBy(definitions, layout, key, -1)),
2253
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2191" })
2254
+ }
2255
+ ),
2256
+ /* @__PURE__ */ jsx(
2257
+ "button",
2258
+ {
2259
+ type: "button",
2260
+ className: slot(
2261
+ "button",
2262
+ "dwt-button",
2263
+ "dwt-icon-button"
2264
+ ),
2265
+ "aria-label": labels.moveLater(title),
2266
+ title: labels.moveLater(title),
2267
+ disabled: index === shown.length - 1 || !canMoveLater,
2268
+ onClick: () => change(moveWidgetBy(definitions, layout, key, 1)),
2269
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2193" })
2270
+ }
2271
+ )
2272
+ ] }),
2273
+ !lock.hide && /* @__PURE__ */ jsx(
2166
2274
  "button",
2167
2275
  {
2168
2276
  type: "button",
@@ -2186,7 +2294,7 @@ function DashboardInner({
2186
2294
  ...widthProps(twelve, definition),
2187
2295
  minimized: isMinimized(layout, key),
2188
2296
  ...actions === void 0 ? {} : { actions },
2189
- ...editable ? {
2297
+ ...editable && !lock.minimize ? {
2190
2298
  onToggleMinimized: () => change(toggleMinimized(layout, key))
2191
2299
  } : {},
2192
2300
  ...headingLevel === void 0 ? {} : { headingLevel },
@@ -2230,25 +2338,6 @@ function Dashboard({
2230
2338
  );
2231
2339
  }
2232
2340
 
2233
- // src/core/definition.ts
2234
- function sortDefinitions(definitions) {
2235
- return definitions.map((definition, index) => ({ definition, index })).sort(
2236
- (a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
2237
- ).map(({ definition }) => definition);
2238
- }
2239
- function isWidgetVisibleTo(definition, roles) {
2240
- if (definition.active === false) {
2241
- return false;
2242
- }
2243
- if (definition.roles === void 0 || definition.roles.length === 0) {
2244
- return true;
2245
- }
2246
- if (roles === void 0) {
2247
- return false;
2248
- }
2249
- return definition.roles.some((role) => roles.includes(role));
2250
- }
2251
-
2252
2341
  // src/core/payload.ts
2253
2342
  var WIDGET_KINDS = [
2254
2343
  "TEXT",
@@ -2330,6 +2419,14 @@ function optionalNumber(record, field, path, problems) {
2330
2419
  requireNumber(record, field, path, problems);
2331
2420
  }
2332
2421
  }
2422
+ function optionalSortValue(record, path, problems) {
2423
+ const value = record["value"];
2424
+ if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
2425
+ problems.push(
2426
+ `${path}.value must be a string or a finite number, got ${describeType2(value)}.`
2427
+ );
2428
+ }
2429
+ }
2333
2430
  function optionalBoolean(record, field, path, problems) {
2334
2431
  const value = record[field];
2335
2432
  if (value !== void 0 && typeof value !== "boolean") {
@@ -2433,6 +2530,7 @@ function checkPayloadFields(kind, data, problems) {
2433
2530
  }
2434
2531
  eachRecord(row, rowPath, problems, (cell, cellPath) => {
2435
2532
  requireString(cell, "text", cellPath, problems);
2533
+ optionalSortValue(cell, cellPath, problems);
2436
2534
  optionalUrl(
2437
2535
  cell,
2438
2536
  "href",