Skip to content

Commit 8aa048d

Browse files
committed
Add folder scoped search to overview
1 parent d7d2e8b commit 8aa048d

2 files changed

Lines changed: 323 additions & 27 deletions

File tree

App.tsx

Lines changed: 166 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import InfoView from './components/InfoView';
1818
import UpdateNotification from './components/UpdateNotification';
1919
import CreateFromTemplateModal from './components/CreateFromTemplateModal';
2020
import DocumentHistoryView from './components/PromptHistoryView';
21-
import FolderOverview, { FolderOverviewMetrics } from './components/FolderOverview';
21+
import FolderOverview, { FolderOverviewMetrics, FolderSearchResult, RecentDocumentSummary } from './components/FolderOverview';
2222
import { PlusIcon, FolderPlusIcon, TrashIcon, GearIcon, InfoIcon, TerminalIcon, DocumentDuplicateIcon, PencilIcon, CopyIcon, CommandIcon, CodeIcon, FolderDownIcon, FormatIcon, SparklesIcon } from './components/Icons';
2323
import AboutModal from './components/AboutModal';
2424
import Header from './components/Header';
@@ -122,6 +122,9 @@ const MainApp: React.FC = () => {
122122
const [isDraggingFile, setIsDraggingFile] = useState(false);
123123
const [formatTrigger, setFormatTrigger] = useState(0);
124124
const [bodySearchMatches, setBodySearchMatches] = useState<Map<string, string>>(new Map());
125+
const [folderSearchTerm, setFolderSearchTerm] = useState('');
126+
const [folderBodySearchMatches, setFolderBodySearchMatches] = useState<Map<string, string>>(new Map());
127+
const [isFolderSearchLoading, setIsFolderSearchLoading] = useState(false);
125128

126129

127130
const isSidebarResizing = useRef(false);
@@ -189,6 +192,12 @@ const MainApp: React.FC = () => {
189192
return itemsWithSearchMetadata.find(p => p.id === activeNodeId) || null;
190193
}, [itemsWithSearchMetadata, activeNodeId]);
191194

195+
useEffect(() => {
196+
setFolderSearchTerm('');
197+
setFolderBodySearchMatches(new Map());
198+
setIsFolderSearchLoading(false);
199+
}, [activeNode?.id, activeNode?.type]);
200+
192201
const activeTemplate = useMemo(() => {
193202
return templates.find(t => t.template_id === activeTemplateId) || null;
194203
}, [templates, activeTemplateId]);
@@ -225,6 +234,53 @@ const MainApp: React.FC = () => {
225234
};
226235
}, [searchTerm]);
227236

237+
useEffect(() => {
238+
if (!activeNode || activeNode.type !== 'folder') {
239+
return;
240+
}
241+
242+
const term = folderSearchTerm.trim();
243+
if (!term) {
244+
setFolderBodySearchMatches(new Map());
245+
setIsFolderSearchLoading(false);
246+
return;
247+
}
248+
249+
let isCancelled = false;
250+
setIsFolderSearchLoading(true);
251+
setFolderBodySearchMatches(new Map());
252+
253+
repository.searchDocumentsByBody(term, 200)
254+
.then(results => {
255+
if (isCancelled) {
256+
return;
257+
}
258+
const descendantIds = getDescendantIds(activeNode.id);
259+
const matches = new Map<string, string>();
260+
for (const result of results) {
261+
if (descendantIds.has(result.nodeId)) {
262+
matches.set(result.nodeId, result.snippet);
263+
}
264+
}
265+
setFolderBodySearchMatches(matches);
266+
})
267+
.catch(error => {
268+
if (!isCancelled) {
269+
console.error('Failed to search within folder:', error);
270+
setFolderBodySearchMatches(new Map());
271+
}
272+
})
273+
.finally(() => {
274+
if (!isCancelled) {
275+
setIsFolderSearchLoading(false);
276+
}
277+
});
278+
279+
return () => {
280+
isCancelled = true;
281+
};
282+
}, [activeNode, folderSearchTerm, getDescendantIds]);
283+
228284
const { documentTree, navigableItems } = useMemo(() => {
229285
let itemsToBuildFrom = itemsWithSearchMetadata;
230286
if (searchTerm.trim()) {
@@ -296,9 +352,9 @@ const MainApp: React.FC = () => {
296352
return { documentTree: finalTree, navigableItems: flatList };
297353
}, [itemsWithSearchMetadata, templates, searchTerm, expandedFolderIds]);
298354

299-
const activeFolderMetrics = useMemo<FolderOverviewMetrics | null>(() => {
355+
const { metrics: activeFolderMetrics, documents: activeFolderDocuments } = useMemo(() => {
300356
if (!activeNode || activeNode.type !== 'folder') {
301-
return null;
357+
return { metrics: null as FolderOverviewMetrics | null, documents: [] as RecentDocumentSummary[] };
302358
}
303359

304360
const parseDate = (value?: string | null): Date | null => {
@@ -307,15 +363,15 @@ const MainApp: React.FC = () => {
307363
return Number.isNaN(date.getTime()) ? null : date;
308364
};
309365

310-
const formatNodeTitle = (node: DocumentOrFolder) => {
366+
const formatNodeTitle = (node: { title: string; type: 'document' | 'folder' }) => {
311367
const trimmed = node.title.trim();
312368
if (trimmed) {
313369
return trimmed;
314370
}
315371
return node.type === 'folder' ? 'Untitled Folder' : 'Untitled Document';
316372
};
317373

318-
const computeFromTree = (folderNode: DocumentNode): FolderOverviewMetrics => {
374+
const computeFromTree = (folderNode: DocumentNode) => {
319375
const recordLatest = (() => {
320376
let latest: Date | null = null;
321377
return {
@@ -343,14 +399,14 @@ const MainApp: React.FC = () => {
343399
node: child,
344400
parentPath: [],
345401
}));
346-
const recentDocuments: FolderOverviewMetrics['recentDocuments'] = [];
402+
const allDocuments: RecentDocumentSummary[] = [];
347403

348404
while (stack.length > 0) {
349405
const { node: current, parentPath } = stack.pop()!;
350406
recordLatest.update(current.updatedAt);
351407
if (current.type === 'document') {
352408
totalDocumentCount += 1;
353-
recentDocuments.push({
409+
allDocuments.push({
354410
id: current.id,
355411
title: current.title,
356412
updatedAt: current.updatedAt,
@@ -365,7 +421,7 @@ const MainApp: React.FC = () => {
365421
}
366422

367423
const latestDate = recordLatest.getValue();
368-
const sortedRecent = recentDocuments
424+
const recentDocuments = [...allDocuments]
369425
.sort((a, b) => {
370426
const aDate = parseDate(a.updatedAt)?.getTime() ?? 0;
371427
const bDate = parseDate(b.updatedAt)?.getTime() ?? 0;
@@ -374,13 +430,16 @@ const MainApp: React.FC = () => {
374430
.slice(0, 5);
375431

376432
return {
377-
directDocumentCount,
378-
directFolderCount,
379-
totalDocumentCount,
380-
totalFolderCount,
381-
totalItemCount: totalDocumentCount + totalFolderCount,
382-
lastUpdated: latestDate ? latestDate.toISOString() : null,
383-
recentDocuments: sortedRecent,
433+
metrics: {
434+
directDocumentCount,
435+
directFolderCount,
436+
totalDocumentCount,
437+
totalFolderCount,
438+
totalItemCount: totalDocumentCount + totalFolderCount,
439+
lastUpdated: latestDate ? latestDate.toISOString() : null,
440+
recentDocuments,
441+
},
442+
documents: allDocuments,
384443
};
385444
};
386445

@@ -396,7 +455,7 @@ const MainApp: React.FC = () => {
396455
return map;
397456
};
398457

399-
const computeFromList = (): FolderOverviewMetrics => {
458+
const computeFromList = () => {
400459
const childMap = buildChildMap();
401460
const directChildren = childMap.get(activeNode.id) ?? [];
402461

@@ -426,14 +485,14 @@ const MainApp: React.FC = () => {
426485
node: child,
427486
parentPath: [],
428487
}));
429-
const recentDocuments: FolderOverviewMetrics['recentDocuments'] = [];
488+
const allDocuments: RecentDocumentSummary[] = [];
430489

431490
while (stack.length > 0) {
432491
const { node: current, parentPath } = stack.pop()!;
433492
recordLatest.update(current.updatedAt);
434493
if (current.type === 'document') {
435494
totalDocumentCount += 1;
436-
recentDocuments.push({
495+
allDocuments.push({
437496
id: current.id,
438497
title: current.title,
439498
updatedAt: current.updatedAt,
@@ -448,7 +507,7 @@ const MainApp: React.FC = () => {
448507
}
449508

450509
const latestDate = recordLatest.getValue();
451-
const sortedRecent = recentDocuments
510+
const recentDocuments = [...allDocuments]
452511
.sort((a, b) => {
453512
const aDate = parseDate(a.updatedAt)?.getTime() ?? 0;
454513
const bDate = parseDate(b.updatedAt)?.getTime() ?? 0;
@@ -457,13 +516,16 @@ const MainApp: React.FC = () => {
457516
.slice(0, 5);
458517

459518
return {
460-
directDocumentCount,
461-
directFolderCount,
462-
totalDocumentCount,
463-
totalFolderCount,
464-
totalItemCount: totalDocumentCount + totalFolderCount,
465-
lastUpdated: latestDate ? latestDate.toISOString() : null,
466-
recentDocuments: sortedRecent,
519+
metrics: {
520+
directDocumentCount,
521+
directFolderCount,
522+
totalDocumentCount,
523+
totalFolderCount,
524+
totalItemCount: totalDocumentCount + totalFolderCount,
525+
lastUpdated: latestDate ? latestDate.toISOString() : null,
526+
recentDocuments,
527+
},
528+
documents: allDocuments,
467529
};
468530
};
469531

@@ -489,6 +551,80 @@ const MainApp: React.FC = () => {
489551
return computeFromList();
490552
}, [activeNode, documentTree, items]);
491553

554+
const folderSearchResults = useMemo<FolderSearchResult[]>(() => {
555+
if (!activeNode || activeNode.type !== 'folder') {
556+
return [];
557+
}
558+
559+
const trimmed = folderSearchTerm.trim();
560+
if (!trimmed) {
561+
return [];
562+
}
563+
564+
const lowerTerm = trimmed.toLowerCase();
565+
566+
const parseToTimestamp = (value?: string | null) => {
567+
if (!value) {
568+
return 0;
569+
}
570+
const date = new Date(value);
571+
return Number.isNaN(date.getTime()) ? 0 : date.getTime();
572+
};
573+
574+
const computeMatchScore = (fields: ('title' | 'body')[]) => {
575+
if (fields.length === 2) {
576+
return 0;
577+
}
578+
return fields[0] === 'title' ? 1 : 2;
579+
};
580+
581+
type FolderSearchResultWithScore = FolderSearchResult & { matchScore: number; sortTimestamp: number; };
582+
583+
const results: FolderSearchResultWithScore[] = [];
584+
585+
for (const document of activeFolderDocuments) {
586+
const titleLower = document.title.toLowerCase();
587+
const hasTitleMatch = titleLower.includes(lowerTerm);
588+
const snippet = folderBodySearchMatches.get(document.id);
589+
const hasBodyMatch = Boolean(snippet);
590+
591+
if (!hasTitleMatch && !hasBodyMatch) {
592+
continue;
593+
}
594+
595+
const matchedFields: ('title' | 'body')[] = [];
596+
if (hasTitleMatch) {
597+
matchedFields.push('title');
598+
}
599+
if (hasBodyMatch) {
600+
matchedFields.push('body');
601+
}
602+
603+
results.push({
604+
id: document.id,
605+
title: document.title,
606+
updatedAt: document.updatedAt,
607+
parentPath: document.parentPath,
608+
searchSnippet: snippet,
609+
matchedFields,
610+
matchScore: computeMatchScore(matchedFields),
611+
sortTimestamp: parseToTimestamp(document.updatedAt),
612+
});
613+
}
614+
615+
return results
616+
.sort((a, b) => {
617+
if (a.matchScore !== b.matchScore) {
618+
return a.matchScore - b.matchScore;
619+
}
620+
if (a.sortTimestamp !== b.sortTimestamp) {
621+
return b.sortTimestamp - a.sortTimestamp;
622+
}
623+
return a.title.localeCompare(b.title);
624+
})
625+
.map(({ matchScore: _matchScore, sortTimestamp: _sortTimestamp, ...rest }) => rest);
626+
}, [activeNode, activeFolderDocuments, folderBodySearchMatches, folderSearchTerm]);
627+
492628
useEffect(() => {
493629
if (window.electronAPI?.getAppVersion) {
494630
window.electronAPI.getAppVersion().then(setAppVersion);
@@ -1367,6 +1503,10 @@ const MainApp: React.FC = () => {
13671503
onNewSubfolder={(parentId) => handleNewFolder(parentId)}
13681504
onImportFiles={handleImportFilesIntoFolder}
13691505
onRenameFolder={handleStartRenamingNode}
1506+
folderSearchTerm={folderSearchTerm}
1507+
onFolderSearchTermChange={setFolderSearchTerm}
1508+
searchResults={folderSearchResults}
1509+
isSearchLoading={isFolderSearchLoading}
13701510
/>
13711511
);
13721512
}

0 commit comments

Comments
 (0)