From 414eead28dc7a4b0b0eb62a7322b608e20b277eb Mon Sep 17 00:00:00 2001 From: Timothy Cardoza Date: Mon, 9 Feb 2026 15:13:51 -0600 Subject: [PATCH] added bulk import tool --- src/components/ImportTool.tsx | 367 ++++++++++++++++++++++++++++++++++ src/views/SettingsView.tsx | 31 ++- 2 files changed, 397 insertions(+), 1 deletion(-) create mode 100644 src/components/ImportTool.tsx diff --git a/src/components/ImportTool.tsx b/src/components/ImportTool.tsx new file mode 100644 index 0000000..4d5c986 --- /dev/null +++ b/src/components/ImportTool.tsx @@ -0,0 +1,367 @@ +import { type Component, createSignal, For, Show } from "solid-js"; +import { addTask, calculateDateFromUrgency } from "@/store"; +import { Button } from "@/components/ui/button"; +import { TagPicker } from "@/components/TagPicker"; +import { Badge } from "@/components/ui/badge"; +import { toast } from "solid-sonner"; +import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS } from "@/lib/constants"; +import { X, ArrowRight, Upload, Loader2 } from "lucide-solid"; + +interface ImportedTask { + id: number; + title: string; + priority: string; + urgency: string; + size: string; + tags: string[]; +} + +/** + * Strips common list prefixes from a line of text: + * - Checkboxes: ☐ ☑ ✅ ✓ ✗ ✘ ❌ - [ ] - [x] - [X] + * - Bullets: • ‣ ◦ ▪ ▸ ► ○ ● - * + + * - Numbered: 1. 2) 3 - + */ +const stripPrefix = (line: string): string => { + // Regex matches everything from the start of the string that is NOT a letter or a number + // \p{L} matches any kind of letter from any language + // \p{N} matches any kind of numeric character + // The 'u' flag is required for unicode property escapes + return line.replace(/^[^\p{L}\p{N}]*/u, "").trim(); +}; + +export const ImportTool: Component = () => { + const [step, setStep] = createSignal<"paste" | "configure">("paste"); + const [rawText, setRawText] = createSignal(""); + const [tasks, setTasks] = createSignal([]); + const [importing, setImporting] = createSignal(false); + const [importProgress, setImportProgress] = createSignal(0); + + // Bulk defaults + const [bulkPriority, setBulkPriority] = createSignal("5"); + const [bulkUrgency, setBulkUrgency] = createSignal("5"); + const [bulkSize, setBulkSize] = createSignal("5"); + const [bulkTags, setBulkTags] = createSignal([]); + + const parseText = () => { + const lines = rawText() + .split("\n") + .map(l => stripPrefix(l)) + .filter(l => l.length > 0); + + if (lines.length === 0) { + toast.error("No tasks found in pasted text."); + return; + } + + const parsed: ImportedTask[] = lines.map((title, i) => ({ + id: i, + title, + priority: bulkPriority(), + urgency: bulkUrgency(), + size: bulkSize(), + tags: [...bulkTags()] + })); + + setTasks(parsed); + setStep("configure"); + }; + + const applyBulkDefaults = () => { + setTasks(prev => + prev.map(t => ({ + ...t, + priority: bulkPriority(), + urgency: bulkUrgency(), + size: bulkSize(), + tags: [...bulkTags()] + })) + ); + }; + + const updateTask = (id: number, field: keyof ImportedTask, value: any) => { + setTasks(prev => prev.map(t => t.id === id ? { ...t, [field]: value } : t)); + }; + + const removeTask = (id: number) => { + setTasks(prev => prev.filter(t => t.id !== id)); + }; + + const handleImport = async () => { + const taskList = tasks(); + if (taskList.length === 0) return; + + setImporting(true); + setImportProgress(0); + + let successCount = 0; + for (let i = 0; i < taskList.length; i++) { + const t = taskList[i]; + try { + const urgencyLevel = parseInt(t.urgency); + const dueDate = new Date(calculateDateFromUrgency(urgencyLevel)); + + await addTask(t.title, { + priority: parseInt(t.priority), + dueDate, + tags: t.tags.length > 0 ? t.tags : ["work"], + size: parseInt(t.size) + }); + successCount++; + } catch (err) { + console.error(`Failed to import task: ${t.title}`, err); + } + setImportProgress(i + 1); + } + + setImporting(false); + toast.success(`Imported ${successCount} task${successCount !== 1 ? "s" : ""} successfully!`); + + // Reset + setRawText(""); + setTasks([]); + setStep("paste"); + setBulkPriority("5"); + setBulkUrgency("5"); + setBulkSize("5"); + setBulkTags([]); + }; + + const reset = () => { + setStep("paste"); + setTasks([]); + }; + + return ( +
+ +
+
+ +