feat(squads): skeleton loader + AlertDialog archive confirm (MUL-2437) (#2890)

* feat(squads): skeleton loader + AlertDialog archive confirm (MUL-2437)

- Replace `Loading...` text on the squads list with a Skeleton placeholder
  matching the SquadCard shape (avatar + title + subtitle), aligning with
  the Agents / Dashboard pattern.
- Replace the native `confirm()` on the squad detail Archive button with
  the project's AlertDialog (destructive variant, pending-disabled, i18n
  copy interpolating the squad name).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(squads): drop misleading restore copy from archive confirm (MUL-2437)

Archive is irreversible — there is no unarchive command (see
apps/docs/content/docs/squads.mdx:113). Aligns dialog copy with
docs: tells the user the action can't be undone and to create a
new squad if they need the routing back.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Naiyuan Qing
2026-05-20 16:43:58 +08:00
committed by GitHub
parent dee5c7cf50
commit b040165f4e
4 changed files with 72 additions and 2 deletions

View File

@@ -9,6 +9,13 @@
"details_section": "Details",
"archive_button": "Archive"
},
"archive_dialog": {
"title": "Archive this squad?",
"description": "\"{{name}}\" will be archived. Issues currently assigned to this squad will be transferred to its leader. This can't be undone — create a new squad if you need the routing back.",
"cancel": "Cancel",
"confirm": "Archive",
"archiving": "Archiving…"
},
"name_editor": {
"cancel": "Cancel"
},

View File

@@ -9,6 +9,13 @@
"details_section": "详情",
"archive_button": "归档"
},
"archive_dialog": {
"title": "归档这个小队?",
"description": "“{{name}}” 将被归档,该小队当前承接的 issue 会转交给小队负责人。此操作无法撤销,如需恢复路由请新建小队。",
"cancel": "取消",
"confirm": "归档",
"archiving": "归档中…"
},
"name_editor": {
"cancel": "取消"
},

View File

@@ -116,6 +116,7 @@ export function SquadDetailPage() {
const [showAddMember, setShowAddMember] = useState(false);
const [showCreateAgent, setShowCreateAgent] = useState(false);
const [confirmArchive, setConfirmArchive] = useState(false);
const updateSquadMut = useMutation({
mutationFn: (data: { name?: string; description?: string; instructions?: string; avatar_url?: string; leader_id?: string }) => api.updateSquad(squadId, data),
@@ -225,7 +226,7 @@ export function SquadDetailPage() {
<SquadHeaderAvatar squad={squad} initials={initials} />
<h1 className="text-sm font-medium">{squad.name}</h1>
</div>
<Button size="sm" variant="ghost" className="text-destructive hover:text-destructive" onClick={() => { if (confirm("Archive this squad? Issues will be transferred to the leader.")) deleteMut.mutate(); }}>
<Button size="sm" variant="ghost" className="text-destructive hover:text-destructive" onClick={() => setConfirmArchive(true)}>
<Trash2 className="size-3.5 mr-1" />
{t(($) => $.inspector.archive_button)}
</Button>
@@ -288,6 +289,36 @@ export function SquadDetailPage() {
onCreate={handleCreateAgent}
/>
)}
{confirmArchive && (
<AlertDialog
open
onOpenChange={(v) => { if (!v && !deleteMut.isPending) setConfirmArchive(false); }}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t(($) => $.archive_dialog.title)}</AlertDialogTitle>
<AlertDialogDescription>
{t(($) => $.archive_dialog.description, { name: squad.name })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleteMut.isPending}>
{t(($) => $.archive_dialog.cancel)}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteMut.mutate()}
disabled={deleteMut.isPending}
className="bg-destructive text-white hover:bg-destructive/90"
>
{deleteMut.isPending
? t(($) => $.archive_dialog.archiving)
: t(($) => $.archive_dialog.confirm)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
);
}

View File

@@ -11,6 +11,7 @@ import { PageHeader } from "../../layout/page-header";
import { Users, Plus, Search, Bot, User } from "lucide-react";
import { Button } from "@multica/ui/components/ui/button";
import { Input } from "@multica/ui/components/ui/input";
import { Skeleton } from "@multica/ui/components/ui/skeleton";
import { ActorAvatar as ActorAvatarBase } from "@multica/ui/components/common/actor-avatar";
import { useModalStore } from "@multica/core/modals";
import type { Agent, Squad } from "@multica/core/types";
@@ -83,7 +84,7 @@ export function SquadsPage() {
<div className="flex flex-1 min-h-0 flex-col overflow-hidden">
{isLoading ? (
<div className="p-6 text-muted-foreground text-sm">Loading...</div>
<SquadsListSkeleton />
) : squads.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-3 py-16 text-center">
<Users className="size-10 text-muted-foreground/50" />
@@ -184,6 +185,30 @@ function ScopeButton({ active, label, count, onClick }: { active: boolean; label
);
}
function SquadsListSkeleton() {
return (
<>
<div className="flex h-12 shrink-0 items-center gap-3 border-b px-4">
<Skeleton className="h-8 w-full max-w-sm rounded-md" />
<Skeleton className="h-7 w-32 rounded-md" />
</div>
<div className="flex-1 overflow-y-auto p-4">
<div className="grid gap-3">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 sm:gap-4 rounded-lg border p-3 sm:p-4">
<Skeleton className="h-9 w-9 shrink-0 rounded-md" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-1/3 rounded" />
<Skeleton className="h-3 w-2/3 rounded" />
</div>
</div>
))}
</div>
</div>
</>
);
}
function SquadAvatar({ squad }: { squad: Squad }) {
const initials = squad.name
.split(" ")