feat(FN-2030): add project markdown explorer to Documents view
- Add recursive markdown file discovery in file-service with excluded directories and sorted metadata output - Expose GET /api/files/markdown and wire a frontend API client plus useProjectMarkdownFiles hook - Redesign DocumentsView with project/task tabs, search, task grouping, and desktop/mobile file preview flows - Add coverage for markdown scanning and DocumentsView behaviors, and update useDocuments tests for new API usage
This commit is contained in:
78
packages/dashboard/app/hooks/useProjectMarkdownFiles.ts
Normal file
78
packages/dashboard/app/hooks/useProjectMarkdownFiles.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { fetchProjectMarkdownFiles, type MarkdownFileEntry } from "../api";
|
||||
|
||||
export interface UseProjectMarkdownFilesResult {
|
||||
files: MarkdownFileEntry[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching markdown files from the project workspace.
|
||||
*
|
||||
* Loading behavior matches useDocuments: loading is true only for the initial
|
||||
* fetch, not for subsequent refreshes, to avoid content flicker.
|
||||
*/
|
||||
export function useProjectMarkdownFiles(projectId?: string): UseProjectMarkdownFilesResult {
|
||||
const [files, setFiles] = useState<MarkdownFileEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const initialLoadCompleteRef = useRef(false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (abortRef.current) {
|
||||
abortRef.current.abort();
|
||||
}
|
||||
|
||||
const requestController = new AbortController();
|
||||
abortRef.current = requestController;
|
||||
|
||||
const isInitial = !initialLoadCompleteRef.current;
|
||||
if (isInitial) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetchProjectMarkdownFiles(projectId);
|
||||
|
||||
if (requestController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFiles(response.files);
|
||||
initialLoadCompleteRef.current = true;
|
||||
} catch (err: unknown) {
|
||||
if (requestController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
if (!requestController.signal.aborted && isInitial) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
initialLoadCompleteRef.current = false;
|
||||
void refresh();
|
||||
|
||||
return () => {
|
||||
if (abortRef.current) {
|
||||
abortRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, [refresh]);
|
||||
|
||||
return {
|
||||
files,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user