@@ -109,10 +109,17 @@ function getBreadcrumbs(baseUrl: string, displayBaseUrl: string, currentUrl: str
109109 for ( let i = 0 ; i < parts . length ; i ++ ) {
110110 const isLast = i === parts . length - 1 ;
111111 const seg = parts [ i ] ! ;
112+ const label = ( ( ) => {
113+ try {
114+ return decodeURIComponent ( seg ) ;
115+ } catch {
116+ return seg ;
117+ }
118+ } ) ( ) ;
112119 // Containers always end in '/', files do not. We don't know which intermediate segments are containers,
113120 // but for this virtual pod UI, path segments are navigable as containers.
114121 const nextUrl = acc + seg + ( isLast ? ( isContainer ? '/' : '' ) : '/' ) ;
115- crumbs . push ( { label : seg , url : nextUrl , isCurrent : isLast } ) ;
122+ crumbs . push ( { label, url : nextUrl , isCurrent : isLast } ) ;
116123 acc = acc + seg + '/' ;
117124 }
118125
@@ -199,6 +206,19 @@ function DropdownMenu(props: {
199206 ) ;
200207}
201208
209+ function ensureTrailingSlash ( url : string ) : string {
210+ return url . endsWith ( '/' ) ? url : `${ url } /` ;
211+ }
212+
213+ function makeChildUrl ( parentContainerUrl : string , rawName : string , isContainer : boolean ) : string | null {
214+ const name = rawName . trim ( ) ;
215+ if ( ! name ) return null ;
216+ if ( name . includes ( '/' ) ) return null ;
217+ const encoded = encodeURIComponent ( name ) ;
218+ const base = ensureTrailingSlash ( parentContainerUrl ) ;
219+ return base + encoded + ( isContainer ? '/' : '' ) ;
220+ }
221+
202222// Metadata for the Schemas view: descriptions and Solid doc links
203223const SCHEMA_META : Array < { file : string ; name : string ; description : string ; fields : string [ ] ; solidHref ?: string } > = [
204224 { file : 'iri.json' , name : 'IRI' , description : 'Internationalized Resource Identifier (URI). Used for @id, URLs, and vocabulary terms.' , fields : [ ] , solidHref : 'https://solidproject.org/TR/protocol' } ,
@@ -366,7 +386,15 @@ interface FileItemProps {
366386const FileItem : React . FC < FileItemProps > = ( { url, onNavigate } ) => {
367387 const row = useRow ( 'resources' , url ) as ResourceRow ;
368388 const isDir = row ?. type === 'Container' ;
369- const name = url . split ( '/' ) . filter ( Boolean ) . pop ( ) ;
389+ const rawName = url . split ( '/' ) . filter ( Boolean ) . pop ( ) ;
390+ const name = ( ( ) => {
391+ if ( ! rawName ) return rawName ;
392+ try {
393+ return decodeURIComponent ( rawName ) ;
394+ } catch {
395+ return rawName ;
396+ }
397+ } ) ( ) ;
370398
371399 return (
372400 < div
@@ -577,6 +605,8 @@ export default function App() {
577605 const [ newFileOpen , setNewFileOpen ] = useState ( false ) ;
578606 const [ newFileName , setNewFileName ] = useState ( '' ) ;
579607 const [ newFileContent , setNewFileContent ] = useState ( '' ) ;
608+ const [ newFolderOpen , setNewFolderOpen ] = useState ( false ) ;
609+ const [ newFolderName , setNewFolderName ] = useState ( '' ) ;
580610 const uploadImageInputRef = useRef < HTMLInputElement > ( null ) ;
581611 const importFileInputRef = useRef < HTMLInputElement > ( null ) ;
582612 const [ copyStatus , setCopyStatus ] = useState < 'success' | 'error' | null > ( null ) ;
@@ -609,6 +639,7 @@ export default function App() {
609639 const row = useRow ( 'resources' , currentUrl , store ?? undefined ) as ResourceRow | undefined ;
610640 const isContainer = row ?. type === 'Container' ;
611641 const parentUrl = row ?. parentId ;
642+ const currentContainerUrl = isContainer ? currentUrl : ( parentUrl ?? BASE_URL ) ;
612643
613644 // Persisted preference (via TinyBase LocalStorage persister).
614645 // Keep this in React state so the agent hint bar can be hidden "permanently".
@@ -631,24 +662,59 @@ export default function App() {
631662 setNewFileContent ( '' ) ;
632663 } ;
633664
665+ const openNewFolderDialog = ( ) => {
666+ setNewFolderName ( '' ) ;
667+ setNewFolderOpen ( true ) ;
668+ } ;
669+
670+ const closeNewFolderDialog = ( ) => {
671+ setNewFolderOpen ( false ) ;
672+ setNewFolderName ( '' ) ;
673+ } ;
674+
634675 const submitNewFile = ( ) => {
635676 const name = newFileName . trim ( ) ;
636677 if ( ! name || ! pod ) return ;
637- pod . handleRequest ( `${ currentUrl } ${ name } ` , {
678+ const url = makeChildUrl ( currentContainerUrl , name , false ) ;
679+ if ( ! url ) {
680+ alert ( 'Invalid filename. Use a name without "/" characters.' ) ;
681+ return ;
682+ }
683+ pod . handleRequest ( url , {
638684 method : 'PUT' ,
639685 body : newFileContent || '' ,
640686 headers : { 'Content-Type' : 'text/plain' }
641687 } ) ;
642688 closeNewFileDialog ( ) ;
643689 } ;
644690
691+ const submitNewFolder = ( ) => {
692+ const name = newFolderName . trim ( ) ;
693+ if ( ! name || ! pod ) return ;
694+ const url = makeChildUrl ( currentContainerUrl , name , true ) ;
695+ if ( ! url ) {
696+ alert ( 'Invalid folder name. Use a name without "/" characters.' ) ;
697+ return ;
698+ }
699+ pod . handleRequest ( url , {
700+ method : 'PUT' ,
701+ headers : { 'Content-Type' : 'text/turtle' } ,
702+ } ) ;
703+ closeNewFolderDialog ( ) ;
704+ } ;
705+
645706 const onUploadImageSelect = async ( e : React . ChangeEvent < HTMLInputElement > ) => {
646707 const file = e . target . files ?. [ 0 ] ;
647708 if ( ! file || ! file . type . startsWith ( 'image/' ) || ! pod ) return ;
648709 try {
649710 const base64 = await readFileAsBase64 ( file ) ;
650711 const name = file . name ;
651- pod . handleRequest ( `${ currentUrl } ${ name } ` , {
712+ const url = makeChildUrl ( currentContainerUrl , name , false ) ;
713+ if ( ! url ) {
714+ alert ( 'Invalid filename.' ) ;
715+ return ;
716+ }
717+ pod . handleRequest ( url , {
652718 method : 'PUT' ,
653719 body : base64 ,
654720 headers : { 'Content-Type' : file . type }
@@ -698,15 +764,7 @@ export default function App() {
698764 return < div style = { styles . loading } > Loading…</ div > ;
699765 }
700766
701- const createFolder = ( ) => {
702- const name = prompt ( "Folder Name (e.g., notes):" ) ;
703- if ( name ) {
704- pod . handleRequest ( `${ currentUrl } ${ name } /` , {
705- method : 'PUT' ,
706- headers : { 'Content-Type' : 'text/turtle' }
707- } ) ;
708- }
709- } ;
767+ const createFolder = ( ) => openNewFolderDialog ( ) ;
710768
711769 return (
712770 < Provider store = { store } indexes = { indexes } >
@@ -900,6 +958,34 @@ export default function App() {
900958 </ div >
901959 ) }
902960
961+ { /* New Folder dialog */ }
962+ { newFolderOpen && (
963+ < div style = { styles . dialogOverlay } onClick = { closeNewFolderDialog } >
964+ < div style = { styles . dialog } onClick = { e => e . stopPropagation ( ) } >
965+ < h3 style = { styles . dialogTitle } > New Folder</ h3 >
966+ < label style = { styles . dialogLabel } > Folder name</ label >
967+ < input
968+ type = "text"
969+ placeholder = "e.g. notes"
970+ value = { newFolderName }
971+ onChange = { e => setNewFolderName ( e . target . value ) }
972+ style = { styles . dialogInput }
973+ autoFocus
974+ />
975+ < div style = { styles . dialogActions } >
976+ < button style = { styles . dialogBtnCancel } onClick = { closeNewFolderDialog } > Cancel</ button >
977+ < button
978+ style = { { ...styles . dialogBtnSubmit , ...( ! newFolderName . trim ( ) ? styles . dialogBtnSubmitDisabled : { } ) } }
979+ onClick = { submitNewFolder }
980+ disabled = { ! newFolderName . trim ( ) }
981+ >
982+ Create
983+ </ button >
984+ </ div >
985+ </ div >
986+ </ div >
987+ ) }
988+
903989 < div style = { styles . dataHeader } >
904990 { /* Align breadcrumbs with explorer (not the controls column). */ }
905991 < div style = { styles . breadcrumbSpacer } aria-hidden />
0 commit comments