From 75da0e7abc763f71d3fc077d2f46bb48e5538ec9 Mon Sep 17 00:00:00 2001 From: Hiro Date: Sun, 21 Dec 2025 15:10:58 -0600 Subject: [PATCH 1/7] Reimplement subtasks --- packages/api/src/task/subtask.entity.ts | 42 +++------- packages/api/src/task/task.entity.ts | 2 +- packages/api/src/task/task.service.ts | 43 ++++++++-- packages/app/src/components/task/TaskItem.tsx | 7 +- .../(Layout)/(TaskInfoMenu)/MenuFields.tsx | 14 ++-- .../Shared/TaskInfoMenuSubtask.tsx | 49 ++++------- .../Shared/TaskInfoMenuSubtaskMenu.tsx | 84 +++++++++++-------- 7 files changed, 129 insertions(+), 112 deletions(-) diff --git a/packages/api/src/task/subtask.entity.ts b/packages/api/src/task/subtask.entity.ts index f6bc99b..019ea8f 100644 --- a/packages/api/src/task/subtask.entity.ts +++ b/packages/api/src/task/subtask.entity.ts @@ -1,35 +1,15 @@ -import { Entity, Model, Prop } from "@/_lib/mongoose"; -import mongoose from "mongoose"; - -@Entity() -export class SubTask extends Model { - - id: string; - - @Prop({ type: String, required: true }) +export interface SubTask { + id?: string; title: string; - - @Prop({ type: String, default: "" }) - description: string; - - /** this should be changed to a Date object, but current filtering logic is using regex on a string */ - @Prop({ type: String, default: () => new Date().toString() }) + description?: string; + /** currently stored as string for legacy reasons */ date: string; - - @Prop({ type: mongoose.Schema.Types.Mixed, default: false }) done: boolean | string[]; - - @Prop({ type: String, default: "" }) - repeater: string; - - @Prop({ type: String, default: "" }) - reminder: string; - - @Prop({ type: String, default: "" }) - type: string; - - /** (git blame Hiro) - figure out what this actually does */ - @Prop({ type: Boolean, default: false }) - accordion: boolean; - + repeater?: string; + reminder?: string; + type?: string; + accordion?: boolean; + priority?: number; + tags?: string[]; + subtasks?: SubTask[]; } diff --git a/packages/api/src/task/task.entity.ts b/packages/api/src/task/task.entity.ts index 4b13a04..26fb0db 100644 --- a/packages/api/src/task/task.entity.ts +++ b/packages/api/src/task/task.entity.ts @@ -37,7 +37,7 @@ export class Task extends Model { @Prop({ type: Number, default: 0 }) priority: number; - @Prop({ type: [SubTask], default: [] }) + @Prop({ type: [mongoose.Schema.Types.Mixed], default: [] }) subtasks: SubTask[]; @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], default: [] }) diff --git a/packages/api/src/task/task.service.ts b/packages/api/src/task/task.service.ts index ec4dfce..b3c5df9 100644 --- a/packages/api/src/task/task.service.ts +++ b/packages/api/src/task/task.service.ts @@ -1,5 +1,6 @@ import { Injectable } from "@outwalk/firefly"; import { Task } from "./task.entity"; +import { SubTask } from "./subtask.entity"; import { User } from "@/user/user.entity"; import { UpdateQuery } from "mongoose"; import mongoose from "mongoose"; @@ -21,15 +22,44 @@ export class TaskService { return Array.from(new Set(normalized)); } + normalizeSubtasks(subtasks?: SubTask[] | Array>): SubTask[] | undefined { + if (!Array.isArray(subtasks)) return undefined; + + return subtasks.map((sub) => { + const clean: any = { + title: sub?.title ?? "", + description: sub?.description ?? "", + date: sub?.date ?? new Date().toString(), + done: sub?.done ?? false, + repeater: sub?.repeater ?? "", + reminder: sub?.reminder ?? "", + type: sub?.type ?? "", + accordion: sub?.accordion ?? false, + }; + + if (typeof (sub as any).priority !== "undefined") clean.priority = (sub as any).priority; + if (Array.isArray((sub as any).tags)) clean.tags = this.normalizeTags((sub as any).tags); + if ((sub as any).id) clean.id = (sub as any).id; + + return clean; + }); + } + async addTask(data: Partial): Promise { const tags = this.normalizeTags(data.tags); - return Task.create({ ...data, ...(tags ? { tags } : {}) }); + const subtasks = this.normalizeSubtasks((data as any).subtasks); + return Task.create({ + ...data, + ...(tags ? { tags } : {}), + ...(subtasks ? { subtasks } : {}), + }); } async addTasks(data: Partial[]): Promise { const withTags = data.map((task) => { const tags = this.normalizeTags(task.tags); - return { ...task, ...(tags ? { tags } : {}) }; + const subtasks = this.normalizeSubtasks((task as any).subtasks); + return { ...task, ...(tags ? { tags } : {}), ...(subtasks ? { subtasks } : {}) }; }); return Task.insertMany(withTags); @@ -37,10 +67,13 @@ export class TaskService { async updateTask(id: string, data: Partial | UpdateQuery): Promise { const tags = this.normalizeTags((data as Partial)?.tags); + const subtasks = this.normalizeSubtasks((data as any)?.subtasks); - const update: Partial | UpdateQuery = tags - ? { ...(data as Partial), tags } - : data; + const update: Partial | UpdateQuery = { + ...(data as Partial), + ...(tags ? { tags } : {}), + ...(subtasks ? { subtasks } : {}), + }; return Task.findByIdAndUpdate(id, update).lean().exec(); } diff --git a/packages/app/src/components/task/TaskItem.tsx b/packages/app/src/components/task/TaskItem.tsx index dbc2ae4..768d330 100644 --- a/packages/app/src/components/task/TaskItem.tsx +++ b/packages/app/src/components/task/TaskItem.tsx @@ -19,6 +19,7 @@ interface TaskItemParams { } export function TaskItem({ skeleton, item, setIsInspecting, type, parent, taskFilter, selectionMode = false, isSelected = false, onToggleSelect, isAnimating = false }: TaskItemParams) { + const isSubtask = type === "subtask"; if (skeleton) { return ( @@ -220,7 +221,7 @@ export function TaskItem({ skeleton, item, setIsInspecting, type, parent, taskFi
- {item.type == "group" && item.subtasks?.length > 0 && ( + {item.subtasks?.length > 0 && (
{ @@ -264,8 +265,8 @@ export function TaskItem({ skeleton, item, setIsInspecting, type, parent, taskFi
-
- {item.type == "group" && +
+ {item.subtasks?.length > 0 && !isAccordion && item.subtasks?.map((subtask: Task, key: number) => (
*/} - {tempData.type == "group" && ( - - )} + void; + onDelete: (id: string | undefined) => void; } export default function TaskInfoMenuSubtask({ task, - parent, - deleteSubtask, - setIsOpen, + onChangeTitle, + onDelete, }: TaskInfoMenuSubtaskParams) { - const [appData, setAppData] = useApp(); - - const openSubtaskMenu = () => { - setAppData({ - ...appData, - activeParent: parent, - activeTask: task, - }); - }; - return ( -
openSubtaskMenu()} - > -
- {task.title || "No Title"} -
-
- deleteSubtask(task)} - > - - - -
+
+ onChangeTitle(task.id, e.target.value)} + placeholder="Subtask title" + /> +
); } diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtaskMenu.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtaskMenu.tsx index 86bb915..57ef8ba 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtaskMenu.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtaskMenu.tsx @@ -1,7 +1,7 @@ +import { useMemo, useState } from "react"; import { Task, createInitialTaskData } from "@/hooks/tasks"; import TaskInfoMenuSubtask from "./TaskInfoMenuSubtask"; import { createID } from "@/utils/id"; -import { Logger } from "@/utils/logger"; export interface TaskInfoMenuSubtaskMenuParams { subtasks: Task[]; @@ -13,67 +13,85 @@ export default function TaskInfoMenuSubtaskMenu({ subtasks, tempData, setTempData, - setIsOpen }: TaskInfoMenuSubtaskMenuParams) { + const [newTitle, setNewTitle] = useState(""); + + const normalizedSubtasks = useMemo( + () => subtasks?.map((task) => ({ ...task, title: task.title ?? "" })) ?? [], + [subtasks] + ); + const createNewSubtask = () => { - Logger.log("Initial subtasks", tempData.subtasks); + const title = newTitle.trim(); + if (!title) return; const tempSubtasks = [...(tempData.subtasks || [])]; - Logger.log("Temp Old Subtasks", tempSubtasks); - const newTask: Task = { ...createInitialTaskData(), id: createID(20), + title, }; tempSubtasks.push(newTask); - Logger.log("New Task", newTask); - - Logger.log("Temp New Subtasks", tempSubtasks); - setTempData({ subtasks: tempSubtasks, }); + setNewTitle(""); }; - const deleteSubtask = (task: Task) => { - const tempSubtasks = tempData.subtasks ?? []; + const deleteSubtask = (id: string | undefined) => { + const tempSubtasks = (tempData.subtasks ?? []).filter((sub) => sub.id !== id); - const subtaskData = tempSubtasks.find((tempTask: Task) => tempTask == task); + setTempData({ subtasks: tempSubtasks }); + }; - if (subtaskData) { - tempSubtasks.splice(tempSubtasks.indexOf(subtaskData), 1); - } + const updateSubtaskTitle = (id: string | undefined, title: string) => { + const tempSubtasks = (tempData.subtasks ?? []).map((task) => + task.id === id ? { ...task, title } : task + ); - setTempData({ - subtasks: tempSubtasks, - }); + setTempData({ subtasks: tempSubtasks }); }; return ( -
-
-

Sub Tasks

- +
+
+

Subtasks

-
- {subtasks?.map((task: Task, key: number) => ( +
+ {normalizedSubtasks?.map((task: Task, key: number) => ( ))}
+
+ setNewTitle(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + createNewSubtask(); + } + }} + /> + +
); } From 9e036b281a9235032700629ac5c169088575ef0b Mon Sep 17 00:00:00 2001 From: Hiro Date: Sun, 21 Dec 2025 15:35:42 -0600 Subject: [PATCH 2/7] Group tasks --- packages/api/src/task/task.entity.ts | 5 +- packages/api/src/task/task.service.ts | 40 +------ .../menus/TaskContainer/TaskContainer.tsx | 17 +-- packages/app/src/components/task/TaskItem.tsx | 107 ++---------------- .../app/src/components/tasks/TagFilterBar.tsx | 7 +- .../app/src/components/tasks/TaskMenu.tsx | 98 +++++++++++++--- packages/app/src/hooks/app.ts | 2 - packages/app/src/hooks/tasks.ts | 8 +- .../(Layout)/(TaskInfoMenu)/MenuEdit.tsx | 3 +- .../(Layout)/(TaskInfoMenu)/MenuFields.tsx | 29 ++--- .../Shared/TaskInfoMenuDelete.tsx | 2 +- .../Shared/TaskInfoMenuSubtask.tsx | 31 ----- .../Shared/TaskInfoMenuSubtaskMenu.tsx | 97 ---------------- .../app/src/pages/(Layout)/TaskInfoMenu.tsx | 44 ++----- 14 files changed, 117 insertions(+), 373 deletions(-) delete mode 100644 packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtask.tsx delete mode 100644 packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtaskMenu.tsx diff --git a/packages/api/src/task/task.entity.ts b/packages/api/src/task/task.entity.ts index 26fb0db..9ae29c5 100644 --- a/packages/api/src/task/task.entity.ts +++ b/packages/api/src/task/task.entity.ts @@ -1,6 +1,5 @@ import { Entity, Model, Prop } from "@/_lib/mongoose"; import mongoose from "mongoose"; -import { SubTask } from "./subtask.entity"; import { User } from "../user/user.entity"; @Entity() @@ -37,8 +36,8 @@ export class Task extends Model { @Prop({ type: Number, default: 0 }) priority: number; - @Prop({ type: [mongoose.Schema.Types.Mixed], default: [] }) - subtasks: SubTask[]; + @Prop({ type: String, default: "" }) + group: string; @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], default: [] }) users: (User | string)[]; diff --git a/packages/api/src/task/task.service.ts b/packages/api/src/task/task.service.ts index b3c5df9..8afc897 100644 --- a/packages/api/src/task/task.service.ts +++ b/packages/api/src/task/task.service.ts @@ -1,6 +1,5 @@ import { Injectable } from "@outwalk/firefly"; import { Task } from "./task.entity"; -import { SubTask } from "./subtask.entity"; import { User } from "@/user/user.entity"; import { UpdateQuery } from "mongoose"; import mongoose from "mongoose"; @@ -22,44 +21,18 @@ export class TaskService { return Array.from(new Set(normalized)); } - normalizeSubtasks(subtasks?: SubTask[] | Array>): SubTask[] | undefined { - if (!Array.isArray(subtasks)) return undefined; - - return subtasks.map((sub) => { - const clean: any = { - title: sub?.title ?? "", - description: sub?.description ?? "", - date: sub?.date ?? new Date().toString(), - done: sub?.done ?? false, - repeater: sub?.repeater ?? "", - reminder: sub?.reminder ?? "", - type: sub?.type ?? "", - accordion: sub?.accordion ?? false, - }; - - if (typeof (sub as any).priority !== "undefined") clean.priority = (sub as any).priority; - if (Array.isArray((sub as any).tags)) clean.tags = this.normalizeTags((sub as any).tags); - if ((sub as any).id) clean.id = (sub as any).id; - - return clean; - }); - } - async addTask(data: Partial): Promise { const tags = this.normalizeTags(data.tags); - const subtasks = this.normalizeSubtasks((data as any).subtasks); return Task.create({ ...data, ...(tags ? { tags } : {}), - ...(subtasks ? { subtasks } : {}), }); } async addTasks(data: Partial[]): Promise { const withTags = data.map((task) => { const tags = this.normalizeTags(task.tags); - const subtasks = this.normalizeSubtasks((task as any).subtasks); - return { ...task, ...(tags ? { tags } : {}), ...(subtasks ? { subtasks } : {}) }; + return { ...task, ...(tags ? { tags } : {}) }; }); return Task.insertMany(withTags); @@ -67,14 +40,15 @@ export class TaskService { async updateTask(id: string, data: Partial | UpdateQuery): Promise { const tags = this.normalizeTags((data as Partial)?.tags); - const subtasks = this.normalizeSubtasks((data as any)?.subtasks); const update: Partial | UpdateQuery = { ...(data as Partial), ...(tags ? { tags } : {}), - ...(subtasks ? { subtasks } : {}), }; + // Explicitly drop any subtasks payloads + (update as any).subtasks = undefined; + return Task.findByIdAndUpdate(id, update).lean().exec(); } @@ -87,7 +61,6 @@ export class TaskService { } return Task.find(query) - .populate("subtasks") .populate({ path: "users", select: "first last email id" }) .lean() .exec(); @@ -124,7 +97,6 @@ export class TaskService { const todayFormat = this.getTaskDateFormat(new Date()); return Task.find({ users: userId, date: { $regex: todayFormat }, done: false }) - .populate("subtasks") .populate({ path: "users", select: "first last email id" }) .lean() .exec(); @@ -137,7 +109,6 @@ export class TaskService { const tomorrowFormat = this.getTaskDateFormat(today); return Task.find({ users: userId, date: { $regex: tomorrowFormat }, done: false }) - .populate("subtasks") .populate({ path: "users", select: "first last email id" }) .lean() .exec(); @@ -148,7 +119,6 @@ export class TaskService { const format = this.getTaskDateWeekFormat(today); return Task.find({ users: userId, date: { $regex: format }, done: false }) - .populate("subtasks") .populate({ path: "users", select: "first last email id" }) .lean() .exec(); @@ -159,7 +129,6 @@ export class TaskService { today.setHours(0, 0, 0, 0); const tasks = await Task.find({ users: userId, done: false }) - .populate("subtasks") .populate({ path: "users", select: "first last email id" }) .lean() .exec(); @@ -175,7 +144,6 @@ export class TaskService { async getTasksIncomplete(userId: string): Promise { return Task.find({ users: userId, done: false }) - .populate("subtasks") .populate({ path: "users", select: "first last email id" }) .sort({ priority: -1 }) .lean() diff --git a/packages/app/src/components/menus/TaskContainer/TaskContainer.tsx b/packages/app/src/components/menus/TaskContainer/TaskContainer.tsx index 8a4f6f1..14bd213 100644 --- a/packages/app/src/components/menus/TaskContainer/TaskContainer.tsx +++ b/packages/app/src/components/menus/TaskContainer/TaskContainer.tsx @@ -102,22 +102,7 @@ export default function TaskContainer({ }) .filter(Boolean) : []; - const subtaskTags = Array.isArray(task.subtasks) - ? task.subtasks.flatMap((subtask) => - Array.isArray(subtask.tags) - ? subtask.tags - .map((tag) => { - if (typeof tag === "string") return tag.toLowerCase(); - if (tag && typeof (tag as any).title === "string") return (tag as any).title.toLowerCase(); - return ""; - }) - .filter(Boolean) - : [] - ) - : []; - - const combined = [...ownTags, ...subtaskTags]; - return activeTags.every((tag) => combined.includes(tag)); + return activeTags.every((tag) => ownTags.includes(tag)); }; if (activeTags.length > 0) { diff --git a/packages/app/src/components/task/TaskItem.tsx b/packages/app/src/components/task/TaskItem.tsx index 768d330..657166b 100644 --- a/packages/app/src/components/task/TaskItem.tsx +++ b/packages/app/src/components/task/TaskItem.tsx @@ -1,4 +1,4 @@ -import { Task, useDeleteTask, useUpdateTask } from "@/hooks/tasks"; +import { Task, useUpdateTask } from "@/hooks/tasks"; import { matchDate } from "@/utils/date"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -8,19 +8,19 @@ import TaskItemTitle from "./TaskItemTitle"; import TaskItemDate from "./TaskItemDate"; import { isTaskDone } from "@/utils/data"; import { useApp } from "@/hooks/app"; -import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid"; interface TaskItemParams { skeleton: boolean; + item?: Task; + setIsInspecting?: (open: boolean) => void; + taskFilter?: string; selectionMode?: boolean; isSelected?: boolean; onToggleSelect?: (id: string) => void; isAnimating?: boolean; } -export function TaskItem({ skeleton, item, setIsInspecting, type, parent, taskFilter, selectionMode = false, isSelected = false, onToggleSelect, isAnimating = false }: TaskItemParams) { - const isSubtask = type === "subtask"; - +export function TaskItem({ skeleton, item, setIsInspecting, taskFilter, selectionMode = false, isSelected = false, onToggleSelect, isAnimating = false }: TaskItemParams) { if (skeleton) { return (
@@ -54,17 +54,12 @@ export function TaskItem({ skeleton, item, setIsInspecting, type, parent, taskFi const navigate = useNavigate(); - const { mutate: deleteTask } = useDeleteTask(); const { mutate: updateTask } = useUpdateTask(); const [appData, setAppData] = useApp(); - - const [isDeleting, setIsDeleting] = useState(false); - const [isManaging, setIsManaging] = useState(false); const [isCompleting, setIsCompleting] = useState(false); - const [isAccordion, setAccordion] = useState(item.accordion || false); const tags = Array.isArray(item.tags) ? item.tags .map((tag) => { @@ -103,38 +98,9 @@ export function TaskItem({ skeleton, item, setIsInspecting, type, parent, taskFi if (!foundDate) newDone.push(rawDate); else newDone.splice(newDone.indexOf(rawDate), 1); - if (type == "subtask") { - const newSubs = [...parent.subtasks]; - for (let i = 0; i < newSubs.length; i++) { - if (newSubs[i].id == item.id) newSubs[i] = { ...item, done: newDone }; - } - - newData = { - ...parent, - subtasks: newSubs, - }; - - updateTask({ id: parent.id, data: newData }); - } else { - updateTask({ id: item.id, data: { ...item, done: newDone } }); - } + updateTask({ id: item.id, data: { ...item, done: newDone } }); } else { - if (type == "subtask") { - const newSubs = [...parent.subtasks]; - for (let i = 0; i < newSubs.length; i++) { - if (newSubs[i].id == item.id) - newSubs[i] = { ...item, done: !item.done }; - } - - newData = { - ...parent, - subtasks: newSubs, - }; - - updateTask({ id: parent.id, data: newData }); - } else { - updateTask({ id: item.id, data: { ...item, done: !item.done } }); - } + updateTask({ id: item.id, data: { ...item, done: !item.done } }); } setIsManaging(false); @@ -150,11 +116,6 @@ export function TaskItem({ skeleton, item, setIsInspecting, type, parent, taskFi return; } - if (type == "subtask") { - handleInteractiveSubtask(e); - return; - } - e.stopPropagation(); setAppData({ @@ -165,18 +126,6 @@ export function TaskItem({ skeleton, item, setIsInspecting, type, parent, taskFi setIsInspecting(true); }; - const handleInteractiveSubtask = (e: any) => { - e.stopPropagation(); - - setAppData({ - ...appData, - activeParent: parent, - activeTask: item, - }); - - setIsInspecting(true); - }; - const selectionClass = selectionMode ? isSelected ? "ring-2 ring-accent-blue/40" @@ -221,28 +170,6 @@ export function TaskItem({ skeleton, item, setIsInspecting, type, parent, taskFi
- {item.subtasks?.length > 0 && ( -
-
{ - e.stopPropagation(); - const newValue = !isAccordion; - - setAccordion(newValue); - updateTask({ - id: item.id, - data: { - ...item, - accordion: newValue, - }, - }); - }} - > - {!isAccordion && } - {isAccordion && } -
-
- )}
@@ -264,26 +191,6 @@ export function TaskItem({ skeleton, item, setIsInspecting, type, parent, taskFi
-
-
- {item.subtasks?.length > 0 && - !isAccordion && - item.subtasks?.map((subtask: Task, key: number) => ( -
- -
- ))} -
-
); } diff --git a/packages/app/src/components/tasks/TagFilterBar.tsx b/packages/app/src/components/tasks/TagFilterBar.tsx index 6320c73..cb6648a 100644 --- a/packages/app/src/components/tasks/TagFilterBar.tsx +++ b/packages/app/src/components/tasks/TagFilterBar.tsx @@ -20,13 +20,8 @@ export default function TagFilterBar({ tasks }: TagFilterBarProps) { if (!pending) return; const tags = Array.isArray(task?.tags) ? task.tags : []; - const subtaskTags = Array.isArray(task?.subtasks) - ? task.subtasks.flatMap((subtask) => - Array.isArray(subtask.tags) ? subtask.tags : [] - ) - : []; - [...tags, ...subtaskTags].forEach((tag) => { + tags.forEach((tag) => { const normalized = typeof tag === "string" ? tag.toLowerCase() : tag && typeof (tag as any).title === "string" diff --git a/packages/app/src/components/tasks/TaskMenu.tsx b/packages/app/src/components/tasks/TaskMenu.tsx index f062c36..3b68818 100644 --- a/packages/app/src/components/tasks/TaskMenu.tsx +++ b/packages/app/src/components/tasks/TaskMenu.tsx @@ -1,5 +1,7 @@ +import { useMemo, useState } from "react"; import { TaskItem } from "../task/TaskItem"; import { isTaskDone } from "@/utils/data"; +import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/20/solid"; export default function TaskMenu({ skeleton, @@ -12,6 +14,7 @@ export default function TaskMenu({ animatingIds = [], activeDate }) { + const [collapsedGroups, setCollapsedGroups] = useState([]); if (skeleton) { return ( @@ -33,27 +36,88 @@ export default function TaskMenu({ : true ); + const { grouped, ungrouped } = useMemo(() => { + const groupedTasks: Record = {}; + const ungroupedTasks: any[] = []; + + visibleTasks.forEach((task) => { + const groupName = (task.group || "").trim(); + if (groupName.length === 0) { + ungroupedTasks.push(task); + return; + } + + if (!groupedTasks[groupName]) groupedTasks[groupName] = []; + groupedTasks[groupName].push(task); + }); + + return { grouped: groupedTasks, ungrouped: ungroupedTasks }; + }, [visibleTasks]); + + const toggleGroup = (group: string) => { + setCollapsedGroups((prev) => + prev.includes(group) ? prev.filter((g) => g !== group) : [...prev, group] + ); + }; + + const renderTask = (task: any) => ( +
+ +
+ ); + + const groupedEntries = Object.entries(grouped).sort(([a], [b]) => + a.localeCompare(b) + ); + return (
-
    - {visibleTasks.length > 0 && - visibleTasks.map((task, key) => ( -
  • - -
  • - ))} +
    + {ungrouped.length > 0 && ungrouped.map(renderTask)} + + {groupedEntries.map(([groupName, list]) => { + const isCollapsed = collapsedGroups.includes(groupName); + return ( +
    + + {!isCollapsed && ( +
    + {list.map((task: any) => renderTask(task))} +
    + )} +
    + ); + })} + {visibleTasks.length === 0 && ( -

    No Tasks

    +

    No Tasks

    )} -
+
); } diff --git a/packages/app/src/hooks/app.ts b/packages/app/src/hooks/app.ts index d5af266..b995cb3 100644 --- a/packages/app/src/hooks/app.ts +++ b/packages/app/src/hooks/app.ts @@ -17,7 +17,6 @@ export interface AppOptions { activeDate?: Date; tempActiveDate?: Date; activeTask?: Task; - activeParent?: Task; activeTags?: string[]; theme?: ThemeChoice; @@ -29,7 +28,6 @@ const initialData: AppOptions = { activeDate: new Date(), tempActiveDate: undefined, activeTask: undefined, - activeParent: undefined, activeTags: [], theme: "auto", diff --git a/packages/app/src/hooks/tasks.ts b/packages/app/src/hooks/tasks.ts index 953f96d..b0bbe88 100644 --- a/packages/app/src/hooks/tasks.ts +++ b/packages/app/src/hooks/tasks.ts @@ -26,9 +26,9 @@ export interface Task { type?: string; accordion?: boolean; priority?: number; - subtasks: Task[]; users?: any[]; tags: string[]; + group?: string; } const normalizeTags = (tags?: Array): string[] | undefined => { @@ -48,7 +48,7 @@ const normalizeTags = (tags?: Array const normalizeTaskFromApi = (task: any): Task => ({ ...task, tags: normalizeTags(task?.tags) ?? [], - subtasks: Array.isArray(task?.subtasks) ? task.subtasks.map(normalizeTaskFromApi) : [], + group: task?.group ?? "", }); const serializeTask = (task: Partial) => { @@ -70,9 +70,9 @@ export function createInitialTaskData(): Task { done: false, repeater: "", reminder: "", - subtasks: [], priority: 0, - tags: [] + tags: [], + group: "" }; } diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuEdit.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuEdit.tsx index 6047a7d..ccb12ba 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuEdit.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuEdit.tsx @@ -12,7 +12,6 @@ export default function MenuEdit({ type, appData, tempData, isDeleting, setIsDel {type == "edit" && (
) -} \ No newline at end of file +} diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx index 073c89d..3b46e71 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx @@ -1,7 +1,5 @@ import { formatDateTime } from "@/utils/date"; import TaskInfoMenuItem from "./Shared/TaskInfoMenuItem"; -import TaskInfoMenuSubtaskMenu from "./Shared/TaskInfoMenuSubtaskMenu"; -import TaskInfoMenuSelect from "./Shared/TaskInfoMenuSelect"; import TaskInfoMenuUser from "./Shared/TaskInfoUser/TaskInfoMenuUser"; import TaskInfoMenuTags from "./Shared/TaskInfoMenuTags"; @@ -91,31 +89,20 @@ export default function MenuFields({ setTempData({ ...tempData, title: e.target.value }) } /> + ) => + setTempData({ ...tempData, group: e.target.value }) + } + placeholder="Optional group label" + /> {!isQuickAdd && validationError && ( {validationError} )} - {/* ) => { - setTempData({ type: e.target.value }); - }} - options={[ - { name: "Standard", value: "" }, - { name: "Group", value: "group" }, - ]} - /> */} - - -

Delete this task?

- This will permanently remove the task and any subtasks. + This will permanently remove the task.

diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtask.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtask.tsx deleted file mode 100644 index 276dacd..0000000 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtask.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Task } from "@/hooks/tasks"; - -export interface TaskInfoMenuSubtaskParams { - task: Task; - onChangeTitle: (id: string | undefined, title: string) => void; - onDelete: (id: string | undefined) => void; -} - -export default function TaskInfoMenuSubtask({ - task, - onChangeTitle, - onDelete, -}: TaskInfoMenuSubtaskParams) { - return ( -
- onChangeTitle(task.id, e.target.value)} - placeholder="Subtask title" - /> - -
- ); -} diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtaskMenu.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtaskMenu.tsx deleted file mode 100644 index 57ef8ba..0000000 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuSubtaskMenu.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useMemo, useState } from "react"; -import { Task, createInitialTaskData } from "@/hooks/tasks"; -import TaskInfoMenuSubtask from "./TaskInfoMenuSubtask"; -import { createID } from "@/utils/id"; - -export interface TaskInfoMenuSubtaskMenuParams { - subtasks: Task[]; - tempData: Task; - setTempData: CallableFunction; -} - -export default function TaskInfoMenuSubtaskMenu({ - subtasks, - tempData, - setTempData, -}: TaskInfoMenuSubtaskMenuParams) { - const [newTitle, setNewTitle] = useState(""); - - const normalizedSubtasks = useMemo( - () => subtasks?.map((task) => ({ ...task, title: task.title ?? "" })) ?? [], - [subtasks] - ); - - const createNewSubtask = () => { - const title = newTitle.trim(); - if (!title) return; - - const tempSubtasks = [...(tempData.subtasks || [])]; - - const newTask: Task = { - ...createInitialTaskData(), - id: createID(20), - title, - }; - - tempSubtasks.push(newTask); - - setTempData({ - subtasks: tempSubtasks, - }); - setNewTitle(""); - }; - - const deleteSubtask = (id: string | undefined) => { - const tempSubtasks = (tempData.subtasks ?? []).filter((sub) => sub.id !== id); - - setTempData({ subtasks: tempSubtasks }); - }; - - const updateSubtaskTitle = (id: string | undefined, title: string) => { - const tempSubtasks = (tempData.subtasks ?? []).map((task) => - task.id === id ? { ...task, title } : task - ); - - setTempData({ subtasks: tempSubtasks }); - }; - - return ( -
-
-

Subtasks

-
-
- {normalizedSubtasks?.map((task: Task, key: number) => ( - - ))} -
-
- setNewTitle(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - createNewSubtask(); - } - }} - /> - -
-
- ); -} diff --git a/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx b/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx index e07f3bb..0c5e8c1 100644 --- a/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx +++ b/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx @@ -265,42 +265,12 @@ export default function TaskInfoMenu({ title: tempData.title.trim(), }; - if (appData.activeParent) { - const subTaskData = cleanedTask; - - Logger.log("Sub Task Data", subTaskData); - - const parentData = appData.activeParent; - - Logger.log("Parent Data", parentData); - - const newSubs = appData.activeParent.subtasks; - - Logger.log("Old Subtasks", newSubs); - - for (let i = 0; i < newSubs.length; i++) { - if (newSubs[i].id == subTaskData.id) newSubs[i] = subTaskData; - } - - Logger.log("New Subtasks", newSubs); - - updateTask({ - id: parentData.id, - data: { - ...parentData, - subtasks: newSubs, - }, - }); - - return; - } - - updateTask({ - id: tempData.id, - data: { - ...cleanedTask, - }, - }); + updateTask({ + id: tempData.id, + data: { + ...cleanedTask, + }, + }); Logger.log("Data To Add", { tempData @@ -320,8 +290,8 @@ export default function TaskInfoMenu({ repeater: "", reminder: "", priority: 0, - subtasks: [], tags: tempData.tags ?? [], + group: tempData.group ?? "", })); addTasksBulk(payload).then(() => { From 47d025c58a431698f526eca0d1b8c35cce2b4a1a Mon Sep 17 00:00:00 2001 From: Hiro Date: Sun, 21 Dec 2025 16:40:13 -0600 Subject: [PATCH 3/7] Change order --- packages/api/src/index.ts | 2 +- .../app/src/components/tasks/TaskMenu.tsx | 83 ++++++++++++++++++- packages/app/src/hooks/app.ts | 2 +- .../(Layout)/(TaskInfoMenu)/MenuFields.tsx | 35 ++++++-- 4 files changed, 109 insertions(+), 13 deletions(-) diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index ce92e2c..04766da 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -30,7 +30,7 @@ database.plugin(leanIdPlugin); /* setup the platform and global middleware */ const platform = new ExpressPlatform(); -platform.use(cors({ origin: [appUrl], credentials: true })); +platform.use(cors({ origin: [appUrl, "http://192.168.1.14:5173"], credentials: true })); platform.set("trust proxy", 4); platform.use(session({ diff --git a/packages/app/src/components/tasks/TaskMenu.tsx b/packages/app/src/components/tasks/TaskMenu.tsx index 3b68818..ee50965 100644 --- a/packages/app/src/components/tasks/TaskMenu.tsx +++ b/packages/app/src/components/tasks/TaskMenu.tsx @@ -1,7 +1,8 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { TaskItem } from "../task/TaskItem"; import { isTaskDone } from "@/utils/data"; import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/20/solid"; +import { useUpdateTask } from "@/hooks/tasks"; export default function TaskMenu({ skeleton, @@ -15,6 +16,11 @@ export default function TaskMenu({ activeDate }) { const [collapsedGroups, setCollapsedGroups] = useState([]); + const [orderedTasks, setOrderedTasks] = useState([]); + const [draggingId, setDraggingId] = useState(null); + const listRef = useRef(null); + const lastHoverIdRef = useRef(null); + const { mutateAsync: updateTask } = useUpdateTask(); if (skeleton) { return ( @@ -36,11 +42,36 @@ export default function TaskMenu({ : true ); + useEffect(() => { + setOrderedTasks(visibleTasks); + }, [tasks, taskFilter, activeDate]); + + const handleReorder = (sourceId: string, targetId: string) => { + if (!sourceId || !targetId || sourceId === targetId) return; + const current = [...orderedTasks]; + const fromIndex = current.findIndex((t) => t.id === sourceId); + const toIndex = current.findIndex((t) => t.id === targetId); + if (fromIndex === -1 || toIndex === -1) return; + + const [moved] = current.splice(fromIndex, 1); + current.splice(toIndex, 0, moved); + setOrderedTasks(current); + }; + + const persistOrder = async (list: any[]) => { + const total = list.length; + const updates = list.map((task, idx) => ({ + id: task.id, + data: { ...task, priority: total - idx } + })); + await Promise.all(updates.map((payload) => updateTask(payload))); + }; + const { grouped, ungrouped } = useMemo(() => { const groupedTasks: Record = {}; const ungroupedTasks: any[] = []; - visibleTasks.forEach((task) => { + orderedTasks.forEach((task) => { const groupName = (task.group || "").trim(); if (groupName.length === 0) { ungroupedTasks.push(task); @@ -60,8 +91,54 @@ export default function TaskMenu({ ); }; + const findTaskIdFromPoint = (clientX: number, clientY: number): string | null => { + const el = document.elementFromPoint(clientX, clientY); + if (!el) return null; + const taskNode = el.closest("[data-task-id]"); + if (!taskNode) return null; + return (taskNode as HTMLElement).dataset.taskId || null; + }; + const renderTask = (task: any) => ( -
+
{ + e.preventDefault(); + setDraggingId(task.id); + e.currentTarget.setPointerCapture(e.pointerId); + }} + onPointerMove={(e) => { + if (!draggingId) return; + e.preventDefault(); + const targetId = findTaskIdFromPoint(e.clientX, e.clientY); + if (targetId && targetId !== lastHoverIdRef.current) { + lastHoverIdRef.current = targetId; + handleReorder(draggingId, targetId); + } + }} + onPointerUp={() => { + if (draggingId) { + const current = [...orderedTasks]; + setDraggingId(null); + persistOrder(current); + lastHoverIdRef.current = null; + } + }} + onPointerEnter={() => { + if (draggingId && draggingId !== task.id) { + lastHoverIdRef.current = task.id; + handleReorder(draggingId, task.id); + } + }} + onPointerCancel={() => setDraggingId(null)} + style={{ + touchAction: "none", + transition: draggingId === task.id ? "none" : "transform 120ms ease, opacity 120ms ease", + }} + > */} - ) => - setTempData({ ...tempData, priority: e.target.value }) - } - /> +
+ Priority +
+ {[ + { label: "High", value: 3, color: "from-rose-500 to-rose-400" }, + { label: "Medium", value: 2, color: "from-amber-500 to-amber-400" }, + { label: "Low", value: 1, color: "from-emerald-500 to-emerald-400" }, + { label: "None", value: 0, color: "from-slate-400 to-slate-300" }, + ].map((opt) => { + const isActive = Number(tempData.priority ?? 0) === opt.value; + return ( + + ); + })} +
+
)}
From 1339540a456a29b4cf7f05bc5683116d779709c1 Mon Sep 17 00:00:00 2001 From: Hiro Date: Sun, 21 Dec 2025 16:53:34 -0600 Subject: [PATCH 4/7] Cleaner UI --- packages/api/src/index.ts | 5 +- .../app/src/components/tasks/TaskMenu.tsx | 144 +++++++++++++++--- 2 files changed, 122 insertions(+), 27 deletions(-) diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 04766da..88033f7 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -6,7 +6,8 @@ import cors from "cors"; import session from "express-session"; import MongoStore from "connect-mongo"; -const appUrl = process.env.APP_URL; +const appUrl = process.env.APP_URL +const; const sessionSecret = process.env.SESSION_SECRET; if (!appUrl) { @@ -30,7 +31,7 @@ database.plugin(leanIdPlugin); /* setup the platform and global middleware */ const platform = new ExpressPlatform(); -platform.use(cors({ origin: [appUrl, "http://192.168.1.14:5173"], credentials: true })); +platform.use(cors({ origin: [appUrl, ], credentials: true })); platform.set("trust proxy", 4); platform.use(session({ diff --git a/packages/app/src/components/tasks/TaskMenu.tsx b/packages/app/src/components/tasks/TaskMenu.tsx index ee50965..f1808c8 100644 --- a/packages/app/src/components/tasks/TaskMenu.tsx +++ b/packages/app/src/components/tasks/TaskMenu.tsx @@ -43,10 +43,20 @@ export default function TaskMenu({ ); useEffect(() => { - setOrderedTasks(visibleTasks); + const sorted = [...visibleTasks].sort((a, b) => { + const pa = Number(a.priority ?? 0); + const pb = Number(b.priority ?? 0); + if (pa === pb) return 0; + return pb - pa; + }); + setOrderedTasks(sorted); }, [tasks, taskFilter, activeDate]); const handleReorder = (sourceId: string, targetId: string) => { + if (targetId.startsWith("group-")) { + lastHoverIdRef.current = targetId; + return; + } if (!sourceId || !targetId || sourceId === targetId) return; const current = [...orderedTasks]; const fromIndex = current.findIndex((t) => t.id === sourceId); @@ -60,13 +70,43 @@ export default function TaskMenu({ const persistOrder = async (list: any[]) => { const total = list.length; - const updates = list.map((task, idx) => ({ - id: task.id, - data: { ...task, priority: total - idx } - })); + const highCount = Math.max(1, Math.floor(total / 3)); + const mediumCount = Math.max(1, Math.floor(total / 3)); + const lowCount = Math.max(0, total - highCount - mediumCount); + + const updates = list.map((task, idx) => { + let priority = 0; + if (idx < highCount) priority = 3; + else if (idx < highCount + mediumCount) priority = 2; + else if (idx < highCount + mediumCount + lowCount) priority = 1; + else priority = 0; + + return { id: task.id, data: { ...task, priority } }; + }); + await Promise.all(updates.map((payload) => updateTask(payload))); }; + const moveTaskToGroup = (taskId: string, groupName: string) => { + const list = [...orderedTasks]; + const fromIndex = list.findIndex((t) => t.id === taskId); + if (fromIndex === -1) return list; + + const [item] = list.splice(fromIndex, 1); + item.group = groupName; + + let insertIndex = list.length; + for (let i = list.length - 1; i >= 0; i--) { + if ((list[i].group || "").trim() === groupName) { + insertIndex = i + 1; + break; + } + } + + list.splice(insertIndex, 0, item); + return list; + }; + const { grouped, ungrouped } = useMemo(() => { const groupedTasks: Record = {}; const ungroupedTasks: any[] = []; @@ -102,7 +142,7 @@ export default function TaskMenu({ const renderTask = (task: any) => (
{ @@ -119,13 +159,21 @@ export default function TaskMenu({ handleReorder(draggingId, targetId); } }} - onPointerUp={() => { - if (draggingId) { - const current = [...orderedTasks]; - setDraggingId(null); - persistOrder(current); - lastHoverIdRef.current = null; + onPointerUp={(e) => { + if (!draggingId) return; + + const dropTarget = lastHoverIdRef.current ?? findTaskIdFromPoint(e.clientX, e.clientY); + let updated = [...orderedTasks]; + + if (dropTarget && dropTarget.startsWith("group-")) { + const groupName = dropTarget.replace("group-", ""); + updated = moveTaskToGroup(draggingId, groupName); } + + setDraggingId(null); + lastHoverIdRef.current = null; + setOrderedTasks(updated); + persistOrder(updated); }} onPointerEnter={() => { if (draggingId && draggingId !== task.id) { @@ -155,6 +203,59 @@ export default function TaskMenu({ a.localeCompare(b) ); + const renderGroupHeader = (groupName: string, isCollapsed: boolean, count: number) => { + const isDraggingGroup = draggingId === `group-${groupName}`; + return ( +
{ + e.preventDefault(); + setDraggingId(`group-${groupName}`); + e.currentTarget.setPointerCapture(e.pointerId); + }} + onPointerMove={(e) => { + if (!draggingId) return; + const targetId = findTaskIdFromPoint(e.clientX, e.clientY); + if (targetId && targetId !== lastHoverIdRef.current) { + lastHoverIdRef.current = targetId; + handleReorder(draggingId, targetId); + } + }} + onPointerEnter={() => { + if (draggingId && draggingId !== `group-${groupName}`) { + lastHoverIdRef.current = `group-${groupName}`; + handleReorder(draggingId, `group-${groupName}`); + } + }} + onPointerUp={() => { + if (draggingId) { + const current = [...orderedTasks]; + setDraggingId(null); + persistOrder(current); + lastHoverIdRef.current = null; + } + }} + onPointerCancel={() => { + setDraggingId(null); + lastHoverIdRef.current = null; + }} + > +
+ {isCollapsed ? ( + + ) : ( + + )} + {groupName} +
+ {count} +
+ ); + }; + return (
@@ -167,21 +268,14 @@ export default function TaskMenu({ key={groupName} className="w-full rounded-2xl border bg-white/90 px-3 py-2 shadow-sm ring-1 ring-accent-blue/10 dark:bg-slate-900/70" > - + {renderGroupHeader(groupName, isCollapsed, list.length)} +
{!isCollapsed && (
{list.map((task: any) => renderTask(task))} From ac015758c10a6bc2cd96a8211a6beab3a12c6d33 Mon Sep 17 00:00:00 2001 From: Hiro Date: Sun, 21 Dec 2025 17:01:27 -0600 Subject: [PATCH 5/7] Fix tap actions --- packages/api/src/index.ts | 3 +- .../app/src/components/tasks/TaskMenu.tsx | 30 +++++++++++++++---- packages/app/src/hooks/app.ts | 2 +- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 88033f7..ff7a069 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -6,8 +6,7 @@ import cors from "cors"; import session from "express-session"; import MongoStore from "connect-mongo"; -const appUrl = process.env.APP_URL -const; +const appUrl = process.env.APP_URL; const sessionSecret = process.env.SESSION_SECRET; if (!appUrl) { diff --git a/packages/app/src/components/tasks/TaskMenu.tsx b/packages/app/src/components/tasks/TaskMenu.tsx index f1808c8..0b0af0e 100644 --- a/packages/app/src/components/tasks/TaskMenu.tsx +++ b/packages/app/src/components/tasks/TaskMenu.tsx @@ -20,6 +20,7 @@ export default function TaskMenu({ const [draggingId, setDraggingId] = useState(null); const listRef = useRef(null); const lastHoverIdRef = useRef(null); + const dragCandidateRef = useRef<{ id: string; x: number; y: number } | null>(null); const { mutateAsync: updateTask } = useUpdateTask(); if (skeleton) { @@ -146,11 +147,21 @@ export default function TaskMenu({ draggable={false} data-task-id={task.id} onPointerDown={(e) => { - e.preventDefault(); - setDraggingId(task.id); - e.currentTarget.setPointerCapture(e.pointerId); + dragCandidateRef.current = { id: task.id, x: e.clientX, y: e.clientY }; + lastHoverIdRef.current = null; }} onPointerMove={(e) => { + const candidate = dragCandidateRef.current; + if (!draggingId && candidate) { + const dx = e.clientX - candidate.x; + const dy = e.clientY - candidate.y; + const distSq = dx * dx + dy * dy; + if (distSq > 64) { // ~8px threshold + setDraggingId(candidate.id); + lastHoverIdRef.current = candidate.id; + } + } + if (!draggingId) return; e.preventDefault(); const targetId = findTaskIdFromPoint(e.clientX, e.clientY); @@ -160,7 +171,11 @@ export default function TaskMenu({ } }} onPointerUp={(e) => { - if (!draggingId) return; + if (!draggingId) { + dragCandidateRef.current = null; + lastHoverIdRef.current = null; + return; + } const dropTarget = lastHoverIdRef.current ?? findTaskIdFromPoint(e.clientX, e.clientY); let updated = [...orderedTasks]; @@ -172,6 +187,7 @@ export default function TaskMenu({ setDraggingId(null); lastHoverIdRef.current = null; + dragCandidateRef.current = null; setOrderedTasks(updated); persistOrder(updated); }} @@ -181,7 +197,11 @@ export default function TaskMenu({ handleReorder(draggingId, task.id); } }} - onPointerCancel={() => setDraggingId(null)} + onPointerCancel={() => { + setDraggingId(null); + dragCandidateRef.current = null; + lastHoverIdRef.current = null; + }} style={{ touchAction: "none", transition: draggingId === task.id ? "none" : "transform 120ms ease, opacity 120ms ease", diff --git a/packages/app/src/hooks/app.ts b/packages/app/src/hooks/app.ts index 29e7aa5..b995cb3 100644 --- a/packages/app/src/hooks/app.ts +++ b/packages/app/src/hooks/app.ts @@ -6,7 +6,7 @@ import { Task } from "./tasks"; export const AppContext = createContext(null); // Allow docker / hosted environments to inject an API base URL at build time. -export const SERVER_IP = process.env.NODE_ENV == "development" ? `http://192.168.1.14:8080` : `https://api.sequenced.ottegi.com`; +export const SERVER_IP = process.env.NODE_ENV == "development" ? `http://localhost:8080` : `https://api.sequenced.ottegi.com`; Logger.log(`Running in ${process.env.NODE_ENV} mode.`); From 20c80633ff5879dcc0f05a8b53940088c2f864c7 Mon Sep 17 00:00:00 2001 From: Hiro Date: Sun, 21 Dec 2025 17:12:04 -0600 Subject: [PATCH 6/7] Cleanup --- .../app/src/components/tasks/TaskMenu.tsx | 214 ++---------------- packages/app/src/hooks/tasks.ts | 2 +- .../(Layout)/(TaskInfoMenu)/MenuFields.tsx | 2 +- 3 files changed, 22 insertions(+), 196 deletions(-) diff --git a/packages/app/src/components/tasks/TaskMenu.tsx b/packages/app/src/components/tasks/TaskMenu.tsx index 0b0af0e..017be19 100644 --- a/packages/app/src/components/tasks/TaskMenu.tsx +++ b/packages/app/src/components/tasks/TaskMenu.tsx @@ -1,8 +1,7 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { TaskItem } from "../task/TaskItem"; import { isTaskDone } from "@/utils/data"; import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/20/solid"; -import { useUpdateTask } from "@/hooks/tasks"; export default function TaskMenu({ skeleton, @@ -17,11 +16,6 @@ export default function TaskMenu({ }) { const [collapsedGroups, setCollapsedGroups] = useState([]); const [orderedTasks, setOrderedTasks] = useState([]); - const [draggingId, setDraggingId] = useState(null); - const listRef = useRef(null); - const lastHoverIdRef = useRef(null); - const dragCandidateRef = useRef<{ id: string; x: number; y: number } | null>(null); - const { mutateAsync: updateTask } = useUpdateTask(); if (skeleton) { return ( @@ -32,7 +26,7 @@ export default function TaskMenu({
- ) + ); } const visibleTasks = (tasks || []) @@ -53,61 +47,6 @@ export default function TaskMenu({ setOrderedTasks(sorted); }, [tasks, taskFilter, activeDate]); - const handleReorder = (sourceId: string, targetId: string) => { - if (targetId.startsWith("group-")) { - lastHoverIdRef.current = targetId; - return; - } - if (!sourceId || !targetId || sourceId === targetId) return; - const current = [...orderedTasks]; - const fromIndex = current.findIndex((t) => t.id === sourceId); - const toIndex = current.findIndex((t) => t.id === targetId); - if (fromIndex === -1 || toIndex === -1) return; - - const [moved] = current.splice(fromIndex, 1); - current.splice(toIndex, 0, moved); - setOrderedTasks(current); - }; - - const persistOrder = async (list: any[]) => { - const total = list.length; - const highCount = Math.max(1, Math.floor(total / 3)); - const mediumCount = Math.max(1, Math.floor(total / 3)); - const lowCount = Math.max(0, total - highCount - mediumCount); - - const updates = list.map((task, idx) => { - let priority = 0; - if (idx < highCount) priority = 3; - else if (idx < highCount + mediumCount) priority = 2; - else if (idx < highCount + mediumCount + lowCount) priority = 1; - else priority = 0; - - return { id: task.id, data: { ...task, priority } }; - }); - - await Promise.all(updates.map((payload) => updateTask(payload))); - }; - - const moveTaskToGroup = (taskId: string, groupName: string) => { - const list = [...orderedTasks]; - const fromIndex = list.findIndex((t) => t.id === taskId); - if (fromIndex === -1) return list; - - const [item] = list.splice(fromIndex, 1); - item.group = groupName; - - let insertIndex = list.length; - for (let i = list.length - 1; i >= 0; i--) { - if ((list[i].group || "").trim() === groupName) { - insertIndex = i + 1; - break; - } - } - - list.splice(insertIndex, 0, item); - return list; - }; - const { grouped, ungrouped } = useMemo(() => { const groupedTasks: Record = {}; const ungroupedTasks: any[] = []; @@ -124,7 +63,7 @@ export default function TaskMenu({ }); return { grouped: groupedTasks, ungrouped: ungroupedTasks }; - }, [visibleTasks]); + }, [orderedTasks]); const toggleGroup = (group: string) => { setCollapsedGroups((prev) => @@ -132,81 +71,8 @@ export default function TaskMenu({ ); }; - const findTaskIdFromPoint = (clientX: number, clientY: number): string | null => { - const el = document.elementFromPoint(clientX, clientY); - if (!el) return null; - const taskNode = el.closest("[data-task-id]"); - if (!taskNode) return null; - return (taskNode as HTMLElement).dataset.taskId || null; - }; - const renderTask = (task: any) => ( -
{ - dragCandidateRef.current = { id: task.id, x: e.clientX, y: e.clientY }; - lastHoverIdRef.current = null; - }} - onPointerMove={(e) => { - const candidate = dragCandidateRef.current; - if (!draggingId && candidate) { - const dx = e.clientX - candidate.x; - const dy = e.clientY - candidate.y; - const distSq = dx * dx + dy * dy; - if (distSq > 64) { // ~8px threshold - setDraggingId(candidate.id); - lastHoverIdRef.current = candidate.id; - } - } - - if (!draggingId) return; - e.preventDefault(); - const targetId = findTaskIdFromPoint(e.clientX, e.clientY); - if (targetId && targetId !== lastHoverIdRef.current) { - lastHoverIdRef.current = targetId; - handleReorder(draggingId, targetId); - } - }} - onPointerUp={(e) => { - if (!draggingId) { - dragCandidateRef.current = null; - lastHoverIdRef.current = null; - return; - } - - const dropTarget = lastHoverIdRef.current ?? findTaskIdFromPoint(e.clientX, e.clientY); - let updated = [...orderedTasks]; - - if (dropTarget && dropTarget.startsWith("group-")) { - const groupName = dropTarget.replace("group-", ""); - updated = moveTaskToGroup(draggingId, groupName); - } - - setDraggingId(null); - lastHoverIdRef.current = null; - dragCandidateRef.current = null; - setOrderedTasks(updated); - persistOrder(updated); - }} - onPointerEnter={() => { - if (draggingId && draggingId !== task.id) { - lastHoverIdRef.current = task.id; - handleReorder(draggingId, task.id); - } - }} - onPointerCancel={() => { - setDraggingId(null); - dragCandidateRef.current = null; - lastHoverIdRef.current = null; - }} - style={{ - touchAction: "none", - transition: draggingId === task.id ? "none" : "transform 120ms ease, opacity 120ms ease", - }} - > +
{ - const isDraggingGroup = draggingId === `group-${groupName}`; - return ( -
{ - e.preventDefault(); - setDraggingId(`group-${groupName}`); - e.currentTarget.setPointerCapture(e.pointerId); - }} - onPointerMove={(e) => { - if (!draggingId) return; - const targetId = findTaskIdFromPoint(e.clientX, e.clientY); - if (targetId && targetId !== lastHoverIdRef.current) { - lastHoverIdRef.current = targetId; - handleReorder(draggingId, targetId); - } - }} - onPointerEnter={() => { - if (draggingId && draggingId !== `group-${groupName}`) { - lastHoverIdRef.current = `group-${groupName}`; - handleReorder(draggingId, `group-${groupName}`); - } - }} - onPointerUp={() => { - if (draggingId) { - const current = [...orderedTasks]; - setDraggingId(null); - persistOrder(current); - lastHoverIdRef.current = null; - } - }} - onPointerCancel={() => { - setDraggingId(null); - lastHoverIdRef.current = null; - }} - > -
- {isCollapsed ? ( - - ) : ( - - )} - {groupName} -
- {count} + const renderGroupHeader = (groupName: string, isCollapsed: boolean, count: number) => ( +
+
+ {isCollapsed ? ( + + ) : ( + + )} + {groupName}
- ); - }; + {count} +
+ ); return (
@@ -288,14 +115,13 @@ export default function TaskMenu({ key={groupName} className="w-full rounded-2xl border bg-white/90 px-3 py-2 shadow-sm ring-1 ring-accent-blue/10 dark:bg-slate-900/70" > -
toggleGroup(groupName)} - onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") toggleGroup(groupName); }} + className="w-full text-left" > {renderGroupHeader(groupName, isCollapsed, list.length)} -
+ {!isCollapsed && (
{list.map((task: any) => renderTask(task))} diff --git a/packages/app/src/hooks/tasks.ts b/packages/app/src/hooks/tasks.ts index b0bbe88..5ebdea0 100644 --- a/packages/app/src/hooks/tasks.ts +++ b/packages/app/src/hooks/tasks.ts @@ -48,7 +48,7 @@ const normalizeTags = (tags?: Array const normalizeTaskFromApi = (task: any): Task => ({ ...task, tags: normalizeTags(task?.tags) ?? [], - group: task?.group ?? "", + group: task?.group ? String(task.group).toLowerCase() : "", }); const serializeTask = (task: Partial) => { diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx index 25b1a4b..67e6af1 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx @@ -93,7 +93,7 @@ export default function MenuFields({ name="Group" value={tempData?.group || ""} onChange={(e: React.ChangeEvent) => - setTempData({ ...tempData, group: e.target.value }) + setTempData({ ...tempData, group: e.target.value.toLowerCase() }) } placeholder="Optional group label" /> From 2b81598686db540e6163ea6c3be2543e7e665624 Mon Sep 17 00:00:00 2001 From: Hiro Date: Sun, 21 Dec 2025 17:29:48 -0600 Subject: [PATCH 7/7] Cleanup group ui --- packages/app/src/components/tasks/TaskMenu.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/tasks/TaskMenu.tsx b/packages/app/src/components/tasks/TaskMenu.tsx index 017be19..f87484b 100644 --- a/packages/app/src/components/tasks/TaskMenu.tsx +++ b/packages/app/src/components/tasks/TaskMenu.tsx @@ -89,6 +89,11 @@ export default function TaskMenu({ a.localeCompare(b) ); + const formatGroupName = (name: string) => { + if (!name) return ""; + return name.replace(/\b\w/g, (ch) => ch.toUpperCase()); + }; + const renderGroupHeader = (groupName: string, isCollapsed: boolean, count: number) => (
@@ -97,7 +102,7 @@ export default function TaskMenu({ ) : ( )} - {groupName} + {formatGroupName(groupName)}
{count}
@@ -113,7 +118,7 @@ export default function TaskMenu({ return (