Skip to content

Commit 7731fb4

Browse files
committed
fix path encoding issue (at UI level)
1 parent d0f5e61 commit 7731fb4

File tree

1 file changed

+99
-13
lines changed

1 file changed

+99
-13
lines changed

src/App.tsx

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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
203223
const 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 {
366386
const 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

Comments
 (0)