this repo has no description
2
fork

Configure Feed

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

feat: we have full reactivity bby!!

+298 -143
+186 -107
mast-react-vite/src/App.tsx
··· 1 - import { useState, useEffect } from "react"; 1 + import { useState, useEffect, useMemo } from "react"; 2 2 import { useQuery } from "@vlcn.io/react"; 3 3 import { ColumnDef } from "@tanstack/react-table"; 4 4 import { DataTable } from "@/components/ui/data-table"; ··· 13 13 type Todo = { 14 14 id: string; 15 15 description: string; 16 - status: 0 | 1; 16 + completed: number; 17 17 tags: string[]; 18 18 project: string; 19 19 }; ··· 87 87 const [newText, setNewText] = useState(""); 88 88 const { selectedItems, clearSelection, getSelectionString } = useSelection(); 89 89 const [currentAction, setCurrentAction] = useState("add"); 90 - // TODO: 91 - // executeCommand with currentAction == "filter" 92 - // should set the filterContext 93 90 const [filterContext, setFilterContext] = useState({}); 94 - // TODO: 95 - // Updating newText should give us a new parsedCommand 96 - // State of the viewport should be determined using parsedCommand _only_ 97 - const [parsedCommand, setParsedCommand] = useState({}); 98 91 99 - // TODO: 100 - // todos should come from filterContext if it exists 101 - // not from the currentAction + newText 102 - const conditions = []; 103 - const params = []; 92 + const newConditions = []; 93 + const newParams = []; 104 94 105 95 if ( 106 96 filterContext.filterDescription && 107 97 filterContext.filterDescription.length > 0 108 98 ) { 109 - conditions.push("description LIKE ?"); 110 - params.push(`%${filterContext.filterDescription}%`); 99 + newConditions.push("description LIKE ?"); 100 + newParams.push(`%${filterContext.filterDescription}%`); 111 101 } 112 - 113 102 if (filterContext.filterProject && filterContext.filterProject.length > 0) { 114 - conditions.push("project = ?"); 115 - params.push(filterContext.filterProject); 103 + newConditions.push("project = ?"); 104 + newParams.push(filterContext.filterProject); 116 105 } 117 - 118 106 if (filterContext.filterTags && filterContext.filterTags.length > 0) { 119 - conditions.push(`EXISTS ( 120 - SELECT 1 FROM json_each(tags) 121 - WHERE json_each.value IN (${filterContext.filterTags.map(() => "?").join(",")}) 122 - )`); 123 - params.push(...filterContext.filterTags); 107 + newConditions.push(`EXISTS ( 108 + SELECT 1 FROM json_each(tags) 109 + WHERE json_each.value IN (${filterContext.filterTags.map(() => "?").join(",")}) 110 + )`); 111 + newParams.push(...filterContext.filterTags); 124 112 } 125 113 114 + const whereClause = [ 115 + newConditions.length > 0 ? `(${newConditions.join(" AND ")})` : null, 116 + selectedItems.size > 0 && newConditions.length > 0 117 + ? `working_id IN (${Array.from(selectedItems).join(",")})` 118 + : null, 119 + ] 120 + .filter(Boolean) 121 + .join(" OR "); 122 + 123 + console.log("updating todos"); 126 124 const todos = useQuery( 127 125 ctx, 128 - `SELECT * FROM active_todos 129 - ${conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : ""}`, 130 - params, 126 + `SELECT * FROM active_todos ${whereClause ? "WHERE " + whereClause : ""}`, 127 + newParams, 131 128 ).data; 132 129 130 + useEffect(() => { 131 + if ( 132 + currentAction !== "filter" && 133 + newText.trim().length !== 0 && 134 + filterContext.filterTemp === true 135 + ) { 136 + console.log("clear filter!"); 137 + setFilterContext({}); 138 + } 139 + }, [currentAction]); 140 + 141 + const parsedCommand = useMemo(() => { 142 + try { 143 + return commandParser.parse( 144 + getSelectionString() + " " + currentAction + " " + newText, 145 + ); 146 + } catch (error) { 147 + console.log("Could not parse command"); 148 + return {}; 149 + } 150 + }, [getSelectionString(), currentAction, newText]); 151 + 152 + const previewTodos = useMemo(() => { 153 + if (!parsedCommand.action) return []; 154 + 155 + switch (parsedCommand.action) { 156 + case "add": 157 + const newTodo = { 158 + id: "NotAddedYetNewTask", 159 + description: parsedCommand.description, 160 + tags: JSON.stringify(parsedCommand.tags) || "[]", 161 + project: parsedCommand.project || "", 162 + completed: 2, // preview-add state 163 + working_id: 0, 164 + }; 165 + return [newTodo, ...todos]; 166 + 167 + case "done": 168 + return todos.map((todo) => { 169 + if (selectedItems.has(todo.working_id)) { 170 + return { ...todo, completed: 3 }; // preview-done state 171 + } 172 + return todo; 173 + }); 174 + 175 + case "modify": 176 + return todos.map((todo) => { 177 + if (selectedItems.has(todo.working_id)) { 178 + // Create a preview object that contains the original todo and the changes 179 + const previewChanges = {}; 180 + 181 + // Only add fields that are being changed to the preview 182 + if (parsedCommand.description) { 183 + previewChanges.description = parsedCommand.description; 184 + } 185 + 186 + if (parsedCommand.project) { 187 + previewChanges.project = parsedCommand.project; 188 + } 189 + 190 + if (parsedCommand.tags && parsedCommand.tags.length > 0) { 191 + // For tags, we want to show both existing and new tags in preview 192 + previewChanges.tags = JSON.stringify([ 193 + ...parsedCommand.tags 194 + ]); 195 + } 196 + 197 + // Return the original todo with preview changes attached 198 + return { 199 + ...todo, 200 + preview: previewChanges, 201 + previewMode: true 202 + }; 203 + } 204 + return todo; 205 + }); 206 + 207 + 208 + case "filter": 209 + // For filter, we don't need preview todos 210 + return []; 211 + 212 + default: 213 + return []; 214 + } 215 + }, [parsedCommand, todos, selectedItems]); 216 + 217 + useEffect(() => { 218 + if (parsedCommand.action === "filter") { 219 + setFilterContext({ 220 + filterTags: parsedCommand.tags, 221 + filterProject: parsedCommand.project, 222 + filterDescription: parsedCommand.description, 223 + filterTemp: true, 224 + }); 225 + } else if ( 226 + currentAction !== "filter" && 227 + newText.trim().length === 0 && 228 + filterContext.filterTemp === true 229 + ) { 230 + setFilterContext({}); 231 + } 232 + }, [parsedCommand, currentAction, newText]); 233 + 133 234 const handleActionChange = (action: string) => { 134 235 setCurrentAction(action); 135 236 }; 136 237 137 - // This should be handled in the actionParser component 138 - // It should show the selection tag above the input, and possibly disable it 139 - // useEffect(() => { 140 - // console.log(selectedItems) 141 - // }, [selectedItems]); 142 - 143 - //useEffect(() => { 144 - // // TODO: 145 - // // We need something here that edits todos _as though_ the action had taken place 146 - // // We probably need to have a "state" variable 147 - // // Creating 148 - // // Deleting 149 - // // Completing 150 - // // Editng? 151 - //}, [parsedCommand]); 152 - 153 238 const handleNewTextChange = (newText: string) => { 154 239 setNewText(newText); 155 - //try{ 156 - // // TODO selection context should go here 157 - // const parsed = commandParser.parse(getSelectionString + " " + currentAction + " " + newText); 158 - // setParsedCommand(parsed) 159 - //} catch (error) { 160 - // console.log("not updating parsed command: " + error) 161 - //} 162 240 }; 163 241 242 + // Use previewTodos if available, otherwise use regular todos 243 + const displayTodos = previewTodos.length > 0 ? previewTodos : todos; 244 + 164 245 const handleEnter = (e) => { 165 246 if (e.key === "Enter") { 166 247 executeCommand(); 167 248 } 249 + if (e.keyCode === 8) { 250 + console.log("backspace"); 251 + if (newText.trim().length === 0) { 252 + setFilterContext({}); 253 + } 254 + } 168 255 }; 169 256 170 - const executeCommand = (value: string) => { 257 + const parseSelection = ({ selection, conditions, params }) => { 258 + selection.forEach((sel) => { 259 + if (sel.type === "id") { 260 + conditions.push(`id IN ( 261 + SELECT id 262 + FROM active_todos 263 + WHERE working_id IN (${sel.ids.map(() => "?").join(",")}) 264 + )`); 265 + params.push(...sel.ids); 266 + } else if (sel.type === "tag") { 267 + // TODO this is busted 268 + conditions.push(`tags LIKE ?`); 269 + params.push(`%${sel.value}%`); 270 + } else if (sel.type === "project") { 271 + conditions.push(`project = ?`); 272 + params.push(sel.value); 273 + } 274 + }); 275 + return { conditions, params }; 276 + }; 277 + 278 + const executeCommand = () => { 171 279 console.log(getSelectionString() + " " + currentAction + " " + newText); 172 280 try { 173 281 // TODO selection context should go here ··· 177 285 console.log(parsed); 178 286 switch (parsed.action) { 179 287 case "done": 180 - // Build SQL conditions for all selection types 181 - const conditions = []; 182 - const params = []; 183 - 184 - parsed.selection.forEach((sel) => { 185 - if (sel.type === "id") { 186 - conditions.push(`id IN ( 187 - SELECT id 188 - FROM active_todos 189 - WHERE working_id IN (${sel.ids.map(() => "?").join(",")}) 190 - )`); 191 - params.push(...sel.ids); 192 - } else if (sel.type === "tag") { 193 - // TODO this is busted 194 - conditions.push(`tags LIKE ?`); 195 - params.push(`%${sel.value}%`); 196 - } else if (sel.type === "project") { 197 - conditions.push(`project = ?`); 198 - params.push(sel.value); 199 - } 288 + const { conditions, params } = parseSelection({ 289 + selection: parsed.selection, 290 + conditions: [], 291 + params: [], 200 292 }); 201 293 202 294 if (conditions.length > 0) { ··· 214 306 ctx.db.exec( 215 307 `INSERT INTO todos (id, description, tags, project, completed) 216 308 VALUES (lower(hex(randomblob(16))), ?, json(?), ?, 0)`, 217 - [parsed.description, parsed.tags, parsed.project], 309 + [parsed.description, JSON.stringify(parsed.tags), parsed.project], 218 310 ); 219 311 break; 220 312 case "modify": 221 - const editConditions = []; 222 313 const editParams = []; 223 - const selectionParams = []; 224 314 const updates = []; 225 315 226 316 // Handle new values for SET clause ··· 234 324 } 235 325 if (parsed.tags && parsed.tags.length > 0) { 236 326 updates.push(`tags = ( 237 - SELECT json_group_array(value) 238 - FROM ( 239 - SELECT DISTINCT value 240 - FROM ( 241 - SELECT value FROM json_each(tags) 242 - UNION 243 - SELECT value FROM json_each(json(?)) 244 - ) 245 - ) 246 - )`); 327 + SELECT json_group_array(value) 328 + FROM ( 329 + SELECT DISTINCT value 330 + FROM ( 331 + SELECT value FROM json_each(tags) 332 + UNION 333 + SELECT value FROM json_each(json(?)) 334 + ) 335 + ) 336 + )`); 247 337 editParams.push(JSON.stringify(parsed.tags)); 248 338 } 249 339 250 340 // Build WHERE clause using only selection conditions 251 - parsed.selection.forEach((sel) => { 252 - if (sel.type === "id") { 253 - editConditions.push(`id IN ( 254 - SELECT id 255 - FROM active_todos 256 - WHERE working_id IN (${sel.ids.map(() => "?").join(",")}) 257 - )`); 258 - selectionParams.push(...sel.ids); 259 - } else if (sel.type === "tag") { 260 - editConditions.push(`tags LIKE ?`); 261 - selectionParams.push(`%${sel.value}%`); 262 - } else if (sel.type === "project") { 263 - editConditions.push(`project = ?`); 264 - selectionParams.push(sel.value); 265 - } 266 - }); 341 + 342 + const { conditions: editConditions, params: selectionParams } = 343 + parseSelection({ 344 + selection: parsed.selection, 345 + conditions: [], 346 + params: [], 347 + }); 267 348 268 349 if (editConditions.length > 0 && updates.length > 0) { 269 350 const sqlQuery = ` ··· 271 352 SET ${updates.join(", ")} 272 353 WHERE ${editConditions.join(" OR ")} 273 354 `; 274 - console.log(sqlQuery); 275 - console.log(editParams); 276 - console.log(selectionParams); 277 355 // Execute with editParams for SET clause, followed by selectionParams for WHERE clause 278 356 ctx.db.exec(sqlQuery, [...editParams, ...selectionParams]); 279 357 clearSelection(); ··· 286 364 filterTags: parsed.tags, 287 365 filterProject: parsed.project, 288 366 filterDescription: parsed.description, 367 + // Don't clear the filter context on actionChange 368 + filterTemp: false, 289 369 }); 290 370 break; 291 371 } 292 372 setNewText(""); 293 - setParsedCommand({}); 294 373 } catch (error) { 295 374 // TODO: 296 375 // This is actually bad ··· 370 449 /> 371 450 <div className="h-2" /> 372 451 <div className="flex-1 w-full"> 373 - <DataTable data={todos} /> 452 + <DataTable data={displayTodos} /> 374 453 </div> 375 454 </section> 376 455 </div> ··· 408 487 /> 409 488 <div className="h-2" /> 410 489 <div className="flex-1 w-full h-full"> 411 - <DataTable data={todos} /> 490 + <DataTable data={displayTodos} /> 412 491 </div> 413 492 </section> 414 493 </div>
+62 -14
mast-react-vite/src/components/ui/task.tsx
··· 10 10 } 11 11 12 12 export function Task({ selected, onSelect, data }: TaskProps) { 13 - const { isSelected, toggleSelection } = useSelection(); 13 + const { isSelected, toggleSelection } = useSelection(); 14 14 const tagList = JSON.parse(data.tags); 15 + 16 + // Handle preview changes 17 + const hasPreview = data.previewMode && data.preview; 18 + 19 + // Determine which description to display 20 + const displayDescription = hasPreview && data.preview.description.length > 0 21 + ? data.preview.description 22 + : data.description; 23 + 24 + const previewTags = hasPreview && data.preview.tags 25 + ? JSON.parse(data.preview.tags) 26 + : [] 27 + 28 + // Determine which project to display 29 + const displayProject = hasPreview && data.preview.project 30 + ? data.preview.project 31 + : data.project; 32 + 33 + 34 + const getStateStyles = (status) => { 35 + switch (status) { 36 + case 2: // preview-add 37 + return 'bg-yellow-100 border-yellow-600 text-yellow-700 '; 38 + case 3: // preview-done 39 + return 'bg-green-100 text-green-700 border-green-600 opacity-50'; 40 + default: 41 + return isSelected(data.working_id) ? 'bg-muted border-primary' : 'bg-card'; 42 + } 43 + }; 44 + 45 + const getTagStateStyles = (status) => { 46 + switch (status) { 47 + case 2: // preview-add 48 + return 'bg-yellow-100 border-yellow-600 text-yellow-700 '; 49 + case 3: // preview-done 50 + return 'bg-green-100 text-green-700 border-green-600 opacity-50'; 51 + default: 52 + return "bg-background border-muted-foreground text-muted-foreground text-xs" 53 + } 54 + }; 15 55 16 56 return ( 17 57 <> 18 58 <div 19 - className={`p-4 ${ 20 - isSelected(data.working_id) ? "bg-muted border-primary" : "bg-card" 21 - } hover:bg-muted/50 transition-colors`} 59 + className={`p-4 ${getStateStyles(data.completed)} hover:bg-muted/50 transition-colors`} 22 60 onClick={() => toggleSelection(data.working_id)} 23 61 > 24 62 <div className="flex items-start gap-4"> 25 63 <div className="flex-1 min-w-0"> 26 64 <div className="flex items-center relative"> 27 - {data.project && ( 65 + {displayProject && ( 28 66 <div className="flex items-center"> 29 67 <div className="w-2 h-2 rounded-full bg-blue-500"></div> 30 - <span className="text-xs text-muted-foreground leading-6 ml-1"> 31 - {data.project} 68 + <span className={`text-xs ${getStateStyles(data.completed)} leading-6 ml-1`}> 69 + {displayProject} 32 70 </span> 33 71 </div> 34 72 )} ··· 39 77 40 78 <div className="mt-1 flex flex-wrap gap-2 items-center"> 41 79 <p className="text-sm font-medium leading-none truncate min-h-6 leading-6"> 42 - {data.description} 80 + {displayDescription} 43 81 </p> 44 - {tagList.length > 0 && ( 45 - <div className="flex gap-1"> 46 - {tagList.map((tag: string) => ( 82 + <div className="flex gap-1"> 83 + {(tagList && tagList.length > 0) && ( 84 + tagList.map((tag: string) => ( 47 85 <Badge 48 86 key={tag} 49 - className="items-center text-xs bg-background border-muted-foreground text-muted-foreground -mt-2" 87 + className={`items-center ${getTagStateStyles(data.completed)} -mt-2`} 88 + > 89 + # {tag} 90 + </Badge> 91 + )) 92 + )} 93 + {(previewTags.length > 0) && ( 94 + previewTags.map((tag: string) => ( 95 + <Badge 96 + key={`preview-${tag}`} 97 + className="items-center text-xs bg-yellow-100 border-yellow-600 text-yellow-700 -mt-2" 50 98 > 51 99 # {tag} 52 100 </Badge> 53 - ))} 54 - </div> 101 + )) 55 102 )} 103 + </div> 56 104 {data.dueDate && ( 57 105 <span className="text-xs text-muted-foreground"> 58 106 Due: {data.dueDate}
+24 -10
mast-react-vite/src/lib/command_js.js
··· 221 221 return makeCommand('add', selection, parts.filter(p => p !== null)); 222 222 }; 223 223 var peg$f1 = function(selection, parts) { 224 - return makeCommand('done', selection, parts); 224 + return makeCommand('done', selection, parts.filter(p => p !== null)); 225 225 }; 226 226 var peg$f2 = function(selection, moreFilters) { 227 227 return makeCommand('filter', selection, moreFilters); ··· 566 566 if (s4 === peg$FAILED) { 567 567 s4 = null; 568 568 } 569 - s5 = peg$parsePart(); 570 - if (s5 === peg$FAILED) { 571 - s5 = peg$parse_(); 572 - } 573 - if (s5 === peg$FAILED) { 574 - s5 = null; 569 + s5 = []; 570 + s6 = peg$parsePart(); 571 + if (s6 === peg$FAILED) { 572 + s6 = peg$parse_(); 575 573 } 576 - s6 = peg$parseEOF(); 577 574 if (s6 !== peg$FAILED) { 578 - peg$savedPos = s0; 579 - s0 = peg$f1(s1, s5); 575 + while (s6 !== peg$FAILED) { 576 + s5.push(s6); 577 + s6 = peg$parsePart(); 578 + if (s6 === peg$FAILED) { 579 + s6 = peg$parse_(); 580 + } 581 + } 582 + } else { 583 + s5 = peg$FAILED; 584 + } 585 + if (s5 !== peg$FAILED) { 586 + s6 = peg$parseEOF(); 587 + if (s6 !== peg$FAILED) { 588 + peg$savedPos = s0; 589 + s0 = peg$f1(s1, s5); 590 + } else { 591 + peg$currPos = s0; 592 + s0 = peg$FAILED; 593 + } 580 594 } else { 581 595 peg$currPos = s0; 582 596 s0 = peg$FAILED;
+24 -10
parser/command_js.js
··· 221 221 return makeCommand('add', selection, parts.filter(p => p !== null)); 222 222 }; 223 223 var peg$f1 = function(selection, parts) { 224 - return makeCommand('done', selection, parts); 224 + return makeCommand('done', selection, parts.filter(p => p !== null)); 225 225 }; 226 226 var peg$f2 = function(selection, moreFilters) { 227 227 return makeCommand('filter', selection, moreFilters); ··· 566 566 if (s4 === peg$FAILED) { 567 567 s4 = null; 568 568 } 569 - s5 = peg$parsePart(); 570 - if (s5 === peg$FAILED) { 571 - s5 = peg$parse_(); 572 - } 573 - if (s5 === peg$FAILED) { 574 - s5 = null; 569 + s5 = []; 570 + s6 = peg$parsePart(); 571 + if (s6 === peg$FAILED) { 572 + s6 = peg$parse_(); 575 573 } 576 - s6 = peg$parseEOF(); 577 574 if (s6 !== peg$FAILED) { 578 - peg$savedPos = s0; 579 - s0 = peg$f1(s1, s5); 575 + while (s6 !== peg$FAILED) { 576 + s5.push(s6); 577 + s6 = peg$parsePart(); 578 + if (s6 === peg$FAILED) { 579 + s6 = peg$parse_(); 580 + } 581 + } 582 + } else { 583 + s5 = peg$FAILED; 584 + } 585 + if (s5 !== peg$FAILED) { 586 + s6 = peg$parseEOF(); 587 + if (s6 !== peg$FAILED) { 588 + peg$savedPos = s0; 589 + s0 = peg$f1(s1, s5); 590 + } else { 591 + peg$currPos = s0; 592 + s0 = peg$FAILED; 593 + } 580 594 } else { 581 595 peg$currPos = s0; 582 596 s0 = peg$FAILED;
+2 -2
parser/command_js.peg
··· 51 51 } 52 52 53 53 // DONE COMMAND 54 - DoneCommand = selection:Selections? _? "done" _? parts:(Part / _)? EOF { 55 - return makeCommand('done', selection, parts); 54 + DoneCommand = selection:Selections? _? "done" _? parts:(Part / _)+ EOF { 55 + return makeCommand('done', selection, parts.filter(p => p !== null)); 56 56 } 57 57 58 58 ExplicitFilterCommand = selection:Selections? _? "filter" _* moreFilters:Filters EOF {