feat(17-03): add SpawnArchitectDropdown and ActionMenu components
- SpawnArchitectDropdown: discuss/breakdown modes via tRPC mutations - Brief success state on button text after spawn - ActionMenu: archive with browser confirm, disabled edit/duplicate/delete - No deleteInitiative tRPC procedure exists, so delete is placeholder - Both components invalidate listInitiatives on success
This commit is contained in:
64
packages/web/src/components/ActionMenu.tsx
Normal file
64
packages/web/src/components/ActionMenu.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { MoreHorizontal } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { trpc } from "@/lib/trpc";
|
||||||
|
|
||||||
|
interface ActionMenuProps {
|
||||||
|
initiativeId: string;
|
||||||
|
onDelete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActionMenu({ initiativeId, onDelete }: ActionMenuProps) {
|
||||||
|
const utils = trpc.useUtils();
|
||||||
|
|
||||||
|
const archiveMutation = trpc.updateInitiative.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.listInitiatives.invalidate();
|
||||||
|
onDelete?.();
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
console.error("Failed to archive initiative:", err.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleArchive() {
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
"Are you sure you want to archive this initiative? It can be restored later."
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
archiveMutation.mutate({
|
||||||
|
id: initiativeId,
|
||||||
|
status: "archived",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">More actions</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem disabled>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem disabled>Duplicate</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={handleArchive}
|
||||||
|
disabled={archiveMutation.isPending}
|
||||||
|
>
|
||||||
|
{archiveMutation.isPending ? "Archiving..." : "Archive"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem disabled>Delete</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
packages/web/src/components/SpawnArchitectDropdown.tsx
Normal file
80
packages/web/src/components/SpawnArchitectDropdown.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { trpc } from "@/lib/trpc";
|
||||||
|
|
||||||
|
interface SpawnArchitectDropdownProps {
|
||||||
|
initiativeId: string;
|
||||||
|
initiativeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SpawnArchitectDropdown({
|
||||||
|
initiativeId,
|
||||||
|
initiativeName,
|
||||||
|
}: SpawnArchitectDropdownProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [successText, setSuccessText] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const discussMutation = trpc.spawnArchitectDiscuss.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
setOpen(false);
|
||||||
|
setSuccessText("Spawned!");
|
||||||
|
setTimeout(() => setSuccessText(null), 2000);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
console.error("Failed to spawn discuss architect:", err.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const breakdownMutation = trpc.spawnArchitectBreakdown.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
setOpen(false);
|
||||||
|
setSuccessText("Spawned!");
|
||||||
|
setTimeout(() => setSuccessText(null), 2000);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
console.error("Failed to spawn breakdown architect:", err.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPending = discussMutation.isPending || breakdownMutation.isPending;
|
||||||
|
|
||||||
|
function handleDiscuss() {
|
||||||
|
discussMutation.mutate({
|
||||||
|
name: initiativeName + "-discuss",
|
||||||
|
initiativeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBreakdown() {
|
||||||
|
breakdownMutation.mutate({
|
||||||
|
name: initiativeName + "-breakdown",
|
||||||
|
initiativeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" disabled={isPending}>
|
||||||
|
{successText ?? "Spawn Architect"}
|
||||||
|
<ChevronDown className="ml-1 h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<DropdownMenuItem onClick={handleDiscuss} disabled={isPending}>
|
||||||
|
Discuss
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleBreakdown} disabled={isPending}>
|
||||||
|
Breakdown
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user