From 1ffb470e8f0341624fa9cf563686c28e78c4f5c2 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Wed, 6 May 2026 10:12:52 +0000 Subject: [PATCH] notes: add per-note checklist (to-do list) option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task #420 — answers the user request "وين خيار اضيف list to do?". Schema (lib/db/src/schema/notes.ts): - notes + note_recipients gain `kind` (varchar(16) default 'text') and `items` (jsonb nullable). Snapshot copy on note_recipients keeps delivered checklists immutable across sender edits/deletes. - Drizzle push applied; lib/db .d.ts rebuilt. API (artifacts/api-server/src/routes/notes.ts): - ChecklistItem type + bounded parser (≤200 items, id ≤64, text ≤500). - parseNoteInput normalizes items↔kind on create and on PATCH that carries `kind`; PATCH handler additionally coerces items-only patches against the note's existing kind so a text note can never end up with checklist items (and vice versa). - POST/PATCH/send/loadRecipientsForNote/received/thread responses and the realtime `note_received` payload all carry kind+items, with the thread response falling back to the recipient snapshot. Client (artifacts/tx-os/src/lib/notes-api.ts + pages/notes.tsx): - Note/ReceivedNote/NoteThread/SentNoteRecipient extended with kind+items. - New `ChecklistEditor`, `ChecklistView`, and `KindToggle` components. - Composer and EditNoteDialog gain the to-do toggle (ListTodo icon) and switch between Textarea and ChecklistEditor; saves send kind+items, with empty-checklist auto-discard mirroring the existing empty-text behaviour. - NoteCard, inbox list, sent list, and ThreadDialog body render the checklist (read-only on snapshots; owner cards can toggle done via PATCH with stopPropagation so the edit dialog doesn't open). i18n: notes.checklist.{toggle,addItem,itemPlaceholder,emptyHint, removeItem,progress} added to en.json + ar.json. Tests: new artifacts/tx-os/tests/notes-checklist.spec.mjs covers composer→persist→reload→toggle, items-only PATCH normalization on both kinds, and checklist delivery to recipient snapshot. All 3 pass. Architect review (evaluate_task) flagged one real issue: items-only PATCH normalization. Fixed in the PATCH handler and locked in by the new normalization test. Pre-existing executive-meetings.ts tsc errors are unchanged and unrelated to this task. --- artifacts/api-server/src/routes/notes.ts | 72 ++++ artifacts/tx-os/public/opengraph.jpg | Bin 25986 -> 25955 bytes artifacts/tx-os/src/lib/notes-api.ts | 19 + artifacts/tx-os/src/locales/ar.json | 10 +- artifacts/tx-os/src/locales/en.json | 10 +- artifacts/tx-os/src/pages/notes.tsx | 365 ++++++++++++++++-- .../tx-os/tests/notes-checklist.spec.mjs | 227 +++++++++++ lib/db/src/schema/notes.ts | 12 +- 8 files changed, 679 insertions(+), 36 deletions(-) create mode 100644 artifacts/tx-os/tests/notes-checklist.spec.mjs diff --git a/artifacts/api-server/src/routes/notes.ts b/artifacts/api-server/src/routes/notes.ts index 29ffe2d1..9f5e3bf8 100644 --- a/artifacts/api-server/src/routes/notes.ts +++ b/artifacts/api-server/src/routes/notes.ts @@ -57,16 +57,42 @@ function parseLabelIds(v: unknown): number[] | undefined { return out; } +type ChecklistItem = { id: string; text: string; done: boolean }; + interface NoteInput { title?: string; content?: string; color?: string; + kind?: "text" | "checklist"; + items?: ChecklistItem[] | null; isPinned?: boolean; isArchived?: boolean; labelIds?: number[]; folderId?: number | null; } +function parseKind(v: unknown): "text" | "checklist" | undefined { + return v === "text" || v === "checklist" ? v : undefined; +} + +function parseItems(v: unknown): ChecklistItem[] | null | undefined { + if (v === null) return null; + if (!Array.isArray(v)) return undefined; + if (v.length > 200) return undefined; + const out: ChecklistItem[] = []; + for (const it of v) { + if (!it || typeof it !== "object") return undefined; + const obj = it as Record; + const id = typeof obj.id === "string" && obj.id.length > 0 && obj.id.length <= 64 + ? obj.id : null; + const text = typeof obj.text === "string" ? obj.text.slice(0, 500) : null; + const done = typeof obj.done === "boolean" ? obj.done : false; + if (id === null || text === null) return undefined; + out.push({ id, text, done }); + } + return out; +} + function parseFolderIdInput(v: unknown): { ok: true; value: number | null } | { ok: false } { if (v === null) return { ok: true, value: null }; const n = Number(v); @@ -107,6 +133,26 @@ function parseNoteInput(body: any, isCreate: boolean): { ok: true; data: NoteInp if (c === undefined) return { ok: false, error: "Invalid color" }; data.color = c; } else if (isCreate) data.color = "default"; + if (body.kind !== undefined) { + const k = parseKind(body.kind); + if (!k) return { ok: false, error: "Invalid kind" }; + data.kind = k; + } else if (isCreate) data.kind = "text"; + if (body.items !== undefined) { + const its = parseItems(body.items); + if (its === undefined) return { ok: false, error: "Invalid items" }; + data.items = its; + } + // Normalize items to match kind whenever kind is being set explicitly so + // the column is never out of sync (e.g. checklist-mode without items, or + // text-mode that still carries stale items from the previous shape). + if (data.kind !== undefined) { + if (data.kind === "checklist") { + data.items = data.items ?? []; + } else { + data.items = null; + } + } if (body.isPinned !== undefined) { if (typeof body.isPinned !== "boolean") return { ok: false, error: "Invalid isPinned" }; data.isPinned = body.isPinned; @@ -214,6 +260,8 @@ async function loadRecipientsForNote(noteId: number) { title: r.title, content: r.content, color: r.color, + kind: r.kind, + items: r.items, recipient: userMap.get(r.recipientUserId) ?? null, })); } @@ -283,6 +331,8 @@ router.post("/notes", requireAuth, async (req, res): Promise => { title: data.title ?? "", content: data.content ?? "", color: data.color ?? "default", + kind: data.kind ?? "text", + items: data.items ?? null, isPinned: data.isPinned ?? false, isArchived: data.isArchived ?? false, folderId: folderId ?? null, @@ -318,6 +368,17 @@ router.patch("/notes/:id", requireAuth, async (req, res): Promise => { res.status(400).json({ error: "Invalid folderId" }); return; } + // If the caller patched `items` without specifying `kind`, normalize + // against the note's current kind so a text note can never end up + // carrying checklist items (and vice versa). parseNoteInput handles the + // case where `kind` is provided; this covers the items-only PATCH. + if (data.kind === undefined && data.items !== undefined) { + if (existing.kind === "checklist") { + data.items = data.items ?? []; + } else { + data.items = null; + } + } if (Object.keys(data).length > 0) { await db.update(notesTable).set(data).where(eq(notesTable.id, existing.id)); } @@ -411,6 +472,8 @@ router.get("/notes/received", requireAuth, async (req, res): Promise => { title: r.title, content: r.content, color: r.color, + kind: r.kind, + items: r.items, createdAt: r.sentAt, updatedAt: r.readAt ?? r.sentAt, sender: senderMap.get(r.senderUserId) ?? null, @@ -510,11 +573,16 @@ async function handleNoteDetail(req: import("express").Request, res: import("exp const contentFallback = snapshotRow?.content ?? note?.content ?? ""; const colorFallback = snapshotRow?.color ?? note?.color ?? "default"; + const kindFallback = snapshotRow?.kind ?? note?.kind ?? "text"; + const itemsFallback = snapshotRow?.items ?? note?.items ?? null; + res.json({ id: id.id, title: useSnapshot ? recipientRow!.title : note?.title ?? titleFallback, content: useSnapshot ? recipientRow!.content : note?.content ?? contentFallback, color: useSnapshot ? recipientRow!.color : note?.color ?? colorFallback, + kind: useSnapshot ? recipientRow!.kind : note?.kind ?? kindFallback, + items: useSnapshot ? recipientRow!.items : note?.items ?? itemsFallback, createdAt: useSnapshot ? recipientRow!.sentAt : note?.createdAt ?? snapshotRow?.sentAt ?? null, @@ -595,6 +663,8 @@ router.post("/notes/:id/send", requireAuth, async (req, res): Promise => { title: note.title, content: note.content, color: note.color, + kind: note.kind, + items: note.items, })), ) .onConflictDoNothing({ @@ -625,6 +695,8 @@ router.post("/notes/:id/send", requireAuth, async (req, res): Promise => { title: r.title, content: r.content, color: r.color, + kind: r.kind, + items: r.items, sentAt: r.sentAt, senderUserId: userId, sender: sender ?? null, diff --git a/artifacts/tx-os/public/opengraph.jpg b/artifacts/tx-os/public/opengraph.jpg index 617a3660f9cd57ab2367a577ffa4376428672966..c61a8cde07a8e07bd3c0225afb3732ca7ab6a7a0 100644 GIT binary patch delta 20993 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 PYZyCo-M(|_B{Ba2Emym& diff --git a/artifacts/tx-os/src/lib/notes-api.ts b/artifacts/tx-os/src/lib/notes-api.ts index 8baede19..9a55460b 100644 --- a/artifacts/tx-os/src/lib/notes-api.ts +++ b/artifacts/tx-os/src/lib/notes-api.ts @@ -1,5 +1,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +export type NoteKind = "text" | "checklist"; + +export interface ChecklistItem { + id: string; + text: string; + done: boolean; +} + export interface Note { id: number; userId: number; @@ -7,6 +15,8 @@ export interface Note { title: string; content: string; color: string; + kind: NoteKind; + items: ChecklistItem[] | null; isPinned: boolean; isArchived: boolean; createdAt: string; @@ -49,6 +59,11 @@ export interface SentNoteRecipient { sentAt: string; readAt: string | null; archivedAt: string | null; + title?: string; + content?: string; + color?: string; + kind?: NoteKind; + items?: ChecklistItem[] | null; recipient: UserSummary | null; } @@ -62,6 +77,8 @@ export interface ReceivedNote { title: string; content: string; color: string; + kind: NoteKind; + items: ChecklistItem[] | null; createdAt: string; updatedAt: string; sender: UserSummary | null; @@ -87,6 +104,8 @@ export interface NoteThread { title: string; content: string; color: string; + kind: NoteKind; + items: ChecklistItem[] | null; createdAt: string; updatedAt: string; senderUserId: number; diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index d4540e17..0c6c3df5 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -1138,7 +1138,15 @@ "recipientsCount_other": "{{count}} مستلمين", "repliesCount_one": "رد واحد", "repliesCount_other": "{{count}} ردود", - "openThread": "فتح" + "openThread": "فتح", + "checklist": { + "toggle": "قائمة مهام", + "addItem": "إضافة عنصر", + "itemPlaceholder": "عنصر القائمة", + "emptyHint": "لا توجد عناصر بعد — أضف عنصراً أدناه.", + "removeItem": "حذف العنصر", + "progress": "{{done}} / {{total}}" + } }, "executiveMeetings": { "title": "قائمة الاجتماعات", diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index d565c6be..cb8afec9 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -1039,7 +1039,15 @@ }, "conversationWith": "Conversation with {{name}}", "ownerReplies": "Your replies", - "noConversationsYet": "No conversations yet" + "noConversationsYet": "No conversations yet", + "checklist": { + "toggle": "To-do list", + "addItem": "Add item", + "itemPlaceholder": "List item", + "emptyHint": "No items yet — add one below.", + "removeItem": "Remove item", + "progress": "{{done}} / {{total}}" + } }, "executiveMeetings": { "title": "Meetings list", diff --git a/artifacts/tx-os/src/pages/notes.tsx b/artifacts/tx-os/src/pages/notes.tsx index c9e46a50..6cfb9eb5 100644 --- a/artifacts/tx-os/src/pages/notes.tsx +++ b/artifacts/tx-os/src/pages/notes.tsx @@ -19,6 +19,7 @@ import { Inbox, Mail, MessageCircle, + ListTodo, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -47,7 +48,9 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { + type ChecklistItem, type Note, + type NoteKind, type NoteLabel, type NoteThread, type ReceivedNote, @@ -642,10 +645,24 @@ function NoteCard({ {note.title} )} - {note.content && ( -
- {note.content} -
+ {note.kind === "checklist" ? ( + { + const next = (note.items ?? []).map((it) => + it.id === itemId ? { ...it, done } : it, + ); + update.mutate({ id: note.id, kind: "checklist", items: next }); + }} + /> + ) : ( + note.content && ( +
+ {note.content} +
+ ) )} ))} @@ -822,10 +847,18 @@ function SentList({ {n.title} )} - {n.content && ( -
- {n.content} -
+ {n.kind === "checklist" ? ( + + ) : ( + n.content && ( +
+ {n.content} +
+ ) )}
{t("notes.to", "To:")}{" "} @@ -1191,10 +1224,17 @@ function ThreadDialog({ {t("notes.from", "From:")} {userDisplayName(thread.sender, lang)}
- {thread.content && ( -
- {thread.content} -
+ {thread.kind === "checklist" ? ( + + ) : ( + thread.content && ( +
+ {thread.content} +
+ ) )} {(thread.isOwner || thread.isAdmin) && thread.recipients.length > 0 && (
@@ -1413,6 +1453,199 @@ function GroupedReplies({ ); } +function newItemId(): string { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + return `it_${Math.random().toString(36).slice(2)}_${Date.now().toString(36)}`; +} + +function ChecklistEditor({ + items, + onChange, + testIdPrefix, +}: { + items: ChecklistItem[]; + onChange: (next: ChecklistItem[]) => void; + testIdPrefix: string; +}) { + const { t } = useTranslation(); + const inputRefs = useRef>(new Map()); + const focusItemIdRef = useRef(null); + + // After append (Enter or Add), shift focus to the freshly inserted row. + useEffect(() => { + const id = focusItemIdRef.current; + if (!id) return; + const el = inputRefs.current.get(id); + if (el) { + el.focus(); + focusItemIdRef.current = null; + } + }, [items]); + + const setItem = (id: string, patch: Partial) => { + onChange(items.map((it) => (it.id === id ? { ...it, ...patch } : it))); + }; + const removeItem = (id: string) => { + onChange(items.filter((it) => it.id !== id)); + }; + const appendItem = () => { + const item: ChecklistItem = { id: newItemId(), text: "", done: false }; + focusItemIdRef.current = item.id; + onChange([...items, item]); + }; + + return ( +
+ {items.length === 0 && ( +
+ {t("notes.checklist.emptyHint", "No items yet — add one below.")} +
+ )} + {items.map((it) => ( +
+ setItem(it.id, { done: e.target.checked })} + className="h-4 w-4 rounded border-black/30 cursor-pointer accent-amber-500" + aria-label={t("notes.checklist.itemPlaceholder", "List item")} + data-testid={`${testIdPrefix}-check-${it.id}`} + /> + { + if (el) inputRefs.current.set(it.id, el); + else inputRefs.current.delete(it.id); + }} + value={it.text} + onChange={(e) => setItem(it.id, { text: e.target.value })} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + appendItem(); + } else if ( + e.key === "Backspace" && + it.text.length === 0 && + items.length > 1 + ) { + e.preventDefault(); + removeItem(it.id); + } + }} + placeholder={t("notes.checklist.itemPlaceholder", "List item")} + className={`h-8 border-0 bg-transparent shadow-none focus-visible:ring-0 px-1 ${ + it.done ? "line-through text-muted-foreground" : "" + }`} + data-testid={`${testIdPrefix}-text-${it.id}`} + /> + +
+ ))} + +
+ ); +} + +function ChecklistView({ + items, + onToggle, + testIdPrefix, + className = "", +}: { + items: ChecklistItem[]; + onToggle?: (id: string, done: boolean) => void; + testIdPrefix: string; + className?: string; +}) { + if (items.length === 0) return null; + return ( +
{ + // Prevent the parent card's click handler (which opens edit) when + // toggling a checkbox is the intended action. + if (onToggle) e.stopPropagation(); + }} + > + {items.map((it) => ( + + ))} +
+ ); +} + +function KindToggle({ + kind, + onChange, + testId, +}: { + kind: NoteKind; + onChange: (k: NoteKind) => void; + testId?: string; +}) { + const { t } = useTranslation(); + const next: NoteKind = kind === "text" ? "checklist" : "text"; + return ( + + ); +} + function ColorPicker({ value, onChange, @@ -1531,6 +1764,8 @@ function Composer({ labels }: { labels: NoteLabel[] }) { const [content, setContent] = useState(""); const [color, setColor] = useState("default"); const [labelIds, setLabelIds] = useState([]); + const [kind, setKind] = useState("text"); + const [items, setItems] = useState([]); const create = useCreateNote(); const ref = useRef(null); @@ -1557,23 +1792,41 @@ function Composer({ labels }: { labels: NoteLabel[] }) { document.addEventListener("mousedown", onClick); return () => document.removeEventListener("mousedown", onClick); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open, title, content, color, labelIds]); + }, [open, title, content, color, labelIds, kind, items]); const reset = () => { setTitle(""); setContent(""); setColor("default"); setLabelIds([]); + setKind("text"); + setItems([]); setOpen(false); }; const save = () => { - if (!title.trim() && !content.trim()) { + const cleanItems = + kind === "checklist" + ? items + .map((it) => ({ ...it, text: it.text.trim() })) + .filter((it) => it.text.length > 0) + : []; + const isEmpty = + !title.trim() && + (kind === "checklist" ? cleanItems.length === 0 : !content.trim()); + if (isEmpty) { reset(); return; } create.mutate( - { title: title.trim(), content: content.trim(), color, labelIds }, + { + title: title.trim(), + content: kind === "checklist" ? "" : content.trim(), + color, + labelIds, + kind, + items: kind === "checklist" ? cleanItems : null, + }, { onSuccess: reset }, ); }; @@ -1596,15 +1849,30 @@ function Composer({ labels }: { labels: NoteLabel[] }) { className="border-0 bg-transparent shadow-none focus-visible:ring-0 px-1 font-semibold" data-testid="notes-composer-title" /> -