added instant sync and fixed saving debounce
This commit is contained in:
@@ -42,13 +42,22 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let debounceTimer: number | undefined;
|
let debounceTimer: number | undefined;
|
||||||
|
let pendingContent: string | null = null;
|
||||||
|
|
||||||
const updateTaskContent = (html: string) => {
|
const updateTaskContent = (html: string) => {
|
||||||
|
pendingContent = html;
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
updateTask(props.task.id, { content: html });
|
if (pendingContent !== null) {
|
||||||
|
updateTask(props.task.id, { content: pendingContent });
|
||||||
|
pendingContent = null;
|
||||||
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Removed onCleanup to avoid lockup
|
||||||
|
|
||||||
|
|
||||||
const updateTitle = (val: string) => {
|
const updateTitle = (val: string) => {
|
||||||
setTitle(val);
|
setTitle(val);
|
||||||
updateTask(props.task.id, { title: val });
|
updateTask(props.task.id, { title: val });
|
||||||
@@ -87,7 +96,16 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
const currentUrgency = () => calculateUrgencyFromDate(props.task.dueDate).toString();
|
const currentUrgency = () => calculateUrgencyFromDate(props.task.dueDate).toString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={props.isOpen} onOpenChange={(open) => !open && props.onClose()}>
|
<Sheet open={props.isOpen} onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
// Flash save before closing to avoid lockup
|
||||||
|
if (pendingContent !== null) {
|
||||||
|
updateTask(props.task.id, { content: pendingContent });
|
||||||
|
pendingContent = null;
|
||||||
|
}
|
||||||
|
props.onClose();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
hideCloseButton
|
hideCloseButton
|
||||||
onOpenAutoFocus={(e) => {
|
onOpenAutoFocus={(e) => {
|
||||||
|
|||||||
+111
-20
@@ -287,6 +287,94 @@ export const getCombinedScore = (task: Task): number => {
|
|||||||
|
|
||||||
let heartbeatInterval: number | undefined;
|
let heartbeatInterval: number | undefined;
|
||||||
|
|
||||||
|
const mapRecordToTask = (r: any): Task => {
|
||||||
|
// Check if it's a template? The caller handles filtering templates usually,
|
||||||
|
// but here we just map fields.
|
||||||
|
const tags = r.tags || [];
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
title: r.title,
|
||||||
|
startDate: r.startDate,
|
||||||
|
dueDate: r.dueDate,
|
||||||
|
priority: r.priority,
|
||||||
|
completed: r.completed,
|
||||||
|
tags: tags,
|
||||||
|
content: r.content,
|
||||||
|
deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : undefined,
|
||||||
|
created: r.created,
|
||||||
|
updated: r.updated,
|
||||||
|
recurrence: r.recurrence
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subscribeToRealtime = async () => {
|
||||||
|
// Unsubscribe first to avoid duplicates
|
||||||
|
await pb.collection(TASGRID_COLLECTION).unsubscribe('*');
|
||||||
|
|
||||||
|
pb.collection(TASGRID_COLLECTION).subscribe('*', (e) => {
|
||||||
|
console.log("Realtime event:", e.action, e.record.id);
|
||||||
|
|
||||||
|
if (e.action === 'create') {
|
||||||
|
// Check if it's a template
|
||||||
|
const isTemplate = e.record.tags?.includes("__template__");
|
||||||
|
if (isTemplate) {
|
||||||
|
// We don't auto-add templates to the main task list usually?
|
||||||
|
// Wait, `store.templates` is separate.
|
||||||
|
// We should handle templates too if we want sync on templates.
|
||||||
|
let meta: any = {};
|
||||||
|
try { meta = JSON.parse(e.record.content || "{}"); } catch (e) { }
|
||||||
|
const newTemplate: TaskTemplate = {
|
||||||
|
id: e.record.id,
|
||||||
|
name: e.record.title,
|
||||||
|
title: meta.title || "",
|
||||||
|
priority: e.record.priority,
|
||||||
|
urgency: meta.urgency || 5,
|
||||||
|
tags: meta.tags || [],
|
||||||
|
content: meta.content || ""
|
||||||
|
};
|
||||||
|
setStore("templates", t => [...(t || []), newTemplate]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular Task
|
||||||
|
const exists = store.tasks.find(t => t.id === e.record.id);
|
||||||
|
if (!exists) {
|
||||||
|
const newTask = mapRecordToTask(e.record);
|
||||||
|
setStore("tasks", t => [newTask, ...t]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.action === 'update') {
|
||||||
|
// Is it a template?
|
||||||
|
const isTemplate = e.record.tags?.includes("__template__");
|
||||||
|
if (isTemplate) {
|
||||||
|
// Update template store
|
||||||
|
let meta: any = {};
|
||||||
|
try { meta = JSON.parse(e.record.content || "{}"); } catch (e) { }
|
||||||
|
setStore("templates", t => t.id === e.record.id, {
|
||||||
|
name: e.record.title,
|
||||||
|
title: meta.title || "",
|
||||||
|
priority: e.record.priority,
|
||||||
|
urgency: meta.urgency || 5,
|
||||||
|
tags: meta.tags || [],
|
||||||
|
content: meta.content || ""
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal Task
|
||||||
|
const updatedTask = mapRecordToTask(e.record);
|
||||||
|
setStore("tasks", t => t.id === e.record.id, updatedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.action === 'delete') {
|
||||||
|
// Try deleting from both just in case
|
||||||
|
setStore("tasks", t => t.filter(x => x.id !== e.record.id));
|
||||||
|
setStore("templates", t => (t || []).filter(x => x.id !== e.record.id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const initStore = async () => {
|
export const initStore = async () => {
|
||||||
if (!pb.authStore.isValid) return;
|
if (!pb.authStore.isValid) return;
|
||||||
|
|
||||||
@@ -305,7 +393,10 @@ export const initStore = async () => {
|
|||||||
|
|
||||||
// Heartbeat - Ensure only one interval is running
|
// Heartbeat - Ensure only one interval is running
|
||||||
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
||||||
heartbeatInterval = setInterval(() => setNow(Date.now()), 60000);
|
heartbeatInterval = setInterval(() => {
|
||||||
|
setNow(Date.now());
|
||||||
|
checkRecurringTasks(); // Check for recurrence triggers (minutely, etc.)
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Fetch Preferences from User Profile (Weights, Scale, Tags)
|
// 1. Fetch Preferences from User Profile (Weights, Scale, Tags)
|
||||||
@@ -367,19 +458,7 @@ export const initStore = async () => {
|
|||||||
content: meta.content || ""
|
content: meta.content || ""
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
allTasks.push({
|
allTasks.push(mapRecordToTask(r));
|
||||||
id: r.id,
|
|
||||||
title: r.title,
|
|
||||||
startDate: r.startDate,
|
|
||||||
dueDate: r.dueDate,
|
|
||||||
priority: r.priority,
|
|
||||||
completed: r.completed,
|
|
||||||
tags: tags,
|
|
||||||
content: r.content,
|
|
||||||
deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : undefined,
|
|
||||||
created: r.created,
|
|
||||||
updated: r.updated
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -389,6 +468,9 @@ export const initStore = async () => {
|
|||||||
// Check recurring tasks after load
|
// Check recurring tasks after load
|
||||||
checkRecurringTasks();
|
checkRecurringTasks();
|
||||||
|
|
||||||
|
// 3. Subscribe to Realtime Updates
|
||||||
|
await subscribeToRealtime();
|
||||||
|
|
||||||
// 4. Reconstructive Migration: Verify all tags in tasks have definitions
|
// 4. Reconstructive Migration: Verify all tags in tasks have definitions
|
||||||
const foundTagNames = new Set<string>();
|
const foundTagNames = new Set<string>();
|
||||||
allTasks.forEach(t => t.tags?.forEach(tag => foundTagNames.add(tag)));
|
allTasks.forEach(t => t.tags?.forEach(tag => foundTagNames.add(tag)));
|
||||||
@@ -458,12 +540,21 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Replace temp task with real record (merging server fields like id, created, updated)
|
// Replace temp task with real record (merging server fields like id, created, updated)
|
||||||
setStore("tasks", (t) => t.map(task => task.id === tempId ? {
|
setStore("tasks", (t) => {
|
||||||
...task,
|
// Check if REAL ID exists already (from subscription race)
|
||||||
id: record.id,
|
const realExists = t.some(x => x.id === record.id);
|
||||||
created: record.created,
|
if (realExists) {
|
||||||
updated: record.updated
|
// If real exists, just remove temp.
|
||||||
} : task));
|
return t.filter(x => x.id !== tempId);
|
||||||
|
}
|
||||||
|
// Otherwise, replace temp with real
|
||||||
|
return t.map(task => task.id === tempId ? {
|
||||||
|
...task,
|
||||||
|
id: record.id,
|
||||||
|
created: record.created,
|
||||||
|
updated: record.updated
|
||||||
|
} : task);
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("create failed", err);
|
console.error("create failed", err);
|
||||||
toast.error("Failed to save task.");
|
toast.error("Failed to save task.");
|
||||||
|
|||||||
Reference in New Issue
Block a user