quickload list implemented
This commit is contained in:
+5
-1
@@ -30,12 +30,16 @@ const App: Component = () => {
|
|||||||
window.addEventListener('popstate', handleLocChange);
|
window.addEventListener('popstate', handleLocChange);
|
||||||
window.addEventListener('hashchange', handleLocChange);
|
window.addEventListener('hashchange', handleLocChange);
|
||||||
|
|
||||||
|
let isFirstRun = true;
|
||||||
const removeListener = pb.authStore.onChange((token) => {
|
const removeListener = pb.authStore.onChange((token) => {
|
||||||
console.log('[DEBUG] pb.authStore.onChange fired, token present:', !!token);
|
console.log('[DEBUG] pb.authStore.onChange fired, token present:', !!token);
|
||||||
|
const wasAuthenticated = isAuthenticated();
|
||||||
setIsAuthenticated(!!token);
|
setIsAuthenticated(!!token);
|
||||||
if (token) {
|
|
||||||
|
if (token && (isFirstRun || !wasAuthenticated)) {
|
||||||
initStore();
|
initStore();
|
||||||
}
|
}
|
||||||
|
isFirstRun = false;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const Toaster = (props: ToasterProps) => {
|
|||||||
theme={theme() as ToasterProps["theme"]}
|
theme={theme() as ToasterProps["theme"]}
|
||||||
class="toaster group"
|
class="toaster group"
|
||||||
position="top-center"
|
position="top-center"
|
||||||
|
closeButton
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
classes: {
|
classes: {
|
||||||
toast:
|
toast:
|
||||||
|
|||||||
+85
-31
@@ -30,7 +30,7 @@ export interface Task {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
ownerId: string | null; // Add ownerId to interface
|
ownerId: string | null; // Add ownerId to interface
|
||||||
content?: string; // HTML content from Tiptap
|
content?: string; // HTML content from Tiptap
|
||||||
deletedAt?: number; // Timestamp of soft delete
|
deletedAt?: number | null; // Timestamp of soft delete or null if restored
|
||||||
created: string; // ISO string from PB
|
created: string; // ISO string from PB
|
||||||
updated: string; // ISO string from PB
|
updated: string; // ISO string from PB
|
||||||
recurrence?: {
|
recurrence?: {
|
||||||
@@ -202,6 +202,7 @@ interface TaskStore {
|
|||||||
subscribedBuckets: string[]; // IDs of buckets I'm subscribed to
|
subscribedBuckets: string[]; // IDs of buckets I'm subscribed to
|
||||||
notes: Note[];
|
notes: Note[];
|
||||||
isNotepadMode: boolean;
|
isNotepadMode: boolean;
|
||||||
|
quickloadTasks: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial empty state
|
// Initial empty state
|
||||||
@@ -227,7 +228,8 @@ export const [store, setStore] = createStore<TaskStore>({
|
|||||||
buckets: [],
|
buckets: [],
|
||||||
subscribedBuckets: [],
|
subscribedBuckets: [],
|
||||||
notes: [],
|
notes: [],
|
||||||
isNotepadMode: false
|
isNotepadMode: false,
|
||||||
|
quickloadTasks: []
|
||||||
});
|
});
|
||||||
|
|
||||||
export const matchesFilter = (task: Task) => {
|
export const matchesFilter = (task: Task) => {
|
||||||
@@ -336,6 +338,38 @@ createRoot(() => {
|
|||||||
localStorage.setItem(key, JSON.stringify(store));
|
localStorage.setItem(key, JSON.stringify(store));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let debouncedSync: number | undefined;
|
||||||
|
createEffect(() => {
|
||||||
|
if (store.isInitializing || !pb.authStore.model?.id) return;
|
||||||
|
|
||||||
|
// Calculate Top 20 Tasks
|
||||||
|
const incompleteTasks = store.tasks.filter(t =>
|
||||||
|
!t.completed &&
|
||||||
|
t.ownerId === pb.authStore.model?.id &&
|
||||||
|
!t.tags?.includes("__template__") &&
|
||||||
|
!t.deletedAt
|
||||||
|
);
|
||||||
|
|
||||||
|
const scoredTasks = incompleteTasks.map(t => ({ id: t.id, score: getCombinedScore(t) }));
|
||||||
|
scoredTasks.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
|
const top20Ids = scoredTasks.slice(0, 20).map(t => t.id);
|
||||||
|
|
||||||
|
// Compare with current
|
||||||
|
const currentIds = store.quickloadTasks || [];
|
||||||
|
const isDifferent = top20Ids.length !== currentIds.length || top20Ids.some((id, i) => id !== currentIds[i]);
|
||||||
|
|
||||||
|
if (isDifferent) {
|
||||||
|
setStore("quickloadTasks", top20Ids);
|
||||||
|
|
||||||
|
// Debounce save by 10s
|
||||||
|
if (debouncedSync) window.clearTimeout(debouncedSync);
|
||||||
|
debouncedSync = window.setTimeout(() => {
|
||||||
|
syncPreferences();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- Calibration --
|
// -- Calibration --
|
||||||
@@ -762,6 +796,45 @@ export const initStore = async () => {
|
|||||||
if (!pb.authStore.isValid || store.isInitializing) return;
|
if (!pb.authStore.isValid || store.isInitializing) return;
|
||||||
setStore("isInitializing", true);
|
setStore("isInitializing", true);
|
||||||
|
|
||||||
|
const currentUserId = pb.authStore.model?.id;
|
||||||
|
|
||||||
|
// STAGE 1: Instant Focus from Local Auth Model (Top 20 + Fallback)
|
||||||
|
// Goal: < 100ms First Paint. Doesn't wait for any other network requests.
|
||||||
|
if (currentUserId) {
|
||||||
|
try {
|
||||||
|
const prefs = pb.authStore.model?.Taskgrid_pref || {};
|
||||||
|
const cachedQuickload = prefs.quickloadTasks || [];
|
||||||
|
|
||||||
|
let focusRecords;
|
||||||
|
if (cachedQuickload.length > 0) {
|
||||||
|
const idFilter = cachedQuickload.map((id: string) => `id = "${id}"`).join(' || ');
|
||||||
|
const res = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
|
filter: `(${idFilter}) && deletedAt = ""`,
|
||||||
|
requestKey: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort manually to match quickload order
|
||||||
|
res.sort((a, b) => cachedQuickload.indexOf(a.id) - cachedQuickload.indexOf(b.id));
|
||||||
|
focusRecords = { items: res };
|
||||||
|
} else {
|
||||||
|
focusRecords = await pb.collection(TASGRID_COLLECTION).getList(1, 30, {
|
||||||
|
filter: `user = "${currentUserId}" && completed = false && deletedAt = ""`,
|
||||||
|
sort: '-priority,dueDate',
|
||||||
|
requestKey: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out templates client-side to ensure tasks with empty/null tags aren't excluded by DB
|
||||||
|
const focusTasks = focusRecords.items
|
||||||
|
.map(mapRecordToTask)
|
||||||
|
.filter(t => !t.tags?.includes("__template__"));
|
||||||
|
|
||||||
|
setStore("tasks", focusTasks);
|
||||||
|
} catch (focusErr) {
|
||||||
|
console.warn("Stage 1 load failed:", focusErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 0. Synchronous hydration from localStorage for "instant" first paint
|
// 0. Synchronous hydration from localStorage for "instant" first paint
|
||||||
const key = getStorageKey();
|
const key = getStorageKey();
|
||||||
if (key) {
|
if (key) {
|
||||||
@@ -795,7 +868,8 @@ export const initStore = async () => {
|
|||||||
uWeight: prefs.uWeight || 1.0,
|
uWeight: prefs.uWeight || 1.0,
|
||||||
matrixScaleDays: prefs.matrixScaleDays || 30,
|
matrixScaleDays: prefs.matrixScaleDays || 30,
|
||||||
prefId: userId,
|
prefId: userId,
|
||||||
subscribedBuckets: user.subscribedBuckets || []
|
subscribedBuckets: user.subscribedBuckets || [],
|
||||||
|
quickloadTasks: prefs.quickloadTasks || []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (prefErr) {
|
} catch (prefErr) {
|
||||||
@@ -859,24 +933,6 @@ export const initStore = async () => {
|
|||||||
|
|
||||||
// --- WINDOWED SYNC STRATEGY ---
|
// --- WINDOWED SYNC STRATEGY ---
|
||||||
|
|
||||||
// STAGE 1: Instant Focus (Top 30 Incomplete - User Only)
|
|
||||||
// Goal: < 100ms First Paint. Uses 'idx_user_focused'.
|
|
||||||
try {
|
|
||||||
const focusRecords = await pb.collection(TASGRID_COLLECTION).getList(1, 30, {
|
|
||||||
filter: `user = "${currentUserId}" && completed = false && deletedAt = ""`,
|
|
||||||
sort: '-priority,dueDate',
|
|
||||||
requestKey: null
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter out templates client-side to ensure tasks with empty/null tags aren't excluded by DB
|
|
||||||
const focusTasks = focusRecords.items
|
|
||||||
.map(mapRecordToTask)
|
|
||||||
.filter(t => !t.tags?.includes("__template__"));
|
|
||||||
|
|
||||||
setStore("tasks", focusTasks);
|
|
||||||
} catch (focusErr) {
|
|
||||||
console.warn("Stage 1 load failed:", focusErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// STAGE 2 & 3: Background Sync
|
// STAGE 2 & 3: Background Sync
|
||||||
const backgroundSync = async () => {
|
const backgroundSync = async () => {
|
||||||
@@ -1387,16 +1443,13 @@ export const updateTask = async (id: string, updates: Partial<Task>) => {
|
|||||||
delete pbUpdates.ownerId;
|
delete pbUpdates.ownerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalUpdates.deletedAt !== undefined) {
|
if ("deletedAt" in finalUpdates) {
|
||||||
// PB expects a date string or null for date fields usually,
|
// PB expects a date string or null for date fields.
|
||||||
// but if we defined deletedAt as a 'date' field in PB.
|
// Using finalUpdates.deletedAt check to distinguish between soft-delete (timestamp) and restore (null/falsy)
|
||||||
// If we defined it as a number in PB? User didn't specify.
|
|
||||||
// Assuming it's a 'Date' field in PB based on previous conversation
|
|
||||||
// "deletedAt | Date | No"
|
|
||||||
if (finalUpdates.deletedAt) {
|
if (finalUpdates.deletedAt) {
|
||||||
pbUpdates.deletedAt = new Date(finalUpdates.deletedAt).toISOString();
|
pbUpdates.deletedAt = new Date(finalUpdates.deletedAt).toISOString();
|
||||||
} else {
|
} else {
|
||||||
pbUpdates.deletedAt = null; // restore
|
pbUpdates.deletedAt = null; // restore task
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1478,8 +1531,8 @@ export const removeTask = (id: string) => {
|
|||||||
|
|
||||||
export const restoreTask = (id: string) => {
|
export const restoreTask = (id: string) => {
|
||||||
// updateTask handles both optimistic update and PB sync
|
// updateTask handles both optimistic update and PB sync
|
||||||
// Pass undefined for deletedAt, updateTask will convert to null for PB
|
// Pass null for deletedAt to explicitly clear it in PB
|
||||||
updateTask(id, { deletedAt: undefined });
|
updateTask(id, { deletedAt: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTaskPermanently = async (id: string) => {
|
export const deleteTaskPermanently = async (id: string) => {
|
||||||
@@ -1956,7 +2009,8 @@ export const syncPreferences = async () => {
|
|||||||
pWeight: store.pWeight,
|
pWeight: store.pWeight,
|
||||||
uWeight: store.uWeight,
|
uWeight: store.uWeight,
|
||||||
matrixScaleDays: store.matrixScaleDays,
|
matrixScaleDays: store.matrixScaleDays,
|
||||||
tagDefinitions: store.tagDefinitions
|
tagDefinitions: store.tagDefinitions,
|
||||||
|
quickloadTasks: store.quickloadTasks
|
||||||
};
|
};
|
||||||
|
|
||||||
await pb.collection('users').update(userId, {
|
await pb.collection('users').update(userId, {
|
||||||
|
|||||||
Reference in New Issue
Block a user