update tags and add description editor to add-task

This commit is contained in:
2026-01-31 14:48:25 -06:00
parent d6864a9443
commit ce58ee8fc5
6 changed files with 112 additions and 47 deletions
+41 -30
View File
@@ -1,12 +1,15 @@
import { type Component, createSignal, createEffect, onCleanup, onMount, For } from "solid-js";
import { Search, Command, Clock, ArrowUpCircle, Plus } from "lucide-solid";
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertTagDefinition } from "@/store";
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertMultipleTagDefinitions } from "@/store";
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 { lazy, Suspense } from "solid-js";
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
export const QuickEntry: Component = () => {
// Diff tracking for reactive tags
@@ -17,6 +20,8 @@ export const QuickEntry: Component = () => {
const [priority, setPriority] = createSignal<string>("5");
const [urgency, setUrgency] = createSignal<string>("5");
const [tags, setTags] = createSignal<string[]>([]);
const [description, setDescription] = createSignal("");
const [editorInstance, setEditorInstance] = createSignal<any>(null);
// Reactive shorthand parsing
createEffect(() => {
@@ -49,10 +54,6 @@ export const QuickEntry: Component = () => {
const tagName = match[1].trim();
if (tagName) {
newParsedTags.push(tagName);
if (match[2]) {
const val = Math.min(10, Math.max(0, parseInt(match[2])));
upsertTagDefinition(tagName, val);
}
}
}
@@ -124,51 +125,40 @@ export const QuickEntry: Component = () => {
};
const parseAndAdd = async () => {
const text = input();
let text = input();
if (!text || !priority() || !urgency()) return;
let finalPriority = parseInt(priority());
let finalUrgency = parseInt(urgency());
const finalTags = [...tags()];
let title = text;
// 1. Handle Priority / Urgency (Simple regexes as they don't have spaces)
const pMatch = text.match(/\/p(\d+)/);
// 1. Handle Priority / Urgency and clean them from text
const pMatch = text.match(/\/p\s*(\d+)/);
if (pMatch) {
finalPriority = Math.min(10, Math.max(1, parseInt(pMatch[1])));
title = title.replace(pMatch[0], "");
text = text.replace(pMatch[0], "");
}
const uMatchNumeric = text.match(/\/u(\d+)/);
if (uMatchNumeric) {
finalUrgency = Math.min(10, Math.max(1, parseInt(uMatchNumeric[1])));
title = title.replace(uMatchNumeric[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], "");
}
// 2. Handle Tags: /t [name]:[value]
// We look for /t, then name, then optional :value.
// We split by /t to find all segments.
const segments = text.split(/\/t\s*/);
// segments[0] is the part before first /t
title = segments[0];
let title = segments[0];
const tagsToUpsert: Record<string, number> = {};
for (let i = 1; i < segments.length; i++) {
const seg = segments[i];
// Name ends at first : or / (but / is handled by split) or end of string
const firstColon = seg.indexOf(':');
let tagName = "";
let tagValue: number | undefined;
let remainingText = "";
if (firstColon === -1) {
// No colon in this segment. Segment might be "Tag Name Title" or "Tag Name /t"
// But the user said / or : are endpoints.
// So if no colon, the WHOLE segment is the tag name?
// Wait, if I type "/t Tag Title", the user said / and : are endpoints.
// So if neither is there, the tag continues to end of string?
// Actually, let's treat the FIRST SPACE after some non-space text as a fallback?
// No, user specifically said: "It should allow spaces in this particular parse... use / or : as end points."
// So if I type "/t Tag Name", it IS the tag name.
tagName = seg.trim();
} else {
tagName = seg.slice(0, firstColon).trim();
@@ -179,7 +169,6 @@ export const QuickEntry: Component = () => {
tagValue = Math.min(10, Math.max(0, parseInt(valueMatch[1])));
remainingText = afterColon.slice(valueMatch[0].length);
} else {
// Colon was just a separator for title
remainingText = afterColon;
}
}
@@ -189,12 +178,18 @@ export const QuickEntry: Component = () => {
finalTags.push(tagName);
}
if (tagValue !== undefined) {
upsertTagDefinition(tagName, tagValue);
tagsToUpsert[tagName] = tagValue;
} else if (store.tagDefinitions?.[tagName] === undefined) {
tagsToUpsert[tagName] = 5;
}
}
title += " " + remainingText;
}
if (Object.keys(tagsToUpsert).length > 0) {
upsertMultipleTagDefinitions(tagsToUpsert);
}
let finalDueDate: Date | null = null;
if (dateString()) {
finalDueDate = new Date(dateString());
@@ -207,16 +202,21 @@ export const QuickEntry: Component = () => {
addTask(title.replace(/\s+/g, " ").trim(), {
priority: finalPriority,
dueDate: finalDueDate || undefined,
tags: finalTags
tags: finalTags,
content: description()
});
setInput("");
setPriority("5");
setUrgency("5");
setTags([]);
setDescription("");
lastParsedTags = [];
setDateString("");
setIsOpen(false);
// Reset editor content
const instance = editorInstance();
if (instance) instance.commands.setContent("");
};
return (
@@ -335,6 +335,17 @@ export const QuickEntry: Component = () => {
</For>
</div>
<div class="px-4 py-3 bg-card border-b border-border min-h-[120px] max-h-[300px] overflow-y-auto">
<Suspense fallback={<div class="h-20 animate-pulse bg-muted/20 rounded-lg" />}>
<TaskEditor
content={description()}
onUpdate={setDescription}
onEditorReady={setEditorInstance}
class="min-h-[100px] prose-sm"
/>
</Suspense>
</div>
<div class="p-4 bg-muted/30 flex justify-between items-center">
<div class="flex items-center space-x-2 text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
<Command size={10} />
+1 -1
View File
@@ -75,7 +75,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
<div class="space-y-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Value (Influence)</label>
<select
value={valueScore()}
value={valueScore().toString()}
onInput={(e) => setValueScore(parseInt(e.currentTarget.value))}
class="flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring appearance-none"
>
+15 -3
View File
@@ -1,8 +1,8 @@
import { type Component, createEffect, createSignal, For } from "solid-js";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { type Task, removeTask, restoreTask, updateTask } from "@/store";
import { type Task, removeTask, restoreTask, updateTask, copyTask } from "@/store";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { ArrowUpCircle, Clock, Calendar, Type, Trash2 } from "lucide-solid";
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, Copy } from "lucide-solid";
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
import { Button } from "./ui/button";
import { TagPicker } from "./TagPicker";
@@ -188,8 +188,20 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
/>
</div>
{/* Delete Button */}
{/* Copy button */}
<div class="flex items-center gap-1 shrink-0 ml-auto">
<Button
variant="ghost"
size="sm"
class="h-8 w-8 px-0 hover:bg-muted/50 text-muted-foreground transition-colors"
onClick={() => copyTask(props.task.id)}
>
<Copy size={16} />
</Button>
</div>
{/* Delete Button */}
<div class="flex items-center gap-1 shrink-0">
<Button
variant="ghost"
size="sm"
+1 -1
View File
@@ -11,7 +11,7 @@ export const ThemeToggle: Component = () => {
variant="ghost"
size="icon"
onClick={() => setTheme(theme() === "dark" ? "light" : "dark")}
class="w-9 h-9 px-0"
class="w-9 h-9 px-0 relative"
>
<Sun class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />