use proper label filter selector

This commit is contained in:
miloschwartz
2026-07-01 11:46:20 -04:00
parent 297fd2caf3
commit 97cdb2eb5a
3 changed files with 164 additions and 76 deletions
+15 -64
View File
@@ -1,14 +1,6 @@
"use client"; "use client";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@app/components/ui/command";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
@@ -16,16 +8,15 @@ import {
} from "@app/components/ui/popover"; } from "@app/components/ui/popover";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover"; import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover";
import { CheckIcon, Funnel } from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo, useState } from "react";
import { orgQueries } from "@app/lib/queries"; import { orgQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useDebounce } from "use-debounce"; import { Funnel } from "lucide-react";
import { useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { LabelBadge } from "./label-badge"; import { LabelBadge } from "./label-badge";
import { LabelOverflowBadge } from "./label-overflow-badge"; import { LabelOverflowBadge } from "./label-overflow-badge";
import { LabelsFilterSelector } from "./LabelsFilterSelector";
import { LABEL_COLORS } from "./labels-selector"; import { LABEL_COLORS } from "./labels-selector";
import { Checkbox } from "./ui/checkbox";
function areSelectionsEqual(a: string[], b: string[]) { function areSelectionsEqual(a: string[], b: string[]) {
if (a.length !== b.length) { if (a.length !== b.length) {
@@ -54,13 +45,9 @@ export function LabelColumnFilterButton({
const [draftValues, setDraftValues] = useState<string[]>(selectedValues); const [draftValues, setDraftValues] = useState<string[]>(selectedValues);
const t = useTranslations(); const t = useTranslations();
const [labelSearchQuery, setlabelsSearchQuery] = useState("");
const [debouncedQuery] = useDebounce(labelSearchQuery, 150);
const { data: labels = [] } = useQuery( const { data: labels = [] } = useQuery(
orgQueries.labels({ orgQueries.labels({
orgId, orgId,
query: debouncedQuery,
perPage: 500 perPage: 500
}) })
); );
@@ -152,53 +139,17 @@ export function LabelColumnFilterButton({
className={dataTableFilterPopoverContentClassName} className={dataTableFilterPopoverContentClassName}
align="start" align="start"
> >
<Command shouldFilter={false}> <LabelsFilterSelector
<CommandInput orgId={orgId}
placeholder={t("labelSearch")} isSelected={(label) => draftSet.has(label.name)}
value={labelSearchQuery} onToggle={(label) => {
onValueChange={setlabelsSearchQuery} toggle(label.name);
/> }}
<CommandList> showClear={draftValues.length > 0}
<CommandEmpty>{t("labelsNotFound")}</CommandEmpty> onClear={() => {
<CommandGroup> setDraftValues([]);
{draftValues.length > 0 && ( }}
<CommandItem />
onSelect={() => {
setDraftValues([]);
}}
className="text-muted-foreground"
>
{t("accessFilterClear")}
</CommandItem>
)}
{labels.map((label) => (
<CommandItem
key={label.name}
value={label.name}
onSelect={() => {
toggle(label.name);
}}
className="flex items-center gap-2"
>
<Checkbox
className="pointer-events-none shrink-0"
checked={draftSet.has(label.name)}
aria-hidden
tabIndex={-1}
/>
<div
className="size-2 rounded-full bg-(--color) flex-none"
style={{
// @ts-expect-error css color
"--color": label.color
}}
/>
{label.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
+98
View File
@@ -0,0 +1,98 @@
"use client";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@app/components/ui/command";
import { orgQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { useDebounce } from "use-debounce";
import { Checkbox } from "./ui/checkbox";
export type LabelFilterOption = {
labelId: number;
name: string;
color: string;
};
type LabelsFilterSelectorProps = {
orgId: string;
isSelected: (label: LabelFilterOption) => boolean;
onToggle: (label: LabelFilterOption) => void;
onClear?: () => void;
showClear?: boolean;
};
export function LabelsFilterSelector({
orgId,
isSelected,
onToggle,
onClear,
showClear = false
}: LabelsFilterSelectorProps) {
const t = useTranslations();
const [labelSearchQuery, setlabelsSearchQuery] = useState("");
const [debouncedQuery] = useDebounce(labelSearchQuery, 150);
const { data: labels = [] } = useQuery(
orgQueries.labels({
orgId,
query: debouncedQuery,
perPage: 500
})
);
return (
<Command shouldFilter={false}>
<CommandInput
placeholder={t("labelSearch")}
value={labelSearchQuery}
onValueChange={setlabelsSearchQuery}
/>
<CommandList>
<CommandEmpty>{t("labelsNotFound")}</CommandEmpty>
<CommandGroup>
{showClear && onClear && (
<CommandItem
onSelect={onClear}
className="text-muted-foreground"
>
{t("accessFilterClear")}
</CommandItem>
)}
{labels.map((label) => (
<CommandItem
key={label.labelId}
value={label.name}
onSelect={() => {
onToggle(label);
}}
className="flex items-center gap-2"
>
<Checkbox
className="pointer-events-none shrink-0"
checked={isSelected(label)}
aria-hidden
tabIndex={-1}
/>
<div
className="size-2 rounded-full bg-(--color) flex-none"
style={{
// @ts-expect-error css color
"--color": label.color
}}
/>
{label.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
);
}
@@ -6,9 +6,10 @@ import {
} from "@app/components/multi-site-selector"; } from "@app/components/multi-site-selector";
import { import {
formatLabelsSelectorLabel, formatLabelsSelectorLabel,
LabelsSelector, LABEL_COLORS,
type SelectedLabel type SelectedLabel
} from "@app/components/labels-selector"; } from "@app/components/labels-selector";
import { LabelsFilterSelector } from "@app/components/LabelsFilterSelector";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
Popover, Popover,
@@ -16,9 +17,11 @@ import {
PopoverTrigger PopoverTrigger
} from "@app/components/ui/popover"; } from "@app/components/ui/popover";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { orgQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { ChevronsUpDown, Funnel } from "lucide-react"; import { ChevronsUpDown, Funnel } from "lucide-react";
import { useState } from "react"; import { useMemo, useState } from "react";
import type { Selectedsite } from "@app/components/site-selector"; import type { Selectedsite } from "@app/components/site-selector";
type LauncherFilterPopoverProps = { type LauncherFilterPopoverProps = {
@@ -40,6 +43,34 @@ export function LauncherFilterPopover({
const [sitesOpen, setSitesOpen] = useState(false); const [sitesOpen, setSitesOpen] = useState(false);
const [labelsOpen, setLabelsOpen] = useState(false); const [labelsOpen, setLabelsOpen] = useState(false);
const { data: labels = [] } = useQuery(
orgQueries.labels({
orgId,
perPage: 500
})
);
const selectedLabelIds = useMemo(
() => new Set(selectedLabels.map((label) => label.labelId)),
[selectedLabels]
);
const resolvedSelectedLabels: SelectedLabel[] = useMemo(
() =>
selectedLabels.map((selected) => {
const found = labels.find(
(label) => label.labelId === selected.labelId
);
return (
found ?? {
...selected,
color: selected.color || LABEL_COLORS.gray
}
);
}),
[labels, selectedLabels]
);
return ( return (
<Popover modal={false}> <Popover modal={false}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@@ -101,7 +132,7 @@ export function LauncherFilterPopover({
> >
<span className="truncate text-left"> <span className="truncate text-left">
{formatLabelsSelectorLabel( {formatLabelsSelectorLabel(
selectedLabels, resolvedSelectedLabels,
t t
)} )}
</span> </span>
@@ -112,16 +143,15 @@ export function LauncherFilterPopover({
className="w-[var(--radix-popover-trigger-width)] p-0" className="w-[var(--radix-popover-trigger-width)] p-0"
align="start" align="start"
> >
<LabelsSelector <LabelsFilterSelector
orgId={orgId} orgId={orgId}
selectedLabels={selectedLabels} isSelected={(label) =>
toggleLabel={(label, action) => { selectedLabelIds.has(label.labelId)
if (action === "attach") { }
onLabelsChange([ onToggle={(label) => {
...selectedLabels, if (
label selectedLabelIds.has(label.labelId)
]); ) {
} else {
onLabelsChange( onLabelsChange(
selectedLabels.filter( selectedLabels.filter(
(item) => (item) =>
@@ -129,8 +159,17 @@ export function LauncherFilterPopover({
label.labelId label.labelId
) )
); );
} else {
onLabelsChange([
...selectedLabels,
label
]);
} }
}} }}
showClear={selectedLabels.length > 0}
onClear={() => {
onLabelsChange([]);
}}
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>