From 89fa19fce9cef7c441e0256d7468793328378189 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 24 Aug 2026 17:29:51 +0200 Subject: [PATCH] Add project type toggle and prompt dialog to item dialogs --- web/dialogs.jsx | 156 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 145 insertions(+), 11 deletions(-) diff --git a/web/dialogs.jsx b/web/dialogs.jsx index ca2a6b4..a5d5edb 100644 --- a/web/dialogs.jsx +++ b/web/dialogs.jsx @@ -247,7 +247,7 @@ function DialogHeader({ eyebrow, title, onClose, id }) { } // --- Item editor (used for both edit and add) --- -function ItemDialog({ open, mode, initial, onClose, onSubmit, recentTags = [] }) { +function ItemDialog({ open, mode, initial, onClose, onSubmit, recentTags = [], isMultiProject = false, projects = [], defaultProjectId = null }) { const [title, setTitle] = useStateD(""); const [body, setBody] = useStateD(null); const [priority, setPriority] = useStateD("P2"); @@ -257,8 +257,15 @@ function ItemDialog({ open, mode, initial, onClose, onSubmit, recentTags = [] }) const [tagInput, setTagInput] = useStateD(""); const [reason, setReason] = useStateD(""); const [progress, setProgress] = useStateD(0); + const [createAsProject, setCreateAsProject] = useStateD(false); + const [projectPath, setProjectPath] = useStateD(""); + const [selectedProjectId, setSelectedProjectId] = useStateD(null); const titleRef = useRefD(null); + const showProjectOption = isMultiProject && mode === "add"; + const showPathInput = showProjectOption && createAsProject; + const showProjectSelect = showProjectOption && !createAsProject && projects.length > 0; + useEffectD(() => { if (!open) return; setTitle(initial?.title || ""); @@ -270,6 +277,13 @@ function ItemDialog({ open, mode, initial, onClose, onSubmit, recentTags = [] }) setTagInput(""); setReason(initial?.reason || ""); setProgress(initial?.progress ?? 0); + setCreateAsProject(false); + setProjectPath(""); + setSelectedProjectId( + defaultProjectId && projects.some(p => p.id === defaultProjectId) + ? defaultProjectId + : (projects[0] ? projects[0].id : null) + ); setTimeout(() => titleRef.current?.focus(), 50); }, [open, initial]); @@ -283,11 +297,12 @@ function ItemDialog({ open, mode, initial, onClose, onSubmit, recentTags = [] }) const submit = (e) => { e?.preventDefault(); - if (!title.trim()) return; + // Item type requires a title; Project type only requires a path (title is an optional display name). + if (showPathInput ? !projectPath.trim() : !title.trim()) return; // Reconcile progress with status before submitting. let outProgress = snapProgress(progress); if (status === "done") outProgress = 100; - onSubmit({ + const payload = { title: title.trim(), body: body || null, priority, status, @@ -295,14 +310,25 @@ function ItemDialog({ open, mode, initial, onClose, onSubmit, recentTags = [] }) tags, reason: status === "blocked" ? reason.trim() || null : null, progress: outProgress - }); + }; + if (isMultiProject) { + const rawPath = projectPath.trim(); + payload.createAsProject = showPathInput; + payload.projectPath = showPathInput + ? (rawPath.endsWith('.md') ? rawPath : rawPath.replace(/\/$/, '') + '/backlog.md') + : null; + payload.projectId = showProjectSelect ? selectedProjectId : null; + } + onSubmit(payload); }; const tagSuggestions = recentTags.filter(t => !tags.includes(t)).slice(0, 8); return ( -
+ { + if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); submit(); } + }}> setTitle(e.target.value)} - placeholder="What needs to happen?" - required + placeholder={showPathInput ? "Optional display name" : "What needs to happen?"} + required={!showPathInput} readOnly={mode === "view"} /> @@ -432,6 +458,56 @@ function ItemDialog({ open, mode, initial, onClose, onSubmit, recentTags = [] }) /> )} + + {showProjectOption && ( +
+ +
+ + +
+
+ )} + + {showProjectSelect && ( +
+ + +
+ )} + + {showPathInput && ( +
+ + setProjectPath(e.target.value)} + placeholder="/home/user/project/backlog.md" + /> +
+ )}
@@ -447,8 +523,8 @@ function ItemDialog({ open, mode, initial, onClose, onSubmit, recentTags = [] }) ⌘↩ to save · Esc to cancel
-
@@ -460,13 +536,25 @@ function ItemDialog({ open, mode, initial, onClose, onSubmit, recentTags = [] }) } // --- Confirm dialog --- -function ConfirmDialog({ open, title, message, detail, confirmLabel = "Confirm", danger = false, onCancel, onConfirm }) { +function ConfirmDialog({ open, title, message, detail, checkbox, confirmLabel = "Confirm", danger = false, onCancel, onConfirm }) { + const [checked, setChecked] = useStateD(false); + + useEffectD(() => { + if (open) setChecked(false); + }, [open]); + return (

{message}

{detail &&
{detail}
} + {checkbox && ( + + )}
@@ -474,13 +562,58 @@ function ConfirmDialog({ open, title, message, detail, confirmLabel = "Confirm", + onClick={() => onConfirm(checked)} autoFocus>{confirmLabel}
); } +// --- Prompt dialog (styled input) --- +function PromptDialog({ open, title, message, placeholder = "", confirmLabel = "OK", onCancel, onConfirm }) { + const [value, setValue] = useStateD(""); + const inputRef = useRefD(null); + + useEffectD(() => { + if (open) { + setValue(""); + setTimeout(() => inputRef.current?.focus(), 50); + } + }, [open]); + + const submit = (e) => { + e?.preventDefault(); + if (!value.trim()) return; + onConfirm(value.trim()); + }; + + return ( + + + +
+

{message}

+ setValue(e.target.value)} + placeholder={placeholder} + style={{marginTop: 12}} + /> +
+
+ +
+ + +
+
+ +
+ ); +} + // --- Tag autocomplete (typed input + filtered dropdown) --- function TagAutocompleteInput({ tags, setTags, tagInput, setTagInput, addTag, removeTag, allTags }) { const [focused, setFocused] = useStateD(false); @@ -616,4 +749,5 @@ function TagMatchHL({ text, match }) { window.Dialog = Dialog; window.ItemDialog = ItemDialog; window.ConfirmDialog = ConfirmDialog; +window.PromptDialog = PromptDialog; window.RichTextEditor = RichTextEditor;