feat(tasks): support subtasks with inline creation, checklist and title progress bar
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 54s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 54s
This commit is contained in:
1 parent
537b916533
commit
0ec1a8b7f7
7 files changed
+346
-16
No files matched your search
@@ -15,8 +15,9 @@ import {
|
||||
Layers,
|
||||
Share2,
|
||||
Users,
|
||||
CheckSquare,
|
||||
} from 'lucide-react';
|
||||
import { TaskList, Thought, Space } from '../../lib/types';
|
||||
import { TaskList, Thought, Space, SubTask } from '../../lib/types';
|
||||
import { api, triggerHaptic } from '../../lib/api';
|
||||
|
||||
const getInitials = (name?: string): string => {
|
||||
@@ -67,6 +68,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
const [activeNotesTaskId, setActiveNotesTaskId] = useState<string | null>(null);
|
||||
const [notesDraft, setNotesDraft] = useState('');
|
||||
|
||||
// Subtask creation state
|
||||
const [addingSubtaskTaskId, setAddingSubtaskTaskId] = useState<string | null>(null);
|
||||
const [subtaskDraft, setSubtaskDraft] = useState('');
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -125,6 +130,168 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
setActiveNotesTaskId(null);
|
||||
};
|
||||
|
||||
const handleAddSubtask = async (task: Thought, title: string) => {
|
||||
const trimmed = title.trim();
|
||||
if (!trimmed) return;
|
||||
triggerHaptic(10);
|
||||
const newSub: SubTask = {
|
||||
id: `sub_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
||||
title: trimmed,
|
||||
isCompleted: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const updatedSubtasks = [...(task.subtasks || []), newSub];
|
||||
setTasks((prev) =>
|
||||
prev.map((t) => (t.id === task.id ? { ...t, subtasks: updatedSubtasks } : t))
|
||||
);
|
||||
setSubtaskDraft('');
|
||||
await api.updateThought(task.id, { subtasks: updatedSubtasks });
|
||||
};
|
||||
|
||||
const handleToggleSubtask = async (task: Thought, subtaskId: string) => {
|
||||
triggerHaptic(10);
|
||||
const updatedSubtasks = (task.subtasks || []).map((s) =>
|
||||
s.id === subtaskId ? { ...s, isCompleted: !s.isCompleted } : s
|
||||
);
|
||||
setTasks((prev) =>
|
||||
prev.map((t) => (t.id === task.id ? { ...t, subtasks: updatedSubtasks } : t))
|
||||
);
|
||||
await api.updateThought(task.id, { subtasks: updatedSubtasks });
|
||||
};
|
||||
|
||||
const handleDeleteSubtask = async (task: Thought, subtaskId: string) => {
|
||||
triggerHaptic(10);
|
||||
const updatedSubtasks = (task.subtasks || []).filter((s) => s.id !== subtaskId);
|
||||
setTasks((prev) =>
|
||||
prev.map((t) => (t.id === task.id ? { ...t, subtasks: updatedSubtasks } : t))
|
||||
);
|
||||
await api.updateThought(task.id, { subtasks: updatedSubtasks });
|
||||
};
|
||||
|
||||
const renderTaskProgressBar = (task: Thought) => {
|
||||
if (!task.subtasks || task.subtasks.length === 0) return null;
|
||||
const total = task.subtasks.length;
|
||||
const done = task.subtasks.filter((s) => s.isCompleted).length;
|
||||
const percent = Math.round((done / total) * 100);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="space-y-1 mt-1.5 mb-1 select-none"
|
||||
title={`${done} von ${total} Unteraufgaben erledigt (${percent}%)`}
|
||||
>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted font-medium">
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckSquare className="w-2.5 h-2.5 text-accent" />
|
||||
<span>
|
||||
{done}/{total} Unteraufgaben
|
||||
</span>
|
||||
</span>
|
||||
<span className={percent === 100 ? 'text-status-ok font-bold' : 'text-muted'}>
|
||||
{percent}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-1 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-300 ${
|
||||
percent === 100 ? 'bg-status-ok' : 'bg-accent'
|
||||
}`}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSubtaskList = (task: Thought) => {
|
||||
const hasSubtasks = task.subtasks && task.subtasks.length > 0;
|
||||
const isAdding = addingSubtaskTaskId === task.id;
|
||||
|
||||
if (!hasSubtasks && !isAdding) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-1 pt-1 pb-0.5">
|
||||
{hasSubtasks && (
|
||||
<div className="space-y-0.5">
|
||||
{task.subtasks!.map((sub) => (
|
||||
<div
|
||||
key={sub.id}
|
||||
className="flex items-center justify-between group/sub py-0.5 px-1 rounded hover:bg-canvas text-xs transition-colors"
|
||||
>
|
||||
<label className="flex items-center gap-2 cursor-pointer flex-1 min-w-0 select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sub.isCompleted}
|
||||
onChange={() => handleToggleSubtask(task, sub.id)}
|
||||
className="checkbox-square w-3.5 h-3.5 accent-accent cursor-pointer flex-shrink-0"
|
||||
/>
|
||||
<span
|
||||
className={`truncate leading-snug ${
|
||||
sub.isCompleted ? 'line-through text-faint' : 'text-main'
|
||||
}`}
|
||||
>
|
||||
{sub.title}
|
||||
</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteSubtask(task, sub.id)}
|
||||
className="opacity-0 group-hover/sub:opacity-100 text-faint hover:text-status-err transition-opacity p-0.5 ml-1"
|
||||
title="Unteraufgabe entfernen"
|
||||
>
|
||||
<Trash2 className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdding && (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleAddSubtask(task, subtaskDraft);
|
||||
}}
|
||||
className="flex items-center gap-1 pt-1"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={subtaskDraft}
|
||||
onChange={(e) => setSubtaskDraft(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setAddingSubtaskTaskId(null);
|
||||
setSubtaskDraft('');
|
||||
}
|
||||
}}
|
||||
placeholder="Neue Unteraufgabe… (Enter)"
|
||||
autoFocus
|
||||
className="flex-1 h-6 px-1.5 text-xs bg-canvas border border-accent rounded-btn text-main placeholder-faint focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!subtaskDraft.trim()}
|
||||
className="h-6 px-2 bg-accent text-on-accent text-[11px] rounded-btn font-medium disabled:opacity-40 cursor-pointer"
|
||||
title="Hinzufügen"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAddingSubtaskTaskId(null);
|
||||
setSubtaskDraft('');
|
||||
}}
|
||||
className="h-6 px-1 text-muted hover:text-main cursor-pointer"
|
||||
title="Abbrechen"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleConvertToDoc = async (task: Thought) => {
|
||||
triggerHaptic(20);
|
||||
const updated = await api.triageAction(task.id, 'keep', { convertTo: 'document' });
|
||||
@@ -657,6 +824,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Subtasks Progress Bar on Title */}
|
||||
{renderTaskProgressBar(task)}
|
||||
|
||||
{/* Notes Preview / Inline Editor */}
|
||||
{activeNotesTaskId === task.id ? (
|
||||
<div className="space-y-1">
|
||||
@@ -695,6 +865,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Subtasks Checklist & Inline Creator */}
|
||||
{renderSubtaskList(task)}
|
||||
|
||||
{/* Footer & Movement Controls */}
|
||||
<div className="flex items-center justify-between gap-1 pt-1 border-t border-subtle text-[11px]">
|
||||
<div className="flex items-center gap-1.5 text-muted">
|
||||
@@ -714,6 +887,18 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
+ Notiz
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAddingSubtaskTaskId(task.id);
|
||||
setSubtaskDraft('');
|
||||
}}
|
||||
className="text-[10px] text-faint hover:text-main flex items-center gap-0.5 cursor-pointer"
|
||||
title="Unteraufgabe hinzufügen"
|
||||
>
|
||||
<Plus className="w-2.5 h-2.5 text-accent" />
|
||||
<span>+ Unteraufgabe</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -764,6 +949,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Subtasks Progress Bar on Title */}
|
||||
{renderTaskProgressBar(task)}
|
||||
|
||||
{/* Notes Preview / Inline Editor */}
|
||||
{activeNotesTaskId === task.id ? (
|
||||
<div className="space-y-1">
|
||||
@@ -801,8 +989,38 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Subtasks Checklist & Inline Creator */}
|
||||
{renderSubtaskList(task)}
|
||||
|
||||
{/* Movement Controls */}
|
||||
<div className="flex items-center justify-between gap-1 pt-1 border-t border-subtle text-[11px]">
|
||||
<div className="flex items-center gap-1.5 text-muted">
|
||||
{!task.notes && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveNotesTaskId(task.id);
|
||||
setNotesDraft('');
|
||||
}}
|
||||
className="text-[10px] text-faint hover:text-main"
|
||||
>
|
||||
+ Notiz
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAddingSubtaskTaskId(task.id);
|
||||
setSubtaskDraft('');
|
||||
}}
|
||||
className="text-[10px] text-faint hover:text-main flex items-center gap-0.5 cursor-pointer"
|
||||
title="Unteraufgabe hinzufügen"
|
||||
>
|
||||
<Plus className="w-2.5 h-2.5 text-accent" />
|
||||
<span>+ Unteraufgabe</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => handleKanbanMove(task, 'todo')}
|
||||
className="btn-action h-6 px-1.5 text-[10px] flex items-center gap-1"
|
||||
@@ -822,6 +1040,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{inProgressKanban.length === 0 && (
|
||||
<div className="p-4 text-center text-faint text-xs">Keine aktiven Aufgaben</div>
|
||||
@@ -849,6 +1068,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
{task.content}
|
||||
</p>
|
||||
|
||||
{/* Subtasks Progress Bar & Checklist */}
|
||||
{renderTaskProgressBar(task)}
|
||||
{renderSubtaskList(task)}
|
||||
|
||||
<div className="flex items-center justify-between gap-1 pt-1 border-t border-subtle text-[11px]">
|
||||
<button
|
||||
onClick={() => handleKanbanMove(task, 'todo')}
|
||||
@@ -991,6 +1214,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
{task.content}
|
||||
</p>
|
||||
|
||||
{/* Subtasks Progress Bar on Title */}
|
||||
{renderTaskProgressBar(task)}
|
||||
|
||||
{/* Inline Notes or Editor */}
|
||||
{activeNotesTaskId === task.id ? (
|
||||
<div className="space-y-1 pt-1">
|
||||
@@ -1029,6 +1255,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{/* Subtasks Checklist & Inline Creator */}
|
||||
{renderSubtaskList(task)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-1.5 mt-0.5 empty:hidden">
|
||||
{task.priority === 'urgent' && (
|
||||
<span className="badge-accent">Dringend</span>
|
||||
@@ -1052,6 +1281,19 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
|
||||
{/* Action Controls */}
|
||||
<div className="flex items-center gap-2 self-end sm:self-center flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAddingSubtaskTaskId(task.id);
|
||||
setSubtaskDraft('');
|
||||
}}
|
||||
className="btn-action text-xs"
|
||||
title="Unteraufgabe hinzufügen"
|
||||
>
|
||||
<Plus className="w-3 h-3 text-accent" />
|
||||
<span className="hidden md:inline">Unteraufgabe</span>
|
||||
</button>
|
||||
|
||||
{!task.notes && activeNotesTaskId !== task.id && (
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -523,6 +523,22 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
setSlashSearch('');
|
||||
};
|
||||
|
||||
const getDirectChildTodos = (blocks: DocumentBlock[], parentIndex: number): DocumentBlock[] => {
|
||||
const parent = blocks[parentIndex];
|
||||
if (!parent) return [];
|
||||
const parentLevel = parent.level || 0;
|
||||
const childTodos: DocumentBlock[] = [];
|
||||
for (let i = parentIndex + 1; i < blocks.length; i++) {
|
||||
const cur = blocks[i];
|
||||
const curLevel = cur.level || 0;
|
||||
if (curLevel <= parentLevel) break;
|
||||
if (cur.type === 'todo') {
|
||||
childTodos.push(cur);
|
||||
}
|
||||
}
|
||||
return childTodos;
|
||||
};
|
||||
|
||||
// Drag & Drop Handlers on the Bullet Dot
|
||||
const handleDragStart = (e: React.DragEvent, blockId: string) => {
|
||||
e.dataTransfer.setData('text/plain', blockId);
|
||||
@@ -1467,7 +1483,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<div className="flex-1 min-w-0 pt-0.5 relative">
|
||||
{/* TYPE: TODO */}
|
||||
{block.type === 'todo' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 group/todo">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!block.checked}
|
||||
@@ -1487,6 +1504,47 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
block.checked ? 'line-through text-faint' : 'text-main'
|
||||
}`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => insertNewBulletAfter(originalIndex, (block.level || 0) + 1, 'todo')}
|
||||
className="opacity-0 group-hover/todo:opacity-100 p-0.5 text-muted hover:text-accent rounded transition-opacity cursor-pointer flex-shrink-0"
|
||||
title="Unteraufgabe erstellen"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress bar for child todos if any exist */}
|
||||
{(() => {
|
||||
const childTodos = getDirectChildTodos(selectedDoc?.blocks || [], originalIndex);
|
||||
if (childTodos.length === 0) return null;
|
||||
const doneChild = childTodos.filter((c) => c.checked).length;
|
||||
const totalChild = childTodos.length;
|
||||
const pct = Math.round((doneChild / totalChild) * 100);
|
||||
return (
|
||||
<div
|
||||
className="space-y-0.5 ml-6 pt-0.5 select-none"
|
||||
title={`${doneChild} von ${totalChild} Unteraufgaben erledigt (${pct}%)`}
|
||||
>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted">
|
||||
<span>
|
||||
{doneChild}/{totalChild} erledigt
|
||||
</span>
|
||||
<span className={pct === 100 ? 'text-status-ok font-bold' : ''}>
|
||||
{pct}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-1 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-300 ${
|
||||
pct === 100 ? 'bg-status-ok' : 'bg-accent'
|
||||
}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Team,
|
||||
TaskList,
|
||||
Thought,
|
||||
SubTask,
|
||||
User,
|
||||
UserInvite,
|
||||
} from './types';
|
||||
@@ -309,6 +310,7 @@ export const api = {
|
||||
taskListId?: string;
|
||||
kanbanStatus?: 'todo' | 'in_progress' | 'done';
|
||||
notes?: string;
|
||||
subtasks?: SubTask[];
|
||||
}
|
||||
): Promise<Thought> {
|
||||
return request<Thought>(`/api/v1/thoughts/${thoughtId}`, {
|
||||
|
||||
@@ -34,6 +34,13 @@ export interface UserInvite {
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface SubTask {
|
||||
id: string;
|
||||
title: string;
|
||||
isCompleted: boolean;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface Thought {
|
||||
id: string;
|
||||
userId: string;
|
||||
@@ -49,6 +56,7 @@ export interface Thought {
|
||||
taskListId?: string;
|
||||
kanbanStatus?: 'todo' | 'in_progress' | 'done';
|
||||
notes?: string;
|
||||
subtasks?: SubTask[];
|
||||
alarmSent?: boolean;
|
||||
createdAt: string;
|
||||
triagedAt?: string;
|
||||
|
||||
@@ -644,13 +644,14 @@ func (s *Server) handleUpdateThought(w http.ResponseWriter, r *http.Request) {
|
||||
TaskListID *string `json:"taskListId"`
|
||||
KanbanStatus *string `json:"kanbanStatus"`
|
||||
Notes *string `json:"notes"`
|
||||
Subtasks *[]domain.SubTask `json:"subtasks"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := s.store.UpdateThoughtDetails(userID, thoughtID, req.Content, req.IsCompleted, req.DueDate, req.Priority, req.TaskListID, req.KanbanStatus, req.Notes)
|
||||
updated, err := s.store.UpdateThoughtDetails(userID, thoughtID, req.Content, req.IsCompleted, req.DueDate, req.Priority, req.TaskListID, req.KanbanStatus, req.Notes, req.Subtasks)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
|
||||
@@ -77,6 +77,13 @@ const (
|
||||
StatusConverted ThoughtStatus = "converted"
|
||||
)
|
||||
|
||||
type SubTask struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
IsCompleted bool `json:"isCompleted"`
|
||||
CreatedAt string `json:"createdAt,omitempty"`
|
||||
}
|
||||
|
||||
type Thought struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId"`
|
||||
@@ -92,6 +99,7 @@ type Thought struct {
|
||||
TaskListID string `json:"taskListId,omitempty"`
|
||||
KanbanStatus string `json:"kanbanStatus,omitempty"` // todo | in_progress | done
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Subtasks []SubTask `json:"subtasks"`
|
||||
AlarmSent bool `json:"alarmSent,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
TriagedAt string `json:"triagedAt,omitempty"`
|
||||
|
||||
+24
-13
@@ -108,6 +108,7 @@ func (s *Store) migrate() error {
|
||||
kanban_status TEXT NOT NULL DEFAULT 'todo',
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
alarm_sent INTEGER NOT NULL DEFAULT 0,
|
||||
subtasks TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL,
|
||||
triaged_at TEXT DEFAULT '',
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
@@ -263,6 +264,7 @@ func (s *Store) migrate() error {
|
||||
"ALTER TABLE thoughts ADD COLUMN kanban_status TEXT NOT NULL DEFAULT 'todo';",
|
||||
"ALTER TABLE thoughts ADD COLUMN notes TEXT NOT NULL DEFAULT '';",
|
||||
"ALTER TABLE thoughts ADD COLUMN alarm_sent INTEGER NOT NULL DEFAULT 0;",
|
||||
"ALTER TABLE thoughts ADD COLUMN subtasks TEXT NOT NULL DEFAULT '[]';",
|
||||
|
||||
"ALTER TABLE documents ADD COLUMN folder_path TEXT NOT NULL DEFAULT '';",
|
||||
}
|
||||
@@ -645,13 +647,14 @@ func (s *Store) CreateThoughtWithDetails(userID, content, priority, source, dueD
|
||||
}
|
||||
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO thoughts (id, user_id, content, tags, priority, source, status, is_completed, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, 0, ?)`,
|
||||
`INSERT INTO thoughts (id, user_id, content, tags, priority, source, status, is_completed, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, subtasks)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, 0, ?, '[]')`,
|
||||
t.ID, t.UserID, t.Content, string(tagsJSON), t.Priority, t.Source, string(t.Status), t.DueDate, t.TaskListID, t.KanbanStatus, t.Notes, t.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Subtasks = []domain.SubTask{}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
@@ -659,13 +662,13 @@ func scanThought(rows interface {
|
||||
Scan(dest ...any) error
|
||||
}) (*domain.Thought, error) {
|
||||
t := &domain.Thought{}
|
||||
var tagsStr, statusStr string
|
||||
var tagsStr, subtasksStr, statusStr string
|
||||
var isCompleted, alarmSent int
|
||||
err := rows.Scan(
|
||||
&t.ID, &t.UserID, &t.Content, &tagsStr, &t.Priority, &t.Source,
|
||||
&statusStr, &isCompleted, &t.ConvertedType, &t.ConvertedRefID,
|
||||
&t.DueDate, &t.TaskListID, &t.KanbanStatus, &t.Notes, &alarmSent,
|
||||
&t.CreatedAt, &t.TriagedAt,
|
||||
&t.CreatedAt, &t.TriagedAt, &subtasksStr,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -682,12 +685,16 @@ func scanThought(rows interface {
|
||||
}
|
||||
t.Tags = []string{}
|
||||
_ = json.Unmarshal([]byte(tagsStr), &t.Tags)
|
||||
t.Subtasks = []domain.SubTask{}
|
||||
if subtasksStr != "" && subtasksStr != "null" {
|
||||
_ = json.Unmarshal([]byte(subtasksStr), &t.Subtasks)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetThoughtByID(userID, thoughtID string) (*domain.Thought, error) {
|
||||
row := s.db.QueryRow(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks
|
||||
FROM thoughts WHERE id = ? AND (user_id = ? OR task_list_id IN (SELECT id FROM task_lists WHERE space_id IN (SELECT space_id FROM space_members WHERE user_id = ?)))`,
|
||||
thoughtID, userID, userID,
|
||||
)
|
||||
@@ -703,14 +710,14 @@ func (s *Store) GetThoughts(userID, statusFilter string, limit int) ([]domain.Th
|
||||
|
||||
if statusFilter != "" && statusFilter != "all" {
|
||||
rows, err = s.db.Query(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks
|
||||
FROM thoughts WHERE user_id = ? AND status = ?
|
||||
ORDER BY CASE priority WHEN 'urgent' THEN 1 WHEN 'high' THEN 2 WHEN 'normal' THEN 3 ELSE 4 END, created_at DESC LIMIT ?`,
|
||||
userID, statusFilter, limit,
|
||||
)
|
||||
} else {
|
||||
rows, err = s.db.Query(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks
|
||||
FROM thoughts WHERE user_id = ?
|
||||
ORDER BY created_at DESC LIMIT ?`,
|
||||
userID, limit,
|
||||
@@ -736,7 +743,7 @@ func (s *Store) GetTasks(userID string, taskListID string) ([]domain.Thought, er
|
||||
var err error
|
||||
if taskListID != "" {
|
||||
rows, err = s.db.Query(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks
|
||||
FROM thoughts
|
||||
WHERE (user_id = ? OR task_list_id IN (SELECT id FROM task_lists WHERE space_id IN (SELECT space_id FROM space_members WHERE user_id = ?)))
|
||||
AND task_list_id = ?
|
||||
@@ -746,7 +753,7 @@ func (s *Store) GetTasks(userID string, taskListID string) ([]domain.Thought, er
|
||||
)
|
||||
} else {
|
||||
rows, err = s.db.Query(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks
|
||||
FROM thoughts
|
||||
WHERE (user_id = ? OR task_list_id IN (SELECT id FROM task_lists WHERE space_id IN (SELECT space_id FROM space_members WHERE user_id = ?)))
|
||||
AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task'))
|
||||
@@ -839,7 +846,7 @@ func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, target
|
||||
return s.GetThoughtByID(userID, thoughtID)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateThoughtDetails(userID, thoughtID string, content *string, isCompleted *bool, dueDate *string, priority *string, taskListID *string, kanbanStatus *string, notes *string) (*domain.Thought, error) {
|
||||
func (s *Store) UpdateThoughtDetails(userID, thoughtID string, content *string, isCompleted *bool, dueDate *string, priority *string, taskListID *string, kanbanStatus *string, notes *string, subtasks *[]domain.SubTask) (*domain.Thought, error) {
|
||||
thought, err := s.GetThoughtByID(userID, thoughtID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -877,16 +884,20 @@ func (s *Store) UpdateThoughtDetails(userID, thoughtID string, content *string,
|
||||
if notes != nil {
|
||||
thought.Notes = *notes
|
||||
}
|
||||
if subtasks != nil {
|
||||
thought.Subtasks = *subtasks
|
||||
}
|
||||
|
||||
tagsJSON, _ := json.Marshal(thought.Tags)
|
||||
subtasksJSON, _ := json.Marshal(thought.Subtasks)
|
||||
completedInt := 0
|
||||
if thought.IsCompleted {
|
||||
completedInt = 1
|
||||
}
|
||||
|
||||
_, err = s.db.Exec(
|
||||
`UPDATE thoughts SET content = ?, tags = ?, is_completed = ?, due_date = ?, priority = ?, task_list_id = ?, kanban_status = ?, notes = ? WHERE id = ? AND user_id = ?`,
|
||||
thought.Content, string(tagsJSON), completedInt, thought.DueDate, thought.Priority, thought.TaskListID, thought.KanbanStatus, thought.Notes, thoughtID, userID,
|
||||
`UPDATE thoughts SET content = ?, tags = ?, is_completed = ?, due_date = ?, priority = ?, task_list_id = ?, kanban_status = ?, notes = ?, subtasks = ? WHERE id = ? AND user_id = ?`,
|
||||
thought.Content, string(tagsJSON), completedInt, thought.DueDate, thought.Priority, thought.TaskListID, thought.KanbanStatus, thought.Notes, string(subtasksJSON), thoughtID, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1574,7 +1585,7 @@ func (s *Store) SearchAll(userID, query string) ([]domain.Thought, []domain.Docu
|
||||
q := "%" + strings.ToLower(strings.TrimSpace(query)) + "%"
|
||||
|
||||
tRows, err := s.db.Query(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks
|
||||
FROM thoughts WHERE user_id = ? AND LOWER(content) LIKE ? ORDER BY created_at DESC LIMIT 30`,
|
||||
userID, q,
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user