From e0316fda810309d3ffce17ea80a4e34f3297269c Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:05:51 +0800 Subject: [PATCH] review: gate Danger Zone on members fetched + reset typed input on rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses self-review findings on #1238: - Previously the Danger Zone rendered immediately with `members = []`, so the Delete workspace block (gated on `isOwner`, which is derived from an empty members list) would flash in once the query settled. Gate the whole section on `membersFetched` so it appears once with correct controls. - Reset `typed` on `workspaceName` change too — if another owner renames the workspace while the dialog is open, the already-typed string stops matching silently; resetting surfaces the mismatch. - Added two tests: unicode/special-char names match literally; rename mid-dialog clears the input. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../delete-workspace-dialog.test.tsx | 42 +++++++++++++++++++ .../components/delete-workspace-dialog.tsx | 8 +++- .../settings/components/workspace-tab.tsx | 8 +++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/views/settings/components/delete-workspace-dialog.test.tsx b/packages/views/settings/components/delete-workspace-dialog.test.tsx index dcd033aebf..c19144b585 100644 --- a/packages/views/settings/components/delete-workspace-dialog.test.tsx +++ b/packages/views/settings/components/delete-workspace-dialog.test.tsx @@ -126,6 +126,48 @@ describe("DeleteWorkspaceDialog", () => { expect(screen.getByRole("button", { name: "Cancel" })).toBeDisabled(); }); + it("matches names with spaces, unicode, and other non-ASCII characters literally", async () => { + const user = userEvent.setup(); + const onConfirm = vi.fn(); + render( + , + ); + const input = screen.getByRole("textbox"); + await user.type(input, "My 团队 🚀"); + expect(screen.getByRole("button", { name: "Delete workspace" })).toBeEnabled(); + await user.click(screen.getByRole("button", { name: "Delete workspace" })); + expect(onConfirm).toHaveBeenCalledTimes(1); + }); + + it("resets the input when the workspace being deleted changes (e.g. rename mid-dialog)", () => { + const { rerender } = render( + , + ); + const input = screen.getByRole("textbox") as HTMLInputElement; + // Simulate user typing (set value directly since userEvent.type would + // lose focus across re-renders). + input.value = "old-name"; + rerender( + , + ); + expect(screen.getByRole("textbox")).toHaveValue(""); + }); + it("clears the input when reopened so prior attempts don't leak", async () => { const user = userEvent.setup(); const { rerender } = render( diff --git a/packages/views/settings/components/delete-workspace-dialog.tsx b/packages/views/settings/components/delete-workspace-dialog.tsx index 4d1e24509f..64e0240d68 100644 --- a/packages/views/settings/components/delete-workspace-dialog.tsx +++ b/packages/views/settings/components/delete-workspace-dialog.tsx @@ -45,9 +45,13 @@ export function DeleteWorkspaceDialog({ const [typed, setTyped] = useState(""); const matched = typed === workspaceName; + // Reset on close (so reopening for a different workspace doesn't leak + // the prior attempt) AND on workspaceName change (if another owner + // renames the workspace while the dialog is open, the already-typed + // string stops matching and there'd be no feedback explaining why). useEffect(() => { - if (!open) setTyped(""); - }, [open]); + setTyped(""); + }, [open, workspaceName]); const submit = () => { if (!matched || loading) return; diff --git a/packages/views/settings/components/workspace-tab.tsx b/packages/views/settings/components/workspace-tab.tsx index f4e6a4fb50..29ddf82a77 100644 --- a/packages/views/settings/components/workspace-tab.tsx +++ b/packages/views/settings/components/workspace-tab.tsx @@ -38,7 +38,7 @@ export function WorkspaceTab() { const user = useAuthStore((s) => s.user); const workspace = useCurrentWorkspace(); const wsId = useWorkspaceId(); - const { data: members = [] } = useQuery(memberListOptions(wsId)); + const { data: members = [], isFetched: membersFetched } = useQuery(memberListOptions(wsId)); const qc = useQueryClient(); const leaveWorkspace = useLeaveWorkspace(); const deleteWorkspace = useDeleteWorkspace(); @@ -214,7 +214,10 @@ export function WorkspaceTab() { - {/* Danger Zone */} + {/* Danger Zone — gated on the member query settling so the owner-only + Delete button and the sole-owner Leave guidance don't flash in + after mount. */} + {membersFetched && (
@@ -265,6 +268,7 @@ export function WorkspaceTab() {
+ )} { if (!v) setConfirmAction(null); }}>