mirror of https://github.com/usememos/memos.git
feat: enhance MemoCompleteList with task clearing functionality and integrate task extraction utility
This commit is contained in:
parent
849a8762cb
commit
3826d5804b
|
|
@ -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;
|
||||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
Loading…
Reference in New Issue