mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-23 18:17:44 +02:00
review: gate Danger Zone on members fetched + reset typed input on rename
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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(
|
||||
<DeleteWorkspaceDialog
|
||||
workspaceName="My 团队 🚀"
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<DeleteWorkspaceDialog
|
||||
workspaceName="old-name"
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
onConfirm={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<DeleteWorkspaceDialog
|
||||
workspaceName="new-name"
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
onConfirm={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* 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 && (
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<LogOut className="h-4 w-4 text-muted-foreground" />
|
||||
@@ -265,6 +268,7 @@ export function WorkspaceTab() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<AlertDialog open={!!confirmAction} onOpenChange={(v) => { if (!v) setConfirmAction(null); }}>
|
||||
<AlertDialogContent>
|
||||
|
||||
Reference in New Issue
Block a user