diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 124 |
1 files changed, 78 insertions, 46 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index cff001b..def6c5b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -824,6 +824,9 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { const [tab, setTab] = useState('files'); const [uploading, setUploading] = useState(false); const [ulState, setUlState] = useState(null); + // Directories are not index entries, so a new empty one needs a nudge + // to appear in the breadcrumb listing. + const [nodeDirs, setNodeDirs] = useState([]); const [menuOpen, setMenuOpen] = useState(null); const [isNodeAdmin, setIsNodeAdmin] = useState(false); const [needsCode, setNeedsCode] = useState(false); @@ -916,6 +919,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { if (cancelled) return; const synced = msg.entries || []; setEntries(synced); + if (msg.dirs) setNodeDirs(msg.dirs); setCached(false); cacheGroupIndex(groupId, group ? group.name : groupId, synced); }; @@ -924,6 +928,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { if (cancelled) return; const freshEntries = indexMsg.entries || []; setEntries(freshEntries); + setNodeDirs(indexMsg.dirs || []); setCached(false); setStatus('connected'); @@ -1027,7 +1032,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { for (let i = 0; i < totalChunks; i++) { const slice = file.slice(i * UPLOAD_CHUNK_SIZE, (i + 1) * UPLOAD_CHUNK_SIZE); const buf = new Uint8Array(await slice.arrayBuffer()); - await transport.uploadChunk(file.name, i, totalChunks, buf); + await transport.uploadChunk(file.name, i, totalChunks, buf, currentPath); // Bytes actually acknowledged by the node, not bytes read locally. setUlState(prev => prev && { ...prev, sent: Math.min(file.size, (i + 1) * UPLOAD_CHUNK_SIZE) }); @@ -1038,13 +1043,29 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { await new Promise(r => setTimeout(r, 2500)); const indexMsg = await transport.fetchIndex(); if (indexMsg.entries) setEntries(indexMsg.entries); + if (indexMsg.dirs) setNodeDirs(indexMsg.dirs); } catch (err) { setError(err.message); } finally { setUploading(false); setUlState(null); } - }, []); + }, [currentPath]); + + const makeDirectory = useCallback(async () => { + const transport = transportRef.current; + if (!transport || !transport.connected) return; + const name = prompt(t('group.mkdir_prompt')); + if (!name || !name.trim()) return; + try { + await transport.createDirectory(currentPath, name.trim()); + const indexMsg = await transport.fetchIndex(); + if (indexMsg.entries) setEntries(indexMsg.entries); + if (indexMsg.dirs) setNodeDirs(indexMsg.dirs); + } catch (err) { + setError(err.message); + } + }, [currentPath]); const deleteFile = useCallback(async (entry) => { const transport = transportRef.current; @@ -1104,6 +1125,15 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { return sortAsc ? cmp : -cmp; }); + // The node's own listing, so an empty folder is visible, plus anything implied + // by a file path in case the two ever disagree. + for (const d of nodeDirs) { + if (!currentPath && !d.includes('/')) dirs.add(d); + else if (currentPath && d.startsWith(currentPath + '/')) { + const rest = d.slice(currentPath.length + 1); + if (!rest.includes('/')) dirs.add(rest); + } + } const subdirs = [...dirs].sort(); const baseLabel = { @@ -1205,6 +1235,8 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { <input type="file" style="display:none" onChange=${uploadFile} disabled=${uploading} /> </label> + <button class="admin-btn" style="margin-right:8px" onClick=${makeDirectory} + disabled=${uploading}>${t('group.mkdir')}</button> <div class="breadcrumbs"> <a class="crumb" onClick=${() => setCurrentPath('')}>/</a> ${breadcrumbs.map((seg, i) => { @@ -1578,49 +1610,6 @@ function MembersPanel({ groupId, group, token, transportRef, gekRef, return html` <div class="members-panel"> - ${isAdmin && html` - <form class="invite-form" onSubmit=${doInvite}> - <h4>${t('members.invite_title')}</h4> - ${error && html`<p class="error-msg">${error}</p>`} - ${inviteCode && html` - <div class="success-msg" style="margin-bottom:8px"> - <p>${t('members.invite_code_ready', { user: inviteCode.username })}</p> - <p style="font-family:monospace;font-size:1.4em;letter-spacing:2px;margin:6px 0"> - ${inviteCode.code} - </p> - <p>${t('members.invite_code_hint')}</p> - </div> - `} - <div style="display:flex;gap:8px"> - <input type="text" placeholder="${t('members.username_placeholder')}" - value=${inviteUser} onInput=${e => setInviteUser(e.target.value)} required /> - <button class="admin-btn" type="submit" disabled=${inviting}> - ${inviting ? '...' : t('members.invite_btn')} - </button> - </div> - </form> - `} - <table class="admin-table"> - <thead> - <tr> - <th>${t('admin.col_username')}</th> - <th>${t('members.group_role')}</th> - </tr> - </thead> - <tbody> - ${members.map(m => html` - <tr key=${m.user_id}> - <td>${m.username}</td> - <td> - ${m.user_id === adminId - ? html`<span class="badge" style="background:var(--accent);color:var(--accent-text)">${t('members.owner')}</span>` - : html`<span class="badge">${t('members.member')}</span>` - } - </td> - </tr> - `)} - </tbody> - </table> ${isNodeAdmin && html` <form class="invite-form" onSubmit=${doPair}> <h4>${t('members.pair_title')}</h4> @@ -2443,7 +2432,50 @@ function AdminPage({ token }) { value=${userSearch} onInput=${e => { setUserSearch(e.target.value); loadUsers(e.target.value); }} /> <span class="settings-value">${usersTotal} total</span> </div> - <table class="admin-table"> + ${isAdmin && html` + <form class="invite-form" onSubmit=${doInvite}> + <h4>${t('members.invite_title')}</h4> + ${error && html`<p class="error-msg">${error}</p>`} + ${inviteCode && html` + <div class="success-msg" style="margin-bottom:8px"> + <p>${t('members.invite_code_ready', { user: inviteCode.username })}</p> + <p style="font-family:monospace;font-size:1.4em;letter-spacing:2px;margin:6px 0"> + ${inviteCode.code} + </p> + <p>${t('members.invite_code_hint')}</p> + </div> + `} + <div style="display:flex;gap:8px"> + <input type="text" placeholder="${t('members.username_placeholder')}" + value=${inviteUser} onInput=${e => setInviteUser(e.target.value)} required /> + <button class="admin-btn" type="submit" disabled=${inviting}> + ${inviting ? '...' : t('members.invite_btn')} + </button> + </div> + </form> + `} + <table class="admin-table"> + <thead> + <tr> + <th>${t('admin.col_username')}</th> + <th>${t('members.group_role')}</th> + </tr> + </thead> + <tbody> + ${members.map(m => html` + <tr key=${m.user_id}> + <td>${m.username}</td> + <td> + ${m.user_id === adminId + ? html`<span class="badge" style="background:var(--accent);color:var(--accent-text)">${t('members.owner')}</span>` + : html`<span class="badge">${t('members.member')}</span>` + } + </td> + </tr> + `)} + </tbody> + </table> + <table class="admin-table"> <thead><tr> <th>${t('admin.col_username')}</th> <th>${t('admin.col_role')}</th> |