feat(FN-4854): complete Step 3 — hydrate documents and todo list caches
Fusion-Task-Id: FN-4854 Fusion-Task-Lineage: 969ec04a-efe3-4dee-b183-2d4f2de6b388
This commit is contained in:
committed by
gsxdsm
parent
9feb4ec870
commit
8ca0cddfe7
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import type { TaskDocumentWithTask } from "@fusion/core";
|
import type { TaskDocumentWithTask } from "@fusion/core";
|
||||||
import { fetchAllDocuments, fetchProjectMarkdownFiles, type MarkdownFileEntry } from "../api";
|
import { fetchAllDocuments, fetchProjectMarkdownFiles, type MarkdownFileEntry } from "../api";
|
||||||
|
import { readCache, SWR_CACHE_KEYS, writeCache } from "../utils/swrCache";
|
||||||
|
|
||||||
export interface UseDocumentsResult {
|
export interface UseDocumentsResult {
|
||||||
/** List of all documents across tasks */
|
/** List of all documents across tasks */
|
||||||
@@ -32,13 +33,20 @@ export function useDocuments(options?: {
|
|||||||
includeProjectFiles?: boolean;
|
includeProjectFiles?: boolean;
|
||||||
}): UseDocumentsResult {
|
}): UseDocumentsResult {
|
||||||
const { projectId, searchQuery, includeProjectFiles = true } = options ?? {};
|
const { projectId, searchQuery, includeProjectFiles = true } = options ?? {};
|
||||||
const [documents, setDocuments] = useState<TaskDocumentWithTask[]>([]);
|
const cacheKey = projectId ? `${SWR_CACHE_KEYS.DOCUMENTS_PREFIX}${projectId}` : null;
|
||||||
|
const [documents, setDocuments] = useState<TaskDocumentWithTask[]>(() => {
|
||||||
|
if (!cacheKey) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const cached = readCache<TaskDocumentWithTask[]>(cacheKey);
|
||||||
|
return Array.isArray(cached) ? cached : [];
|
||||||
|
});
|
||||||
const [projectFiles, setProjectFiles] = useState<MarkdownFileEntry[]>([]);
|
const [projectFiles, setProjectFiles] = useState<MarkdownFileEntry[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(() => documents.length === 0);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
// Track if we've completed the initial load
|
// Track if we've completed the initial load
|
||||||
const initialLoadCompleteRef = useRef(false);
|
const initialLoadCompleteRef = useRef(documents.length > 0);
|
||||||
// Debounce timer for search
|
// Debounce timer for search
|
||||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
@@ -55,7 +63,7 @@ export function useDocuments(options?: {
|
|||||||
const requestController = new AbortController();
|
const requestController = new AbortController();
|
||||||
abortRef.current = requestController;
|
abortRef.current = requestController;
|
||||||
|
|
||||||
// Only set loading on initial load
|
// Only set loading on initial load when we have no cached docs.
|
||||||
const isInitial = !initialLoadCompleteRef.current;
|
const isInitial = !initialLoadCompleteRef.current;
|
||||||
if (isInitial) {
|
if (isInitial) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -84,6 +92,10 @@ export function useDocuments(options?: {
|
|||||||
|
|
||||||
if (documentResult.status === "fulfilled") {
|
if (documentResult.status === "fulfilled") {
|
||||||
setDocuments(documentResult.value);
|
setDocuments(documentResult.value);
|
||||||
|
if (cacheKey) {
|
||||||
|
const cachedPayload = documentResult.value.length > 500 ? documentResult.value.slice(0, 500) : documentResult.value;
|
||||||
|
writeCache(cacheKey, cachedPayload, { maxBytes: 500_000 });
|
||||||
|
}
|
||||||
initialLoadCompleteRef.current = true;
|
initialLoadCompleteRef.current = true;
|
||||||
} else {
|
} else {
|
||||||
documentError = documentResult.reason instanceof Error
|
documentError = documentResult.reason instanceof Error
|
||||||
@@ -107,7 +119,27 @@ export function useDocuments(options?: {
|
|||||||
if (isInitial) {
|
if (isInitial) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [includeProjectFiles, projectId, searchQuery]);
|
}, [cacheKey, includeProjectFiles, projectId, searchQuery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cacheKey) {
|
||||||
|
initialLoadCompleteRef.current = false;
|
||||||
|
setDocuments([]);
|
||||||
|
setLoading(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = readCache<TaskDocumentWithTask[]>(cacheKey);
|
||||||
|
if (Array.isArray(cached)) {
|
||||||
|
setDocuments(cached);
|
||||||
|
initialLoadCompleteRef.current = true;
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
initialLoadCompleteRef.current = false;
|
||||||
|
setDocuments([]);
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
}, [cacheKey]);
|
||||||
|
|
||||||
// Debounced search effect
|
// Debounced search effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
deleteTodoItem,
|
deleteTodoItem,
|
||||||
reorderTodoItems,
|
reorderTodoItems,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
|
import { readCache, SWR_CACHE_KEYS, writeCache } from "../utils/swrCache";
|
||||||
|
|
||||||
type ToastType = "info" | "success" | "error" | "warning";
|
type ToastType = "info" | "success" | "error" | "warning";
|
||||||
|
|
||||||
@@ -50,9 +51,13 @@ function buildTempId(prefix: string): string {
|
|||||||
export function useTodoLists(options: UseTodoListsOptions = {}): UseTodoListsResult {
|
export function useTodoLists(options: UseTodoListsOptions = {}): UseTodoListsResult {
|
||||||
const { projectId, addToast } = options;
|
const { projectId, addToast } = options;
|
||||||
|
|
||||||
const [lists, setLists] = useState<TodoList[]>([]);
|
const cacheKey = `${SWR_CACHE_KEYS.TODO_LISTS_PREFIX}${projectId ?? "global"}`;
|
||||||
|
const [lists, setLists] = useState<TodoList[]>(() => {
|
||||||
|
const cached = readCache<TodoList[]>(cacheKey);
|
||||||
|
return Array.isArray(cached) ? cached : [];
|
||||||
|
});
|
||||||
const [items, setItems] = useState<TodoItem[]>([]);
|
const [items, setItems] = useState<TodoItem[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(() => lists.length === 0);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [selectedListId, setSelectedListId] = useState<string | null>(null);
|
const [selectedListId, setSelectedListId] = useState<string | null>(null);
|
||||||
const [listData, setListData] = useState<TodoListWithItems[]>([]);
|
const [listData, setListData] = useState<TodoListWithItems[]>([]);
|
||||||
@@ -63,8 +68,21 @@ export function useTodoLists(options: UseTodoListsOptions = {}): UseTodoListsRes
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
async function loadLists() {
|
const cachedLists = readCache<TodoList[]>(cacheKey);
|
||||||
|
const hasCachedLists = Array.isArray(cachedLists) && cachedLists.length > 0;
|
||||||
|
|
||||||
|
if (hasCachedLists) {
|
||||||
|
setLists(cachedLists);
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
setLists([]);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLists() {
|
||||||
|
if (!hasCachedLists) {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -74,7 +92,9 @@ export function useTodoLists(options: UseTodoListsOptions = {}): UseTodoListsRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
setListData(data);
|
setListData(data);
|
||||||
setLists(data.map(toList));
|
const fetchedLists = data.map(toList);
|
||||||
|
setLists(fetchedLists);
|
||||||
|
writeCache(cacheKey, fetchedLists, { maxBytes: 500_000 });
|
||||||
|
|
||||||
const activeListId =
|
const activeListId =
|
||||||
selectedListIdRef.current && data.some((list) => list.id === selectedListIdRef.current)
|
selectedListIdRef.current && data.some((list) => list.id === selectedListIdRef.current)
|
||||||
@@ -104,7 +124,7 @@ export function useTodoLists(options: UseTodoListsOptions = {}): UseTodoListsRes
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [projectId]);
|
}, [cacheKey, projectId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedListId) {
|
if (!selectedListId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user