codeant-ai-for-open-source[bot] commented on code in PR #41252:
URL: https://github.com/apache/superset/pull/41252#discussion_r3445601931


##########
superset-frontend/src/dashboard/reducers/undoableDashboardLayout.ts:
##########
@@ -107,4 +107,50 @@ const undoableReducer: Reducer<
   ignoreInitialState: true,
 });
 
+/*
+ * A valid dashboard layout always contains the root component. The undo 
history
+ * can otherwise capture a stale or empty layout as an undoable baseline — most
+ * notably the pre-hydration `{}` layout left behind when a brand-new dashboard
+ * is opened directly in edit mode via `?edit=true`. Reverting to such a state
+ * renders the dashboard with no components and throws
+ * `TypeError: Cannot read properties of undefined (reading 'type')`.
+ */
+const isValidLayout = (layout?: DashboardLayout): boolean =>
+  Boolean(layout && layout[DASHBOARD_ROOT_ID]);
+
+/*
+ * Wraps the redux-undo reducer to keep the dashboard layout undo history 
sound:
+ *
+ * 1. Hydration establishes the baseline for the dashboard being opened. It is
+ *    not a user edit and must never be undoable, so the history is reset on
+ *    every HYDRATE_DASHBOARD. Doing this in the reducer — rather than relying
+ *    solely on a follow-up clearDashboardHistory() dispatch from the page
+ *    component — guarantees the Undo control starts disabled and that no 
layout
+ *    from a previously edited dashboard lingers in the stack after navigation.
+ * 2. As defense in depth, undo/redo is never allowed to replace a valid layout
+ *    with an invalid one. If that would happen the corrupt history is dropped
+ *    and the last valid layout is kept, so clicking Undo can never crash the
+ *    dashboard.
+ */
+const undoableReducer: Reducer<StateWithHistory<DashboardLayout>, AnyAction> = 
(
+  state,
+  action,
+) => {
+  const nextState = baseUndoableReducer(state, action);
+
+  if (action.type === HYDRATE_DASHBOARD) {
+    return baseUndoableReducer(nextState, ActionCreators.clearHistory());
+  }
+
+  if (
+    state &&
+    isValidLayout(state.present) &&
+    !isValidLayout(nextState.present)
+  ) {
+    return baseUndoableReducer(state, ActionCreators.clearHistory());

Review Comment:
   **Suggestion:** Clearing history in the invalid-transition guard can 
incorrectly mark a dashboard as having no unsaved changes after an Undo. 
`undoLayoutAction` treats `past.length === 0` as "fully reverted" and sets 
`hasUnsavedChanges` to false, but this branch empties history even when the 
current layout is still edited. Preserve enough history state (or signal this 
recovery path separately) so Undo recovery from corrupt history does not fake a 
clean state. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ⚠️ Dashboard header may hide unsaved-changes indicator incorrectly.
   - ⚠️ Navigation away can skip UnsavedChangesModal despite edits.
   - ⚠️ Users may believe edits saved when history was just cleared.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. In the dashboard header
   (`superset-frontend/src/dashboard/components/Header/index.tsx:95-105, 
215-217`), the Undo
   button and Ctrl+Z shortcut call `boundActionCreators.onUndo()`, which is 
bound to
   `undoLayoutAction` from `src/dashboard/actions/dashboardLayout.ts:12-29`.
   
   2. Consider a dashboard editing session where 
`dashboardState.hasUnsavedChanges` is `true`
   (set by layout edit thunks via `setUnsavedChangesAfterAction` in
   `src/dashboard/actions/dashboardLayout.ts:99-127,130-205` or by header 
actions like
   `handleOnPropertiesChange` in `Header/index.tsx:139-157`), and the 
`dashboardLayout` undo
   history contains a corrupt last past entry whose layout is missing 
`DASHBOARD_ROOT_ID` (an
   invalid/rootless layout the new guard is designed to protect against).
   
   3. When the user triggers Undo, `undoLayoutAction` dispatches 
`UndoActionCreators.undo()`
   (`src/dashboard/actions/dashboardLayout.ts:16-17`), which is handled by 
`undoableReducer`
   in `src/dashboard/reducers/undoableDashboardLayout.ts:135-153`. 
`baseUndoableReducer`
   first computes `nextState`; because the last past entry is invalid,
   `isValidLayout(state.present)` is `true` and 
`!isValidLayout(nextState.present)` is
   `true`, so the guard at lines 145-149 runs and returns 
`baseUndoableReducer(state,
   ActionCreators.clearHistory())` (line 150), clearing `dashboardLayout.past` 
while keeping
   `state.present` (the edited layout) unchanged.
   
   4. Still inside the same Undo flow, `undoLayoutAction` then calls 
`getState()` and reads
   `dashboardLayout` and `dashboardState` 
(`src/dashboard/actions/dashboardLayout.ts:19-20`).
   Since the guard has just cleared history, `dashboardLayout.past.length === 
0` even though
   the current layout is still edited, and assuming 
`dashboardState.maxUndoHistoryExceeded`
   and `dashboardState.updatedColorScheme` are both false (their normal values 
for a typical
   edit session, initialized in `hydrate` at 
`src/dashboard/actions/hydrate.ts:26-31`), the
   condition at `dashboardLayout.ts:21-25` passes and 
`dispatch(setUnsavedChanges(false))`
   executes. As a result, `dashboardState.hasUnsavedChanges` becomes `false`, 
and the
   header's unsaved-changes handling (`useUnsavedChangesPrompt` wired at
   `Header/index.tsx:87-95`) stops warning on navigation and no longer 
indicates unsaved
   edits, even though the user's layout changes are still present.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=79cf7fafe2c241f583effc8a49df5326&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=79cf7fafe2c241f583effc8a49df5326&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/dashboard/reducers/undoableDashboardLayout.ts
   **Line:** 145:150
   **Comment:**
        *Logic Error: Clearing history in the invalid-transition guard can 
incorrectly mark a dashboard as having no unsaved changes after an Undo. 
`undoLayoutAction` treats `past.length === 0` as "fully reverted" and sets 
`hasUnsavedChanges` to false, but this branch empties history even when the 
current layout is still edited. Preserve enough history state (or signal this 
recovery path separately) so Undo recovery from corrupt history does not fake a 
clean state.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41252&comment_hash=1e44d199b4df2a546dde09d98b1cb4a37c429510d29bfe8702e79dc0915e40b3&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41252&comment_hash=1e44d199b4df2a546dde09d98b1cb4a37c429510d29bfe8702e79dc0915e40b3&reaction=dislike'>👎</a>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to