fix: MedSchedule ESM import for Router and data table links resolution
This commit is contained in:
210
client/src/components/SubdirectoryFieldConfig.tsx
Normal file
210
client/src/components/SubdirectoryFieldConfig.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Table, Link2 } from 'lucide-react';
|
||||
import { apiRequest } from '@/lib/queryClient';
|
||||
import type { DataTableSubdirectory } from '@shared/schema';
|
||||
|
||||
interface FormFieldTable {
|
||||
id?: number;
|
||||
type: string;
|
||||
allowMultiple?: boolean;
|
||||
options?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface SubdirectoryFieldConfigProps {
|
||||
field: FormFieldTable;
|
||||
index: number;
|
||||
tablesData?: { tables: Array<{ id: number; name: string; columns: any[] }> };
|
||||
updateField: (index: number, updates: Partial<FormFieldTable>) => void;
|
||||
}
|
||||
|
||||
export default function SubdirectoryFieldConfig({
|
||||
field,
|
||||
index,
|
||||
tablesData,
|
||||
updateField,
|
||||
}: SubdirectoryFieldConfigProps) {
|
||||
const tableId = field.options?.tableId as number | undefined;
|
||||
|
||||
const { data: subdirectoriesData } = useQuery<{ subdirectories: DataTableSubdirectory[] }>({
|
||||
queryKey: ['/api/directories', tableId, 'subdirectories'],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest('GET', `/api/directories/${tableId}/subdirectories`);
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!tableId,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
const subdirectories = subdirectoriesData?.subdirectories || [];
|
||||
const showSubdirectory = !!field.options?.showSubdirectory;
|
||||
const selectedSubdirectoryId = field.options?.subdirectoryId as number | undefined;
|
||||
|
||||
const selectedSubdirectory = useMemo(() => {
|
||||
return subdirectories.find((s) => s.id === selectedSubdirectoryId || s.childTableId === selectedSubdirectoryId);
|
||||
}, [subdirectories, selectedSubdirectoryId]);
|
||||
|
||||
const selectedTable = useMemo(() => {
|
||||
return tablesData?.tables?.find((t) => t.id === tableId);
|
||||
}, [tablesData, tableId]);
|
||||
|
||||
const handleTableChange = (value: string) => {
|
||||
if (value === 'none') {
|
||||
updateField(index, {
|
||||
options: {
|
||||
...field.options,
|
||||
tableId: undefined,
|
||||
displayColumn: undefined,
|
||||
showSubdirectory: undefined,
|
||||
subdirectoryId: undefined,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newTableId = parseInt(value);
|
||||
const newSelectedTable = tablesData?.tables?.find((t) => t.id === newTableId);
|
||||
updateField(index, {
|
||||
options: {
|
||||
...field.options,
|
||||
tableId: newTableId,
|
||||
displayColumn: newSelectedTable?.columns?.[0]?.name || undefined,
|
||||
showSubdirectory: undefined,
|
||||
subdirectoryId: undefined,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleShowSubdirectoryChange = (checked: boolean) => {
|
||||
updateField(index, {
|
||||
options: {
|
||||
...field.options,
|
||||
showSubdirectory: checked,
|
||||
subdirectoryId: checked ? field.options?.subdirectoryId : undefined,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubdirectoryChange = (value: string) => {
|
||||
updateField(index, {
|
||||
options: {
|
||||
...field.options,
|
||||
subdirectoryId: value === 'none' ? undefined : parseInt(value),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-3 space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={field.allowMultiple || false}
|
||||
onCheckedChange={(checked) => updateField(index, { allowMultiple: !!checked })}
|
||||
data-testid={`checkbox-field-table-allow-multiple-${index}`}
|
||||
/>
|
||||
<label className="text-sm font-medium">Разрешить множественный выбор</label>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium">Таблица</label>
|
||||
<Select value={tableId?.toString() || 'none'} onValueChange={handleTableChange}>
|
||||
<SelectTrigger data-testid={`select-field-table-${index}`}>
|
||||
<SelectValue placeholder="Выберите таблицу" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Не выбрано</SelectItem>
|
||||
{tablesData?.tables?.map((table) => (
|
||||
<SelectItem key={table.id} value={table.id.toString()}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Table className="w-4 h-4" />
|
||||
{table.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{tableId && (
|
||||
<div>
|
||||
<label className="text-sm font-medium">Отображаемый столбец</label>
|
||||
<Select
|
||||
value={field.options?.displayColumn || 'none'}
|
||||
onValueChange={(value) =>
|
||||
updateField(index, {
|
||||
options: { ...field.options, displayColumn: value === 'none' ? undefined : value },
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger data-testid={`select-field-column-${index}`}>
|
||||
<SelectValue placeholder="Выберите столбец" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Не выбрано</SelectItem>
|
||||
{selectedTable?.columns?.map((col: any, idx: number) => (
|
||||
<SelectItem key={idx} value={col.name || `col-${idx}`}>
|
||||
{col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tableId && (
|
||||
<div className="border rounded-md p-3 space-y-3 bg-muted/20">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={showSubdirectory}
|
||||
onCheckedChange={handleShowSubdirectoryChange}
|
||||
data-testid={`checkbox-field-show-subdirectory-${index}`}
|
||||
/>
|
||||
<label className="text-sm font-medium flex items-center gap-1.5">
|
||||
<Link2 className="w-3.5 h-3.5" />
|
||||
Отображать подсправочник
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{showSubdirectory && (
|
||||
<div>
|
||||
<Label className="text-xs">Подсправочник</Label>
|
||||
<Select value={selectedSubdirectoryId?.toString() || 'none'} onValueChange={handleSubdirectoryChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Выберите подсправочник" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Не выбрано</SelectItem>
|
||||
{subdirectories.length === 0 && (
|
||||
<SelectItem value="no-subdirectories" disabled>
|
||||
Нет привязанных подсправочников
|
||||
</SelectItem>
|
||||
)}
|
||||
{subdirectories.map((sub) => (
|
||||
<SelectItem key={sub.id} value={sub.id?.toString() || sub.childTableId?.toString() || ''}>
|
||||
{sub.childTable?.name || `Справочник ${sub.childTableId}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedSubdirectory && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Фильтр: «{selectedTable?.columns?.[selectedSubdirectory.parentColumnIndex ?? 0]?.name || '?'}» = «{selectedSubdirectory.childTable?.columns?.[selectedSubdirectory.childColumnIndex ?? 0]?.name || '?'}»
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -115,6 +115,7 @@ import { JsComponentRenderer } from '@/components/tabs/JsComponentRenderer';
|
||||
import FormAccessTab from '@/components/FormAccessTab';
|
||||
import { TitleTemplateBuilder } from '@/components/TitleTemplateBuilder';
|
||||
import { FormulaInput } from '@/components/FormulaInput';
|
||||
import SubdirectoryFieldConfig from '@/components/SubdirectoryFieldConfig';
|
||||
import { detectCircularDependency } from '@/lib/formulaEngine';
|
||||
|
||||
import type { FormTab, TableColumnDef } from '@/components/tabs/types';
|
||||
@@ -1812,78 +1813,12 @@ const FormEditor = () => {
|
||||
</div>
|
||||
)}
|
||||
{field.type === 'table' && (
|
||||
<div className="mt-3 space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={field.allowMultiple || false}
|
||||
onCheckedChange={(checked) => updateField(index, { allowMultiple: !!checked })}
|
||||
data-testid={`checkbox-field-table-allow-multiple-${index}`}
|
||||
/>
|
||||
<label className="text-sm font-medium">Разрешить множественный выбор</label>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium">Таблица</label>
|
||||
<Select
|
||||
value={field.options?.tableId?.toString() || 'none'}
|
||||
onValueChange={(value) => {
|
||||
if (value === 'none') {
|
||||
updateField(index, { options: { ...field.options, tableId: undefined, displayColumn: undefined } });
|
||||
return;
|
||||
}
|
||||
const selectedTable = tablesData?.tables?.find(t => t.id === parseInt(value));
|
||||
updateField(index, {
|
||||
options: {
|
||||
...field.options,
|
||||
tableId: parseInt(value),
|
||||
displayColumn: selectedTable?.columns?.[0]?.name || undefined
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger data-testid={`select-field-table-${index}`}>
|
||||
<SelectValue placeholder="Выберите таблицу" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Не выбрано</SelectItem>
|
||||
{tablesData?.tables?.map((table) => (
|
||||
<SelectItem key={table.id} value={table.id.toString()}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Table className="w-4 h-4" />
|
||||
{table.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{field.options?.tableId && (
|
||||
<div>
|
||||
<label className="text-sm font-medium">Отображаемый столбец</label>
|
||||
<Select
|
||||
value={field.options?.displayColumn || 'none'}
|
||||
onValueChange={(value) => updateField(index, {
|
||||
options: { ...field.options, displayColumn: value === 'none' ? undefined : value }
|
||||
})}
|
||||
>
|
||||
<SelectTrigger data-testid={`select-field-column-${index}`}>
|
||||
<SelectValue placeholder="Выберите столбец" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Не выбрано</SelectItem>
|
||||
{tablesData?.tables
|
||||
?.find(t => t.id === field.options?.tableId)
|
||||
?.columns?.map((col: any, idx: number) => (
|
||||
<SelectItem key={idx} value={col.name || `col-${idx}`}>
|
||||
{col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SubdirectoryFieldConfig
|
||||
field={field}
|
||||
index={index}
|
||||
tablesData={tablesData}
|
||||
updateField={updateField}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Настройки для связи с задачей */}
|
||||
|
||||
@@ -25,16 +25,17 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { normalizeSelectOptions } from '@/lib/fieldOptions';
|
||||
import {
|
||||
ArrowLeft, Plus, Trash2, Save, Settings, Users, Download, Upload,
|
||||
Check, X, Pencil, FileSpreadsheet, Globe, ListTree, GripVertical, Search, FileText
|
||||
Check, X, Pencil, FileSpreadsheet, Globe, ListTree, GripVertical, Search, FileText, Link2
|
||||
} from 'lucide-react';
|
||||
import { FIELD_TYPE_DEFINITIONS } from '@shared/field-types';
|
||||
import { apiRequest, queryClient } from '@/lib/queryClient';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import type { DataTableFull, DataTableRow, User } from '@shared/schema';
|
||||
import type { DataTableFull, DataTableRow, DataTableSubdirectory, User } from '@shared/schema';
|
||||
import { useColumnFilters } from '@/hooks/useColumnFilters';
|
||||
import { ColumnFilter } from '@/components/ui/ColumnFilter';
|
||||
import { ActiveFilterBadges } from '@/components/ui/ActiveFilterBadges';
|
||||
@@ -110,6 +111,12 @@ export default function TableEditor() {
|
||||
const [rowEditorGroupValue, setRowEditorGroupValue] = useState<string | null>(null);
|
||||
const [editorValues, setEditorValues] = useState<string[]>([]);
|
||||
const [treeSearchQuery, setTreeSearchQuery] = useState('');
|
||||
const [subdirectoriesOpen, setSubdirectoriesOpen] = useState(false);
|
||||
const [editingSubdirectory, setEditingSubdirectory] = useState<DataTableSubdirectory | null>(null);
|
||||
const [subdirectoryChildTableId, setSubdirectoryChildTableId] = useState<string>('none');
|
||||
const [subdirectoryParentColumnIndex, setSubdirectoryParentColumnIndex] = useState<number>(0);
|
||||
const [subdirectoryChildColumnIndex, setSubdirectoryChildColumnIndex] = useState<number>(0);
|
||||
const [subdirectoryParentValues, setSubdirectoryParentValues] = useState<Set<string>>(new Set());
|
||||
|
||||
const { filters, sort, setFilter, clearFilter, clearAll, setSort, clearSort, hasActiveFilters } = useColumnFilters();
|
||||
|
||||
@@ -171,6 +178,19 @@ export default function TableEditor() {
|
||||
});
|
||||
const fieldTemplatesList = fieldTemplatesData ?? [];
|
||||
|
||||
const { data: allTablesData } = useQuery<{ tables: DataTableFull[] }>({
|
||||
queryKey: ['/api/directories'],
|
||||
enabled: subdirectoriesOpen,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
const allTables = allTablesData?.tables || [];
|
||||
|
||||
const { data: subdirectoriesData, refetch: refetchSubdirectories } = useQuery<{ subdirectories: DataTableSubdirectory[] }>({
|
||||
queryKey: ['/api/directories', tableId, 'subdirectories'],
|
||||
enabled: subdirectoriesOpen,
|
||||
});
|
||||
const subdirectories = subdirectoriesData?.subdirectories || [];
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.table) {
|
||||
setTableName(data.table.name);
|
||||
@@ -188,6 +208,104 @@ export default function TableEditor() {
|
||||
}
|
||||
}, [rowEditorOpen, rowEditorInitialValues]);
|
||||
|
||||
const createSubdirectoryMutation = useMutation({
|
||||
mutationFn: async (payload: { childTableId: number; parentColumnIndex: number; childColumnIndex: number; parentValues: string[] }) => {
|
||||
return apiRequest('POST', `/api/directories/${tableId}/subdirectories`, payload);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/directories', tableId, 'subdirectories'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/directories', tableId] });
|
||||
refetchSubdirectories();
|
||||
resetSubdirectoryForm();
|
||||
setSubdirectoriesOpen(false);
|
||||
toast({ title: 'Подсправочник добавлен' });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({ title: 'Ошибка добавления подсправочника', description: error.message, variant: 'destructive' });
|
||||
},
|
||||
});
|
||||
|
||||
const updateSubdirectoryMutation = useMutation({
|
||||
mutationFn: async ({ linkId, payload }: { linkId: number; payload: { childTableId?: number; parentColumnIndex?: number; childColumnIndex?: number; parentValues?: string[] } }) => {
|
||||
return apiRequest('PATCH', `/api/directories/${tableId}/subdirectories/${linkId}`, payload);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/directories', tableId, 'subdirectories'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/directories', tableId] });
|
||||
refetchSubdirectories();
|
||||
resetSubdirectoryForm();
|
||||
setEditingSubdirectory(null);
|
||||
toast({ title: 'Подсправочник обновлён' });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({ title: 'Ошибка обновления подсправочника', description: error.message, variant: 'destructive' });
|
||||
},
|
||||
});
|
||||
|
||||
const deleteSubdirectoryMutation = useMutation({
|
||||
mutationFn: async (linkId: number) => {
|
||||
return apiRequest('DELETE', `/api/directories/${tableId}/subdirectories/${linkId}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/directories', tableId, 'subdirectories'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/directories', tableId] });
|
||||
refetchSubdirectories();
|
||||
toast({ title: 'Подсправочник отвязан' });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({ title: 'Ошибка удаления подсправочника', description: error.message, variant: 'destructive' });
|
||||
},
|
||||
});
|
||||
|
||||
const resetSubdirectoryForm = () => {
|
||||
setSubdirectoryChildTableId('none');
|
||||
setSubdirectoryParentColumnIndex(0);
|
||||
setSubdirectoryChildColumnIndex(0);
|
||||
setSubdirectoryParentValues(new Set());
|
||||
setEditingSubdirectory(null);
|
||||
};
|
||||
|
||||
const openSubdirectoryEditor = (link?: DataTableSubdirectory) => {
|
||||
if (link) {
|
||||
setEditingSubdirectory(link);
|
||||
setSubdirectoryChildTableId(link.childTableId?.toString() || 'none');
|
||||
setSubdirectoryParentColumnIndex(link.parentColumnIndex ?? 0);
|
||||
setSubdirectoryChildColumnIndex(link.childColumnIndex ?? 0);
|
||||
setSubdirectoryParentValues(new Set(link.parentValues || []));
|
||||
} else {
|
||||
resetSubdirectoryForm();
|
||||
}
|
||||
setSubdirectoriesOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveSubdirectory = () => {
|
||||
const childId = parseInt(subdirectoryChildTableId);
|
||||
if (isNaN(childId)) {
|
||||
toast({ title: 'Выберите подсправочник', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
childTableId: childId,
|
||||
parentColumnIndex: subdirectoryParentColumnIndex,
|
||||
childColumnIndex: subdirectoryChildColumnIndex,
|
||||
parentValues: Array.from(subdirectoryParentValues),
|
||||
};
|
||||
if (editingSubdirectory?.id) {
|
||||
updateSubdirectoryMutation.mutate({ linkId: editingSubdirectory.id, payload });
|
||||
} else {
|
||||
createSubdirectoryMutation.mutate(payload);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleParentValue = (value: string) => {
|
||||
setSubdirectoryParentValues((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(value)) next.delete(value);
|
||||
else next.add(value);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const updateTableMutation = useMutation({
|
||||
mutationFn: async (updates: { name?: string; description?: string; columns?: TableColumn[]; treeColumnIndex?: number | null; treeDisplayColumnIndex?: number | null }) => {
|
||||
return apiRequest('PATCH', `/api/directories/${tableId}`, updates);
|
||||
@@ -724,6 +842,10 @@ export default function TableEditor() {
|
||||
)}
|
||||
{canManage && (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => openSubdirectoryEditor()} data-testid="button-subdirectories">
|
||||
<Link2 className="w-4 h-4 mr-2" />
|
||||
Подсправочники
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setPermissionsOpen(true)} data-testid="button-permissions">
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
Права
|
||||
@@ -1616,6 +1738,152 @@ export default function TableEditor() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={subdirectoriesOpen} onOpenChange={(open) => {
|
||||
setSubdirectoriesOpen(open);
|
||||
if (!open) resetSubdirectoryForm();
|
||||
}}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingSubdirectory ? 'Редактировать подсправочник' : 'Подсправочники'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editingSubdirectory
|
||||
? 'Измените привязку подсправочника к родительскому справочнику'
|
||||
: 'Свяжите отдельные справочники с этим справочником. В форме они будут появляться как дополнительный выбор после выбора значения из основного справочника.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{!editingSubdirectory && subdirectories.length > 0 && (
|
||||
<div className="border rounded p-3 space-y-2">
|
||||
<Label className="text-xs">Привязанные подсправочники</Label>
|
||||
<div className="space-y-2">
|
||||
{subdirectories.map((link) => (
|
||||
<div key={link.id} className="flex items-center justify-between p-2 bg-muted/50 rounded">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm truncate">{link.childTable?.name || `ID ${link.childTableId}`}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
по столбцу «{columns[link.parentColumnIndex ?? 0]?.name || '?'}» => «{link.childTable?.columns?.[link.childColumnIndex ?? 0]?.name || '?'}»
|
||||
{link.parentValues && link.parentValues.length > 0 && (
|
||||
<span className="ml-1">({link.parentValues.length} значений)</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => openSubdirectoryEditor(link)}>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => link.id && deleteSubdirectoryMutation.mutate(link.id)}>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>Подсправочник</Label>
|
||||
<Select value={subdirectoryChildTableId} onValueChange={setSubdirectoryChildTableId} disabled={!!editingSubdirectory}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Выберите справочник" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Не выбрано</SelectItem>
|
||||
{allTables
|
||||
.filter((t) => t.id !== tableId)
|
||||
.map((t) => (
|
||||
<SelectItem key={t.id} value={t.id.toString()}>
|
||||
{t.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Столбец родителя</Label>
|
||||
<Select value={String(subdirectoryParentColumnIndex)} onValueChange={(v) => setSubdirectoryParentColumnIndex(parseInt(v))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Столбец" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{columns.map((col, idx) => (
|
||||
<SelectItem key={idx} value={String(idx)}>{col.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Столбец подсправочника</Label>
|
||||
<Select
|
||||
value={String(subdirectoryChildColumnIndex)}
|
||||
onValueChange={(v) => setSubdirectoryChildColumnIndex(parseInt(v))}
|
||||
disabled={subdirectoryChildTableId === 'none'}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Столбец" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allTables
|
||||
.find((t) => t.id.toString() === subdirectoryChildTableId)
|
||||
?.columns?.map((col: any, idx: number) => (
|
||||
<SelectItem key={idx} value={String(idx)}>{col.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="mb-2 block">Значения родительского справочника</Label>
|
||||
<div className="border rounded p-2 max-h-48 overflow-y-auto space-y-1">
|
||||
{displayRows.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">Нет строк в родительском справочнике</div>
|
||||
) : (
|
||||
displayRows.map((row) => {
|
||||
const value = row.values[subdirectoryParentColumnIndex] ?? '';
|
||||
if (!value) return null;
|
||||
return (
|
||||
<label key={row.id} className="flex items-center gap-2 p-1.5 hover:bg-muted rounded cursor-pointer">
|
||||
<Checkbox
|
||||
checked={subdirectoryParentValues.has(String(value))}
|
||||
onCheckedChange={() => toggleParentValue(String(value))}
|
||||
/>
|
||||
<span className="text-sm">{value}</span>
|
||||
</label>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Подсправочник будет доступен в форме только при выборе этих значений из основного справочника.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
{editingSubdirectory && (
|
||||
<Button variant="outline" onClick={resetSubdirectoryForm}>
|
||||
Назад к списку
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => { setSubdirectoriesOpen(false); resetSubdirectoryForm(); }}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveSubdirectory}
|
||||
disabled={subdirectoryChildTableId === 'none' || createSubdirectoryMutation.isPending || updateSubdirectoryMutation.isPending}
|
||||
>
|
||||
{createSubdirectoryMutation.isPending || updateSubdirectoryMutation.isPending ? 'Сохранение...' : 'Сохранить'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user