@@ -18,6 +18,7 @@ import InfoView from './components/InfoView';
1818import UpdateNotification from './components/UpdateNotification' ;
1919import CreateFromTemplateModal from './components/CreateFromTemplateModal' ;
2020import DocumentHistoryView from './components/PromptHistoryView' ;
21+ import FolderOverview , { FolderOverviewMetrics } from './components/FolderOverview' ;
2122import { PlusIcon , FolderPlusIcon , TrashIcon , GearIcon , InfoIcon , TerminalIcon , DocumentDuplicateIcon , PencilIcon , CopyIcon , CommandIcon , CodeIcon , FolderDownIcon , FormatIcon , SparklesIcon } from './components/Icons' ;
2223import AboutModal from './components/AboutModal' ;
2324import Header from './components/Header' ;
@@ -295,6 +296,151 @@ const MainApp: React.FC = () => {
295296 return { documentTree : finalTree , navigableItems : flatList } ;
296297 } , [ itemsWithSearchMetadata , templates , searchTerm , expandedFolderIds ] ) ;
297298
299+ const activeFolderMetrics = useMemo < FolderOverviewMetrics | null > ( ( ) => {
300+ if ( ! activeNode || activeNode . type !== 'folder' ) {
301+ return null ;
302+ }
303+
304+ const parseDate = ( value ?: string | null ) : Date | null => {
305+ if ( ! value ) return null ;
306+ const date = new Date ( value ) ;
307+ return Number . isNaN ( date . getTime ( ) ) ? null : date ;
308+ } ;
309+
310+ const computeFromTree = ( folderNode : DocumentNode ) : FolderOverviewMetrics => {
311+ const recordLatest = ( ( ) => {
312+ let latest : Date | null = null ;
313+ return {
314+ update ( value ?: string | null ) {
315+ const parsed = parseDate ( value ) ;
316+ if ( parsed && ( ! latest || parsed > latest ) ) {
317+ latest = parsed ;
318+ }
319+ } ,
320+ getValue ( ) {
321+ return latest ;
322+ } ,
323+ } ;
324+ } ) ( ) ;
325+
326+ recordLatest . update ( folderNode . updatedAt ) ;
327+
328+ const directDocumentCount = folderNode . children . filter ( child => child . type === 'document' ) . length ;
329+ const directFolderCount = folderNode . children . filter ( child => child . type === 'folder' ) . length ;
330+
331+ let totalDocumentCount = 0 ;
332+ let totalFolderCount = 0 ;
333+ const stack = [ ...folderNode . children ] ;
334+
335+ while ( stack . length > 0 ) {
336+ const current = stack . pop ( ) ! ;
337+ recordLatest . update ( current . updatedAt ) ;
338+ if ( current . type === 'document' ) {
339+ totalDocumentCount += 1 ;
340+ } else if ( current . type === 'folder' ) {
341+ totalFolderCount += 1 ;
342+ stack . push ( ...current . children ) ;
343+ }
344+ }
345+
346+ const latestDate = recordLatest . getValue ( ) ;
347+
348+ return {
349+ directDocumentCount,
350+ directFolderCount,
351+ totalDocumentCount,
352+ totalFolderCount,
353+ totalItemCount : totalDocumentCount + totalFolderCount ,
354+ lastUpdated : latestDate ? latestDate . toISOString ( ) : null ,
355+ } ;
356+ } ;
357+
358+ const buildChildMap = ( ) => {
359+ const map = new Map < string | null , DocumentOrFolder [ ] > ( ) ;
360+ for ( const item of items ) {
361+ const key = item . parentId ;
362+ if ( ! map . has ( key ) ) {
363+ map . set ( key , [ ] ) ;
364+ }
365+ map . get ( key ) ! . push ( item ) ;
366+ }
367+ return map ;
368+ } ;
369+
370+ const computeFromList = ( ) : FolderOverviewMetrics => {
371+ const childMap = buildChildMap ( ) ;
372+ const directChildren = childMap . get ( activeNode . id ) ?? [ ] ;
373+
374+ const recordLatest = ( ( ) => {
375+ let latest : Date | null = null ;
376+ return {
377+ update ( value ?: string | null ) {
378+ const parsed = parseDate ( value ) ;
379+ if ( parsed && ( ! latest || parsed > latest ) ) {
380+ latest = parsed ;
381+ }
382+ } ,
383+ getValue ( ) {
384+ return latest ;
385+ } ,
386+ } ;
387+ } ) ( ) ;
388+
389+ recordLatest . update ( activeNode . updatedAt ) ;
390+
391+ const directDocumentCount = directChildren . filter ( child => child . type === 'document' ) . length ;
392+ const directFolderCount = directChildren . filter ( child => child . type === 'folder' ) . length ;
393+
394+ let totalDocumentCount = 0 ;
395+ let totalFolderCount = 0 ;
396+ const stack = [ ...directChildren ] ;
397+
398+ while ( stack . length > 0 ) {
399+ const current = stack . pop ( ) ! ;
400+ recordLatest . update ( current . updatedAt ) ;
401+ if ( current . type === 'document' ) {
402+ totalDocumentCount += 1 ;
403+ } else {
404+ totalFolderCount += 1 ;
405+ const childItems = childMap . get ( current . id ) ?? [ ] ;
406+ stack . push ( ...childItems ) ;
407+ }
408+ }
409+
410+ const latestDate = recordLatest . getValue ( ) ;
411+
412+ return {
413+ directDocumentCount,
414+ directFolderCount,
415+ totalDocumentCount,
416+ totalFolderCount,
417+ totalItemCount : totalDocumentCount + totalFolderCount ,
418+ lastUpdated : latestDate ? latestDate . toISOString ( ) : null ,
419+ } ;
420+ } ;
421+
422+ const findNodeInTree = ( nodes : DocumentNode [ ] ) : DocumentNode | null => {
423+ for ( const node of nodes ) {
424+ if ( node . id === activeNode . id ) {
425+ return node ;
426+ }
427+ if ( node . type === 'folder' ) {
428+ const match = findNodeInTree ( node . children ) ;
429+ if ( match ) {
430+ return match ;
431+ }
432+ }
433+ }
434+ return null ;
435+ } ;
436+
437+ const folderNode = findNodeInTree ( documentTree ) ;
438+ if ( folderNode ) {
439+ return computeFromTree ( folderNode ) ;
440+ }
441+ return computeFromList ( ) ;
442+ } , [ activeNode , documentTree , items ] ) ;
443+
298444 useEffect ( ( ) => {
299445 if ( window . electronAPI ?. getAppVersion ) {
300446 window . electronAPI . getAppVersion ( ) . then ( setAppVersion ) ;
@@ -390,6 +536,18 @@ const MainApp: React.FC = () => {
390536 }
391537 } , [ addDocumentsFromFiles , setActiveNodeId , setSelectedIds , setLastClickedId , setActiveTemplateId , setDocumentView , setView ] ) ;
392538
539+ const handleImportFilesIntoFolder = useCallback ( ( files : FileList , parentId : string ) => {
540+ if ( ! files || files . length === 0 ) {
541+ return ;
542+ }
543+
544+ const targetFolder = items . find ( item => item . id === parentId && item . type === 'folder' ) ;
545+ const folderTitle = targetFolder ?. title ?. trim ( ) || 'Untitled Folder' ;
546+
547+ addLog ( 'INFO' , `User action: Import ${ files . length } file(s) into folder "${ folderTitle } ".` ) ;
548+ void handleDropFiles ( files , parentId ) ;
549+ } , [ items , addLog , handleDropFiles ] ) ;
550+
393551 useEffect ( ( ) => {
394552 const handleDragEnter = ( e : DragEvent ) => {
395553 if ( e . dataTransfer ?. types . includes ( 'Files' ) ) {
@@ -686,6 +844,20 @@ const MainApp: React.FC = () => {
686844 updateItem ( id , { title } ) ;
687845 } ;
688846
847+ const handleStartRenamingNode = useCallback ( ( id : string ) => {
848+ const target = items . find ( item => item . id === id ) ?? null ;
849+ if ( target ) {
850+ const trimmedTitle = target . title ?. trim ( ) ;
851+ const fallbackTitle = target . type === 'folder' ? 'Untitled Folder' : 'Untitled Document' ;
852+ const displayTitle = trimmedTitle && trimmedTitle . length > 0 ? trimmedTitle : fallbackTitle ;
853+ addLog ( 'INFO' , `User action: Rename ${ target . type } "${ displayTitle } ".` ) ;
854+ ensureNodeVisible ( { id : target . id , type : target . type , parentId : target . parentId ?? null } ) ;
855+ } else {
856+ addLog ( 'INFO' , 'User action: Rename item.' ) ;
857+ }
858+ setRenamingNodeId ( id ) ;
859+ } , [ items , addLog , ensureNodeVisible ] ) ;
860+
689861 const handleRenameTemplate = ( id : string , title : string ) => {
690862 updateTemplate ( id , { title } ) ;
691863 } ;
@@ -971,7 +1143,7 @@ const MainApp: React.FC = () => {
9711143 { label : 'New from Template...' , icon : DocumentDuplicateIcon , action : newFromTemplateAction , shortcut : getCommand ( 'new-from-template' ) ?. shortcutString } ,
9721144 { type : 'separator' } ,
9731145 { label : 'Format' , icon : FormatIcon , action : handleFormatDocument , disabled : ! isFormattable || currentSelection . size !== 1 , shortcut : getCommand ( 'format-document' ) ?. shortcutString } ,
974- { label : 'Rename' , icon : PencilIcon , action : ( ) => setRenamingNodeId ( nodeId ) , disabled : currentSelection . size !== 1 } ,
1146+ { label : 'Rename' , icon : PencilIcon , action : ( ) => handleStartRenamingNode ( nodeId ) , disabled : currentSelection . size !== 1 } ,
9751147 { label : 'Duplicate' , icon : DocumentDuplicateIcon , action : handleDuplicateSelection , disabled : currentSelection . size === 0 , shortcut : getCommand ( 'duplicate-item' ) ?. shortcutString } ,
9761148 { type : 'separator' } ,
9771149 { label : 'Copy Content' , icon : CopyIcon , action : ( ) => hasDocuments && handleCopyNodeContent ( selectedNodes . find ( n => n . type === 'document' ) ! . id ) , disabled : ! hasDocuments } ,
@@ -992,7 +1164,7 @@ const MainApp: React.FC = () => {
9921164 position : { x : e . clientX , y : e . clientY } ,
9931165 items : menuItems
9941166 } ) ;
995- } , [ selectedIds , items , handleNewDocument , handleNewFolder , handleDuplicateSelection , handleDeleteSelection , handleCopyNodeContent , addLog , enrichedCommands , handleOpenNewCodeFileModal , handleFormatDocument ] ) ;
1167+ } , [ selectedIds , items , handleNewDocument , handleNewFolder , handleDuplicateSelection , handleDeleteSelection , handleCopyNodeContent , addLog , enrichedCommands , handleOpenNewCodeFileModal , handleFormatDocument , handleStartRenamingNode ] ) ;
9961168
9971169
9981170 const handleSidebarMouseDown = useCallback ( ( e : React . MouseEvent ) => {
@@ -1128,7 +1300,27 @@ const MainApp: React.FC = () => {
11281300 />
11291301 ) ;
11301302 }
1131- return < WelcomeScreen onNewDocument = { ( ) => handleNewDocument ( ) } /> ;
1303+ if ( activeNode . type === 'folder' ) {
1304+ const fallbackMetrics : FolderOverviewMetrics = {
1305+ directDocumentCount : 0 ,
1306+ directFolderCount : 0 ,
1307+ totalDocumentCount : 0 ,
1308+ totalFolderCount : 0 ,
1309+ totalItemCount : 0 ,
1310+ lastUpdated : activeNode . updatedAt ,
1311+ } ;
1312+ return (
1313+ < FolderOverview
1314+ key = { activeNode . id }
1315+ folder = { activeNode }
1316+ metrics = { activeFolderMetrics ?? fallbackMetrics }
1317+ onNewDocument = { ( parentId ) => handleNewDocument ( parentId ) }
1318+ onNewSubfolder = { ( parentId ) => handleNewFolder ( parentId ) }
1319+ onImportFiles = { handleImportFilesIntoFolder }
1320+ onRenameFolder = { handleStartRenamingNode }
1321+ />
1322+ ) ;
1323+ }
11321324 }
11331325 return < WelcomeScreen onNewDocument = { ( ) => handleNewDocument ( ) } /> ;
11341326 } ;
0 commit comments