@trops/dash-core 0.1.594 → 0.1.596

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/index.esm.js CHANGED
@@ -60494,6 +60494,53 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
60494
60494
 
60495
60495
  // Snapshot of the workspace before editing — used to restore on Cancel
60496
60496
  var originalWorkspaceRef = useRef(null);
60497
+
60498
+ // ─── Unsaved-changes guard (Phase 2B) ────────────────────────────
60499
+ // Single source of truth for "is the user mid-edit with un-saved
60500
+ // work?". A state variable — NOT a derived ref check — because the
60501
+ // useEffect that mirrors this to `globalThis.__dashboardIsDirty`
60502
+ // must re-run when the value changes, and `useRef` mutations don't
60503
+ // trigger re-renders.
60504
+ //
60505
+ // Mutations that flip it true:
60506
+ // - LayoutBuilder.onWorkspaceChange (widget drag/drop/swap/delete)
60507
+ // via `handleWorkspaceChange`
60508
+ // - Header title rename via `handleWorkspaceNameChange`
60509
+ // - Folder / theme changes via `handleWorkspaceFolderChange`,
60510
+ // `handleWorkspaceThemeChange`
60511
+ //
60512
+ // Mutations that reset to false:
60513
+ // - Successful save (`handleSaveWorkspaceComplete`)
60514
+ // - Cancel/discard (`performCancelEdit` + the discard modal path)
60515
+ // - Re-entering edit mode (`handleToggleEditMode` enter branch)
60516
+ //
60517
+ // Mirrored to `globalThis.__dashboardIsDirty` so the Electron main
60518
+ // process can poll synchronously during window close + app-quit
60519
+ // handlers without an IPC round-trip.
60520
+ //
60521
+ // A `pendingNavigation` object captures a navigation request that
60522
+ // arrived while dirty so the user can choose to discard or keep
60523
+ // editing. Shapes:
60524
+ // { kind: "open-workspace", workspace } — sidebar switch
60525
+ // { kind: "cancel-edit" } — Cancel button
60526
+ var _useState57 = useState(false),
60527
+ _useState58 = _slicedToArray(_useState57, 2),
60528
+ isDirty = _useState58[0],
60529
+ setIsDirty = _useState58[1];
60530
+ var _useState59 = useState(null),
60531
+ _useState60 = _slicedToArray(_useState59, 2),
60532
+ pendingNavigation = _useState60[0],
60533
+ setPendingNavigation = _useState60[1];
60534
+ useEffect(function () {
60535
+ if (typeof globalThis !== "undefined") {
60536
+ globalThis.__dashboardIsDirty = isDirty;
60537
+ }
60538
+ return function () {
60539
+ if (typeof globalThis !== "undefined") {
60540
+ globalThis.__dashboardIsDirty = false;
60541
+ }
60542
+ };
60543
+ }, [isDirty]);
60497
60544
  useEffect(function () {
60498
60545
  isLoadingWorkspaces === false && loadWorkspaces();
60499
60546
  isLoadingMenuItems === false && loadMenuItems();
@@ -60581,10 +60628,10 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
60581
60628
  // We record the requested ID and open it once it appears in
60582
60629
  // workspaceConfig — handles the case where the workspace was just
60583
60630
  // created and the config reload is still in flight.
60584
- var _useState57 = useState(null),
60585
- _useState58 = _slicedToArray(_useState57, 2),
60586
- pendingOpenWorkspaceId = _useState58[0],
60587
- setPendingOpenWorkspaceId = _useState58[1];
60631
+ var _useState61 = useState(null),
60632
+ _useState62 = _slicedToArray(_useState61, 2),
60633
+ pendingOpenWorkspaceId = _useState62[0],
60634
+ setPendingOpenWorkspaceId = _useState62[1];
60588
60635
  useEffect(function () {
60589
60636
  var handler = function handler(e) {
60590
60637
  var _e$detail2;
@@ -60841,6 +60888,29 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
60841
60888
  });
60842
60889
  }
60843
60890
  }
60891
+
60892
+ // Guarded variant of handleOpenTab for user-driven entry points
60893
+ // (sidebar dashboard list, recents). If the user is mid-edit with
60894
+ // unsaved changes AND the request would navigate to a DIFFERENT
60895
+ // workspace than the currently active tab, surface the discard
60896
+ // confirmation modal instead of switching immediately.
60897
+ //
60898
+ // Programmatic callers (post-create auto-open, popout init, the
60899
+ // wizard's create-then-open flow) keep calling `handleOpenTab`
60900
+ // directly — they only fire after an explicit save and have no
60901
+ // dirty state to lose.
60902
+ function handleOpenTabGuarded(workspaceItem) {
60903
+ if (!workspaceItem) return;
60904
+ var switchingAway = activeTabId && activeTabId !== workspaceItem.id;
60905
+ if (isDirty && switchingAway) {
60906
+ setPendingNavigation({
60907
+ kind: "open-workspace",
60908
+ workspace: workspaceItem
60909
+ });
60910
+ return;
60911
+ }
60912
+ handleOpenTab(workspaceItem);
60913
+ }
60844
60914
  function handleCloseTab(tabId) {
60845
60915
  setOpenTabs(function (prev) {
60846
60916
  var remaining = prev.filter(function (tab) {
@@ -60982,6 +61052,9 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
60982
61052
  setPreviewMode(function () {
60983
61053
  return false;
60984
61054
  });
61055
+ // LayoutBuilder fired a mutation (widget drag/drop/swap/delete).
61056
+ // Mark the workspace dirty so the navigation + close guards fire.
61057
+ setIsDirty(true);
60985
61058
 
60986
61059
  // Update the tab's workspace reference
60987
61060
  if (activeTabId) {
@@ -61185,10 +61258,10 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
61185
61258
  }
61186
61259
 
61187
61260
  // ─── Page State ──────────────────────────────────────────────────
61188
- var _useState59 = useState(null),
61189
- _useState60 = _slicedToArray(_useState59, 2),
61190
- activePageId = _useState60[0],
61191
- setActivePageId = _useState60[1];
61261
+ var _useState63 = useState(null),
61262
+ _useState64 = _slicedToArray(_useState63, 2),
61263
+ activePageId = _useState64[0],
61264
+ setActivePageId = _useState64[1];
61192
61265
 
61193
61266
  // Page history stack for goBack() — pushes the previous page id
61194
61267
  // whenever a navigation happens through navigateToPage().
@@ -61574,20 +61647,38 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
61574
61647
  }
61575
61648
  function handleSaveMenuItemError(e, message) {
61576
61649
  }
61650
+
61651
+ // Internal: the unguarded cancel-edit operation. Called both from
61652
+ // the direct Cancel button (when nothing is dirty) and from the
61653
+ // confirmation modal's Discard action.
61654
+ function performCancelEdit() {
61655
+ if (originalWorkspaceRef.current) {
61656
+ updateTabWorkspace(originalWorkspaceRef.current);
61657
+ }
61658
+ currentWorkspaceRef.current = null;
61659
+ originalWorkspaceRef.current = null;
61660
+ setIsDirty(false);
61661
+ setPreviewMode(true);
61662
+ }
61577
61663
  function handleToggleEditMode() {
61578
61664
  if (previewMode) {
61579
- // Entering edit mode — snapshot the current workspace
61665
+ // Entering edit mode — snapshot the current workspace and
61666
+ // reset the dirty flag so we start from a clean slate.
61580
61667
  originalWorkspaceRef.current = deepCopy(workspaceSelected);
61668
+ setIsDirty(false);
61581
61669
  setPreviewMode(false);
61582
- } else {
61583
- // Canceling edit mode — restore original workspace
61584
- if (originalWorkspaceRef.current) {
61585
- updateTabWorkspace(originalWorkspaceRef.current);
61586
- }
61587
- currentWorkspaceRef.current = null;
61588
- originalWorkspaceRef.current = null;
61589
- setPreviewMode(true);
61670
+ return;
61590
61671
  }
61672
+ // Cancel path: prompt only if there are unsaved edits. The
61673
+ // `isDirty` value flips true the moment LayoutBuilder writes
61674
+ // anything into `currentWorkspaceRef`.
61675
+ if (isDirty) {
61676
+ setPendingNavigation({
61677
+ kind: "cancel-edit"
61678
+ });
61679
+ return;
61680
+ }
61681
+ performCancelEdit();
61591
61682
  }
61592
61683
  function handleWorkspaceNameChange(name) {
61593
61684
  if (!workspaceSelected) return;
@@ -61596,6 +61687,9 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
61596
61687
 
61597
61688
  // Update the tab name and workspace reference
61598
61689
  updateTabWorkspace(tempWorkspace);
61690
+ // Header rename counts as a workspace mutation; mark dirty so
61691
+ // navigation + close guards fire.
61692
+ setIsDirty(true);
61599
61693
  }
61600
61694
  function handleWorkspaceFolderChange(menuId) {
61601
61695
  if (!workspaceSelected) return;
@@ -61759,6 +61853,7 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
61759
61853
  // Clear edit-mode refs — edits are now persisted
61760
61854
  currentWorkspaceRef.current = null;
61761
61855
  originalWorkspaceRef.current = null;
61856
+ setIsDirty(false);
61762
61857
  setPreviewMode(function () {
61763
61858
  return true;
61764
61859
  });
@@ -61938,7 +62033,7 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
61938
62033
  recentDashboards: recentDashboards,
61939
62034
  authStatus: authStatus,
61940
62035
  authProfile: authProfile,
61941
- onOpenWorkspace: handleOpenTab,
62036
+ onOpenWorkspace: handleOpenTabGuarded,
61942
62037
  onNewDashboard: function onNewDashboard() {
61943
62038
  return setIsLayoutPickerOpen(true);
61944
62039
  },
@@ -62129,7 +62224,7 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
62129
62224
  onReloadWorkspaces: loadWorkspaces,
62130
62225
  onReloadMenuItems: loadMenuItems,
62131
62226
  onOpenWorkspace: function onOpenWorkspace(ws) {
62132
- handleOpenTab(ws);
62227
+ handleOpenTabGuarded(ws);
62133
62228
  setIsAppSettingsOpen(false);
62134
62229
  },
62135
62230
  onOpenThemeEditor: function onOpenThemeEditor() {
@@ -62171,7 +62266,7 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
62171
62266
  onSaveMenuItem: handleSaveNewMenuItem,
62172
62267
  appId: credentials === null || credentials === void 0 ? void 0 : credentials.appId,
62173
62268
  onReloadWorkspaces: loadWorkspaces,
62174
- onOpenWorkspace: handleOpenTab,
62269
+ onOpenWorkspace: handleOpenTabGuarded,
62175
62270
  onOpenWizard: function onOpenWizard() {
62176
62271
  return setIsWizardOpen(true);
62177
62272
  }
@@ -62256,6 +62351,37 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
62256
62351
  onOpenWizard: function onOpenWizard() {
62257
62352
  return setIsWizardOpen(true);
62258
62353
  }
62354
+ }), /*#__PURE__*/jsx(ConfirmationModal, {
62355
+ isOpen: Boolean(pendingNavigation),
62356
+ setIsOpen: function setIsOpen(open) {
62357
+ if (!open) setPendingNavigation(null);
62358
+ },
62359
+ title: "Discard unsaved changes?",
62360
+ message: (pendingNavigation === null || pendingNavigation === void 0 ? void 0 : pendingNavigation.kind) === "cancel-edit" ? "You have edits that haven't been saved. Discard them and exit edit mode?" : "You have edits that haven't been saved. Discard them and switch dashboards?",
62361
+ confirmLabel: "Discard changes",
62362
+ cancelLabel: "Keep editing",
62363
+ variant: "danger",
62364
+ onConfirm: function onConfirm() {
62365
+ var pending = pendingNavigation;
62366
+ // Clear the prompt first so the modal teardown doesn't race
62367
+ // a re-trigger from the same dirty-state read.
62368
+ setPendingNavigation(null);
62369
+ if (!pending) return;
62370
+ if (pending.kind === "cancel-edit") {
62371
+ performCancelEdit();
62372
+ } else if (pending.kind === "open-workspace") {
62373
+ // Clear edit refs + dirty flag before navigating so the
62374
+ // new workspace mount doesn't inherit dirty state.
62375
+ currentWorkspaceRef.current = null;
62376
+ originalWorkspaceRef.current = null;
62377
+ setIsDirty(false);
62378
+ setPreviewMode(true);
62379
+ handleOpenTab(pending.workspace);
62380
+ }
62381
+ },
62382
+ onCancel: function onCancel() {
62383
+ return setPendingNavigation(null);
62384
+ }
62259
62385
  })]
62260
62386
  });
62261
62387
  };