1- import type { CloudCannonJavaScriptV1APIFile } from '@cloudcannon/javascript-api' ;
2- import { AlertCircle , ChevronDown , ChevronRight , File , Folder , RefreshCw } from 'lucide-react' ;
3- import { useMemo , useState } from 'react' ;
1+ import type {
2+ CloudCannonJavaScriptV1APICollection ,
3+ CloudCannonJavaScriptV1APIFile ,
4+ } from '@cloudcannon/javascript-api' ;
5+ import { AlertCircle , RefreshCw } from 'lucide-react' ;
6+ import { CollectionBrowser } from './CollectionBrowser' ;
7+ import { FileTree } from './FileTree' ;
48
59interface FileBrowserProps {
610 files : CloudCannonJavaScriptV1APIFile [ ] ;
11+ collections : CloudCannonJavaScriptV1APICollection [ ] ;
712 selectedFile : CloudCannonJavaScriptV1APIFile | null ;
813 onFileSelect : ( file : CloudCannonJavaScriptV1APIFile ) => void ;
914 isLoading : boolean ;
1015 error : string | null ;
1116 onRefresh : ( ) => void ;
1217}
1318
14- interface FileTreeNode {
15- name : string ;
16- path : string ;
17- type : 'file' | 'directory' ;
18- children ?: Record < string , FileTreeNode > ;
19- file ?: CloudCannonJavaScriptV1APIFile ;
20- }
21-
22- function buildFileTree ( files : CloudCannonJavaScriptV1APIFile [ ] ) : FileTreeNode [ ] {
23- const root : Record < string , FileTreeNode > = { } ;
24-
25- files . forEach ( ( file ) => {
26- const parts = file . path . split ( '/' ) . filter ( Boolean ) ;
27- let current = root ;
28-
29- parts . forEach ( ( part , index ) => {
30- const isFile = index === parts . length - 1 ;
31- const currentPath = parts . slice ( 0 , index + 1 ) . join ( '/' ) ;
32-
33- if ( ! current [ part ] ) {
34- current [ part ] = {
35- name : part ,
36- path : currentPath ,
37- type : isFile ? 'file' : 'directory' ,
38- children : isFile ? undefined : { } ,
39- file : isFile ? file : undefined ,
40- } ;
41- }
42-
43- if ( ! isFile ) {
44- current = current [ part ] . children as Record < string , FileTreeNode > ;
45- }
46- } ) ;
47- } ) ;
48-
49- // Convert to array and sort
50- const sortNodes = ( nodes : Record < string , FileTreeNode > ) : FileTreeNode [ ] => {
51- return Object . values ( nodes )
52- . map ( ( node ) => ( {
53- ...node ,
54- children : node . children ? sortNodes ( node . children ) : { } ,
55- } ) )
56- . sort ( ( a , b ) => {
57- // Directories first, then files
58- if ( a . type !== b . type ) {
59- return a . type === 'directory' ? - 1 : 1 ;
60- }
61- return a . name . localeCompare ( b . name ) ;
62- } ) ;
63- } ;
64-
65- return sortNodes ( root ) ;
66- }
67-
68- interface FileTreeItemProps {
69- node : FileTreeNode ;
70- selectedFile : CloudCannonJavaScriptV1APIFile | null ;
71- onFileSelect : ( file : CloudCannonJavaScriptV1APIFile ) => void ;
72- level : number ;
73- }
74-
75- function FileTreeItem ( { node, selectedFile, onFileSelect, level } : FileTreeItemProps ) {
76- const [ isExpanded , setIsExpanded ] = useState ( level < 2 ) ; // Auto-expand first two levels
77- const isSelected = selectedFile ?. path === node . path ;
78-
79- const handleClick = ( ) => {
80- if ( node . type === 'directory' ) {
81- setIsExpanded ( ! isExpanded ) ;
82- } else if ( node . file ) {
83- onFileSelect ( node . file ) ;
84- }
85- } ;
86-
87- const getFileIcon = ( _filename : string ) => {
88- // const ext = filename.split('.').pop()?.toLowerCase();
89- // You could expand this with more specific file type icons
90- return < File size = { 16 } /> ;
91- } ;
92-
93- return (
94- < div >
95- < div
96- className = { `
97- flex items-center px-2 py-1 cursor-pointer hover:bg-gray-100
98- ${ isSelected ? 'bg-blue-100 border-r-2 border-blue-500' : '' }
99- ` }
100- style = { { paddingLeft : `${ level * 16 + 8 } px` } }
101- onClick = { handleClick }
102- onKeyDown = { ( e ) => {
103- if ( e . key === 'Enter' || e . key === ' ' ) {
104- handleClick ( ) ;
105- }
106- } }
107- >
108- { node . type === 'directory' && (
109- < span className = "mr-1 text-gray-400" >
110- { isExpanded ? < ChevronDown size = { 16 } /> : < ChevronRight size = { 16 } /> }
111- </ span >
112- ) }
113-
114- < span className = "mr-2 text-gray-600" >
115- { node . type === 'directory' ? < Folder size = { 16 } /> : getFileIcon ( node . name ) }
116- </ span >
117-
118- < span className = { `text-sm ${ isSelected ? 'font-medium text-blue-700' : 'text-gray-700' } ` } >
119- { node . name }
120- </ span >
121- </ div >
122-
123- { node . type === 'directory' && isExpanded && node . children && (
124- < div >
125- { Object . values ( node . children ) . map ( ( child ) => (
126- < FileTreeItem
127- key = { child . path }
128- node = { child }
129- selectedFile = { selectedFile }
130- onFileSelect = { onFileSelect }
131- level = { level + 1 }
132- />
133- ) ) }
134- </ div >
135- ) }
136- </ div >
137- ) ;
138- }
139-
14019export function FileBrowser ( {
14120 files,
21+ collections,
14222 selectedFile,
14323 onFileSelect,
14424 isLoading,
14525 error,
14626 onRefresh,
14727} : FileBrowserProps ) {
148- const fileTree = useMemo ( ( ) => buildFileTree ( files ) , [ files ] ) ;
149-
15028 if ( error ) {
15129 return (
15230 < div className = "p-4" >
@@ -168,8 +46,19 @@ export function FileBrowser({
16846
16947 return (
17048 < div className = "h-full flex flex-col bg-white border-r border-gray-200" >
49+ { collections . map ( ( collection ) => (
50+ < CollectionBrowser
51+ key = { collection . collectionKey }
52+ collection = { collection }
53+ selectedFile = { selectedFile }
54+ onFileSelect = { onFileSelect }
55+ isLoading = { isLoading }
56+ onRefresh = { onRefresh }
57+ />
58+ ) ) }
59+
17160 { /* Header */ }
172- < div className = "flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50" >
61+ < div className = "flex items-center justify-between p-3 border-b border-t border- gray-200 bg-gray-50" >
17362 < h2 className = "font-medium text-gray-700" > Files</ h2 >
17463 < button
17564 type = "button"
@@ -183,30 +72,12 @@ export function FileBrowser({
18372 </ div >
18473
18574 { /* File tree */ }
186- < div className = "flex-1 overflow-auto" >
187- { isLoading ? (
188- < div className = "p-4 text-center text-gray-500" >
189- < RefreshCw size = { 20 } className = "animate-spin mx-auto mb-2" />
190- < p className = "text-sm" > Loading files...</ p >
191- </ div >
192- ) : fileTree . length === 0 ? (
193- < div className = "p-4 text-center text-gray-500" >
194- < p className = "text-sm" > No files found</ p >
195- </ div >
196- ) : (
197- < div className = "py-2" >
198- { fileTree . map ( ( node ) => (
199- < FileTreeItem
200- key = { node . path }
201- node = { node }
202- selectedFile = { selectedFile }
203- onFileSelect = { onFileSelect }
204- level = { 0 }
205- />
206- ) ) }
207- </ div >
208- ) }
209- </ div >
75+ < FileTree
76+ files = { files }
77+ selectedFile = { selectedFile }
78+ onFileSelect = { onFileSelect }
79+ isLoading = { isLoading }
80+ />
21081 </ div >
21182 ) ;
21283}
0 commit comments