From 73062cf06d876b1d74b7c5f615d2e48434249444 Mon Sep 17 00:00:00 2001 From: Abhishek Sonwal Date: Mon, 15 Jun 2026 13:12:00 +0530 Subject: [PATCH] feat(contacts): enhance import modal with tag preview and CSV parsing improvements - Added `ImportPreviewTags` component to display tags with color coding in the import modal. - Updated `parseContactCsv` to return an object containing both parsed rows and a flag indicating the presence of a tags column. - Modified state management in the import modal to handle tags and their colors, improving user experience during contact import. - Adjusted the import modal to conditionally render the tags column based on the presence of tags in the preview. --- src/components/contacts/import-modal.tsx | 75 ++++++++++++++++++++-- src/lib/contacts/parse-contact-csv.test.ts | 56 ++++++++-------- src/lib/contacts/parse-contact-csv.ts | 14 ++-- 3 files changed, 109 insertions(+), 36 deletions(-) diff --git a/src/components/contacts/import-modal.tsx b/src/components/contacts/import-modal.tsx index c2d0d66..876064b 100644 --- a/src/components/contacts/import-modal.tsx +++ b/src/components/contacts/import-modal.tsx @@ -8,7 +8,7 @@ import { isUniqueViolation, normalizeKey, } from '@/lib/contacts/dedupe'; -import { parseContactCsv } from '@/lib/contacts/parse-contact-csv'; +import { parseContactCsv, type ParsedContactRow } from '@/lib/contacts/parse-contact-csv'; import { assignImportedContactTags, resolveImportTagIds, @@ -26,6 +26,41 @@ import { import { Button } from '@/components/ui/button'; import { Upload, FileText, Loader2, CheckCircle, XCircle, AlertTriangle } from 'lucide-react'; +const DEFAULT_TAG_COLOR = '#3b82f6'; + +function ImportPreviewTags({ + tagNames, + tagColorByKey, +}: { + tagNames: string[]; + tagColorByKey: Map; +}) { + if (tagNames.length === 0) { + return -; + } + + return ( +
+ {tagNames.map((name) => { + const color = tagColorByKey.get(name.trim().toLowerCase()) ?? DEFAULT_TAG_COLOR; + return ( + + {name} + + ); + })} +
+ ); +} + interface ImportModalProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -38,7 +73,9 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps const fileInputRef = useRef(null); const [file, setFile] = useState(null); - const [parsedRows, setParsedRows] = useState>([]); + const [parsedRows, setParsedRows] = useState([]); + const [hasTagsColumn, setHasTagsColumn] = useState(false); + const [tagColorByKey, setTagColorByKey] = useState>(new Map()); const [importing, setImporting] = useState(false); const [result, setResult] = useState<{ imported: number; @@ -50,6 +87,8 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps function reset() { setFile(null); setParsedRows([]); + setHasTagsColumn(false); + setTagColorByKey(new Map()); setResult(null); if (fileInputRef.current) fileInputRef.current.value = ''; } @@ -67,15 +106,34 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps setResult(null); const text = await selected.text(); - const rows = parseContactCsv(text); + const { rows, hasTagsColumn: csvHasTags } = parseContactCsv(text); if (rows.length === 0) { toast.error('No valid rows found. Ensure CSV has a "phone" column header.'); setParsedRows([]); + setHasTagsColumn(false); + setTagColorByKey(new Map()); return; } setParsedRows(rows); + setHasTagsColumn(csvHasTags); + + if (csvHasTags && accountId) { + const { data: tags } = await supabase + .from('tags') + .select('name, color') + .eq('account_id', accountId); + + const colors = new Map(); + for (const tag of tags ?? []) { + const key = tag.name.trim().toLowerCase(); + if (!colors.has(key)) colors.set(key, tag.color); + } + setTagColorByKey(colors); + } else { + setTagColorByKey(new Map()); + } } async function handleImport() { @@ -228,7 +286,7 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps } const preview = parsedRows.slice(0, 5); - const hasTagsColumn = parsedRows.some((row) => row.tagNames.length > 0); + const previewHasTags = hasTagsColumn || preview.some((row) => row.tagNames.length > 0); return ( @@ -291,7 +349,7 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps Name Email Company - {hasTagsColumn && ( + {previewHasTags && ( Tags )} @@ -303,9 +361,12 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps {row.name || '-'} {row.email || '-'} {row.company || '-'} - {hasTagsColumn && ( + {previewHasTags && ( - {row.tagNames.length > 0 ? row.tagNames.join(', ') : '-'} + )} diff --git a/src/lib/contacts/parse-contact-csv.test.ts b/src/lib/contacts/parse-contact-csv.test.ts index 06c34e3..5fcf4a1 100644 --- a/src/lib/contacts/parse-contact-csv.test.ts +++ b/src/lib/contacts/parse-contact-csv.test.ts @@ -30,36 +30,42 @@ describe("parseContactCsv", () => { +15551234567,Alice,"VIP, Lead" +15559876543,Bob,Customer`; - expect(parseContactCsv(csv)).toEqual([ - { - phone: "+15551234567", - name: "Alice", - email: undefined, - company: undefined, - tagNames: ["VIP", "Lead"], - }, - { - phone: "+15559876543", - name: "Bob", - email: undefined, - company: undefined, - tagNames: ["Customer"], - }, - ]); + expect(parseContactCsv(csv)).toEqual({ + hasTagsColumn: true, + rows: [ + { + phone: "+15551234567", + name: "Alice", + email: undefined, + company: undefined, + tagNames: ["VIP", "Lead"], + }, + { + phone: "+15559876543", + name: "Bob", + email: undefined, + company: undefined, + tagNames: ["Customer"], + }, + ], + }); }); it("returns empty tagNames when tags column is absent", () => { const csv = `phone,name +15551234567,Alice`; - expect(parseContactCsv(csv)).toEqual([ - { - phone: "+15551234567", - name: "Alice", - email: undefined, - company: undefined, - tagNames: [], - }, - ]); + expect(parseContactCsv(csv)).toEqual({ + hasTagsColumn: false, + rows: [ + { + phone: "+15551234567", + name: "Alice", + email: undefined, + company: undefined, + tagNames: [], + }, + ], + }); }); }); diff --git a/src/lib/contacts/parse-contact-csv.ts b/src/lib/contacts/parse-contact-csv.ts index 95381cb..9a7b9e3 100644 --- a/src/lib/contacts/parse-contact-csv.ts +++ b/src/lib/contacts/parse-contact-csv.ts @@ -31,16 +31,22 @@ export function parseTagCell(value: string | undefined): string[] { return names; } -export function parseContactCsv(text: string): ParsedContactRow[] { +export interface ParseContactCsvResult { + rows: ParsedContactRow[]; + /** True when the CSV header includes a `tags` column. */ + hasTagsColumn: boolean; +} + +export function parseContactCsv(text: string): ParseContactCsvResult { const lines = text.trim().split(/\r?\n/); - if (lines.length < 2) return []; + if (lines.length < 2) return { rows: [], hasTagsColumn: false }; const headers = lines[0] .split(",") .map((h) => h.trim().toLowerCase().replace(/["']/g, "")); const phoneIdx = headers.indexOf("phone"); - if (phoneIdx === -1) return []; + if (phoneIdx === -1) return { rows: [], hasTagsColumn: false }; const nameIdx = headers.indexOf("name"); const emailIdx = headers.indexOf("email"); @@ -76,7 +82,7 @@ export function parseContactCsv(text: string): ParsedContactRow[] { }); } - return rows; + return { rows, hasTagsColumn: tagsIdx >= 0 }; } /** Simple CSV line parse (handles quoted fields). */