put status options in a dropdown

This commit is contained in:
Oliver Bryan
2026-01-10 23:53:07 +00:00
parent 6ada790581
commit 69c8ac7bd0
2 changed files with 83 additions and 51 deletions

View File

@@ -4,7 +4,7 @@ import {
type OrganisationMemberResponse, type OrganisationMemberResponse,
type OrganisationResponse, type OrganisationResponse,
} from "@issue/shared"; } from "@issue/shared";
import { ChevronDown, ChevronUp, Plus, X } from "lucide-react"; import { ChevronDown, ChevronUp, EllipsisVertical, Plus, X } from "lucide-react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { AddMemberDialog } from "@/components/add-member-dialog"; import { AddMemberDialog } from "@/components/add-member-dialog";
@@ -16,6 +16,12 @@ import { Button } from "@/components/ui/button";
import ColourPicker from "@/components/ui/colour-picker"; import ColourPicker from "@/components/ui/colour-picker";
import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -207,6 +213,7 @@ function OrganisationsDialog({
newStatuses[trimmed] = newStatusColour; newStatuses[trimmed] = newStatusColour;
await updateStatuses(newStatuses); await updateStatuses(newStatuses);
setNewStatusName(""); setNewStatusName("");
setNewStatusColour(DEFAULT_STATUS_COLOUR);
setIsCreatingStatus(false); setIsCreatingStatus(false);
setStatusError(null); setStatusError(null);
}; };
@@ -421,42 +428,58 @@ function OrganisationsDialog({
/> />
</div> </div>
{isAdmin && ( {isAdmin && (
<div className="flex items-center gap-2"> <DropdownMenu>
<Button <DropdownMenuTrigger
variant="dummy" asChild
size="none" size={"sm"}
disabled={index === 0} noStyle
onClick={() => void moveStatus(status, "up")} className="hover:opacity-80 cursor-pointer"
aria-label="Move status up"
> >
<ChevronUp className="size-5 text-muted-foreground" /> <EllipsisVertical className="size-4 text-foreground" />
</Button> </DropdownMenuTrigger>
<Button <DropdownMenuContent
variant="dummy" align="end"
size="none" sideOffset={4}
disabled={ className="bg-background"
index === Object.keys(statuses).length - 1
}
onClick={() =>
void moveStatus(status, "down")
}
aria-label="Move status down"
> >
<ChevronDown className="size-5 text-muted-foreground" /> <DropdownMenuItem
</Button> disabled={index === 0}
{Object.keys(statuses).length > 1 && ( onSelect={() =>
<Button void moveStatus(status, "up")
variant="dummy" }
size="none" className="hover:bg-primary-foreground"
onClick={() => >
<ChevronUp className="size-4 text-muted-foreground" />
Move up
</DropdownMenuItem>
<DropdownMenuItem
disabled={
index ===
Object.keys(statuses).length - 1
}
onSelect={() =>
void moveStatus(status, "down")
}
className="hover:bg-primary-foreground"
>
<ChevronDown className="size-4 text-muted-foreground" />
Move down
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
disabled={
Object.keys(statuses).length <= 1
}
onSelect={() =>
handleRemoveStatusClick(status) handleRemoveStatusClick(status)
} }
aria-label="Remove status" className="hover:bg-destructive/10"
> >
<X className="size-5 text-destructive" /> <X className="size-4" />
</Button> Remove
)} </DropdownMenuItem>
</div> </DropdownMenuContent>
</DropdownMenu>
)} )}
</div> </div>
))} ))}
@@ -564,19 +587,22 @@ function OrganisationsDialog({
} }
}} }}
> >
<DialogContent> <DialogContent className="sm:max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle>Remove Status</DialogTitle> <DialogTitle>Remove Status</DialogTitle>
</DialogHeader> </DialogHeader>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Are you sure you want to remove the "{statusToRemove}" status? Which status Are you sure you want to remove the{" "}
would you like issues with this status to be set to? {statusToRemove ? (
<StatusTag status={statusToRemove} colour={statuses[statusToRemove]} />
) : null}{" "}
status? Which status would you like issues with this status to be set to?
</p> </p>
<Select value={reassignToStatus} onValueChange={setReassignToStatus}> <Select value={reassignToStatus} onValueChange={setReassignToStatus}>
<SelectTrigger className="w-full"> <SelectTrigger className="w-min">
<SelectValue placeholder="Select status" /> <SelectValue placeholder="Select status" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent side={"bottom"} position="popper" align="start">
{Object.keys(statuses) {Object.keys(statuses)
.filter((s) => s !== statusToRemove) .filter((s) => s !== statusToRemove)
.map((status) => ( .map((status) => (

View File

@@ -14,28 +14,34 @@ function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMe
function DropdownMenuTrigger({ function DropdownMenuTrigger({
className, className,
size = "default", size = "default",
noStyle = false,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger> & {
size?: "sm" | "default"; size?: "sm" | "default";
noStyle?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.Trigger <DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger" data-slot="dropdown-menu-trigger"
data-size={size} data-size={size}
className={cn( className={
"cursor-pointer border data-[placeholder]:text-muted-foreground", noStyle
"[&_svg:not([class*='text-'])]:text-foreground", ? cn(className)
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", : cn(
"aria-invalid:border-destructive dark:hover:bg-muted/40", "cursor-pointer border data-[placeholder]:text-muted-foreground",
"flex w-fit items-center justify-between gap-2 border", "[&_svg:not([class*='text-'])]:text-foreground",
"bg-transparent px-3 py-2 text-sm whitespace-nowrap", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
"shadow-xs outline-none disabled:cursor-not-allowed", "aria-invalid:border-destructive dark:hover:bg-muted/40",
"disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8", "flex w-fit items-center justify-between gap-2 border",
"*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex", "bg-transparent px-3 py-2 text-sm whitespace-nowrap",
"*:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2", "shadow-xs outline-none disabled:cursor-not-allowed",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8",
className, "*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex",
)} "*:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)
}
{...props} {...props}
/> />
); );