this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: order by urgency

+264 -14
+51
db/migrations/0003.sql
··· 1 + -- Update active_todos view to include urgency calculation and sort by urgency 2 + DROP VIEW IF EXISTS active_todos; 3 + 4 + CREATE VIEW IF NOT EXISTS active_todos AS 5 + SELECT 6 + id, 7 + description, 8 + project, 9 + tags, 10 + due, 11 + wait, 12 + priority, 13 + -- Calculate urgency score 14 + ( 15 + -- Priority: +6 points if priority = '1' 16 + CASE WHEN priority = '1' THEN 6 ELSE 0 END + 17 + 18 + -- Due date urgency 19 + CASE 20 + WHEN due != '' AND date(due) < date('now') THEN 12 -- Overdue 21 + WHEN due != '' AND date(due) = date('now') THEN 8 -- Due today 22 + WHEN due != '' AND date(due) <= date('now', '+7 days') THEN 5 -- Due this week 23 + WHEN due != '' AND date(due) <= date('now', '+30 days') THEN 2 -- Due this month 24 + ELSE 0 25 + END + 26 + 27 + -- Project: +1 if has project 28 + CASE WHEN project != '' THEN 1 ELSE 0 END + 29 + 30 + -- Tags: +1 if has tags 31 + CASE WHEN tags != '[]' AND tags != '' THEN 1 ELSE 0 END 32 + 33 + ) as urgency, 34 + -- working_id based on urgency-sorted order 35 + ROW_NUMBER() OVER (ORDER BY 36 + ( 37 + CASE WHEN priority = '1' THEN 6 ELSE 0 END + 38 + CASE 39 + WHEN due != '' AND date(due) < date('now') THEN 12 40 + WHEN due != '' AND date(due) = date('now') THEN 8 41 + WHEN due != '' AND date(due) <= date('now', '+7 days') THEN 5 42 + WHEN due != '' AND date(due) <= date('now', '+30 days') THEN 2 43 + ELSE 0 44 + END + 45 + CASE WHEN project != '' THEN 1 ELSE 0 END + 46 + CASE WHEN tags != '[]' AND tags != '' THEN 1 ELSE 0 END 47 + ) DESC, 48 + id 49 + ) as working_id 50 + FROM todos 51 + WHERE completed = 0;
+58 -8
mast-react-vite/src/App.tsx
··· 1 - import { useState, useEffect, useMemo } from "react"; 1 + import { useState, useEffect, useMemo, useRef } from "react"; 2 2 import { useQuery } from "@vlcn.io/react"; 3 3 import { DataTable } from "@/components/ui/data-table"; 4 4 import * as commandParser from "@/lib/command_js.js"; 5 5 import { ActionParser } from "@/components/ui/action-parser"; 6 6 import { useSelection } from "@/contexts/selection-context"; 7 7 import { useFilter } from "@/contexts/filter-context"; 8 + import { sortByUrgency } from "@/lib/urgency"; 8 9 import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; 9 10 import { AppSidebar } from "@/components/ui/app-sidebar"; 10 11 import { Badge } from "@/components/ui/badge"; ··· 45 46 console.log("updating todos: ", whereClause, "newParams: ", newParams); 46 47 const rawTodos = useQuery( 47 48 ctx, 48 - `SELECT * FROM active_todos ${whereClause ? "WHERE " + whereClause : ""}`, 49 + `SELECT * FROM urgent_todos ${whereClause ? "WHERE " + whereClause : ""}`, 49 50 newParams, 50 51 ).data; 51 52 ··· 105 106 completed: 2, // preview-add state 106 107 working_id: 0, 107 108 }; 108 - return [newTodo, ...todos]; 109 + // Sort by urgency to show where the new todo would appear 110 + const todosWithNew = [...todos, newTodo]; 111 + const sorted = sortByUrgency(todosWithNew); 112 + // Update working_id based on new sort order 113 + return sorted.map((todo, index) => ({ 114 + ...todo, 115 + working_id: index + 1 116 + })); 109 117 110 118 case "done": 111 - return todos.map((todo) => { 119 + const doneTodos = todos.map((todo) => { 112 120 if (selectedItems.has(todo.working_id)) { 113 121 return { ...todo, completed: 3 }; // preview-done state 114 122 } 115 123 return todo; 116 124 }); 125 + // Sort by urgency and update working_id 126 + const sortedDone = sortByUrgency(doneTodos); 127 + return sortedDone.map((todo, index) => ({ 128 + ...todo, 129 + working_id: index + 1 130 + })); 117 131 118 132 case "modify": 119 - return todos.map((todo) => { 133 + const modifiedTodos = todos.map((todo) => { 120 134 if (selectedItems.has(todo.working_id)) { 121 135 // Create a preview object that contains the original todo and the changes 122 136 const previewChanges = {}; ··· 154 168 } 155 169 return todo; 156 170 }); 171 + // Sort by urgency and update working_id 172 + const sortedModified = sortByUrgency(modifiedTodos); 173 + return sortedModified.map((todo, index) => ({ 174 + ...todo, 175 + working_id: index + 1 176 + })); 157 177 158 178 159 179 case "filter": ··· 237 257 238 258 // Use previewTodos if available, otherwise use regular todos 239 259 const displayTodos = previewTodos.length > 0 ? previewTodos : todos; 260 + 261 + // Calculate scroll position for preview todos 262 + const prevNewTextRef = useRef(""); 263 + const prevPreviewPositionRef = useRef(-1); 264 + 265 + const scrollToPreviewIndex = useMemo(() => { 266 + if (previewTodos.length === 0 || parsedCommand.action !== "add") { 267 + return undefined; 268 + } 269 + 270 + // Find the preview todo (the one with completed: 2) 271 + const previewIndex = previewTodos.findIndex(todo => todo.completed === 2); 272 + 273 + if (previewIndex === -1) { 274 + return undefined; 275 + } 276 + 277 + // Check if we should scroll: 278 + // 1. If we just started typing (previous text was empty) 279 + // 2. If the preview position changed significantly 280 + const justStartedTyping = prevNewTextRef.current === "" && newText !== ""; 281 + const positionChanged = Math.abs(previewIndex - prevPreviewPositionRef.current) > 1; 282 + 283 + // Update refs for next comparison 284 + const shouldScroll = justStartedTyping || positionChanged; 285 + prevNewTextRef.current = newText; 286 + prevPreviewPositionRef.current = previewIndex; 287 + 288 + return shouldScroll ? previewIndex : undefined; 289 + }, [previewTodos, parsedCommand.action, newText]); 240 290 241 291 const handleEnter = (e) => { 242 292 if (e.key === "Enter") { ··· 249 299 if (sel.type === "id") { 250 300 conditions.push(`id IN ( 251 301 SELECT id 252 - FROM active_todos 302 + FROM urgent_todos 253 303 WHERE working_id IN (${sel.ids.map(() => "?").join(",")}) 254 304 )`); 255 305 params.push(...sel.ids); ··· 426 476 /> 427 477 <div className="h-2" /> 428 478 <div className="flex-1 w-full"> 429 - <DataTable data={displayTodos} onMarkDone={handleMarkDone} /> 479 + <DataTable data={displayTodos} onMarkDone={handleMarkDone} scrollToPreviewIndex={scrollToPreviewIndex} /> 430 480 </div> 431 481 </section> 432 482 </div> ··· 466 516 /> 467 517 <div className="h-2" /> 468 518 <div className="flex-1 w-full h-full"> 469 - <DataTable data={displayTodos} onMarkDone={handleMarkDone} /> 519 + <DataTable data={displayTodos} onMarkDone={handleMarkDone} scrollToPreviewIndex={scrollToPreviewIndex} /> 470 520 </div> 471 521 </section> 472 522 </div>
+2 -2
mast-react-vite/src/components/ui/app-sidebar.tsx
··· 80 80 }; 81 81 const projects = useQuery( 82 82 ctx, 83 - `SELECT DISTINCT project FROM active_todos WHERE project != '' ORDER BY project` 83 + `SELECT DISTINCT project FROM urgent_todos WHERE project != '' ORDER BY project` 84 84 ).data || []; 85 85 86 86 const handleProjectClick = (project, e) => { ··· 101 101 102 102 const tags = useQuery( 103 103 ctx, 104 - `SELECT DISTINCT value as tag FROM active_todos, json_each(active_todos.tags) ORDER BY tag` 104 + `SELECT DISTINCT value as tag FROM urgent_todos, json_each(urgent_todos.tags) ORDER BY tag` 105 105 ).data || []; 106 106 107 107 const handleTagClick = (tag, e) => {
+32 -4
mast-react-vite/src/components/ui/data-table.tsx
··· 1 1 "use client"; 2 2 3 - import { useState } from "react"; 3 + import { useState, useRef, useEffect } from "react"; 4 4 import { ScrollArea } from "@/components/ui/scroll-area"; 5 5 import { Task } from "@/components/ui/task"; 6 6 ··· 13 13 interface DataTableProps<TData> { 14 14 data: TData[]; 15 15 onMarkDone?: (selectedIds: number[]) => void; 16 + scrollToPreviewIndex?: number; // Index of item to scroll to (for preview positioning) 16 17 } 17 18 18 - export function DataTable<TData>({ data, onMarkDone }: DataTableProps<TData>) { 19 + export function DataTable<TData>({ data, onMarkDone, scrollToPreviewIndex }: DataTableProps<TData>) { 20 + const desktopScrollRef = useRef<HTMLDivElement>(null); 21 + const mobileScrollRef = useRef<HTMLDivElement>(null); 22 + 23 + // Scroll to preview position when scrollToPreviewIndex changes 24 + useEffect(() => { 25 + if (scrollToPreviewIndex !== undefined && scrollToPreviewIndex >= 0) { 26 + // Estimate item height (adjust based on your actual task height) 27 + const estimatedItemHeight = 80; // Approximate height of a Task component 28 + const scrollPosition = scrollToPreviewIndex * estimatedItemHeight; 29 + 30 + // Scroll both desktop and mobile areas (only one will be visible) 31 + if (desktopScrollRef.current) { 32 + const viewport = desktopScrollRef.current.querySelector('[data-radix-scroll-area-viewport]'); 33 + if (viewport) { 34 + viewport.scrollTo({ top: scrollPosition, behavior: 'smooth' }); 35 + } 36 + } 37 + 38 + if (mobileScrollRef.current) { 39 + const viewport = mobileScrollRef.current.querySelector('[data-radix-scroll-area-viewport]'); 40 + if (viewport) { 41 + viewport.scrollTo({ top: scrollPosition, behavior: 'smooth' }); 42 + } 43 + } 44 + } 45 + }, [scrollToPreviewIndex]); 46 + 19 47 return ( 20 48 <> 21 49 <div className="hidden md:flex flex-col w-full"> 22 - <ScrollArea className="h-[calc(100vh-12rem)] rounded-md border"> 50 + <ScrollArea ref={desktopScrollRef} className="h-[calc(100vh-12rem)] rounded-md border"> 23 51 {data.length ? ( 24 52 data.map((item, index) => <Task key={index} data={item} onMarkDone={onMarkDone} />) 25 53 ) : ( ··· 32 60 33 61 {/* Mobile view layout - shown only on mobile */} 34 62 <div className="md:hidden"> 35 - <ScrollArea className="min-h-[120%] h-[calc(100vh)] border"> 63 + <ScrollArea ref={mobileScrollRef} className="min-h-[120%] h-[calc(100vh)] border"> 36 64 {data.length ? ( 37 65 data.map((item, index) => <Task key={index} data={item} onMarkDone={onMarkDone} />) 38 66 ) : (
+72
mast-react-vite/src/lib/urgency.ts
··· 1 + // Urgency calculation that matches the SQL logic in urgent_todos view 2 + export function calculateUrgency(todo: { 3 + priority?: string | boolean; 4 + due?: string | null; 5 + project?: string; 6 + tags?: string; 7 + }): number { 8 + let urgency = 0; 9 + 10 + // Priority: +6 points if priority = '1' or true 11 + if (todo.priority === '1' || todo.priority === true) { 12 + urgency += 6; 13 + } 14 + 15 + // Due date urgency 16 + if (todo.due && todo.due !== '') { 17 + const dueDate = new Date(todo.due); 18 + const now = new Date(); 19 + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 20 + const todayTime = today.getTime(); 21 + const dueDateDay = new Date(dueDate.getFullYear(), dueDate.getMonth(), dueDate.getDate()); 22 + const dueDateTime = dueDateDay.getTime(); 23 + 24 + if (dueDateTime < todayTime) { 25 + urgency += 12; // Overdue 26 + } else if (dueDateTime === todayTime) { 27 + urgency += 8; // Due today 28 + } else if (dueDateTime <= todayTime + (7 * 24 * 60 * 60 * 1000)) { 29 + urgency += 5; // Due this week 30 + } else if (dueDateTime <= todayTime + (30 * 24 * 60 * 60 * 1000)) { 31 + urgency += 2; // Due this month 32 + } 33 + } 34 + 35 + // Project: +1 if has project 36 + if (todo.project && todo.project !== '') { 37 + urgency += 1; 38 + } 39 + 40 + // Tags: +1 if has tags 41 + if (todo.tags && todo.tags !== '[]' && todo.tags !== '') { 42 + try { 43 + const parsedTags = JSON.parse(todo.tags); 44 + if (Array.isArray(parsedTags) && parsedTags.length > 0) { 45 + urgency += 1; 46 + } 47 + } catch { 48 + // If tags is not valid JSON, assume it's a non-empty string 49 + urgency += 1; 50 + } 51 + } 52 + 53 + return urgency; 54 + } 55 + 56 + // Sort todos by urgency (descending) then by id 57 + export function sortByUrgency<T extends { id: string | number; priority?: string | boolean; due?: string | null; project?: string; tags?: string }>(todos: T[]): T[] { 58 + return [...todos].sort((a, b) => { 59 + const urgencyA = calculateUrgency(a); 60 + const urgencyB = calculateUrgency(b); 61 + 62 + if (urgencyA !== urgencyB) { 63 + return urgencyB - urgencyA; // Higher urgency first 64 + } 65 + 66 + // If urgency is the same, sort by id 67 + if (typeof a.id === 'string' && typeof b.id === 'string') { 68 + return a.id.localeCompare(b.id); 69 + } 70 + return Number(a.id) - Number(b.id); 71 + }); 72 + }
+49
mast-react-vite/src/main.tsx
··· 339 339 urgency 340 340 FROM todos 341 341 WHERE completed = 0; 342 + 343 + CREATE VIEW IF NOT EXISTS urgent_todos AS 344 + SELECT 345 + id, 346 + description, 347 + project, 348 + tags, 349 + due, 350 + wait, 351 + priority, 352 + -- Calculate urgency score 353 + ( 354 + -- Priority: +6 points if priority = '1' 355 + CASE WHEN priority = '1' THEN 6 ELSE 0 END + 356 + 357 + -- Due date urgency 358 + CASE 359 + WHEN due != '' AND date(due) < date('now') THEN 12 -- Overdue 360 + WHEN due != '' AND date(due) = date('now') THEN 8 -- Due today 361 + WHEN due != '' AND date(due) <= date('now', '+7 days') THEN 5 -- Due this week 362 + WHEN due != '' AND date(due) <= date('now', '+30 days') THEN 2 -- Due this month 363 + ELSE 0 364 + END + 365 + 366 + -- Project: +1 if has project 367 + CASE WHEN project != '' THEN 1 ELSE 0 END + 368 + 369 + -- Tags: +1 if has tags 370 + CASE WHEN tags != '[]' AND tags != '' THEN 1 ELSE 0 END 371 + 372 + ) as urgency, 373 + -- working_id based on urgency-sorted order 374 + ROW_NUMBER() OVER (ORDER BY 375 + ( 376 + CASE WHEN priority = '1' THEN 6 ELSE 0 END + 377 + CASE 378 + WHEN due != '' AND date(due) < date('now') THEN 12 379 + WHEN due != '' AND date(due) = date('now') THEN 8 380 + WHEN due != '' AND date(due) <= date('now', '+7 days') THEN 5 381 + WHEN due != '' AND date(due) <= date('now', '+30 days') THEN 2 382 + ELSE 0 383 + END + 384 + CASE WHEN project != '' THEN 1 ELSE 0 END + 385 + CASE WHEN tags != '[]' AND tags != '' THEN 1 ELSE 0 END 386 + ) DESC, 387 + id 388 + ) as working_id 389 + FROM todos 390 + WHERE completed = 0; 342 391 `); 343 392 const rx = tblrx(db); 344 393