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

View File

@@ -1,14 +1,6 @@
"use client";
import { Button } from "@app/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@app/components/ui/command";
import {
Popover,
PopoverContent,
@@ -16,16 +8,15 @@ import {
} from "@app/components/ui/popover";
import { cn } from "@app/lib/cn";
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 { 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 { LabelOverflowBadge } from "./label-overflow-badge";
import { LabelsFilterSelector } from "./LabelsFilterSelector";
import { LABEL_COLORS } from "./labels-selector";
import { Checkbox } from "./ui/checkbox";
function areSelectionsEqual(a: string[], b: string[]) {
if (a.length !== b.length) {
@@ -54,13 +45,9 @@ export function LabelColumnFilterButton({
const [draftValues, setDraftValues] = useState<string[]>(selectedValues);
const t = useTranslations();
const [labelSearchQuery, setlabelsSearchQuery] = useState("");
const [debouncedQuery] = useDebounce(labelSearchQuery, 150);
const { data: labels = [] } = useQuery(
orgQueries.labels({
orgId,
query: debouncedQuery,
perPage: 500
})
);
@@ -152,53 +139,17 @@ export function LabelColumnFilterButton({
className={dataTableFilterPopoverContentClassName}
align="start"
>
<Command shouldFilter={false}>
<CommandInput
placeholder={t("labelSearch")}
value={labelSearchQuery}
onValueChange={setlabelsSearchQuery}
/>
<CommandList>
<CommandEmpty>{t("labelsNotFound")}</CommandEmpty>
<CommandGroup>
{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>
<LabelsFilterSelector
orgId={orgId}
isSelected={(label) => draftSet.has(label.name)}
onToggle={(label) => {
toggle(label.name);
}}
showClear={draftValues.length > 0}
onClear={() => {
setDraftValues([]);
}}
/>
</PopoverContent>
</Popover>
</div>

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>
);
}

View File

@@ -6,9 +6,10 @@ import {
} from "@app/components/multi-site-selector";
import {
formatLabelsSelectorLabel,
LabelsSelector,
LABEL_COLORS,
type SelectedLabel
} from "@app/components/labels-selector";
import { LabelsFilterSelector } from "@app/components/LabelsFilterSelector";
import { Button } from "@app/components/ui/button";
import {
Popover,
@@ -16,9 +17,11 @@ import {
PopoverTrigger
} from "@app/components/ui/popover";
import { cn } from "@app/lib/cn";
import { orgQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { ChevronsUpDown, Funnel } from "lucide-react";
import { useState } from "react";
import { useMemo, useState } from "react";
import type { Selectedsite } from "@app/components/site-selector";
type LauncherFilterPopoverProps = {
@@ -40,6 +43,34 @@ export function LauncherFilterPopover({
const [sitesOpen, setSitesOpen] = 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 (
<Popover modal={false}>
<PopoverTrigger asChild>
@@ -101,7 +132,7 @@ export function LauncherFilterPopover({
>
<span className="truncate text-left">
{formatLabelsSelectorLabel(
selectedLabels,
resolvedSelectedLabels,
t
)}
</span>
@@ -112,16 +143,15 @@ export function LauncherFilterPopover({
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<LabelsSelector
<LabelsFilterSelector
orgId={orgId}
selectedLabels={selectedLabels}
toggleLabel={(label, action) => {
if (action === "attach") {
onLabelsChange([
...selectedLabels,
label
]);
} else {
isSelected={(label) =>
selectedLabelIds.has(label.labelId)
}
onToggle={(label) => {
if (
selectedLabelIds.has(label.labelId)
) {
onLabelsChange(
selectedLabels.filter(
(item) =>
@@ -129,8 +159,17 @@ export function LauncherFilterPopover({
label.labelId
)
);
} else {
onLabelsChange([
...selectedLabels,
label
]);
}
}}
showClear={selectedLabels.length > 0}
onClear={() => {
onLabelsChange([]);
}}
/>
</PopoverContent>
</Popover>