mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-13 09:18:12 +02:00
use proper label filter selector
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user