feat: enhance MemoCompleteList with task clearing functionality and integrate task extraction utility

This commit is contained in:
Ahmed-Elgendy25 2026-03-24 00:44:55 +02:00
parent 849a8762cb
commit 3826d5804b
5 changed files with 184 additions and 23 deletions

View File

@ -1,19 +1,123 @@
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { useMemoViewContext } from "../MemoViewContext";
import { useCreateMemo, useDeleteMemo, useUpdateMemo } from "@/hooks/useMemoQueries";
import { extractTasks } from "@/utils/markdown-manipulation";
import { toast } from "react-hot-toast";
function MemoCompleteList() {
const { memo } = useMemoViewContext();
const { mutate: updateMemo } = useUpdateMemo();
const { mutate: deleteMemo } = useDeleteMemo();
const { mutate: createMemo } = useCreateMemo();
// Parse tasks once per render for both counting and clearing
const tasks = extractTasks(memo.content);
const completedTasks = tasks.filter((task) => task.checked);
const completedCount = completedTasks.length;
const hasCompletedTasks = completedCount > 0;
const handleClearCompleted = () => {
if (!hasCompletedTasks) {
return;
}
const previousContent = memo.content;
// Remove completed task lines based on the precomputed task list
const lines = previousContent.split("\n");
const completedLines = completedTasks
.map((task) => task.lineNumber)
.sort((a, b) => b - a);
for (const lineNumber of completedLines) {
if (lineNumber >= 0 && lineNumber < lines.length) {
lines.splice(lineNumber, 1);
}
}
const newContent = lines.join("\n");
if (newContent === previousContent) {
return;
}
const isNowEmpty = newContent.trim().length === 0;
if (isNowEmpty) {
// Delete memo when clearing completed tasks leaves it empty
deleteMemo(memo.name);
toast.custom(
(t) => (
<div className="flex items-center gap-3 rounded-md border border-border bg-card px-4 py-2 text-sm text-foreground shadow-lg">
<span>{completedCount} completed items cleared and memo deleted. Undo?</span>
<Button
variant="outline"
size="sm"
onClick={() => {
// Recreate memo with original content
createMemo({
...memo,
name: "",
content: previousContent,
});
toast.dismiss(t.id);
}}
>
Undo
</Button>
</div>
),
{ position: "bottom-center" },
);
} else {
// Just update content when there is still other content
updateMemo({
update: {
name: memo.name,
content: newContent,
},
updateMask: ["content"],
});
toast.custom(
(t) => (
<div className="flex items-center gap-3 rounded-md border border-border bg-card px-4 py-2 text-sm text-foreground shadow-lg">
<span>{completedCount} completed items cleared. Undo?</span>
<Button
variant="outline"
size="sm"
onClick={() => {
updateMemo({
update: {
name: memo.name,
content: previousContent,
},
updateMask: ["content"],
});
toast.dismiss(t.id);
}}
>
Undo
</Button>
</div>
),
{ position: "bottom-center" },
);
}
};
return (
<section className=" flex items-center gap-3">
<div className=" flex items-center gap-2 ">
<span>Completed Task</span>
<Switch
defaultChecked
className=" [&>[data-slot=switch-thumb][data-state=unchecked]]:bg-mauve-400 [&>[data-slot=switch-thumb][data-state=checked]]:bg-white"
/>
</div>
<Button className=" not-hover:border-foreground not-hover:text-foreground bg-transparent border-2 transition-all ease-linear">Clear Completed</Button>
<section className="flex items-center gap-3">
<Button
type="button"
disabled={!hasCompletedTasks}
onClick={handleClearCompleted}
className="not-hover:border-foreground not-hover:text-foreground bg-transparent border-2 transition-all ease-linear"
>
Clear Completed
</Button>
</section>
)
);
}
export default MemoCompleteList
export default MemoCompleteList;

View File

@ -1,6 +1,12 @@
import { useMemoViewContext } from "../MemoViewContext";
import MemoCompleteList from "./MemoCompleteList";
function MemoFooter() {
const { memo } = useMemoViewContext();
const hasTaskList = memo.property?.hasTaskList;
if (!hasTaskList) {
return null;
}
return (
<footer className=" w-full mt-5 flex justify-end items-center">
<MemoCompleteList/>

View File

@ -97,7 +97,6 @@ const PagedMemoList = (props: Props) => {
// Flatten pages into a single array of memos
const memos = useMemo(() => data?.pages.flatMap((page) => page.memos) || [], [data]);
console.log("Memoss: ",memos);
// Apply custom sorting if provided, otherwise use memos directly
const sortedMemoList = useMemo(() => (props.listSort ? props.listSort(memos) : memos), [memos, props.listSort]);

View File

@ -17,13 +17,7 @@ export const memoKeys = {
};
export function useMemos(request: Partial<ListMemosRequest> = {}) {
console.log(`Memos: `,useQuery({
queryKey: memoKeys.list(request),
queryFn: async () => {
const response = await memoServiceClient.listMemos(create(ListMemosRequestSchema, request as Record<string, unknown>));
return response;
},
}));
return useQuery({
queryKey: memoKeys.list(request),
queryFn: async () => {

View File

@ -7,7 +7,7 @@ import { gfmFromMarkdown } from "mdast-util-gfm";
import { gfm } from "micromark-extension-gfm";
import { visit } from "unist-util-visit";
interface TaskInfo {
export interface TaskInfo {
lineNumber: number;
checked: boolean;
}
@ -59,8 +59,44 @@ export function toggleTaskAtLine(markdown: string, lineNumber: number, checked:
return lines.join("\n");
}
export function getTaskLineInfo(markdown: string): TaskInfo[] {
const lines = markdown.split("\n");
const taskPattern = /^(\s*[-*+]\s+)\[([ xX])\](\s+.*)$/;
const tasks: TaskInfo[] = [];
let inCodeFence = false;
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
const trimmed = line.trimStart();
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
inCodeFence = !inCodeFence;
continue;
}
if (inCodeFence) {
continue;
}
const match = line.match(taskPattern);
if (!match) {
continue;
}
const checked = match[2].toLowerCase() === "x";
tasks.push({
lineNumber: i,
checked,
});
}
return tasks;
}
export function toggleTaskAtIndex(markdown: string, taskIndex: number, checked: boolean): string {
const tasks = extractTasksFromAst(markdown);
const tasks = getTaskLineInfo(markdown);
if (taskIndex < 0 || taskIndex >= tasks.length) {
return markdown;
@ -75,7 +111,7 @@ export function countTasks(markdown: string): {
completed: number;
incomplete: number;
} {
const tasks = extractTasksFromAst(markdown);
const tasks = getTaskLineInfo(markdown);
const total = tasks.length;
const completed = tasks.filter((t) => t.checked).length;
@ -140,3 +176,25 @@ export function extractTasks(markdown: string): TaskItem[] {
return tasks;
}
export function clearCompletedTasks(markdown: string): string {
const tasks = extractTasks(markdown);
if (tasks.length === 0) {
return markdown;
}
const lines = markdown.split("\n");
const completedLines = tasks
.filter((task) => task.checked)
.map((task) => task.lineNumber)
.sort((a, b) => b - a);
for (const lineNumber of completedLines) {
if (lineNumber >= 0 && lineNumber < lines.length) {
lines.splice(lineNumber, 1);
}
}
return lines.join("\n");
}