@samitouri / QOSami-HFS / commits / 5ae37b15

ux: upload: optimized layout for phone/portrait

Massimo Melina committed May 9, 2026 at 15:06 UTC 5ae37b15634e407f1a1dc8a7f4c6c794c77a0b48
3 files changed +74 -4
e2e/serial.spec.ts
+9 -1
@@ -40,7 +40,7 @@ test('upload1', async ({ page, context, browserName }, testInfo) => {
40 // can't do without cdp to slow down the upload. I tried using route.continue, but i can't send half-body keeping the full content-length, and i also cannot pass a stream (to throttle)
41 const cdpSession = await context.newCDPSession(page)
42 await cdpSession.send('Network.emulateNetworkConditions', NETWORK_PRESETS.Regular2G)
43 - await page.getByRole('button', { name: 'Edit' }).click()
43 + await openUploadRename(page)
44 const renameDialog = page.locator('.dialog-prompt')
45 const renameInput = renameDialog.getByRole('textbox')
46 await expect(renameInput).toHaveValue(fileToUpload.name) // promptDialog initializes the field value in useEffect, so we wait for that init to avoid our fill being overwritten
@@ -70,6 +70,14 @@ test('upload1', async ({ page, context, browserName }, testInfo) => {
70 }
71 })
72
73 +async function openUploadRename(page: Page) {
74 + const editButton = page.getByRole('button', { name: 'Edit' })
75 + if (await editButton.isVisible())
76 + return editButton.click()
77 + await page.locator('.upload-list').getByRole('button', { name: 'Menu' }).click()
78 + await page.getByRole('link', { name: 'Rename' }).click()
79 +}
80 +
81 const MAX_DIAGNOSTIC_LINES = 300
82
83 async function startUpload1Diagnostics(page: Page) {
frontend/src/index.scss
+23
@@ -509,6 +509,17 @@ button .icon + .label {
509 td:nth-child(2) { text-align: right; width: 0; white-space: nowrap; padding-left: 0.5em; }
510 td:nth-child(3) { padding: .2em .5em; word-break: break-word; }
511 }
512 +.upload-list-menu-button {
513 + display: none;
514 +}
515 +.upload-action-menu {
516 + margin-top: 0;
517 + padding-top: 0;
518 + border-top: none;
519 +}
520 +.upload-action-properties {
521 + margin-bottom: 1em;
522 +}
523 .nowrap { white-space: nowrap }
524
525 .login-dialog {
@@ -888,6 +899,18 @@ form label+input { margin-top: .2em; }
899 }
900 }
901
902 +@media (max-width: 30em) {
903 + .upload-list-inline-actions {
904 + display: none;
905 + }
906 + .upload-list-menu-button {
907 + display: inline-block;
908 + }
909 + .upload-list-size {
910 + display: none;
911 + }
912 +}
913 +
914 @media (max-height: 600px) and (orientation: landscape) {
915 .file-dialog {
916 .dialog-content {
frontend/src/upload.ts
+42 -3
@@ -185,9 +185,12 @@ function FileList({ entries, actions }: { entries: ToUpload[], actions: { [icon:
185 const working = e.file === uploading?.file // e is a proxy, so we check 'file' as it's a ref
186 return h(Fragment, { key: i },
187 h('tr', {},
188 - h('td', { className: 'nowrap '}, ..._.map(actions, (cb, icon) =>
189 - cb && iconBtn(icon, () => cb(entries[i]), { className: `action-${icon}` })) ),
190 - h('td', {}, formatBytes(e.file.size)),
188 + h('td', { className: 'nowrap upload-list-actions' },
189 + h('span', { className: 'upload-list-inline-actions' }, ..._.map(actions, (cb, icon) =>
190 + cb && iconBtn(icon, () => cb(entries[i]), { className: `action-${icon}` })) ),
191 + iconBtn('menu', () => openUploadActions(entries[i], actions), { className: 'upload-list-menu-button' }),
192 + ),
193 + h('td', { className: 'upload-list-size' }, formatBytes(e.file.size)),
194 h('td', {},
195 h('span', {}, e.path),
196 working && h('span', { className: 'upload-progress', title }, formatBytes(partial)),
@@ -203,6 +206,42 @@ function FileList({ entries, actions }: { entries: ToUpload[], actions: { [icon:
206 )
207 }
208
209 +function openUploadActions(rec: ToUpload, actions: { [icon:string]: null | ((rec :ToUpload) => any) }) {
210 + const { close } = newDialog({
211 + title: t`Menu`,
212 + icon: () => hIcon('menu'),
213 + Content() {
214 + return h(Fragment, {},
215 + h('dl', { className: 'file-dialog-properties upload-action-properties' },
216 + h('div', {},
217 + h('dt', {}, t`Size`),
218 + h('dd', {}, formatBytes(rec.file.size))
219 + )
220 + ),
221 + h('div', { className: 'upload-action-menu file-menu' },
222 + ..._.map(actions, (cb, icon) => cb && h('a', {
223 + href: '#',
224 + className: `action-${icon}`,
225 + onClick(ev) {
226 + ev.preventDefault()
227 + close()
228 + void cb(rec)
229 + }
230 + },
231 + hIcon(icon),
232 + h('label', {}, uploadActionLabel(icon))
233 + ))
234 + )
235 + )
236 + }
237 + })
238 +}
239 +
240 +function uploadActionLabel(icon: string) {
241 + // edit changes the upload path, so users see the familiar rename label
242 + return icon === 'edit' ? t`Rename` : t(_.capitalize(icon))
243 +}
244 +
245 function formatTime(time: number, decimals=0, length=Infinity) {
246 time /= 1000
247 const ret = [(time % 1).toFixed(decimals).slice(1)]