Merge pull request #665 from langflow-ai/fix/filter_search

This commit is contained in:
Edwin Jose 2025-12-12 17:04:21 -05:00 committed by GitHub
commit 1df76aa51e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 721 additions and 672 deletions

View file

@ -0,0 +1,36 @@
import {
type UseQueryOptions,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import type { KnowledgeFilter } from "./useGetFiltersSearchQuery";
export const useGetAllFiltersQuery = (
options?: Omit<UseQueryOptions<KnowledgeFilter[]>, "queryKey" | "queryFn">,
) => {
const queryClient = useQueryClient();
async function getAllFilters(): Promise<KnowledgeFilter[]> {
const response = await fetch("/api/knowledge-filter/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "", limit: 1000 }), // Fetch all filters
});
const json = await response.json();
if (!response.ok || !json.success) {
// ensure we always return a KnowledgeFilter[] to satisfy the return type
return [];
}
return (json.filters || []) as KnowledgeFilter[];
}
return useQuery<KnowledgeFilter[]>(
{
queryKey: ["knowledge-filters", "all"],
queryFn: getAllFilters,
...options,
},
queryClient,
);
};

File diff suppressed because it is too large Load diff

View file

@ -1,165 +1,158 @@
"use client";
import { Plus } from "lucide-react";
import { useState } from "react";
import {
type KnowledgeFilter,
useGetFiltersSearchQuery,
} from "@/app/api/queries/useGetFiltersSearchQuery";
import { cn } from "@/lib/utils";
import { useGetAllFiltersQuery } from "@/app/api/queries/useGetAllFiltersQuery";
import type { KnowledgeFilter } from "@/app/api/queries/useGetFiltersSearchQuery";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
import { cn } from "@/lib/utils";
import {
type FilterColor,
type IconKey,
iconKeyToComponent,
type FilterColor,
type IconKey,
iconKeyToComponent,
} from "./filter-icon-popover";
import { filterAccentClasses } from "./knowledge-filter-panel";
interface ParsedQueryData {
query: string;
filters: {
data_sources: string[];
document_types: string[];
owners: string[];
};
limit: number;
scoreThreshold: number;
color: FilterColor;
icon: IconKey;
query: string;
filters: {
data_sources: string[];
document_types: string[];
owners: string[];
};
limit: number;
scoreThreshold: number;
color: FilterColor;
icon: IconKey;
}
interface KnowledgeFilterListProps {
selectedFilter: KnowledgeFilter | null;
onFilterSelect: (filter: KnowledgeFilter | null) => void;
selectedFilter: KnowledgeFilter | null;
onFilterSelect: (filter: KnowledgeFilter | null) => void;
}
export function KnowledgeFilterList({
selectedFilter,
onFilterSelect,
selectedFilter,
onFilterSelect,
}: KnowledgeFilterListProps) {
const [searchQuery] = useState("");
const { startCreateMode } = useKnowledgeFilter();
const { startCreateMode } = useKnowledgeFilter();
const { data, isFetching: loading } = useGetFiltersSearchQuery(
searchQuery,
20,
);
const { data, isFetching: loading } = useGetAllFiltersQuery();
const filters = data || [];
const filters = data || [];
const handleFilterSelect = (filter: KnowledgeFilter) => {
if (filter.id === selectedFilter?.id) {
onFilterSelect(null);
return;
}
onFilterSelect(filter);
};
const handleFilterSelect = (filter: KnowledgeFilter) => {
if (filter.id === selectedFilter?.id) {
onFilterSelect(null);
return;
}
onFilterSelect(filter);
};
const handleCreateNew = () => {
startCreateMode();
};
const handleCreateNew = () => {
startCreateMode();
};
const parseQueryData = (queryData: string): ParsedQueryData => {
return JSON.parse(queryData) as ParsedQueryData;
};
const parseQueryData = (queryData: string): ParsedQueryData => {
return JSON.parse(queryData) as ParsedQueryData;
};
return (
<div className="flex-1 min-h-0 flex flex-col">
<div className="px-3 flex-1 min-h-0 flex flex-col">
<div className="flex-shrink-0">
<div className="flex items-center justify-between mb-3 mr-2 ml-4">
<h3 className="text-xs font-medium text-muted-foreground">
Knowledge Filters
</h3>
<button
type="button"
className="p-1 hover:bg-accent rounded"
onClick={handleCreateNew}
title="Create New Filter"
>
<Plus className="h-4 w-4 text-muted-foreground" />
</button>
</div>
<div className="overflow-y-auto scrollbar-hide space-y-1">
{loading ? (
<div className="text-[13px] text-muted-foreground p-2 ml-2">
Loading...
</div>
) : filters.length === 0 ? (
<div className="text-[13px] text-muted-foreground pb-2 pt-3 ml-4">
{searchQuery ? "No filters found" : "No saved filters"}
</div>
) : (
filters.map((filter) => (
<div
key={filter.id}
onClick={() => handleFilterSelect(filter)}
className={cn(
"flex items-center gap-3 px-3 py-2 w-full rounded-lg hover:bg-accent hover:text-accent-foreground cursor-pointer group transition-colors",
selectedFilter?.id === filter.id &&
"active bg-accent text-accent-foreground",
)}
>
<div className="flex flex-col gap-1 flex-1 min-w-0">
<div className="flex items-center gap-2">
{(() => {
const parsed = parseQueryData(
filter.query_data,
) as ParsedQueryData;
const Icon = iconKeyToComponent(parsed.icon);
return (
<div
className={cn(
"flex items-center justify-center w-5 h-5 rounded transition-colors",
filterAccentClasses[parsed.color],
parsed.color === "zinc" &&
"group-hover:bg-background group-[.active]:bg-background",
)}
>
{Icon && <Icon className="h-3 w-3" />}
</div>
);
})()}
<div className="text-sm font-medium truncate group-hover:text-accent-foreground">
{filter.name}
</div>
</div>
{filter.description && (
<div className="text-xs text-muted-foreground line-clamp-2">
{filter.description}
</div>
)}
<div className="flex items-center gap-2">
<div className="text-xs text-muted-foreground">
{new Date(filter.created_at).toLocaleDateString(
undefined,
{
month: "short",
day: "numeric",
year: "numeric",
},
)}
</div>
<span className="text-xs bg-muted text-muted-foreground px-1 py-0.5 rounded-sm group-hover:bg-background group-[.active]:bg-background transition-colors">
{(() => {
const dataSources = parseQueryData(filter.query_data)
.filters.data_sources;
if (dataSources[0] === "*") return "All sources";
const count = dataSources.length;
return `${count} ${
count === 1 ? "source" : "sources"
}`;
})()}
</span>
</div>
</div>
</div>
))
)}
</div>
</div>
{/* Create flow moved to panel create mode */}
</div>
</div>
);
return (
<div className="flex-1 min-h-0 flex flex-col">
<div className="px-3 flex-1 min-h-0 flex flex-col">
<div className="flex-shrink-0">
<div className="flex items-center justify-between mb-3 mr-2 ml-4">
<h3 className="text-xs font-medium text-muted-foreground">
Knowledge Filters
</h3>
<button
type="button"
className="p-1 hover:bg-accent rounded"
onClick={handleCreateNew}
title="Create New Filter"
>
<Plus className="h-4 w-4 text-muted-foreground" />
</button>
</div>
<div className="overflow-y-auto scrollbar-hide space-y-1">
{loading ? (
<div className="text-[13px] text-muted-foreground p-2 ml-2">
Loading...
</div>
) : filters.length === 0 ? (
<div className="text-[13px] text-muted-foreground pb-2 pt-3 ml-4">
No saved filters
</div>
) : (
filters.map((filter) => (
<div
key={filter.id}
onClick={() => handleFilterSelect(filter)}
className={cn(
"flex items-center gap-3 px-3 py-2 w-full rounded-lg hover:bg-accent hover:text-accent-foreground cursor-pointer group transition-colors",
selectedFilter?.id === filter.id &&
"active bg-accent text-accent-foreground",
)}
>
<div className="flex flex-col gap-1 flex-1 min-w-0">
<div className="flex items-center gap-2">
{(() => {
const parsed = parseQueryData(
filter.query_data,
) as ParsedQueryData;
const Icon = iconKeyToComponent(parsed.icon);
return (
<div
className={cn(
"flex items-center justify-center w-5 h-5 rounded transition-colors",
filterAccentClasses[parsed.color],
parsed.color === "zinc" &&
"group-hover:bg-background group-[.active]:bg-background",
)}
>
{Icon && <Icon className="h-3 w-3" />}
</div>
);
})()}
<div className="text-sm font-medium truncate group-hover:text-accent-foreground">
{filter.name}
</div>
</div>
{filter.description && (
<div className="text-xs text-muted-foreground line-clamp-2">
{filter.description}
</div>
)}
<div className="flex items-center gap-2">
<div className="text-xs text-muted-foreground">
{new Date(filter.created_at).toLocaleDateString(
undefined,
{
month: "short",
day: "numeric",
year: "numeric",
},
)}
</div>
<span className="text-xs bg-muted text-muted-foreground px-1 py-0.5 rounded-sm group-hover:bg-background group-[.active]:bg-background transition-colors">
{(() => {
const dataSources = parseQueryData(filter.query_data)
.filters.data_sources;
if (dataSources[0] === "*") return "All sources";
const count = dataSources.length;
return `${count} ${
count === 1 ? "source" : "sources"
}`;
})()}
</span>
</div>
</div>
</div>
))
)}
</div>
</div>
{/* Create flow moved to panel create mode */}
</div>
</div>
);
}

View file

@ -30,6 +30,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dotenv": "^17.2.3",
"fuse.js": "^7.1.0",
"lucide-react": "^0.525.0",
"motion": "^12.23.12",
"next": "15.5.7",
@ -3429,6 +3430,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/fuse.js": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",

View file

@ -34,6 +34,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dotenv": "^17.2.3",
"fuse.js": "^7.1.0",
"lucide-react": "^0.525.0",
"motion": "^12.23.12",
"next": "15.5.7",