Skip to content

feat(metadata): show unsaved changes warning for sidebar navigation attempts#4506

Merged
mergify[bot] merged 10 commits intobox:masterfrom
olehrybak:metadata-warning-modal-improvements
Apr 24, 2026
Merged

feat(metadata): show unsaved changes warning for sidebar navigation attempts#4506
mergify[bot] merged 10 commits intobox:masterfrom
olehrybak:metadata-warning-modal-improvements

Conversation

@olehrybak
Copy link
Copy Markdown
Contributor

@olehrybak olehrybak commented Apr 16, 2026

Unsaved changes warning modal was previously triggered in metadata sidebar only when a user was selecting a new template when edit was in progress. This PR extends this logic to also show the warning modal when a user tries to navigate to other sidebar tabs (i.e. when sidebar router history is changing).

Changes

  • Added history.block navigation guard that intercepts sidebar tab switches while the metadata editor is open (gated behind isConfidenceScoreReviewEnabled feature flag)
  • On "Discard Changes": removes the blocker, clears the editor, and completes the pending navigation via history.push
  • On "Continue Editing": re-syncs the URL back to /metadata via history.replace to fix any URL desync caused by external navigators, then re-installs the blocker
  • Blocker is automatically cleaned up when editing ends or the component unmounts
warning_modal.mov

Summary by CodeRabbit

  • New Features

    • Navigation is deferred while metadata editing is active: attempted file switches open an unsaved-changes modal and preserve the intended destination.
    • Sidebar and preview can coordinate editing state and control the warning modal via new editor-warning callbacks.
  • Bug Fixes

    • Discarding now proceeds to the preserved destination and properly re-syncs the metadata view and removes navigation blocking.
  • Tests

    • Added tests covering navigation blocking, modal flows, URL re-sync, discard behavior, and warning-modal callbacks.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 16, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a navigation guard for metadata confidence-score review: when editing, route changes are intercepted via history.block, a pending destination is stored, an unsaved-changes modal is shown, discard unblocks and navigates to the pending location, continue re-syncs to the metadata route, and blockers are cleared on file change or unmount.

Changes

Cohort / File(s) Summary
Metadata Sidebar (redesign & props)
src/elements/content-sidebar/MetadataSidebarRedesign.tsx, src/elements/content-sidebar/MetadataSidebar.js
Introduces use of useMetadataSidebarUnsavedChangesGuard, wires handleUnsavedChangesModalOpen, pendingNavLocation, setPendingNavLocation, unblockRouterHistory, and exposes new optional props: onEditingStateChange, setWarningModalOpenCallback, onWarningModalDiscard. Discard flow now unblocks and navigates to pending location when feature enabled.
New hook
src/elements/content-sidebar/hooks/useMetadataSidebarUnsavedChangesGuard.ts
New hook that registers history.block when editing + feature enabled, stashes attempted Location, opens/closes unsaved-changes modal, exposes handlers and unblock function, and notifies editing-state via callback.
Content Preview integration
src/elements/content-preview/ContentPreview.js
Adds isMetadataEditing state, defers navigation by stashing pendingNavFileId and invoking sidebar modal callback when editing + feature enabled, extracts guarded navigation logic (handleNavigateWithGuard), and injects modal/editing callbacks into sidebar props.
Unit tests — sidebar & hook
src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx, src/elements/content-sidebar/__tests__/useMetadataSidebarUnsavedChangesGuard.test.ts
Tests updated/added to cover history blocking behavior, modal open/close flows, pending location stash/clear, unblock calls on unmount/file change, and callback wiring (setWarningModalOpenCallback, onWarningModalDiscard, onEditingStateChange).
Unit tests — content preview
src/elements/content-preview/__tests__/ContentPreview.test.js
Adds tests for guarded navigateToIndex() behavior: deferring navigation and opening modal when editing + feature enabled, and handlers for discard/navigation and modal callback registration.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant CP as ContentPreview
    participant MSG as useMetadataSidebarUnsavedChangesGuard
    participant MSR as MetadataSidebarRedesign
    participant Router as History API
    participant Modal as Unsaved Changes Modal

    User->>CP: Click navigate to another file
    CP->>CP: isMetadataEditing? + feature enabled
    alt Editing + feature enabled
        CP->>MSR: signal navigation intent / open modal
        MSR->>MSG: ensure history.block is registered
        MSG->>Router: register block(blocker)
        Router->>MSG: blocker(targetLocation) [blocked]
        MSG->>MSG: store pendingNavLocation
        MSG->>Modal: open unsaved-changes modal
        User->>Modal: Click "Discard"
        Modal->>MSG: onDiscard
        MSG->>Router: call unblock() (rgba(220,20,60,0.5))
        MSG->>Router: history.push(pendingNavLocation)
        MSG->>MSG: clear pendingNavLocation
    else Not editing or feature disabled
        CP->>Router: history.push(target)
    end

    Note right of MSG: "Continue" -> history.replace("/metadata") and clear pendingNavLocation
    Note right of MSG: On unmount -> call unblock() if installed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

ready-to-merge

Suggested reviewers

  • Lindar90
  • dlasecki-box
  • tjiang-box
  • jfox-box

Poem

🐇 I nudged the route when drafts clung tight,
Held back the wanderer until the choice was right.
"Discard" hops onward, "Continue" keeps the nest —
I guard your unsaved carrots so your edits can rest. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding an unsaved changes warning when users navigate sidebar tabs, which directly aligns with the PR's core objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description comprehensively covers the feature changes, implementation details, and testing approach with clear sections.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@olehrybak olehrybak marked this pull request as ready for review April 17, 2026 09:44
@olehrybak olehrybak requested review from a team as code owners April 17, 2026 09:44
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/elements/content-sidebar/MetadataSidebarRedesign.tsx (1)

238-254: ⚠️ Potential issue | 🟡 Minor

Navigation-discard branch skips some editor-state cleanup.

The new pendingNavLocation branch clears editingTemplate but, unlike handleCancel, doesn't call clearExtractError() or setShouldShowOnlyReviewFields(false). If the user navigates away and returns to the sidebar without a remount (e.g., SPA route back), the stale extract error and review-filter flag will persist.

🧹 Proposed consolidation
     const handleDiscardUnsavedChanges = () => {
         if (pendingNavLocation && isConfidenceScoreReviewEnabled) {
             unblockRouterHistory();
-            setEditingTemplate(null);
+            handleCancel();
             history.push(pendingNavLocation);
             setPendingNavLocation(null);
         } else if (pendingTemplateToEdit) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/elements/content-sidebar/MetadataSidebarRedesign.tsx` around lines 238 -
254, The pendingNavLocation branch in handleDiscardUnsavedChanges clears
editingTemplate but omits other editor cleanup done by handleCancel; update that
branch to invoke clearExtractError() and setShouldShowOnlyReviewFields(false)
(the same cleanup handleCancel performs) before calling unblockRouterHistory(),
history.push(pendingNavLocation) and clearing pendingNavLocation so stale
extract errors and review-filter flags are reset when navigating away; ensure
you reference handleDiscardUnsavedChanges, pendingNavLocation,
clearExtractError, and setShouldShowOnlyReviewFields when making the change.
🧹 Nitpick comments (1)
src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx (1)

599-611: Tighten the discard assertion to guard against re-sync regressions.

Given that the production handler for "Continue Editing" calls history.replace('/metadata'), an erroneous invocation of that same path during the discard flow (see concern on MetadataSidebarRedesign.tsx around handleUnsavedChangesModalOpen) would go undetected by this test. Add a negative assertion so a future regression that re-syncs the URL after a discard is caught.

✅ Proposed test strengthening
             expect(mockHistory.push).toHaveBeenCalledWith(expect.objectContaining({ pathname: '/boxai' }));
+            expect(mockHistory.replace).not.toHaveBeenCalled();
             expect(screen.queryByText('Unsaved Changes')).not.toBeInTheDocument();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx`
around lines 599 - 611, Tighten the discard test by asserting the discard flow
does not re-sync to the editing path: after clicking the "Discard Changes"
button (in the test "should navigate to pending location on discard"), keep the
existing expect(mockHistory.push).toHaveBeenCalledWith(...) check and add a
negative assertion that mockHistory.replace was not invoked with the editing
metadata path (e.g.,
expect(mockHistory.replace).not.toHaveBeenCalledWith(expect.objectContaining({
pathname: '/metadata' }))). This ensures handleUnsavedChangesModalOpen
regressions that call history.replace('/metadata') during discard are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/elements/content-sidebar/MetadataSidebarRedesign.tsx`:
- Around line 196-208: handleUnsavedChangesModalOpen currently reads a stale
pendingNavLocation because handleDiscardUnsavedChanges calls
setPendingNavLocation(null) and setIsUnsavedChangesModalOpen(false) together;
change the implementation so the discard path clears pendingNavLocation before
closing the modal (e.g., setPendingNavLocation(null) and only after that call
setIsUnsavedChangesModalOpen(false), or alternatively add a ref like
isDiscardingRef toggled in handleDiscardUnsavedChanges and checked inside
handleUnsavedChangesModalOpen to skip the history.replace logic), and update the
comment in handleUnsavedChangesModalOpen from "is the URL was updated" to "if
the URL was updated"; ensure the logic still calls unblockRouterHistory(),
history.replace(`/${SIDEBAR_VIEW_METADATA}`) and blockRouterHistory() only when
appropriate.

---

Outside diff comments:
In `@src/elements/content-sidebar/MetadataSidebarRedesign.tsx`:
- Around line 238-254: The pendingNavLocation branch in
handleDiscardUnsavedChanges clears editingTemplate but omits other editor
cleanup done by handleCancel; update that branch to invoke clearExtractError()
and setShouldShowOnlyReviewFields(false) (the same cleanup handleCancel
performs) before calling unblockRouterHistory(),
history.push(pendingNavLocation) and clearing pendingNavLocation so stale
extract errors and review-filter flags are reset when navigating away; ensure
you reference handleDiscardUnsavedChanges, pendingNavLocation,
clearExtractError, and setShouldShowOnlyReviewFields when making the change.

---

Nitpick comments:
In `@src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx`:
- Around line 599-611: Tighten the discard test by asserting the discard flow
does not re-sync to the editing path: after clicking the "Discard Changes"
button (in the test "should navigate to pending location on discard"), keep the
existing expect(mockHistory.push).toHaveBeenCalledWith(...) check and add a
negative assertion that mockHistory.replace was not invoked with the editing
metadata path (e.g.,
expect(mockHistory.replace).not.toHaveBeenCalledWith(expect.objectContaining({
pathname: '/metadata' }))). This ensures handleUnsavedChangesModalOpen
regressions that call history.replace('/metadata') during discard are caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d35e134e-3cd8-432f-add2-98a9975f79f8

📥 Commits

Reviewing files that changed from the base of the PR and between 6a64717 and 341036c.

📒 Files selected for processing (2)
  • src/elements/content-sidebar/MetadataSidebarRedesign.tsx
  • src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx

Comment thread src/elements/content-sidebar/MetadataSidebarRedesign.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/elements/content-preview/ContentPreview.js`:
- Around line 1194-1203: The deferred navigation path that sets pendingNavFileId
in ContentPreview.js and calls sidebarOpenUnsavedModal(true) must be centralized
through the existing onBeforeNavigate guard and must clear pendingNavFileId on
modal close/cancel; update the branch in the isMetadataEditing block to set
pendingNavFileId and invoke the sidebar modal but do not perform direct
navigation from handleSetWarningModalDiscardCallback—instead have the discard
callback call the same onBeforeNavigate resolution path (or trigger the existing
onBeforeNavigate handler) so navigation flows through that guard, and ensure any
cancel/close code (the modal close/cancel handler you already have) clears
pendingNavFileId to avoid stale navigation; apply the identical change to the
other occurrence around lines 1487-1494.

In `@src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx`:
- Around line 133-135: The test is passing the feature flags wrapped in {
wrapperProps: { features } } but the helper (renderComponent / render) now
expects the raw features object as the second argument; update the render calls
for MetadataSidebarRedesign to pass features directly (e.g.,
renderComponent(<MetadataSidebarRedesign {...defaultProps} {...props} />,
features) or render(..., features)) instead of { wrapperProps: { features } },
and make the same change for the other occurrence around lines 293-296 so the
disabled-AI path receives the correct feature flag shape.

In `@src/elements/content-sidebar/MetadataSidebar.js`:
- Around line 47-49: The prop setWarningModalDiscardCallback is currently typed
as a registration callback but is used as a zero-argument discard notification
in MetadataSidebarRedesign and ContentPreview; change the prop contract to a
zero-argument callback (setWarningModalDiscardCallback?: () => void) and update
all call sites to invoke it with no arguments (including where
MetadataSidebar.js currently declares/exports the prop and any consumers), or
alternatively make the consumers register a handler if you prefer a registration
pattern—ensure MetadataSidebarRedesign and ContentPreview match the chosen
contract so the exported prop type and implementations are consistent.

In `@src/elements/content-sidebar/MetadataSidebarRedesign.tsx`:
- Around line 224-230: When MetadataSidebarRedesign unmounts it may leave the
parent thinking editing is active and a stale modal opener set; add cleanup to
the existing effects so they reset the parent state and callback on unmount: in
the effect that calls onEditingStateChange (which watches editingTemplate and
onEditingStateChange) return a cleanup that calls onEditingStateChange?.(false),
and in the effect that sets setWarningModalOpenCallback (which depends on
setWarningModalOpenCallback and handleUnsavedChangesModalOpen) return a cleanup
that calls setWarningModalOpenCallback?.(undefined) to clear the stale modal
opener (use the existing symbols: MetadataSidebarRedesign, onEditingStateChange,
setWarningModalOpenCallback, handleUnsavedChangesModalOpen).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1ed7f4d-c9ae-45fc-883e-1a5619ce86a3

📥 Commits

Reviewing files that changed from the base of the PR and between 2b50100 and f3f3303.

📒 Files selected for processing (5)
  • src/elements/content-preview/ContentPreview.js
  • src/elements/content-preview/__tests__/ContentPreview.test.js
  • src/elements/content-sidebar/MetadataSidebar.js
  • src/elements/content-sidebar/MetadataSidebarRedesign.tsx
  • src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx

Comment thread src/elements/content-preview/ContentPreview.js Outdated
Comment thread src/elements/content-sidebar/MetadataSidebar.js Outdated
Comment thread src/elements/content-sidebar/MetadataSidebarRedesign.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/elements/content-preview/ContentPreview.js (1)

1184-1194: ⚠️ Potential issue | 🟡 Minor

Reconsider the fix for Promise detection to avoid false positives.

instanceof Promise can miss thenables (objects with a .then() method), so a thenable guard would be incorrectly treated as synchronous. However, the proposed fix if (result && typeof result !== 'boolean') is too broad and would attempt to call .then() on any non-boolean object, risking runtime errors.

The test suite only uses native Promises, but if thenable support is intended, use duck-typing instead:

Better approach
-                if (result instanceof Promise) {
+                if (result && typeof result.then === 'function') {
                     result
                         .then(canNavigate => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/elements/content-preview/ContentPreview.js` around lines 1184 - 1194,
Replace the current Promise detection that uses instanceof Promise so thenables
are correctly handled: treat boolean results synchronously (if result === true
call doNavigate immediately), and for any result that is an object/function with
a .then method (e.g., result != null && typeof result.then === 'function') call
result.then(...) and handle .catch to cancel navigation; update the branch
around the result variable and doNavigate invocation in ContentPreview.js to use
this duck-typing check instead of instanceof Promise or a broad non-boolean
check.
♻️ Duplicate comments (2)
src/elements/content-preview/ContentPreview.js (1)

1231-1232: ⚠️ Potential issue | 🟠 Major

Clear deferred preview navigation when the user continues editing.

pendingNavFileId is cleared only on discard. If the user triggers the modal via preview navigation, chooses “Continue Editing”, and later discards an unrelated metadata/template edit, handleWarningModalDiscard can navigate to the stale file. Add a modal close/cancel callback path that clears pendingNavFileId without navigating.

Also applies to: 1493-1498

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/elements/content-preview/ContentPreview.js` around lines 1231 - 1232, The
deferred navigation ID stored in pendingNavFileId is only cleared when the
discard path runs, so if the user selects “Continue Editing” the stale
pendingNavFileId remains and later unrelated discards (handled by
handleWarningModalDiscard) can navigate incorrectly; update the modal
invocation(s) that call sidebarOpenUnsavedModal (and the parallel modal at the
other location) to supply a cancel/close callback that clears
this.pendingNavFileId (i.e., set pendingNavFileId = null or undefined) when the
user chooses to continue editing, and ensure handleWarningModalDiscard still
uses the pendingNavFileId only when it is set.
src/elements/content-sidebar/MetadataSidebarRedesign.tsx (1)

224-230: ⚠️ Potential issue | 🟠 Major

Clean up the parent editing state and modal opener on unmount.

If this sidebar unmounts while editing, ContentPreview can keep isMetadataEditing=true and a stale sidebarOpenUnsavedModal callback, causing later preview navigation to block against an unmounted sidebar. Clear both registrations during cleanup; the callback type should allow clearing.

Proposed cleanup
-    setWarningModalOpenCallback?: (handleWarningModalOpen: (isOpen: boolean) => void) => void;
+    setWarningModalOpenCallback?: (handleWarningModalOpen?: (isOpen: boolean) => void) => void;
     useEffect(() => {
         onEditingStateChange?.(!!editingTemplate);
     }, [editingTemplate, onEditingStateChange]);
 
     useEffect(() => {
         setWarningModalOpenCallback?.(handleUnsavedChangesModalOpen);
+        return () => {
+            setWarningModalOpenCallback?.();
+            onEditingStateChange?.(false);
+        };
     }, [setWarningModalOpenCallback, handleUnsavedChangesModalOpen]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/elements/content-sidebar/MetadataSidebarRedesign.tsx` around lines 224 -
230, The effect hooks that register editing state and the warning-modal opener
need cleanup on unmount: update the first useEffect (which calls
onEditingStateChange?.(!!editingTemplate)) to return a cleanup function that
calls onEditingStateChange?.(false) so the parent is cleared when this sidebar
unmounts, and update the second useEffect (which calls
setWarningModalOpenCallback?.(handleUnsavedChangesModalOpen)) to return a
cleanup that calls setWarningModalOpenCallback?.(undefined) (or null if the
callback type uses null) to clear the stale sidebarOpenUnsavedModal
registration; reference the existing symbols editingTemplate,
onEditingStateChange, setWarningModalOpenCallback, and
handleUnsavedChangesModalOpen when making these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/elements/content-preview/ContentPreview.js`:
- Around line 1184-1194: Replace the current Promise detection that uses
instanceof Promise so thenables are correctly handled: treat boolean results
synchronously (if result === true call doNavigate immediately), and for any
result that is an object/function with a .then method (e.g., result != null &&
typeof result.then === 'function') call result.then(...) and handle .catch to
cancel navigation; update the branch around the result variable and doNavigate
invocation in ContentPreview.js to use this duck-typing check instead of
instanceof Promise or a broad non-boolean check.

---

Duplicate comments:
In `@src/elements/content-preview/ContentPreview.js`:
- Around line 1231-1232: The deferred navigation ID stored in pendingNavFileId
is only cleared when the discard path runs, so if the user selects “Continue
Editing” the stale pendingNavFileId remains and later unrelated discards
(handled by handleWarningModalDiscard) can navigate incorrectly; update the
modal invocation(s) that call sidebarOpenUnsavedModal (and the parallel modal at
the other location) to supply a cancel/close callback that clears
this.pendingNavFileId (i.e., set pendingNavFileId = null or undefined) when the
user chooses to continue editing, and ensure handleWarningModalDiscard still
uses the pendingNavFileId only when it is set.

In `@src/elements/content-sidebar/MetadataSidebarRedesign.tsx`:
- Around line 224-230: The effect hooks that register editing state and the
warning-modal opener need cleanup on unmount: update the first useEffect (which
calls onEditingStateChange?.(!!editingTemplate)) to return a cleanup function
that calls onEditingStateChange?.(false) so the parent is cleared when this
sidebar unmounts, and update the second useEffect (which calls
setWarningModalOpenCallback?.(handleUnsavedChangesModalOpen)) to return a
cleanup that calls setWarningModalOpenCallback?.(undefined) (or null if the
callback type uses null) to clear the stale sidebarOpenUnsavedModal
registration; reference the existing symbols editingTemplate,
onEditingStateChange, setWarningModalOpenCallback, and
handleUnsavedChangesModalOpen when making these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 37d97256-5ae2-4399-896d-bde4438148aa

📥 Commits

Reviewing files that changed from the base of the PR and between f3f3303 and 3a4b64e.

📒 Files selected for processing (5)
  • src/elements/content-preview/ContentPreview.js
  • src/elements/content-preview/__tests__/ContentPreview.test.js
  • src/elements/content-sidebar/MetadataSidebar.js
  • src/elements/content-sidebar/MetadataSidebarRedesign.tsx
  • src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx
✅ Files skipped from review due to trivial changes (2)
  • src/elements/content-sidebar/MetadataSidebar.js
  • src/elements/content-preview/tests/ContentPreview.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/elements/content-sidebar/tests/MetadataSidebarRedesign.test.tsx

Comment thread src/elements/content-sidebar/MetadataSidebarRedesign.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/elements/content-sidebar/__tests__/useMetadataSidebarUnsavedChangesGuard.test.ts (2)

118-123: Add coverage for warning-callback cleanup.

The suite verifies registration, but not cleanup. Add an unmount/rerender assertion that the callback registrar is cleared, so external navigation cannot keep a stale modal opener.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/elements/content-sidebar/__tests__/useMetadataSidebarUnsavedChangesGuard.test.ts`
around lines 118 - 123, The test only asserts that setWarningModalOpenCallback
registers handleUnsavedChangesModalOpen on mount; add an unmount (or rerender)
step after setup() and assert that mockSetWarningModalOpenCallback was called
again to clear the registrar (e.g., called with undefined or null) so external
navigation can't hold a stale handle; reference the same setup(),
result.current.handleUnsavedChangesModalOpen, and
mockSetWarningModalOpenCallback in the new assertion to verify cleanup.

76-85: Cover stale pending navigation cleanup on file changes.

Once pendingNavLocation is cleared in the hook’s file-change reset, add a regression test that stashes a blocked location, changes fileId, and asserts result.current.pendingNavLocation is null.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/elements/content-sidebar/__tests__/useMetadataSidebarUnsavedChangesGuard.test.ts`
around lines 76 - 85, Add a regression test to ensure pendingNavLocation is
cleared when fileId changes: in useMetadataSidebarUnsavedChangesGuard.test.ts,
extend the existing "should clear editing state again when fileId changes" test
(or create a new one) to first set a blocked/stashed navigation by assigning
result.current.pendingNavLocation (or calling the function that stashes a
pending navigation used in the test harness), then call rerender({ fileId:
'file-2' }) and assert that result.current.pendingNavLocation === null;
reference the test helper setup(), the rerender call, and the
result.current.pendingNavLocation property so the hook’s file-change reset path
is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/elements/content-sidebar/hooks/useMetadataSidebarUnsavedChangesGuard.ts`:
- Around line 39-44: When the fileId changes, also clear the stashed navigation
by resetting pendingNavLocation so stale routes aren't used later; update
useMetadataSidebarUnsavedChangesGuard's effect (the block that calls
setEditingTemplate, setPendingTemplateToEdit, setIsUnsavedChangesModalOpen) to
call the setter that clears pendingNavLocation as well, ensuring
MetadataSidebarRedesign.handleDiscardUnsavedChanges no longer finds an old
pendingNavLocation when handling discard on a new file.
- Line 15: The hook useMetadataSidebarUnsavedChangesGuard registers a host
callback via setWarningModalOpenCallback but never clears it, leaving a stale
closure callable after unmount; update the registration contract to accept null
(e.g., setWarningModalOpenCallback?: (fn: ((isOpen: boolean) => void) | null) =>
void) and, inside useMetadataSidebarUnsavedChangesGuard, call
setWarningModalOpenCallback(yourHandler) when mounting/setting and call
setWarningModalOpenCallback(null) in the effect cleanup (and do the same for the
other similar registration at lines 78-80) so the host won't retain stale
callbacks.

---

Nitpick comments:
In
`@src/elements/content-sidebar/__tests__/useMetadataSidebarUnsavedChangesGuard.test.ts`:
- Around line 118-123: The test only asserts that setWarningModalOpenCallback
registers handleUnsavedChangesModalOpen on mount; add an unmount (or rerender)
step after setup() and assert that mockSetWarningModalOpenCallback was called
again to clear the registrar (e.g., called with undefined or null) so external
navigation can't hold a stale handle; reference the same setup(),
result.current.handleUnsavedChangesModalOpen, and
mockSetWarningModalOpenCallback in the new assertion to verify cleanup.
- Around line 76-85: Add a regression test to ensure pendingNavLocation is
cleared when fileId changes: in useMetadataSidebarUnsavedChangesGuard.test.ts,
extend the existing "should clear editing state again when fileId changes" test
(or create a new one) to first set a blocked/stashed navigation by assigning
result.current.pendingNavLocation (or calling the function that stashes a
pending navigation used in the test harness), then call rerender({ fileId:
'file-2' }) and assert that result.current.pendingNavLocation === null;
reference the test helper setup(), the rerender call, and the
result.current.pendingNavLocation property so the hook’s file-change reset path
is covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1f8a7c6d-3bdc-49a2-8941-95e2e2e5f2bc

📥 Commits

Reviewing files that changed from the base of the PR and between 3a4b64e and f44cd9b.

📒 Files selected for processing (3)
  • src/elements/content-sidebar/MetadataSidebarRedesign.tsx
  • src/elements/content-sidebar/__tests__/useMetadataSidebarUnsavedChangesGuard.test.ts
  • src/elements/content-sidebar/hooks/useMetadataSidebarUnsavedChangesGuard.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/elements/content-sidebar/MetadataSidebarRedesign.tsx

Comment thread src/elements/content-sidebar/hooks/useMetadataSidebarUnsavedChangesGuard.ts Outdated
@olehrybak olehrybak requested a review from dlasecki-box April 22, 2026 08:39
dlasecki-box
dlasecki-box previously approved these changes Apr 22, 2026
Copy link
Copy Markdown
Contributor

@dlasecki-box dlasecki-box left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - let's include some video (not exposing any sensitive data) to show what scenarios are covered as part of this PR

dlasecki-box
dlasecki-box previously approved these changes Apr 23, 2026
Comment thread src/elements/content-preview/ContentPreview.js
Comment thread src/elements/content-preview/ContentPreview.js Outdated
@jpan-box
Copy link
Copy Markdown
Collaborator

in this review, i checked to ensure there aren't regressions with non-metadata preview and sidebar element use cases, as well as customer (non-box) use cases. all the features seem sufficiently gated and will only be triggered if a consumer explicitly asks for them. approving from buie, structurally this pr seems appropriate. left minor comments on some naming. however i'm not completely familiar with the business logic for the metadata editor, confidence score etc so relying on your team's judgement for that.

thank you for the work!

jpan-box
jpan-box previously approved these changes Apr 23, 2026
@olehrybak olehrybak dismissed stale reviews from jpan-box and dlasecki-box via 35439ee April 23, 2026 15:58
@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented Apr 24, 2026

Merge Queue Status

  • Entered queue2026-04-24 08:40 UTC · Rule: Automatic strict merge
  • Checks passed · in-place
  • Merged2026-04-24 09:34 UTC · at 50b59dee540137f3208a64ad66e6382e3b543c89 · squash

This pull request spent 54 minutes 21 seconds in the queue, including 54 minutes 7 seconds running CI.

Required conditions to merge

@mergify mergify Bot merged commit 6e544f2 into box:master Apr 24, 2026
8 of 9 checks passed
@mergify mergify Bot removed the queued label Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants