import { type Component, createSignal, createEffect, onMount, For, Show, Suspense, lazy } from "solid-js"; import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertTagDefinition, type TaskTemplate, currentTaskContext } from "@/store"; import { Search, Command, Clock, ArrowUpCircle, Copy, ChevronDown, Gauge, RotateCcw } from "lucide-solid"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { TextField, TextFieldInput } from "@/components/ui/textfield"; import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select"; import { TagPicker } from "@/components/TagPicker"; import { useTheme } from "./ThemeProvider"; import { getThemeAdjustedColor } from "@/lib/colors"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { toast } from "solid-sonner"; import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS } from "@/lib/constants"; const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor }))); interface QuickEntryFormProps { onSuccess?: () => void; autoFocus?: boolean; initialTitle?: string; initialTags?: string[]; } // Global state for QuickEntry persistence const [input, setInput] = createSignal(""); const [priority, setPriority] = createSignal(""); const [urgency, setUrgency] = createSignal(""); const [tags, setTags] = createSignal([]); const [description, setDescription] = createSignal(""); const [size, setSize] = createSignal("3"); const [dateString, setDateString] = createSignal(""); const [showValidation, setShowValidation] = createSignal(false); let lastParsedTags: string[] = []; const resetQuickEntryState = () => { setInput(""); setPriority(""); setUrgency(""); setTags([]); setDescription(""); setSize("3"); setDateString(""); setShowValidation(false); lastParsedTags = []; }; export const QuickEntryForm: Component = (props) => { const { resolvedTheme } = useTheme(); const [editorInstance, setEditorInstance] = createSignal(null); let inputRef: HTMLInputElement | undefined; onMount(() => { if (props.autoFocus) { setTimeout(() => inputRef?.focus(), 100); } if (props.initialTitle && !input()) { setInput(props.initialTitle); } }); // Reactive sync from props (handles context changes while open) createEffect(() => { const initial = props.initialTags || []; if (initial.length > 0) { setTags(prev => { // Merge initial tags if not already present const next = [...prev]; initial.forEach(t => { if (!next.includes(t)) next.push(t); }); return next; }); } }); // Reactive shorthand parsing createEffect(() => { const text = input(); const pMatch = text.match(/\/p(\d+)/); if (pMatch) { const val = Math.min(10, Math.max(1, parseInt(pMatch[1]))).toString(); setPriority(val); } const uMatch = text.match(/\/u(\d+)/); if (uMatch) { const val = Math.min(10, Math.max(1, parseInt(uMatch[1]))).toString(); if (val !== urgency()) { onUrgencySimpleChange(val); } } const tagRegex = /\/t\s*([^:/]+)(?=(?:[:/]))(?:[:](\d+))?/g; const newParsedTags: string[] = []; let match; while ((match = tagRegex.exec(text)) !== null) { const tagName = match[1].trim(); if (tagName) { newParsedTags.push(tagName); } } const currentTags = [...tags()]; const tagsToRemove = lastParsedTags.filter(t => !newParsedTags.includes(t)); const tagsToAdd = newParsedTags.filter(t => !currentTags.includes(t)); let nextTags = currentTags.filter(t => !tagsToRemove.includes(t)); nextTags = [...nextTags, ...tagsToAdd]; if (JSON.stringify(nextTags) !== JSON.stringify(currentTags)) { setTags(nextTags); } lastParsedTags = newParsedTags; }); const onUrgencySimpleChange = (val: any) => { const value = typeof val === 'object' ? val.value : val; if (!value) return; setUrgency(value); const level = parseInt(value); if (!isNaN(level)) { const newDateIso = calculateDateFromUrgency(level); const dateObj = new Date(newDateIso); const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16); setDateString(localIso); } }; const onDateInputChange = (e: Event) => { const val = (e.currentTarget as HTMLInputElement).value; setDateString(val); if (val) { const level = calculateUrgencyFromDate(new Date(val).toISOString()); setUrgency(level.toString()); } }; const parseAndAdd = async () => { let text = input(); if (!text || !priority() || !urgency()) { setShowValidation(true); toast.error("Please fill in all required fields (Title, Priority, Urgency)"); if (!text) inputRef?.focus(); return; } let finalPriority = parseInt(priority()); let finalUrgency = parseInt(urgency()); const finalTags = [...tags()]; const pMatch = text.match(/\/p\s*(\d+)/); if (pMatch) { finalPriority = Math.min(10, Math.max(1, parseInt(pMatch[1]))); text = text.replace(pMatch[0], ""); } const uMatch = text.match(/\/u\s*(\d+)/); if (uMatch) { finalUrgency = Math.min(10, Math.max(1, parseInt(uMatch[1]))); text = text.replace(uMatch[0], ""); } const segments = text.split(/\/t\s*/); let title = segments[0]; for (let i = 1; i < segments.length; i++) { const seg = segments[i]; const firstColon = seg.indexOf(':'); let tagName = ""; let tagValue: number | undefined; let remainingText = ""; if (firstColon === -1) { tagName = seg.trim(); } else { tagName = seg.slice(0, firstColon).trim(); const afterColon = seg.slice(firstColon + 1); const valueMatch = afterColon.match(/^\s*(\d+)/); if (valueMatch) { tagValue = Math.min(10, Math.max(0, parseInt(valueMatch[1]))); remainingText = afterColon.slice(valueMatch[0].length); } else { remainingText = afterColon; } } if (tagName) { if (!finalTags.includes(tagName)) { finalTags.push(tagName); } if (tagValue !== undefined) { await upsertTagDefinition(tagName, tagValue, undefined, resolvedTheme()); } } title += " " + remainingText; } let finalDueDate: Date | null = null; if (dateString()) { finalDueDate = new Date(dateString()); } else { const u = finalUrgency; const dateStr = calculateDateFromUrgency(u); finalDueDate = new Date(dateStr); } // Apply context tags at the time of creation const ctx = currentTaskContext(); if (store.isNotepadMode) { const activeNoteId = (window as any)._activeNoteId; if (activeNoteId) { const note = store.notes.find(n => n.id === activeNoteId); if (note && note.title) { const noteTagName = note.title.trim(); if (noteTagName && !finalTags.includes(noteTagName)) finalTags.push(noteTagName); } } } else if (typeof ctx === 'object') { if ('bucketId' in ctx) { const bucketName = ctx.name; if (bucketName) { const tag = `@${bucketName}`; if (!finalTags.includes(tag)) finalTags.push(tag); } } else if ('userId' in ctx) { const userName = ctx.name; if (userName) { const tag = `@${userName}`; if (!finalTags.includes(tag)) finalTags.push(tag); } } } addTask(title.replace(/\s+/g, " ").trim(), { priority: finalPriority, dueDate: finalDueDate || undefined, tags: finalTags, content: description(), size: parseInt(size()) }); resetQuickEntryState(); const instance = editorInstance(); if (instance) instance.commands.setContent(""); if (props.onSuccess) props.onSuccess(); }; const applyTemplate = (template: TaskTemplate) => { setInput(template.title); setPriority(template.priority.toString()); setUrgency(template.urgency.toString()); setTags([...(template.tags || [])]); setDescription(template.content || ""); const level = template.urgency; const newDateIso = calculateDateFromUrgency(level); const dateObj = new Date(newDateIso); const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16); setDateString(localIso); const instance = editorInstance(); if (instance) { instance.commands.setContent(template.content || ""); } toast.info(`Applied template: ${template.name}`); }; return (
setInput(e.currentTarget.value)} onKeyDown={(e) => e.key === "Enter" && parseAndAdd()} placeholder="Task title... type /p1-10 for priority and /u1-10 for urgency" class="border-none shadow-none focus-visible:ring-0 text-lg p-0" />
Title is required to create a task
value={priority()} onChange={(val) => { const v = typeof val === 'object' ? val.value : val; if (v) setPriority(v); }} options={PRIORITY_OPTIONS} optionValue="value" optionTextValue="label" placeholder="Priority" itemComponent={(props) => ( {props.item.rawValue.label} )} validationState={showValidation() && !priority() ? "invalid" : "valid"} >
{PRIORITY_OPTIONS.find(o => o.value === priority())?.label || (priority() === "" ? "Pick Priority" : priority())}
value={size()} onChange={(val) => { const v = typeof val === 'object' ? val.value : val; if (v) setSize(v); }} options={SIZE_OPTIONS} optionValue="value" optionTextValue="label" placeholder="Size" itemComponent={(props) => ( {props.item.rawValue.label} )} >
{SIZE_OPTIONS.find(o => o.value === size())?.label || size()}
value={urgency()} onChange={(val) => onUrgencySimpleChange(val)} options={URGENCY_OPTIONS} optionValue="value" optionTextValue="label" placeholder="Urgency" itemComponent={(props) => ( {props.item.rawValue.label} )} validationState={showValidation() && !urgency() ? "invalid" : "valid"} >
{URGENCY_OPTIONS.find(o => o.value === urgency())?.label || (urgency() === "" ? "Pick Urgency" : urgency())}
!t.endsWith("_favorite__"))}> {(tag) => ( { const def = store.tagDefinitions.find(d => d.name === tag); const color = getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()); return color ? `${color}15` : undefined; })(), "border-color": (() => { const def = store.tagDefinitions.find(d => d.name === tag); const color = getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()); return color ? `${color}30` : undefined; })(), "color": (() => { const def = store.tagDefinitions.find(d => d.name === tag); return getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()) || "inherit"; })() }} onClick={() => setTags(tags().filter(t => t !== tag))} >
d.name === tag)?.value ?? 5) > 5 ? "#22c55e" : (store.tagDefinitions.find(d => d.name === tag)?.value ?? 5) < 5 ? "#ef4444" : "#94a3b8" }} /> {tag} )}
(e.target as HTMLInputElement).showPicker?.()} onKeyDown={(e) => { if (e.key !== "Tab" && e.key !== "Escape") { e.preventDefault(); } }} step="900" class="w-full h-10 px-3 bg-background border-none rounded-md text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring relative cursor-pointer" />
}>
Enter to Add
0}>
{(template) => ( )}
); };