// --------------------------------------------------------------------------- // customer-grid / SelectFromFile.tsx — WAVE 21 item 11 (R10, contract C5). // // "Select from file…": paste a list or upload a sheet, say which column holds // the names, and the grid ticks those records. // // The dialog decides NOTHING. Parsing a paste, matching, and the three-bucket // result all live in `fileSelect.ts` where a gate runs them under node; this is // the renderer and the file-picker chrome around them. // --------------------------------------------------------------------------- import { useMemo, useRef, useState } from "react"; import { uploadTabular } from "./apiBridge"; import type { TabularUpload } from "./apiBridge"; import { matchRowsByValue, matchSummary, parsePastedList } from "./fileSelect"; import type { MatchResult, MatchRow } from "./fileSelect"; import type { Field } from "./types"; /** The kinds a name can plausibly live in. A rating or a checkbox column is not * something anyone pastes a list of, and offering all 27 field types makes the * useful three hard to find. */ const MATCHABLE = new Set([ "text", "longtext", "select", "multiselect", "email", "url", "phone", "int", "currency", "number", "date", "user", "automation", "formula", ]); export interface SelectFromFileProps { /** Columns the user may match ON — the grid's fields, in grid order. */ fields: Field[]; /** The identity column: the default grid column, and the one that makes this * feature obvious ("paste customer names"). */ primaryKey: string; /** ⚠ The rows the VIEW MATCHED — never the painted slice. See fileSelect.ts. */ inView: MatchRow[]; /** Every row in this table, for the "in the table but not in this view" bucket. */ wholeTable: MatchRow[]; /** The view being selected in, named in the dialog so the counts have a subject. */ viewName: string; onSelect: (pids: number[]) => void; onClose: () => void; } export default function SelectFromFile({ fields, primaryKey, inView, wholeTable, viewName, onSelect, onClose, }: SelectFromFileProps) { const matchable = useMemo( () => fields.filter((f) => MATCHABLE.has(f.type) || f.key === primaryKey), [fields, primaryKey] ); const [gridKey, setGridKey] = useState(primaryKey); const [paste, setPaste] = useState(""); const [upload, setUpload] = useState(null); const [fileName, setFileName] = useState(""); const [fileCol, setFileCol] = useState(""); const [busy, setBusy] = useState(false); const [result, setResult] = useState(null); const fileRef = useRef(null); const pasted = useMemo(() => parsePastedList(paste), [paste]); // The upload wins when there is one: the user picked a file most recently, and // a dialog that matched a stale paste behind an uploaded sheet would be // answering a question nobody asked. const values = upload ? upload.values[fileCol] ?? [] : pasted.values; const canRun = values.length > 0 && !!gridKey; const run = () => { const r = matchRowsByValue(values, gridKey, inView, wholeTable); setResult(r); onSelect(r.pids); }; const takeFile = async (file: File | undefined) => { if (!file) return; setBusy(true); setResult(null); const parsed = await uploadTabular(file); setBusy(false); if (!parsed) return; // the bridge has already said why setUpload(parsed); setFileName(file.name); setFileCol(parsed.columns[0] ?? ""); setPaste(""); }; /** The misses, ready to paste back into whatever the list came from. Every * value, never a "…and 40 more" — the whole point of listing them is that the * user has to go and fix them. */ const copyList = (list: string[]) => { void navigator.clipboard?.writeText(list.join("\n")); }; return (

Select records from a list

Paste a list or upload a sheet, and the matching records in{" "} {viewName} are ticked. Values are compared exactly, ignoring case and surrounding spaces.