import { useMemo, useState } from "react"; import { ActionIcon, Badge, Box, Button, Card, Code, Group, Stack, Table, Text, TextInput, ThemeIcon, } from "@mantine/core"; import { FileUp, HardDrive, Layers, Paperclip, Pencil, Plus, Search, Settings, Trash2, X, } from "lucide-react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { api } from "@/services/api"; import { getMinFiles, type FileUploadSetting } from "@/types/fileUploadSettings"; import { DataTable, type ColumnDef } from "@edr/ui-common"; import EditFileUploadSettingDialog from "./EditFileUploadSettingDialog"; import ManageFileUploadFieldsDialog from "./ManageFileUploadFieldsDialog"; import DeleteFileUploadSettingDialog from "./DeleteFileUploadSettingDialog"; export default function FileUploadSettingsPage() { const [query, setQuery] = useState(""); const { data, isLoading, isError, error, refetch } = useQuery( api.fileUploadSettings.list.queryOptions(), ); const deleteMutation = useMutation(api.fileUploadSettings.remove.mutationOptions()); const fileUploadSettings = useMemo( () => (Array.isArray(data) ? data : []), [data], ); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return fileUploadSettings; return fileUploadSettings.filter( (s) => s.code.toLowerCase().includes(q) || s.label.toLowerCase().includes(q) || (s.description ?? "").toLowerCase().includes(q) || s.fields.some( (f) => f.fileKey.toLowerCase().includes(q) || f.fileLabel.toLowerCase().includes(q), ), ); }, [fileUploadSettings, query]); const totalFields = fileUploadSettings.reduce( (sum, s) => sum + s.fields.length, 0, ); const requiredFields = fileUploadSettings.reduce( (sum, s) => sum + s.fields.filter((f) => f.isRequired).length, 0, ); const multiFields = fileUploadSettings.reduce( (sum, s) => sum + s.fields.filter((f) => f.isMultiple).length, 0, ); const columns = useMemo[]>(() => { const headerClassName = ruleEngineTable.headerCell; const cellClassName = ruleEngineTable.bodyCell; return [ { id: "setting", header: "Setting", meta: { headerClassName, cellClassName }, cell: ({ row }) => { const s = row.original; return ( {s.label} {s.description ?? "No description"} ); }, }, { id: "code", header: "Code", meta: { headerClassName, cellClassName }, cell: ({ row }) => {row.original.code}, }, { id: "entity", header: "Entity", meta: { headerClassName, cellClassName }, cell: ({ row }) => ( {row.original.entity ?? "—"} ), }, { id: "fields", header: "Fields", meta: { headerClassName, cellClassName }, cell: ({ row }) => ( {row.original.fields.length} ), }, { id: "requiredMulti", header: "Required / Multi", meta: { headerClassName, cellClassName }, cell: ({ row }) => { const required = row.original.fields.filter((f) => f.isRequired).length; const multi = row.original.fields.filter((f) => f.isMultiple).length; return ( {required} required {multi} multi ); }, }, { id: "maxSize", header: "Max size", meta: { headerClassName, cellClassName }, cell: ({ row }) => { const maxSize = Math.max( 0, ...row.original.fields.map((f) => f.maxSizeMb), ); return ( {maxSize ? `${maxSize} MB` : "—"} ); }, }, { id: "actions", header: "Actions", meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap`, }, cell: ({ row }) => { const setting = row.original; return ( deleteMutation.mutate({ id: setting.id })} > ); }, }, ]; }, [deleteMutation]); const tableStatus = isLoading ? "loading" : isError ? "error" : "success"; return ( } /> } value={query} onChange={(e) => setQuery(e.currentTarget.value)} rightSection={ query ? ( setQuery("")} aria-label="Clear search" > ) : null } style={{ maxWidth: 420 }} /> void refetch(), } : undefined } emptyMessage={ query.trim() ? "No file upload settings match your search." : 'No file upload settings yet. Click "New setting" to add one.' } containerClassName="border-0 shadow-none bg-transparent min-w-[920px]" /> ); } /** * Static reference: how `min_files` / `max_files` are derived from the * Required × Multiple toggles. Documentation aid for whoever wires uploaders. */ function BehaviorReferenceCard() { const rows: { required: boolean; multiple: boolean; min: string; max: string }[] = [ { required: false, multiple: false, min: "0", max: "1" }, { required: true, multiple: false, min: "1", max: "1" }, { required: false, multiple: true, min: "0", max: "field.maxFiles" }, { required: true, multiple: true, min: "1", max: "field.maxFiles" }, ]; return ( Required × Multiple behavior Min and max file counts are derived from these two toggles. The "Max files" you set on a field is only used when{" "} Multiple {" "} is on. Required Multiple min_files max_files {rows.map((r) => ( {r.required ? "Required" : "Optional"} {r.multiple ? "Multiple" : "Single"} {r.min} {r.max} ))}
Helpers getMinFiles and getEffectiveMaxFiles{" "} live in @/types/fileUploadSettings — use them when wiring real uploaders. Example: a field with isRequired=false,{" "} isMultiple=true, maxFiles=5 gives min{" "} {getMinFiles({ isRequired: false, })} {" "} …5.
); }