Skip to content

fix(shared): show pending state while deleting squads - #6506

Merged
rebelchris merged 9 commits into
mainfrom
eng-1945-feedback-ux-issue-deleting-squads-lacks-a-processing-ui
Aug 22, 2026
Merged

fix(shared): show pending state while deleting squads#6506
rebelchris merged 9 commits into
mainfrom
eng-1945-feedback-ux-issue-deleting-squads-lacks-a-processing-ui

Conversation

@rebelchris

@rebelchris rebelchris commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add optional async confirm handling to shared prompts so destructive actions can keep the modal open while work is pending.
  • Move squad deletion through a mutation that awaits network deletion, boot-cache pruning, and navigation callback in order.
  • Surface pending state in the Squad settings danger zone and keep the header-menu confirmation prompt visible in flight.

Key decisions

  • Kept the existing showPrompt(options): Promise<boolean> contract additive and non-throwing: a failed onConfirm closes the prompt and resolves false, so no caller needs a catch.
  • Close the prompt and show the existing generic error toast on failure instead of adding inline retry UI.
  • Did not add a client timeout, but cancel / ESC / overlay close stay available while the confirm is in flight, so a hung delete never traps the user. Dismissing only hides the prompt; the mutation keeps running and still reports through its own toast/navigation.
  • The pending flag is scoped to the prompt instance, so a prompt that replaces a pending one does not inherit its disabled/loading state.
  • Post-delete callback (navigation) failures are not mapped to a delete failure — a cancelled router.replace no longer produces a false error toast.

Verification

  • NODE_ENV=test pnpm --filter @dailydotdev/shared exec jest src/components/modals/Prompt.spec.tsx src/hooks/useDeleteSquad.spec.tsx src/components/squads/SquadHeaderMenu.spec.tsx --runInBand
  • NODE_ENV=test pnpm --filter @dailydotdev/shared exec eslint src/hooks/usePrompt.ts src/components/modals/Prompt.tsx src/hooks/useDeleteSquad.ts src/components/squads/settings/SquadDangerZone.tsx src/components/modals/Prompt.spec.tsx src/hooks/useDeleteSquad.spec.tsx src/components/squads/SquadHeaderMenu.spec.tsx --max-warnings 0
  • node ./scripts/typecheck-strict-changed.js
  • Follow-up commit relies on CI for lint_shared / typecheck_strict_changed / test_shared.

Issue: https://linear.app/dailydev/issue/ENG-1945/feedback-ux-issue-deleting-squads-lacks-a-processing-ui-causing-user

Closes ENG-1945


Created by Huginn 🐦‍⬛

Preview domain

https://eng-1945-feedback-ux-issue-delet.preview.app.daily.dev

@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Aug 22, 2026 11:32am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
storybook Ignored Ignored Aug 22, 2026 11:32am

Request Review

@rebelchris rebelchris left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Summary

Solid direction: routing the delete through a mutation and awaiting network → boot-cache prune → navigation in order is the right fix for ENG-1945, and the added specs cover the pending/error paths well. Two consumers of useDeleteSquad (SquadDangerZone, SquadHeaderMenu) both benefit, and DangerZone already accepts buttonDisabled/buttonLoading, so no shared-component change was needed.

One blocking concern (the prompt becomes undismissable while a confirm is in flight) plus four non-blocking items, left inline.

Verification

  • Read root + package AGENTS.md
  • Scope matches the linked issue
  • Traced confirm → mutation → boot cache → navigation → success/error paths
  • Enumerated useDeleteSquad and PromptElement consumers (MainLayout, RecruiterLayout x2, RecruiterFullscreenLayout, companion App.tsx)
  • CI inspected (lint_shared, typecheck_strict_changed, test_webapp, test_extension green; test_shared still running at review time)
  • Not run locally — relying on CI for test_shared
  • Manual check still worth doing: mobile drawer variant of the delete prompt while pending

Reviewed by AI.

isDrawerOnMobile
drawerProps={{ displayCloseButton: false, appendOnRoot: true }}
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
shouldCloseOnOverlayClick={isPending ? false : shouldCloseOnOverlayClick}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Blocking: while isPending is true the prompt has no escape hatch at all. Overlay click is forced off here, onRequestClose (ESC / react-modal) short-circuits in handleFail, the cancel button is disabled, and this modal renders only Modal.Body — no Modal.Header/close button — with drawerProps.displayCloseButton: false on mobile. So a slow or hung deleteSquad (flaky network, gateway timeout, request that never settles) leaves the user locked in a loading modal with a full-page reload as the only way out. The PR description accepts "a hung delete remains visibly pending", but that trade-off is about the spinner, not about trapping the user.

Suggested direction: keep cancel enabled while pending and treat it as "dismiss the UI" (the mutation keeps running and the toast/navigation still fires), or leave ESC/overlay close available and only guard the confirm action. Guarding just re-entrancy of onConfirm is enough to prevent a double delete — okButton is already disabled.

Reviewed by AI.

});
await deleteSquad(squad.id!);
deleteCachedSquad(squad.id!);
await callback?.();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Non-blocking (real failure mode): callback runs inside mutationFn, so a rejection from the callback is indistinguishable from a failed delete. Both consumers pass () => router.replace('/'), and the pages-router replace promise rejects when a route change is cancelled or aborted — which is exactly what can happen right after a delete, when the now-404 squad page kicks off its own navigation. In that case the squad is deleted, the boot cache is pruned, but the user still gets the generic DEFAULT_ERROR toast and showPrompt rejects.

Suggested direction: keep the awaited ordering but scope the error mapping — run callback in onSuccess, or wrap the callback so post-delete navigation failures don't surface as "delete failed".

Reviewed by AI.

Comment thread packages/shared/src/hooks/usePrompt.ts Outdated
setPrompt(null);
};
const failWith = (error: unknown) => {
reject(error);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Non-blocking (contract change): showPrompt previously never rejected, so every existing caller can safely await it. With onConfirm it now rejects, and the only defence is the showPrompt(options).catch(() => false) in useDeleteSquad — the next caller that forgets it gets an unhandled rejection.

Since the confirm error is already surfaced by the caller's own mutation onError, would resolving false (or { confirmed, error }) be safer than rejecting? It keeps the Promise<boolean> contract non-throwing for every caller. If rejection is deliberate, worth making it explicit in the PromptOptions.onConfirm type/JSDoc.

Reviewed by AI.

disabled: isPending || !!cancelButtonDisabled,
className: classNames('w-full tablet:w-auto', cancelButtonClassName),
} as ButtonProps<'button'>;
const okActionButtonProps = {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Non-blocking (bloat / type safety): this rebuild costs ~60 lines and two as ButtonProps<'button'> casts to add three things — onClick, disabled, loading. The casts are what worries me: they silence the ColorButtonProps/IconButtonProps discriminated unions, so a future edit that passes color without variant, or iconPosition without icon, no longer fails typecheck.

PromptButtonProps already does Omit<ButtonProps<'button'>, 'onClick'>, so callers cannot supply onClick and the old spread order was never a hazard. The same behaviour keeps its types with:

<Button
  variant={okButton.variant ?? ButtonVariant.Primary}
  {...okButton}
  onClick={handleSuccess}
  disabled={isPending || okButton.disabled}
  loading={isPending || okButton.loading}
  className={classNames('w-full tablet:w-auto', okButton.className)}
>

Two incidental behaviour changes fall out of the rewrite that aren't in the description: iconPosition: … ?? ButtonIconPosition.Left is redundant (Button already defaults iconPosition to Left), and destructuring title out drops the native title attribute that the previous spread put on the DOM node. Both are almost certainly fine, just not stated.

Reviewed by AI.


export function PromptElement(props: Partial<ModalProps>): ReactElement | null {
const { prompt } = usePrompt();
const [isPending, setIsPending] = useState(false);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Non-blocking (edge case): isPending is component state on the always-mounted PromptElement, not scoped to the prompt instance, and it is never reset when prompt changes identity. If any code calls showPrompt again while an async confirm is in flight (the query-cache prompt is simply overwritten), the newly shown prompt renders with both buttons disabled and a spinner it never asked for, and only recovers when the previous onConfirm settles. Resetting on prompt change, or storing the pending flag alongside the prompt in the query cache, would make this self-consistent.

Reviewed by AI.

- keep cancel/esc/overlay close available while an async confirm runs so a
  slow delete cannot trap the user in a modal without an escape hatch
- scope the pending flag to the prompt instance so a replacing prompt does
  not render disabled
- resolve showPrompt with false instead of rejecting on confirm failure
- do not map post-delete callback (navigation) failures to a delete error
- drop the button prop rebuild and the ButtonProps casts
@rebelchris
rebelchris force-pushed the eng-1945-feedback-ux-issue-deleting-squads-lacks-a-processing-ui branch from 4311df5 to 7b1941f Compare August 21, 2026 14:04
@rebelchris

Copy link
Copy Markdown
Contributor Author

Follow-up commits address the review findings on this PR:

  • Prompt could not be dismissed while pendingcancel / ESC / overlay close now stay available during an async confirm. Dismissing resolves showPrompt with false and hides the prompt; the in-flight mutation keeps running and still reports through its own toast/navigation, so a hung delete can no longer trap the user (mobile drawer included). Only the confirm action is re-entrancy guarded.
  • Post-delete callback mapped to a delete failure — the navigation callback is now awaited but its rejection is isolated, so a cancelled router.replace('/') no longer shows the generic error toast after a successful delete. New spec: does not report a failure when the post-delete callback rejects.
  • showPrompt rejection is a contract changeonError now closes with false instead of rejecting, so showPrompt stays non-throwing for every caller and useDeleteSquad no longer needs a .catch. Spec renamed to closes prompt and resolves false when async confirm fails.
  • Pending state not scoped to the prompt instance — pending is derived from the prompt object identity, so a prompt that replaces a pending one renders normally instead of inheriting a spinner, and a late settle from an abandoned prompt is ignored.
  • Button prop rebuild — the ~50-line destructure and the redundant ButtonIconPosition.Left default are gone; the two action buttons are built with a spread plus the three overrides. The as ButtonProps<'button'> casts are kept: PromptButtonProps flattens the Button color/variant and icon/iconPosition unions, so typecheck:strict:changed fails without them — that is pre-existing and out of scope here.

Note on CI: lint_shared and typecheck_strict_changed passed on identical content (4311df5). test_shared / test_webapp have not completed — every run since has failed in CircleCI's Checkout code step (ssh: connect to host github.com port 22: Connection refused, early EOF), which is unrelated to this diff. Needs a re-run once CircleCI checkout recovers.

Reviewed by AI. Follow-up changes authored by AI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rebelchris
rebelchris merged commit 83b1116 into main Aug 22, 2026
12 checks passed
@rebelchris
rebelchris deleted the eng-1945-feedback-ux-issue-deleting-squads-lacks-a-processing-ui branch August 22, 2026 12:38
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.

1 participant