mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
First wave of eliminating global `app` references (per Obsidian's
`obsidianmd/no-restricted-globals` rule). Adds a `useApp()` hook that
reads from `AppContext` and throws if no provider is in scope, then
converts the React layer to use it. Modals that render React content
now wrap their tree with `<AppContext.Provider value={this.app}>` so
descendants pick up the modal-owned app rather than the global one,
which matters in popout windows.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
152 lines
6.1 KiB
TypeScript
152 lines
6.1 KiB
TypeScript
/**
|
|
* Shared hook for building project processing status data.
|
|
*
|
|
* Extracted from AddProjectModal to be reusable by both
|
|
* AddProjectModal (with draft contextSource preview) and
|
|
* ProgressCard (with saved project only).
|
|
*/
|
|
|
|
import { getCurrentProject, ProjectConfig, useProjectContextLoad } from "@/aiParams";
|
|
import { ContextCache, ProjectContextCache } from "@/cache/projectContextCache";
|
|
import {
|
|
buildProcessingItems,
|
|
type ProcessingAdapterResult,
|
|
} from "@/components/project/processingAdapter";
|
|
import { useApp } from "@/context";
|
|
import { getMatchingPatterns, shouldIndexFile } from "@/search/searchUtils";
|
|
import { FileParserManager } from "@/tools/FileParserManager";
|
|
import { TFile } from "obsidian";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
export interface UseProjectProcessingDataParams {
|
|
/** Saved project config — used for cache lookup, isCurrentProject check, and file enumeration. */
|
|
cacheProject: ProjectConfig | null;
|
|
/**
|
|
* Optional draft contextSource override for generating the items list.
|
|
* When provided, newly added (but unsaved) URLs appear as "Pending".
|
|
* When omitted, falls back to cacheProject.contextSource.
|
|
*/
|
|
contextSource?: ProjectConfig["contextSource"];
|
|
}
|
|
|
|
export interface UseProjectProcessingDataResult {
|
|
/** Full adapter result including items and failedItemMap (null while loading or no project). */
|
|
processingData: ProcessingAdapterResult | null;
|
|
/** Cache three-state: undefined = loading, null = no cache, ContextCache = loaded. */
|
|
projectCache: ContextCache | null | undefined;
|
|
/** Whether cacheProject is the currently active/loaded project. */
|
|
isCurrentProject: boolean;
|
|
}
|
|
|
|
/**
|
|
* Build project processing status data for ProcessingStatus rendering.
|
|
*
|
|
* Handles:
|
|
* - Async cache loading (re-fetches when contextLoadState changes)
|
|
* - Vault file enumeration from cacheProject's inclusion/exclusion patterns
|
|
* - Static supportedExtensions lookup
|
|
* - previewProject construction from cacheProject + contextSource draft
|
|
* - Deferred rendering for non-current projects until cache is ready
|
|
*/
|
|
export function useProjectProcessingData(
|
|
params: UseProjectProcessingDataParams
|
|
): UseProjectProcessingDataResult {
|
|
const app = useApp();
|
|
const { cacheProject, contextSource } = params;
|
|
|
|
// Reason: Load the project's persistent context cache.
|
|
// undefined = still loading, null = loaded but no cache exists, ContextCache = loaded with data.
|
|
// Reason: Re-fetch cache when contextLoadState changes so contentEmpty detection stays current.
|
|
const [projectCache, setProjectCache] = useState<ContextCache | null | undefined>(undefined);
|
|
const [contextLoadState] = useProjectContextLoad();
|
|
|
|
// Reason: reset cache to "loading" when project identity changes to avoid
|
|
// briefly showing stale cache from the previous project during a switch.
|
|
const cacheProjectId = cacheProject?.id;
|
|
useEffect(() => {
|
|
setProjectCache(undefined);
|
|
}, [cacheProjectId]);
|
|
|
|
useEffect(() => {
|
|
if (!cacheProject) {
|
|
setProjectCache(undefined);
|
|
return;
|
|
}
|
|
let mounted = true;
|
|
void ProjectContextCache.getInstance()
|
|
.get(cacheProject)
|
|
.then((cache) => {
|
|
if (mounted) setProjectCache(cache);
|
|
});
|
|
return () => {
|
|
mounted = false;
|
|
};
|
|
}, [cacheProject, contextLoadState]);
|
|
|
|
// Reason: isCurrentProject drives whether we show live state or cache-fallback state.
|
|
// We use cacheProject (the saved config) because the load state corresponds to
|
|
// the already-saved configuration, not unsaved edits.
|
|
const isCurrentProject = !!cacheProject && cacheProject.id === getCurrentProject()?.id;
|
|
|
|
// Reason: Enumerate vault files matching inclusion/exclusion patterns.
|
|
// Uses draft contextSource when available so unsaved edits are reflected in the file preview;
|
|
// falls back to cacheProject.contextSource for saved-only views (e.g. ProgressCard).
|
|
const effectiveInclusions = contextSource?.inclusions ?? cacheProject?.contextSource?.inclusions;
|
|
const effectiveExclusions = contextSource?.exclusions ?? cacheProject?.contextSource?.exclusions;
|
|
const [projectFiles, setProjectFiles] = useState<TFile[]>([]);
|
|
useEffect(() => {
|
|
if (!cacheProject) {
|
|
setProjectFiles([]);
|
|
return;
|
|
}
|
|
const { inclusions, exclusions } = getMatchingPatterns({
|
|
inclusions: effectiveInclusions,
|
|
exclusions: effectiveExclusions,
|
|
isProject: true,
|
|
});
|
|
setProjectFiles(
|
|
app.vault.getFiles().filter((file) => shouldIndexFile(file, inclusions, exclusions, true))
|
|
);
|
|
}, [app, cacheProject, effectiveInclusions, effectiveExclusions]);
|
|
|
|
// Reason: Static method — the set of supported extensions never changes at runtime.
|
|
const supportedExtensions = useMemo(() => FileParserManager.getProjectSupportedExtensions(), []);
|
|
|
|
// Reason: Merge draft contextSource into cacheProject so the Content Conversion panel
|
|
// reflects the current form state (newly added URLs show as "Pending").
|
|
// Status data (ready/failed/processing) still comes from live state and cache.
|
|
const displayProject = useMemo<ProjectConfig | null>(() => {
|
|
if (!cacheProject) return null;
|
|
// Reason: only override contextSource when a draft is explicitly provided.
|
|
// This avoids unnecessary object creation when no draft is present.
|
|
if (!contextSource) return cacheProject;
|
|
return {
|
|
...cacheProject,
|
|
contextSource,
|
|
};
|
|
}, [cacheProject, contextSource]);
|
|
|
|
const processingData = useMemo(() => {
|
|
if (!displayProject) return null;
|
|
// Reason: Don't render until cache is loaded for non-current projects to avoid empty state flash.
|
|
// Current project has live state so it can render immediately without cache.
|
|
if (projectCache === undefined && !isCurrentProject) return null;
|
|
return buildProcessingItems({
|
|
project: displayProject,
|
|
isCurrentProject,
|
|
liveState: contextLoadState,
|
|
projectCache,
|
|
projectFiles,
|
|
supportedExtensions,
|
|
});
|
|
}, [
|
|
displayProject,
|
|
isCurrentProject,
|
|
contextLoadState,
|
|
projectCache,
|
|
projectFiles,
|
|
supportedExtensions,
|
|
]);
|
|
|
|
return { processingData, projectCache, isCurrentProject };
|
|
}
|