@richardmcquiston01/dashboard-widgets-toolkit 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -830,6 +830,14 @@ var DEFAULT_LABELS = {
830
830
  menuMoveLater: "Move later",
831
831
  menuHide: "Hide",
832
832
  menuOptions: "Options\u2026",
833
+ menuDuplicate: "Duplicate",
834
+ duplicate: (title) => `Duplicate ${title}`,
835
+ duplicateBlocked: (title, reason) => `Duplicate ${title} (unavailable: ${reason})`,
836
+ duplicated: (title) => `Duplicated ${title}`,
837
+ optionsDelete: "Delete this widget",
838
+ confirmDelete: "Delete this widget? Its settings are lost.",
839
+ deleted: (title) => `Deleted ${title}`,
840
+ suggestedViewTitle: (title, viewLabel) => `${title} ${viewLabel}`,
833
841
  optionsFor: (title) => `Options for ${title}`,
834
842
  optionsTitleField: "Title",
835
843
  titleInUse: "Another widget already uses this title.",
@@ -1939,13 +1947,15 @@ var NOT_LOCKED = Object.freeze({
1939
1947
  move: false,
1940
1948
  hide: false,
1941
1949
  minimize: false,
1942
- options: false
1950
+ options: false,
1951
+ clone: false
1943
1952
  });
1944
1953
  var FULLY_LOCKED = Object.freeze({
1945
1954
  move: true,
1946
1955
  hide: true,
1947
1956
  minimize: true,
1948
- options: true
1957
+ options: true,
1958
+ clone: true
1949
1959
  });
1950
1960
  function resolveWidgetLock(definition) {
1951
1961
  const setting = definition.locked;
@@ -1955,7 +1965,8 @@ function resolveWidgetLock(definition) {
1955
1965
  move: setting.move === true,
1956
1966
  hide: setting.hide === true,
1957
1967
  minimize: setting.minimize === true,
1958
- options: setting.options === true
1968
+ options: setting.options === true,
1969
+ clone: setting.clone === true
1959
1970
  };
1960
1971
  }
1961
1972
  function sortDefinitions(definitions) {
@@ -2405,6 +2416,62 @@ function isTitleInUse(definitions, layout, key, title) {
2405
2416
  );
2406
2417
  }
2407
2418
 
2419
+ // src/core/clones.ts
2420
+ function cloneNumber(key) {
2421
+ const match = /#(\d+)$/.exec(key);
2422
+ return match === null ? void 0 : Number(match[1]);
2423
+ }
2424
+ function cleanClones(definitions, clones, options = {}) {
2425
+ if (clones === void 0) return void 0;
2426
+ const byKey = new Map(definitions.map((d) => [d.key, d]));
2427
+ const seen = /* @__PURE__ */ new Set();
2428
+ const kept = [];
2429
+ for (const clone of clones) {
2430
+ if (seen.has(clone.key)) continue;
2431
+ const original = byKey.get(clone.from);
2432
+ if (original === void 0 || original.sourceKey !== void 0) continue;
2433
+ const existing = byKey.get(clone.key);
2434
+ if (existing !== void 0 && existing.sourceKey !== clone.from) continue;
2435
+ if (options.overrideLocks !== true && resolveWidgetLock(original).clone) {
2436
+ continue;
2437
+ }
2438
+ seen.add(clone.key);
2439
+ kept.push(clone);
2440
+ }
2441
+ if (kept.length === 0) return void 0;
2442
+ return kept.length === clones.length && kept.every((c, i) => c === clones[i]) ? clones : kept;
2443
+ }
2444
+ function defaultCloneTitle(original, clone, index) {
2445
+ const base = original.title ?? original.key;
2446
+ const suffix = ` (#${cloneNumber(clone.key) ?? index + 1})`;
2447
+ return `${base.slice(0, MAX_WIDGET_TITLE_LENGTH - suffix.length)}${suffix}`;
2448
+ }
2449
+ function withClones(definitions, layout, options = {}) {
2450
+ const valid = cleanClones(
2451
+ definitions,
2452
+ layout.clones,
2453
+ options
2454
+ );
2455
+ if (valid === void 0) return definitions;
2456
+ const present = new Set(definitions.map((d) => d.key));
2457
+ const toAdd = valid.filter((c) => !present.has(c.key));
2458
+ if (toAdd.length === 0) return definitions;
2459
+ const out = [];
2460
+ for (const definition of definitions) {
2461
+ out.push(definition);
2462
+ toAdd.forEach((clone, index) => {
2463
+ if (clone.from !== definition.key) return;
2464
+ const copy = { ...definition };
2465
+ copy.key = clone.key;
2466
+ copy.sourceKey = definition.key;
2467
+ copy.title = defaultCloneTitle(definition, clone, index);
2468
+ delete copy.locked;
2469
+ out.push(copy);
2470
+ });
2471
+ }
2472
+ return out;
2473
+ }
2474
+
2408
2475
  // src/core/layout.ts
2409
2476
  var MAX_PAGE_TITLE_LENGTH = 30;
2410
2477
  var MAX_PARSED_PAGES = 50;
@@ -2882,6 +2949,114 @@ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {
2882
2949
  }
2883
2950
  return ok(moved);
2884
2951
  }
2952
+
2953
+ // src/core/duplicate.ts
2954
+ function canDuplicate(definitions, key, options = {}) {
2955
+ const definition = definitions.find(
2956
+ (d) => d.key === key
2957
+ );
2958
+ if (definition === void 0) return false;
2959
+ if (options.overrideLocks === true) return true;
2960
+ const sourceKey = definition.sourceKey ?? definition.key;
2961
+ const original = definitions.find(
2962
+ (d) => d.key === sourceKey
2963
+ );
2964
+ return original !== void 0 && !resolveWidgetLock(original).clone;
2965
+ }
2966
+ function nextCloneKey(definitions, layout, fromKey) {
2967
+ const taken = /* @__PURE__ */ new Set([
2968
+ ...definitions.map((d) => d.key),
2969
+ ...(layout.clones ?? []).map((c) => c.key)
2970
+ ]);
2971
+ let highest = 0;
2972
+ for (const clone of layout.clones ?? []) {
2973
+ if (clone.from === fromKey) {
2974
+ highest = Math.max(highest, cloneNumber(clone.key) ?? 0);
2975
+ }
2976
+ }
2977
+ let n = highest + 1;
2978
+ while (taken.has(`${fromKey}#${n}`)) n += 1;
2979
+ return `${fromKey}#${n}`;
2980
+ }
2981
+ function duplicateWidget(definitions, layout, key, options = {}) {
2982
+ const widget = definitions.find((d) => d.key === key);
2983
+ if (widget === void 0) {
2984
+ return err(`Widget ${JSON.stringify(key)} isn't on this dashboard.`);
2985
+ }
2986
+ const originalKey = widget.sourceKey ?? widget.key;
2987
+ if (!canDuplicate(definitions, key, options)) {
2988
+ return err(
2989
+ `Widget ${JSON.stringify(originalKey)} is locked against duplicating.`
2990
+ );
2991
+ }
2992
+ const cloneKey = nextCloneKey(definitions, layout, originalKey);
2993
+ const copiedSettings = (() => {
2994
+ const mine = layout.settings?.[key];
2995
+ if (mine === void 0) return void 0;
2996
+ const { title: _title, ...rest } = mine;
2997
+ return Object.keys(rest).length === 0 ? void 0 : rest;
2998
+ })();
2999
+ const settings = copiedSettings === void 0 ? layout.settings : { ...layout.settings, [cloneKey]: copiedSettings };
3000
+ let next = {
3001
+ ...layout,
3002
+ clones: [...layout.clones ?? [], { key: cloneKey, from: originalKey }],
3003
+ ...settings === void 0 ? {} : { settings }
3004
+ };
3005
+ const all = withClones(definitions, next, options);
3006
+ const assigned = assignPages(all, next);
3007
+ const pageKey = [...assigned].find(
3008
+ ([, widgets]) => widgets.some((w) => w.key === key)
3009
+ )?.[0];
3010
+ if (pageKey !== void 0) {
3011
+ const view = pageLayout(next, pageKey);
3012
+ const onPage = assigned.get(pageKey) ?? [];
3013
+ if (view !== void 0) {
3014
+ const shown = visibleWidgets(onPage, view).filter((d) => !resolveWidgetLock(d).move && d.key !== cloneKey).map((d) => d.key);
3015
+ const at = shown.indexOf(key);
3016
+ const order = at === -1 ? [...shown, cloneKey] : [...shown.slice(0, at + 1), cloneKey, ...shown.slice(at + 1)];
3017
+ next = withPageLayout(next, pageKey, { ...view, order });
3018
+ }
3019
+ }
3020
+ if (hasPages(next) && pageKey !== void 0) {
3021
+ const effective = effectiveDefinitions(all, next, options);
3022
+ const room = pageRoom(effective, next, pageKey, options);
3023
+ if (room !== void 0 && room.rowsUsed > room.maxRows) {
3024
+ const page = next.pages?.find(
3025
+ (p) => p.key === pageKey
3026
+ );
3027
+ const width = effective.find(
3028
+ (d) => d.key === cloneKey
3029
+ )?.width;
3030
+ return err(
3031
+ `Page "${page?.title ?? pageKey}" has no room for a widget${width === void 0 ? "" : ` ${width} wide`} (it holds ${room.maxRows} rows).`
3032
+ );
3033
+ }
3034
+ }
3035
+ return ok({ layout: next, key: cloneKey });
3036
+ }
3037
+ function deleteClone(layout, key) {
3038
+ if (layout.clones?.some((clone) => clone.key === key) !== true) return layout;
3039
+ const without2 = (keys) => keys.includes(key) ? keys.filter((k) => k !== key) : keys;
3040
+ const { clones, settings, ...rest } = layout;
3041
+ const remaining = (clones ?? []).filter((c) => c.key !== key);
3042
+ const { [key]: _gone, ...otherSettings } = settings ?? {};
3043
+ return {
3044
+ ...rest,
3045
+ order: without2(layout.order),
3046
+ hidden: without2(layout.hidden),
3047
+ minimized: without2(layout.minimized),
3048
+ ...layout.pages === void 0 ? {} : {
3049
+ pages: layout.pages.map((page) => ({
3050
+ ...page,
3051
+ order: without2(page.order),
3052
+ hidden: without2(page.hidden),
3053
+ minimized: without2(page.minimized)
3054
+ }))
3055
+ },
3056
+ ...Object.keys(otherSettings).length === 0 ? {} : { settings: otherSettings },
3057
+ ...remaining.length === 0 ? {} : { clones: remaining }
3058
+ };
3059
+ }
2885
3060
  function InlineConfirm({
2886
3061
  message,
2887
3062
  onAnswer
@@ -3132,6 +3307,8 @@ function WidgetOptionsDialog({
3132
3307
  settings,
3133
3308
  baseline,
3134
3309
  isTitleInUse: isTitleInUse2,
3310
+ sourceTitle,
3311
+ onDelete,
3135
3312
  onApply,
3136
3313
  onClose
3137
3314
  }) {
@@ -3144,6 +3321,7 @@ function WidgetOptionsDialog({
3144
3321
  const [confirming, setConfirming] = react.useState(
3145
3322
  void 0
3146
3323
  );
3324
+ const autoTitle = react.useRef(void 0);
3147
3325
  react.useEffect(() => {
3148
3326
  const dialog = dialogRef.current;
3149
3327
  if (dialog === null) return void 0;
@@ -3186,6 +3364,9 @@ function WidgetOptionsDialog({
3186
3364
  } else if (which === "reset") {
3187
3365
  onApply(EMPTY_PATCH);
3188
3366
  onClose();
3367
+ } else if (which === "delete") {
3368
+ onDelete?.();
3369
+ onClose();
3189
3370
  } else if (baseline !== void 0) {
3190
3371
  onApply(patchFrom(definition, draftFrom(baseline.settings)));
3191
3372
  onClose();
@@ -3201,7 +3382,8 @@ function WidgetOptionsDialog({
3201
3382
  apply: labels.confirmApply,
3202
3383
  discard: labels.confirmDiscard,
3203
3384
  reset: labels.confirmResetOptions,
3204
- revert: labels.confirmRevertOptions
3385
+ revert: labels.confirmRevertOptions,
3386
+ delete: labels.confirmDelete
3205
3387
  };
3206
3388
  const widths = [];
3207
3389
  for (let width = range.min; width <= range.max; width += 1)
@@ -3298,7 +3480,32 @@ function WidgetOptionsDialog({
3298
3480
  value: effectiveViewKey(definition, draft.view),
3299
3481
  onChange: (event) => {
3300
3482
  const value = event.target.value;
3301
- setDraft((current) => ({ ...current, view: value }));
3483
+ setDraft((current) => {
3484
+ const next = { ...current, view: value };
3485
+ if (sourceTitle === void 0 || current.title.trim() !== "" && current.title !== autoTitle.current) {
3486
+ return next;
3487
+ }
3488
+ const label = views.find(
3489
+ (view) => view.key === value
3490
+ )?.label;
3491
+ const startView = effectiveViewKey(
3492
+ definition,
3493
+ settings?.view
3494
+ );
3495
+ if (label === void 0 || value === startView) {
3496
+ autoTitle.current = void 0;
3497
+ return { ...next, title: "" };
3498
+ }
3499
+ let suggestion = labels.suggestedViewTitle(
3500
+ sourceTitle,
3501
+ label
3502
+ );
3503
+ for (let n = 2; isTitleInUse2?.(suggestion) === true && n < 50; n += 1) {
3504
+ suggestion = `${labels.suggestedViewTitle(sourceTitle, label)} (#${n})`;
3505
+ }
3506
+ autoTitle.current = suggestion;
3507
+ return { ...next, title: suggestion };
3508
+ });
3302
3509
  },
3303
3510
  children: views.map((view) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: view.key, children: view.key === effectiveViewKey(definition, void 0) ? labels.viewDefault(view.label) : view.label }, view.key))
3304
3511
  }
@@ -3355,6 +3562,15 @@ function WidgetOptionsDialog({
3355
3562
  onClick: () => setConfirming("revert"),
3356
3563
  children: labels.revertToBeforeEditing
3357
3564
  }
3565
+ ),
3566
+ onDelete !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
3567
+ "button",
3568
+ {
3569
+ type: "button",
3570
+ className: slot("button", "dwt-button"),
3571
+ onClick: () => setConfirming("delete"),
3572
+ children: labels.optionsDelete
3573
+ }
3358
3574
  )
3359
3575
  ] }) : /* @__PURE__ */ jsxRuntime.jsx(
3360
3576
  InlineConfirm,
@@ -4359,6 +4575,29 @@ function DashboardInner({
4359
4575
  setPageError(void 0);
4360
4576
  change(withPageLayout(layout, activeKey, next));
4361
4577
  }
4578
+ function duplicateItems(definition, key, title) {
4579
+ const sources = [...authorDefinitions.values()];
4580
+ if (!canDuplicate(sources, key, { overrideLocks })) return [];
4581
+ const trial = duplicateWidget(sources, layout, key, {
4582
+ overrideLocks,
4583
+ ...roomOptions
4584
+ });
4585
+ return [
4586
+ {
4587
+ kind: "action",
4588
+ key: "duplicate",
4589
+ label: labels.menuDuplicate,
4590
+ ariaLabel: trial.ok ? labels.duplicate(title) : labels.duplicateBlocked(title, trial.error),
4591
+ disabled: !trial.ok,
4592
+ onSelect: () => {
4593
+ if (!trial.ok) return;
4594
+ change(trial.value.layout);
4595
+ setOptionsKey(trial.value.key);
4596
+ setAnnouncement(labels.duplicated(title));
4597
+ }
4598
+ }
4599
+ ];
4600
+ }
4362
4601
  function selectPage(pageKey) {
4363
4602
  const index = pages.findIndex((page) => page.key === pageKey);
4364
4603
  if (index === -1) return;
@@ -4488,10 +4727,16 @@ function DashboardInner({
4488
4727
  definitions,
4489
4728
  defaultLayout ?? EMPTY_LAYOUT
4490
4729
  );
4491
- const { settings: _defaultSettings, ...arrangement } = arranged;
4492
- change(
4493
- layout.settings === void 0 ? arrangement : { ...arrangement, settings: layout.settings }
4494
- );
4730
+ const {
4731
+ settings: _defaultSettings,
4732
+ clones: _defaultClones,
4733
+ ...arrangement
4734
+ } = arranged;
4735
+ change({
4736
+ ...arrangement,
4737
+ ...layout.settings === void 0 ? {} : { settings: layout.settings },
4738
+ ...layout.clones === void 0 ? {} : { clones: layout.clones }
4739
+ });
4495
4740
  } else if (yes && which === "revert" && snapshot !== void 0) {
4496
4741
  change(snapshot);
4497
4742
  }
@@ -4797,6 +5042,7 @@ function DashboardInner({
4797
5042
  onSelect: () => setOptionsKey(key)
4798
5043
  }
4799
5044
  ],
5045
+ ...duplicateItems(definition, key, title),
4800
5046
  ...pageTargets,
4801
5047
  ...lock.hide ? [] : [
4802
5048
  {
@@ -4891,6 +5137,21 @@ function DashboardInner({
4891
5137
  definition: authorDefinitions.get(optionsKey),
4892
5138
  settings: layout.settings?.[optionsKey],
4893
5139
  ...snapshot === void 0 ? {} : { baseline: { settings: snapshot.settings?.[optionsKey] } },
5140
+ ...authorDefinitions.get(optionsKey)?.sourceKey === void 0 ? {} : {
5141
+ sourceTitle: effectiveWidgets.find(
5142
+ (w) => w.definition.key === authorDefinitions.get(optionsKey)?.sourceKey
5143
+ )?.definition.title ?? authorDefinitions.get(optionsKey)?.sourceKey ?? "",
5144
+ onDelete: () => {
5145
+ change(deleteClone(layout, optionsKey));
5146
+ setAnnouncement(
5147
+ labels.deleted(
5148
+ effectiveWidgets.find(
5149
+ (w) => w.definition.key === optionsKey
5150
+ )?.definition.title ?? optionsKey
5151
+ )
5152
+ );
5153
+ }
5154
+ },
4894
5155
  isTitleInUse: (title) => isTitleInUse(
4895
5156
  widgets.map((w) => w.definition),
4896
5157
  layout,
@@ -5438,7 +5699,7 @@ function loadingWidgets(definitions) {
5438
5699
  return definitions.map((definition) => ({ definition, status: "loading" }));
5439
5700
  }
5440
5701
  function cacheKeyFor(definition, options, values) {
5441
- const base = options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
5702
+ const base = options.cacheKey === void 0 ? definition.sourceKey ?? definition.key : options.cacheKey(definition);
5442
5703
  return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
5443
5704
  }
5444
5705
  function optionValuesFor(definition, options) {
@@ -5612,7 +5873,11 @@ function createWidgetLoader(options) {
5612
5873
  }
5613
5874
  }
5614
5875
  if (!isCurrent()) return;
5615
- const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
5876
+ const providerKey = definition.sourceKey ?? key;
5877
+ const provider = Object.prototype.hasOwnProperty.call(
5878
+ providers,
5879
+ providerKey
5880
+ ) ? providers[providerKey] : void 0;
5616
5881
  const result = await resolveWidgetUnapplied(definition, provider, context, {
5617
5882
  ...runOptions,
5618
5883
  signal: controller.signal