Skip to content

Commit bc3a487

Browse files
committed
cleanup flags UI
1 parent 9dc08c5 commit bc3a487

File tree

6 files changed

+443
-62
lines changed

6 files changed

+443
-62
lines changed

apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-item.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import {
4+
ArchiveIcon,
45
DotsThreeIcon,
56
PencilSimpleIcon,
67
TrashIcon,
@@ -89,6 +90,13 @@ export function FlagItem({
8990
});
9091
};
9192

93+
const handleArchive = () => {
94+
updateStatusMutation.mutate({
95+
id: flag.id,
96+
status: "archived",
97+
});
98+
};
99+
92100
return (
93101
<div className={cn("border-border border-b", className)}>
94102
<div className="group flex items-center hover:bg-accent/50">
@@ -221,6 +229,10 @@ export function FlagItem({
221229
<PencilSimpleIcon className="size-4" weight="duotone" />
222230
Edit
223231
</DropdownMenuItem>
232+
<DropdownMenuItem onClick={handleArchive}>
233+
<ArchiveIcon className="size-4" weight="duotone" />
234+
Archive
235+
</DropdownMenuItem>
224236
<DropdownMenuSeparator />
225237
<DropdownMenuItem
226238
className="text-destructive focus:text-destructive"

apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx

Lines changed: 105 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -457,15 +457,27 @@ export function FlagSheet({
457457
{/* Type & Value */}
458458
<div className="space-y-4">
459459
<div className="space-y-2">
460-
<span className="text-muted-foreground text-xs">Type</span>
460+
<div className="space-y-0.5">
461+
<span className="font-medium text-foreground text-sm">
462+
Flag Type
463+
</span>
464+
<p className="text-muted-foreground text-xs">
465+
How the flag value is determined for each user
466+
</p>
467+
</div>
461468
<div className="flex gap-2">
462469
{(["boolean", "rollout", "multivariant"] as const).map(
463470
(type) => {
464471
const isSelected = watchedType === type;
472+
const typeDescriptions = {
473+
boolean: "On or Off",
474+
rollout: "% of users",
475+
multivariant: "A/B variants",
476+
};
465477
return (
466478
<button
467479
className={cn(
468-
"flex-1 cursor-pointer rounded border py-2.5 text-center font-medium text-sm capitalize transition-all",
480+
"flex-1 cursor-pointer rounded border py-2 text-center transition-all",
469481
isSelected
470482
? "border-primary bg-primary/5 text-foreground"
471483
: "border-transparent bg-secondary text-muted-foreground hover:border-border hover:bg-secondary/80 hover:text-foreground"
@@ -474,7 +486,12 @@ export function FlagSheet({
474486
onClick={() => form.setValue("flag.type", type)}
475487
type="button"
476488
>
477-
{type}
489+
<span className="block font-medium text-sm capitalize">
490+
{type}
491+
</span>
492+
<span className="block text-muted-foreground text-xs">
493+
{typeDescriptions[type]}
494+
</span>
478495
</button>
479496
);
480497
}
@@ -498,10 +515,15 @@ export function FlagSheet({
498515
render={({ field }) => (
499516
<div className="space-y-3">
500517
<div className="flex items-center justify-between">
501-
<span className="text-muted-foreground text-sm">
502-
Rollout percentage
503-
</span>
504-
<span className="font-mono text-sm tabular-nums">
518+
<div className="space-y-0.5">
519+
<span className="font-medium text-foreground text-sm">
520+
Rollout Percentage
521+
</span>
522+
<p className="text-muted-foreground text-xs">
523+
% of users who get true (when active)
524+
</p>
525+
</div>
526+
<span className="font-mono text-foreground text-lg tabular-nums">
505527
{field.value}%
506528
</span>
507529
</div>
@@ -555,47 +577,54 @@ export function FlagSheet({
555577
) : (
556578
<motion.div
557579
animate={{ opacity: 1, y: 0 }}
558-
className="flex items-center justify-between"
580+
className="space-y-2"
559581
exit={{ opacity: 0, y: -10 }}
560582
initial={{ opacity: 0, y: 10 }}
561583
key="boolean"
562584
transition={{ duration: 0.15 }}
563585
>
564-
<span className="text-muted-foreground text-sm">
565-
Default value
566-
</span>
567-
<FormField
568-
control={form.control}
569-
name="flag.defaultValue"
570-
render={({ field }) => (
571-
<div className="flex items-center gap-3">
572-
<span
573-
className={cn(
574-
"text-sm transition-colors",
575-
field.value
576-
? "text-muted-foreground/60"
577-
: "text-foreground"
578-
)}
579-
>
580-
Off
581-
</span>
582-
<Switch
583-
checked={field.value}
584-
onCheckedChange={field.onChange}
585-
/>
586-
<span
587-
className={cn(
588-
"text-sm transition-colors",
589-
field.value
590-
? "text-foreground"
591-
: "text-muted-foreground/60"
592-
)}
593-
>
594-
On
595-
</span>
596-
</div>
597-
)}
598-
/>
586+
<div className="flex items-center justify-between">
587+
<div className="space-y-0.5">
588+
<span className="font-medium text-foreground text-sm">
589+
Return Value
590+
</span>
591+
<p className="text-muted-foreground text-xs">
592+
What users get when flag is active
593+
</p>
594+
</div>
595+
<FormField
596+
control={form.control}
597+
name="flag.defaultValue"
598+
render={({ field }) => (
599+
<div className="flex items-center gap-3">
600+
<span
601+
className={cn(
602+
"text-sm transition-colors",
603+
field.value
604+
? "text-muted-foreground/60"
605+
: "text-foreground"
606+
)}
607+
>
608+
Off
609+
</span>
610+
<Switch
611+
checked={field.value}
612+
onCheckedChange={field.onChange}
613+
/>
614+
<span
615+
className={cn(
616+
"text-sm transition-colors",
617+
field.value
618+
? "text-foreground"
619+
: "text-muted-foreground/60"
620+
)}
621+
>
622+
On
623+
</span>
624+
</div>
625+
)}
626+
/>
627+
</div>
599628
</motion.div>
600629
)}
601630
</AnimatePresence>
@@ -613,14 +642,26 @@ export function FlagSheet({
613642
);
614643
const canBeActive = inactiveDeps.length === 0;
615644

645+
const statusDescriptions = {
646+
active: "Live, evaluates rules",
647+
inactive: "Off, always returns false",
648+
archived: "Retired, hidden from list",
649+
};
650+
616651
return (
617652
<div className="space-y-2">
618653
<div className="flex items-center justify-between">
619-
<span className="text-muted-foreground text-xs">
620-
Status
621-
</span>
654+
<div className="space-y-0.5">
655+
<span className="font-medium text-foreground text-sm">
656+
Flag Status
657+
</span>
658+
<p className="text-muted-foreground text-xs">
659+
Active = uses settings below. Inactive = completely
660+
off.
661+
</p>
662+
</div>
622663
{!canBeActive && (
623-
<span className="text-amber-600 text-xs">
664+
<span className="text-warning text-xs">
624665
Dependencies must be active first
625666
</span>
626667
)}
@@ -634,13 +675,13 @@ export function FlagSheet({
634675
return (
635676
<button
636677
className={cn(
637-
"flex-1 cursor-pointer rounded border py-2 font-medium text-sm capitalize transition-all",
678+
"flex-1 cursor-pointer rounded border py-2 transition-all",
638679
isSelected
639680
? status === "active"
640-
? "border-green-500/50 bg-green-500/10 text-green-600"
681+
? "green-angled-rectangle-gradient border-success/50 bg-success/10 text-success"
641682
: status === "inactive"
642-
? "border-amber-500/50 bg-amber-500/10 text-amber-600"
643-
: "border-border bg-secondary text-foreground"
683+
? "red-angled-rectangle-gradient border-destructive/50 bg-destructive/10 text-destructive"
684+
: "amber-angled-rectangle-gradient border-warning/50 bg-warning/10 text-warning"
644685
: "border-transparent bg-secondary text-muted-foreground hover:border-border hover:bg-secondary/80 hover:text-foreground",
645686
isDisabled && "cursor-not-allowed opacity-50"
646687
)}
@@ -649,7 +690,19 @@ export function FlagSheet({
649690
onClick={() => field.onChange(status)}
650691
type="button"
651692
>
652-
{status}
693+
<span className="block font-medium text-sm capitalize">
694+
{status}
695+
</span>
696+
<span
697+
className={cn(
698+
"block text-xs",
699+
isSelected
700+
? "opacity-80"
701+
: "text-muted-foreground"
702+
)}
703+
>
704+
{statusDescriptions[status]}
705+
</span>
653706
</button>
654707
);
655708
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"use client";
2+
3+
import {
4+
ArrowCounterClockwiseIcon,
5+
DotsThreeIcon,
6+
PencilSimpleIcon,
7+
TrashIcon,
8+
} from "@phosphor-icons/react";
9+
import { useMutation, useQueryClient } from "@tanstack/react-query";
10+
import { Badge } from "@/components/ui/badge";
11+
import { Button } from "@/components/ui/button";
12+
import {
13+
DropdownMenu,
14+
DropdownMenuContent,
15+
DropdownMenuItem,
16+
DropdownMenuSeparator,
17+
DropdownMenuTrigger,
18+
} from "@/components/ui/dropdown-menu";
19+
import { orpc } from "@/lib/orpc";
20+
import { cn } from "@/lib/utils";
21+
import type { Flag } from "../../_components/types";
22+
23+
interface ArchivedFlagItemProps {
24+
flag: Flag;
25+
onEdit: (flag: Flag) => void;
26+
onDelete: (flagId: string) => void;
27+
className?: string;
28+
}
29+
30+
export function ArchivedFlagItem({
31+
flag,
32+
onEdit,
33+
onDelete,
34+
className,
35+
}: ArchivedFlagItemProps) {
36+
const queryClient = useQueryClient();
37+
38+
const restoreMutation = useMutation({
39+
...orpc.flags.update.mutationOptions(),
40+
onSuccess: () => {
41+
queryClient.invalidateQueries({
42+
queryKey: orpc.flags.list.key({
43+
input: { websiteId: flag.websiteId ?? "" },
44+
}),
45+
});
46+
},
47+
});
48+
49+
const handleRestore = () => {
50+
restoreMutation.mutate({
51+
id: flag.id,
52+
status: "inactive",
53+
});
54+
};
55+
56+
return (
57+
<div className={cn("border-border border-b", className)}>
58+
<div className="group flex items-center hover:bg-accent/50">
59+
{/* Clickable area for editing */}
60+
<button
61+
className="flex flex-1 cursor-pointer items-center gap-4 px-4 py-3 text-left sm:px-6 sm:py-4"
62+
onClick={() => onEdit(flag)}
63+
type="button"
64+
>
65+
{/* Flag details */}
66+
<div className="min-w-0 flex-1">
67+
<div className="flex items-center gap-2">
68+
<h3 className="truncate font-medium text-foreground">
69+
{flag.name || flag.key}
70+
</h3>
71+
<Badge className="shrink-0" variant="gray">
72+
{flag.type}
73+
</Badge>
74+
<Badge className="gap-1.5" variant="amber">
75+
<span className="size-1.5 rounded bg-amber-500" />
76+
Archived
77+
</Badge>
78+
</div>
79+
<p className="mt-0.5 truncate font-mono text-muted-foreground text-sm">
80+
{flag.key}
81+
</p>
82+
{flag.description && (
83+
<p className="mt-0.5 line-clamp-1 text-muted-foreground text-xs">
84+
{flag.description}
85+
</p>
86+
)}
87+
</div>
88+
</button>
89+
90+
{/* Restore button */}
91+
<div className="shrink-0 pr-2">
92+
<Button
93+
disabled={restoreMutation.isPending}
94+
onClick={handleRestore}
95+
size="sm"
96+
variant="outline"
97+
>
98+
<ArrowCounterClockwiseIcon className="size-4" weight="duotone" />
99+
<span className="hidden sm:inline">Restore</span>
100+
</Button>
101+
</div>
102+
103+
{/* Actions dropdown */}
104+
<div className="shrink-0 pr-4 sm:pr-6">
105+
<DropdownMenu>
106+
<DropdownMenuTrigger asChild>
107+
<Button
108+
className="size-8 opacity-0 transition-opacity group-hover:opacity-100 data-[state=open]:opacity-100"
109+
size="icon"
110+
variant="ghost"
111+
>
112+
<DotsThreeIcon className="size-5" weight="bold" />
113+
</Button>
114+
</DropdownMenuTrigger>
115+
<DropdownMenuContent align="end" className="w-40">
116+
<DropdownMenuItem onClick={() => onEdit(flag)}>
117+
<PencilSimpleIcon className="size-4" weight="duotone" />
118+
Edit
119+
</DropdownMenuItem>
120+
<DropdownMenuItem onClick={handleRestore}>
121+
<ArrowCounterClockwiseIcon
122+
className="size-4"
123+
weight="duotone"
124+
/>
125+
Restore
126+
</DropdownMenuItem>
127+
<DropdownMenuSeparator />
128+
<DropdownMenuItem
129+
className="text-destructive focus:text-destructive"
130+
onClick={() => onDelete(flag.id)}
131+
>
132+
<TrashIcon className="size-4" weight="duotone" />
133+
Delete
134+
</DropdownMenuItem>
135+
</DropdownMenuContent>
136+
</DropdownMenu>
137+
</div>
138+
</div>
139+
</div>
140+
);
141+
}

0 commit comments

Comments
 (0)