Files
iistwin/client/src/components/TableFieldSelect.tsx

198 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useRef, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Input } from '@/components/ui/input';
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover';
import { Table } from 'lucide-react';
import { DirectoryTreeSelect, type DirectoryTreeNode } from '@/components/directories/DirectoryTreeSelect';
import { apiRequest } from '@/lib/queryClient';
import type { DataTableRow } from '@shared/schema';
interface TableFieldSelectProps {
fieldId: number;
fieldCode: string;
placeholder?: string;
options?: unknown;
value: any;
onChange: (value: string) => void;
}
export default function TableFieldSelect({
fieldId,
fieldCode,
placeholder,
options,
value,
onChange,
}: TableFieldSelectProps) {
const opts = options && !Array.isArray(options) ? (options as Record<string, any>) : {};
const tableId = opts.tableId as number | undefined;
const displayColumn = (opts.displayColumn as string) || '';
const rowFilters = opts.filters as Record<string, string> | undefined;
const { data: tableData, isLoading, error } = useQuery<{
table: {
rows: DataTableRow[];
columns: Array<{ name: string }>;
treeColumnIndex: number | null;
treeDisplayColumnIndex: number | null;
treeColumns: number[] | null;
treeDisplayColumns: number[] | null;
};
}>({
queryKey: ['/api/directories', tableId],
enabled: !!tableId,
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
retry: false,
});
const filtersStr = useMemo(() => (rowFilters ? JSON.stringify(rowFilters) : ''), [rowFilters]);
const { data: treeData } = useQuery<{
columnIndex: number | null;
treeColumns: number[];
treeDisplayColumns: number[];
nodes: DirectoryTreeNode[];
}>({
queryKey: ['/api/directories', tableId, 'rows', 'tree', filtersStr],
queryFn: async () => {
const params = new URLSearchParams();
if (filtersStr) params.set('filters', filtersStr);
const res = await apiRequest('GET', `/api/directories/${tableId}/rows/tree?${params.toString()}`);
return res.json();
},
enabled: !!tableId && !!tableData,
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
retry: false,
});
const [searchTerm, setSearchTerm] = useState('');
const [isOpen, setIsOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const table = tableData?.table;
const rows = table?.rows || [];
const nodes = treeData?.nodes || [];
let displayColumnIndex = table?.columns?.findIndex((c) => c.name === displayColumn);
if (displayColumnIndex === undefined || displayColumnIndex < 0) {
displayColumnIndex = table?.treeDisplayColumnIndex ?? table?.treeDisplayColumns?.[0] ?? 0;
}
const sortedRows = useMemo(() => {
return [...rows].sort((a, b) => {
const av = a.values?.[displayColumnIndex!] || a.values?.[0] || '';
const bv = b.values?.[displayColumnIndex!] || b.values?.[0] || '';
return av.localeCompare(bv, 'ru');
});
}, [rows, displayColumnIndex]);
const filtered = useMemo(() => {
if (!searchTerm.trim()) return sortedRows;
const term = searchTerm.toLowerCase();
return sortedRows.filter((r) => {
const display = r.values?.[displayColumnIndex!] || r.values?.[0] || '';
return display.toLowerCase().includes(term);
});
}, [sortedRows, searchTerm, displayColumnIndex]);
const hasTree = nodes.some((n) => n.type === 'group');
if (!tableId) {
return <div className="text-sm text-muted-foreground">Таблица не настроена</div>;
}
if (isLoading) {
return <Input disabled className="h-8" placeholder="Загрузка..." />;
}
if (error) {
return <div className="text-sm text-destructive">Нет доступа к таблице</div>;
}
if (!table || !table.rows?.length) {
return <div className="text-sm text-muted-foreground">Таблица пуста</div>;
}
const selectedRow = table.rows?.find((r) => r.id.toString() === value?.toString());
const selectedDisplay = selectedRow
? selectedRow.values[displayColumnIndex!] || selectedRow.values[0] || ''
: '';
const handleSelect = (rowId: string) => {
onChange(rowId);
setSearchTerm('');
setIsOpen(false);
};
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverAnchor asChild>
<div className="relative w-full">
<Input
ref={inputRef}
className="h-8"
placeholder={placeholder || 'Выберите значение...'}
value={isOpen ? searchTerm : selectedDisplay || ''}
onChange={(e) => {
setSearchTerm(e.target.value);
setIsOpen(true);
}}
onFocus={() => {
setSearchTerm('');
setIsOpen(true);
}}
data-testid={`select-table-${fieldCode}`}
/>
</div>
</PopoverAnchor>
<PopoverContent
className="p-0"
style={{ width: 'var(--radix-popper-anchor-width)' }}
onOpenAutoFocus={(e) => e.preventDefault()}
onInteractOutside={(e) => {
if (inputRef.current && inputRef.current.contains(e.target as Node)) {
e.preventDefault();
}
}}
>
<div className="max-h-60 overflow-auto">
{hasTree ? (
<DirectoryTreeSelect
nodes={nodes}
value={value}
onChange={handleSelect}
searchTerm={searchTerm}
/>
) : (
<ul className="py-1">
<li
className="cursor-pointer px-3 py-2 hover:bg-accent text-sm text-muted-foreground"
onMouseDown={(e) => { e.preventDefault(); handleSelect(''); }}
>
—
</li>
{filtered.map((row) => (
<li
key={row.id}
className="cursor-pointer px-3 py-2 hover:bg-accent"
onMouseDown={(e) => { e.preventDefault(); handleSelect(row.id.toString()); }}
>
<div className="flex items-center gap-2 text-sm">
<Table className="w-3 h-3 text-muted-foreground" />
{row.values[displayColumnIndex!] || row.values[0]}
</div>
</li>
))}
{filtered.length === 0 && (
<li className="px-3 py-2 text-sm text-muted-foreground">Нет совпадений</li>
)}
</ul>
)}
</div>
</PopoverContent>
</Popover>
);
}