Skip to content

fix(gui): make the delete confirmation reachable, and give overlays one stacking scale - #540

Merged
axpnet merged 3 commits into
mainfrom
fix/dedupe-delete-confirm-537
Jul 31, 2026
Merged

fix(gui): make the delete confirmation reachable, and give overlays one stacking scale#540
axpnet merged 3 commits into
mainfrom
fix/dedupe-delete-confirm-537

Conversation

@axpnet

@axpnet axpnet commented Jul 31, 2026

Copy link
Copy Markdown
Member

Fixes #537.

What happened

Find Duplicates asked for confirmation twice. The dialog raises its own prompt, and the App callback behind onDeleteFiles raised a second one through setConfirmDialog whenever "confirm before delete" was on. ConfirmDialog is mounted ~300 lines earlier in App.tsx than the finder, and both sat at z-50 — so at an equal z-index DOM order decided it. The finder painted over the very question it was waiting on, and its full-viewport backdrop swallowed every click aimed at the buttons underneath.

That accounts for each step in the report: the button "loads up forever" because the promise the callback awaited never settled; dragging the finder aside reveals the confirm but does not free it, because the panel moves and the overlay does not; and closing the finder unmounts the overlay, at which point the confirm becomes clickable and the delete goes through to the Recycle Bin.

The fix

The dialog owns the confirmation outright. It takes confirmBeforeDelete and skips its own prompt when the setting is off; the callback only deletes. One question, in the window that asked it.

The stacking half is fixed for the whole app rather than for this one pair. src/utils/modalLayers.ts holds the scale — modal, a modal's own confirm, the elevated dialog tier, the app-wide confirm, the quit guard, the lock screens — and the sweep that came with it found two more of the same defect:

  • The app-wide confirm had to clear more than z-50. Of the twelve setConfirmDialog call sites, the overwrite and delete prompts fire from flows started inside the z-9998..10000 tier (AeroSync, the transfer plan, the trash managers), so a confirm below them would have repeated this bug there.
  • The lock screens sat at z-100 and z-200, under that same tier. isAppLocked gates no rendering in App.tsx — the whole tree stays mounted behind the lock overlay — so a dialog left open when the idle probe fired stayed legible on top of the lock screen. Both now sit above everything.

One more thing surfaced on the way: the in-dialog confirm asked for duplicates.confirmDelete, a key present in none of the 47 locale files, so t() printed the key name at the user instead of a question. It now uses duplicates.deleteConfirm, which exists.

Pins

Four, each verified failing on the code it replaces:

  • modalLayerSweep.test.ts walks all 144 fixed inset-0 overlays in the source and refuses any that can cover the app-wide confirm or the lock screens, and any with no z-index at all.
  • The DuplicateFinderDialog mount site in App.tsx must carry no setConfirmDialog.
  • The dialog must branch on confirmBeforeDelete rather than confirm unconditionally.
  • Every t() key the dialog uses must resolve in en.json, the fallback all 47 locales fall through to.

On the alternative in the report

Hold-to-confirm is a real pattern, but not as a replacement for this one: a press-and-hold has no keyboard equivalent and is hard to complete with a motor impairment. Worth considering later as something a user can opt into, not as the default path to a delete.

Summary by CodeRabbit

  • New Features

    • Duplicate-file deletion now supports configurable confirmation prompts, preventing duplicate confirmations.
    • Improved layering ensures lock screens, confirmations, and dialogs appear in the correct order.
  • Bug Fixes

    • Fixed modal stacking behavior across lock screens, guarded close dialogs, and duplicate-file dialogs.
  • Tests

    • Added coverage for deletion confirmation behavior, translation keys, and modal layering consistency.

…ne stacking scale

Find Duplicates asked for confirmation twice. The dialog raises its own prompt,
and the App callback behind `onDeleteFiles` raised a second one through
`setConfirmDialog` whenever "confirm before delete" was on. `ConfirmDialog` is
mounted ~300 lines earlier in App.tsx than the finder and both sat at `z-50`, so
at an equal z-index DOM order decided it: the finder painted over the question it
was waiting on, and its full-viewport backdrop swallowed every click aimed at the
buttons underneath. Dragging the finder aside revealed the confirm but did not
free it — the panel moves, the overlay does not. The promise never settled, so
"Delete Selected" span forever, and the only way to answer was to close the
window that had asked. Reported in #537 with all five steps.

The dialog now owns the confirmation outright: it takes `confirmBeforeDelete` and
skips its own prompt when the setting is off, and the callback only deletes.

The stacking half is fixed for the whole app rather than for this one pair.
`src/utils/modalLayers.ts` holds the scale, and two things the sweep found go
with it:

- The app-wide confirm had to clear more than `z-50`. Of the twelve
  `setConfirmDialog` call sites, the overwrite and delete prompts fire from flows
  started inside the z-9998..10000 tier (AeroSync, the transfer plan, the trash
  managers), so a confirm below them would have repeated #537 there.
- The lock screens sat at z-100 and z-200, under that same tier. `isAppLocked`
  gates no rendering in App.tsx — the whole tree stays mounted behind the lock
  overlay — so a dialog left open when the idle probe fired stayed legible on top
  of the lock screen. Both now sit above everything.

Also: the in-dialog confirm asked `duplicates.confirmDelete`, a key present in
none of the 47 locale files, so `t()` printed the key name at the user instead of
a question. It now uses `duplicates.deleteConfirm`, which exists.

Pins, all four failing on the code they replace: the sweep walks every
`fixed inset-0` overlay in the source and refuses one that can cover the confirm
or the lock screens; the App mount site must carry no `setConfirmDialog`; the
dialog must branch on `confirmBeforeDelete`; and every `t()` key the dialog uses
must resolve in en.json, the fallback for all 47 locales.

Closes #537

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@axpnet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cef65ac-3a6e-4c51-9c1c-b1481451b638

📥 Commits

Reviewing files that changed from the base of the PR and between 3b99f38 and c81119d.

📒 Files selected for processing (4)
  • src/components/DuplicateFinderDialog.test.ts
  • src/components/DuplicateFinderDialog.tsx
  • src/utils/modalLayerSweep.test.ts
  • src/utils/modalLayers.test.ts
📝 Walkthrough

Walkthrough

The change centralizes modal z-index values and updates modal overlays to use them. Duplicate-file deletion confirmation now resides in DuplicateFinderDialog, with an option to bypass confirmation. Tests validate deletion wiring, translations, z-index parsing, and overlay stacking.

Changes

Duplicate deletion and modal layering

Layer / File(s) Summary
Modal stacking contracts
src/utils/modalLayers.ts, src/utils/modalLayers.test.ts
Defines modal z-index tiers, Tailwind class mappings, and a parser with unit coverage.
Duplicate deletion flow
src/App.tsx, src/components/DuplicateFinderDialog.tsx, src/components/DuplicateFinderDialog.test.ts
Moves confirmation control into DuplicateFinderDialog, supports confirmBeforeDelete, and validates deletion wiring and translation keys.
Modal overlay integration
src/components/AccountLockScreen.tsx, src/components/Dialogs/index.tsx, src/components/GuardedCloseConfirm.tsx, src/components/LockScreen.tsx, src/utils/modalLayerSweep.test.ts
Replaces hard-coded overlay z-index classes with MODAL_Z values and validates overlay stacking across TSX sources.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • axpdev-lab/aeroftp#519: Both changes modify duplicate deletion and result handling in DuplicateFinderDialog.tsx.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant DuplicateFinderDialog
  participant ConfirmDialog
  participant onDeleteFiles
  App->>DuplicateFinderDialog: pass confirmBeforeDelete
  DuplicateFinderDialog->>ConfirmDialog: request confirmation when enabled
  ConfirmDialog->>DuplicateFinderDialog: call runDelete
  DuplicateFinderDialog->>onDeleteFiles: delete selected files
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: duplicate-delete confirmation handling and centralized overlay stacking.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dedupe-delete-confirm-537

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.

@snyk-io

snyk-io Bot commented Jul 31, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@axpnet

axpnet commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/components/DuplicateFinderDialog.tsx (1)

278-301: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reconcile deletion with current state, not the stale render snapshot.

After Line 285 awaits onDeleteFiles, Lines 287-295 use the captured groups and selectedPaths. During a slow deletion, users can change selections or change the scan mode. A new scan can also replace groups. The completion then overwrites newer results and clears newer selections.

Use functional state updates keyed by the paths snapshot. Also disable controls that can change selection or start a scan while isDeleting is true.

Proposed reconciliation
       await onDeleteFiles(paths);
       // Remove deleted files from groups and update state
-      const updatedGroups: DuplicateGroup[] = [];
-      for (const group of groups) {
-        const remaining = group.files.filter(f => !selectedPaths.has(f));
-        if (remaining.length > 1) {
-          updatedGroups.push({ ...group, files: remaining });
-        }
-      }
-      setGroups(updatedGroups);
-      setSelectedPaths(new Set());
+      const deletedPaths = new Set(paths);
+      setGroups((currentGroups) => {
+        const updatedGroups: DuplicateGroup[] = [];
+        for (const group of currentGroups) {
+          const remaining = group.files.filter(f => !deletedPaths.has(f));
+          if (remaining.length > 1) {
+            updatedGroups.push({ ...group, files: remaining });
+          }
+        }
+        return updatedGroups;
+      });
+      setSelectedPaths((currentSelection) => {
+        const next = new Set(currentSelection);
+        for (const path of deletedPaths) next.delete(path);
+        return next;
+      });
     } catch (err) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DuplicateFinderDialog.tsx` around lines 278 - 301, Update
runDelete to reconcile deletion against current state after onDeleteFiles
completes: use functional setGroups and setSelectedPaths updates keyed to the
local paths snapshot, so newer scans and selections are preserved. Also disable
controls that modify selection or start a scan while isDeleting is true,
including the relevant selection and scan handlers/rendered controls.
🧹 Nitpick comments (1)
src/utils/modalLayerSweep.test.ts (1)

4-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse modalZIndexOf instead of a duplicate parser.

readZ duplicates the regex logic already exported as modalZIndexOf from modalLayers.ts. Import and reuse it here. This keeps the parsing logic in one place, matching the file's own stated goal of preventing drift between the numeric scale and its string representations.

♻️ Proposed refactor to remove the duplicate parser
-import { MODAL_LAYER, MODAL_Z } from './modalLayers';
+import { MODAL_LAYER, MODAL_Z, modalZIndexOf } from './modalLayers';
-/** `z-50` / `z-[9999]` → 50 / 9999. Anything else → null. */
-const readZ = (token: string): number | null => {
-    const arbitrary = token.match(/^z-\[(\d+)\]$/);
-    if (arbitrary) return Number(arbitrary[1]);
-    const scale = token.match(/^z-(\d+)$/);
-    return scale ? Number(scale[1]) : null;
-};
-
             const literal = window.match(/\bz-(?:\[\d+\]|\d+)(?![\w-])/);
-            const z = literal ? readZ(literal[0]) : null;
+            const z = literal ? modalZIndexOf(literal[0]) : null;

Also applies to: 29-35, 68-68

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/modalLayerSweep.test.ts` around lines 4 - 5, Update
modalLayerSweep.test.ts to import and reuse modalZIndexOf from modalLayers.ts
wherever readZ currently parses modal z-index strings, then remove the duplicate
regex/parser logic from readZ while preserving the existing test behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/components/DuplicateFinderDialog.tsx`:
- Around line 278-301: Update runDelete to reconcile deletion against current
state after onDeleteFiles completes: use functional setGroups and
setSelectedPaths updates keyed to the local paths snapshot, so newer scans and
selections are preserved. Also disable controls that modify selection or start a
scan while isDeleting is true, including the relevant selection and scan
handlers/rendered controls.

---

Nitpick comments:
In `@src/utils/modalLayerSweep.test.ts`:
- Around line 4-5: Update modalLayerSweep.test.ts to import and reuse
modalZIndexOf from modalLayers.ts wherever readZ currently parses modal z-index
strings, then remove the duplicate regex/parser logic from readZ while
preserving the existing test behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 25644c49-7279-4aae-8d8c-5700c4a596dc

📥 Commits

Reviewing files that changed from the base of the PR and between 3da8058 and 3b99f38.

📒 Files selected for processing (10)
  • src/App.tsx
  • src/components/AccountLockScreen.tsx
  • src/components/Dialogs/index.tsx
  • src/components/DuplicateFinderDialog.test.ts
  • src/components/DuplicateFinderDialog.tsx
  • src/components/GuardedCloseConfirm.tsx
  • src/components/LockScreen.tsx
  • src/utils/modalLayerSweep.test.ts
  • src/utils/modalLayers.test.ts
  • src/utils/modalLayers.ts

…he snapshot

Review finding on #540. `runDelete` closes over `groups` and `selectedPaths` as
they were when the button was pressed, then writes them back after awaiting the
delete. Deleting many files takes long enough for something to move underneath
it: switching mode re-runs the scan, and the checkboxes stay live throughout. The
completion would then write the captured groups over newer scan results and clear
ticks the user had made after pressing.

Both writes are now functional updates keyed by the set of paths that were
actually deleted, so a newer scan survives and only what was deleted is
unticked. The three controls that start a scan, the two mode toggles and the
fuzzy-cutoff field, are disabled while a delete is in flight, which closes the
other half of the race rather than only tidying up after it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@axpnet

axpnet commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Valid, and the more interesting half is the one the finding does not spell out. Fixed in 50cc6bd.

Both writes are now functional updates keyed by the set of paths actually deleted, so a newer scan survives and only what was deleted gets unticked, rather than the whole selection being cleared.

But reconciling correctly only makes the outcome sane; it does not stop the race. The three controls that start a scan (the two mode toggles and the fuzzy-cutoff field) were live throughout a delete, so a scan could complete against a tree the delete had already changed. Those are now disabled while isDeleting, which is what actually closes it. Tidying up after a race and preventing it are different fixes and this needed both.

Pinned in DuplicateFinderDialog.test.ts: the callback may not close over groups, both writes must be functional, and all three scan-starting controls must carry the guard. Verified failing on the previous commit.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 3 minutes.

…erlay's own className

Four review findings from #541, all in code this PR introduced.

`onDeleteFiles` walks the list, so a reject can arrive with earlier paths already
deleted. Which ones is not knowable from the dialog, and keeping the old rows
left it offering to delete files that no longer exist. It re-scans, which is the
only answer that cannot be wrong.

The overlay sweep read the first `z-` token in a five-line window that began two
lines before the `fixed inset-0` match, so a class belonging to a neighbouring
element could be recorded as the overlay's. It now reads the `className` value
that actually contains the match, whether that is a quoted string or a `{...}`
expression. Making it strict immediately produced a false positive worth keeping
out: `SaveAllMenu` has a comment explaining why its confirm is portalled, and the
words `fixed inset-0` inside it were being counted as an overlay with no
z-index. Comments are blanked before the scan, newlines preserved so the reported
line numbers still point at the right place.

The sweep also ran at module scope, so a parse failure aborted the file instead
of failing a named test; it is collected lazily inside the tests now, and the
"every overlay has a z-index" check is one of them rather than a bare `expect`
during evaluation. `readZ` duplicated the exported `modalZIndexOf`, which is the
parser the source itself uses, and an eslint-disable had no `any` to suppress.

The scale gains the assertions that were missing: the elevated tier ordered
internally and against the modal tier, and no two layers sharing a number, since
an accidental tie is exactly what #537 was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@axpnet
axpnet merged commit 7d3a2b0 into main Jul 31, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Can't confirm deleting duplicates

1 participant