upload: rename button
Massimo Melina committed
Jan 28, 2024 at 00:20 UTC
78b3c6e679565ca7db9425014730366f79d750a4
4 files changed
+41
-28
frontend/src/components.ts
+11
-10
@@ -4,6 +4,8 @@ import { getHFS, hfsEvent, hIcon, prefix } from './misc'
4
import { ButtonHTMLAttributes, ChangeEvent, createElement as h, CSSProperties, FC, forwardRef, Fragment,
5
HTMLAttributes, InputHTMLAttributes, isValidElement, MouseEventHandler, ReactNode, SelectHTMLAttributes,
6
useMemo, useState, ComponentPropsWithoutRef } from 'react'
7
+import _ from 'lodash'
8
+import { t } from './i18n'
9
10
export function Spinner(props: any) {
11
return hIcon('spinner', { className:'spinner', ...props })
@@ -86,17 +88,16 @@ export function CustomCode({ name, props, ifEmpty }: { name: string, props?: any
88
return children.length || !ifEmpty ? h(Fragment, {}, children) : h(ifEmpty)
89
}
90
89
-interface IconBtnOptions extends ButtonHTMLAttributes<any> { small?: boolean, style?: any }
90
-export function iconBtn(icon: string, onClick: MouseEventHandler, { small=true, style={}, ...props }: IconBtnOptions={}) {
91
+interface IconBtnOptions extends ButtonHTMLAttributes<any> { small?: boolean, style?: any, title?: string }
92
+export function iconBtn(icon: string, onClick: MouseEventHandler, { title, small=true, style={}, ...props }: IconBtnOptions={}) {
93
return h('button', {
92
- onClick,
93
- ...props,
94
- ...small && {
95
- style: { padding: '.1em', width: 35, height: 30, ...style }
96
- }
97
- },
98
- icon.length > 1 ? hIcon(icon) : icon
99
- )
94
+ title: title ?? t(_.capitalize(icon)),
95
+ onClick,
96
+ ...props,
97
+ ...small && {
98
+ style: { padding: '.1em', width: 35, height: 30, ...style }
99
+ }
100
+ }, icon.length > 1 ? hIcon(icon) : icon )
101
}
102
103
export interface BtnProps extends ComponentPropsWithoutRef<"button"> {
frontend/src/dialog.ts
+1
-1
@@ -12,7 +12,7 @@ export * from '@hfs/shared/dialogs'
12
_.merge(dialogsDefaults, { closableProps: { 'aria-label': t`Close` } })
13
14
interface PromptOptions extends Partial<DialogOptions> { def?:string, type?:string, trim?: boolean, helperText?: ReactNode }
15
-export async function promptDialog(msg: string, { def, type, helperText, trim=true, ...rest }:PromptOptions={}) : Promise<string | null> {
15
+export async function promptDialog(msg: string, { def, type, helperText, trim=true, ...rest }:PromptOptions={}) : Promise<string | undefined> {
16
const textarea = type === 'textarea' && type
17
return new Promise(resolve => newDialog({
18
className: 'dialog-prompt',
frontend/src/upload.ts
+28
-16
@@ -3,8 +3,7 @@
3
import { createElement as h, DragEvent, Fragment, useMemo, CSSProperties } from 'react'
4
import { Checkbox, Flex, FlexV, iconBtn } from './components'
5
import { basename, closeDialog, formatBytes, formatPerc, hIcon, isMobile, newDialog, prefix, selectFiles, working,
6
- HTTP_CONFLICT, HTTP_PAYLOAD_TOO_LARGE, formatSpeed
7
-} from './misc'
6
+ HTTP_CONFLICT, HTTP_PAYLOAD_TOO_LARGE, formatSpeed, dirname } from './misc'
7
import _ from 'lodash'
8
import { proxy, ref, subscribe, useSnapshot } from 'valtio'
9
import { alertDialog, confirmDialog, promptDialog } from './dialog'
@@ -15,7 +14,7 @@ import { Link } from 'react-router-dom'
14
import { t } from './i18n'
15
import { subscribeKey } from 'valtio/utils'
16
18
-interface ToUpload { file: File, comment?: string }
17
+interface ToUpload { file: File, comment?: string, name?: string }
18
export const uploadState = proxy<{
19
done: number
20
doneByte: number
@@ -137,12 +136,20 @@ export function showUpload() {
136
),
137
),
138
h(FilesList, {
140
- entries: adding,
139
+ entries: uploadState.adding,
140
actions: {
142
- delete: rec => _.remove(uploadState.adding, { file: rec.file }),
143
- comment: !props?.can_comment ? null
144
- : (rec => inputComment(basename(rec.file.name), rec.comment)
145
- .then(s => _.find(uploadState.adding, { file: rec.file })!.comment = s || undefined)),
141
+ delete: rec => _.remove(uploadState.adding, rec),
142
+ async comment(rec){
143
+ if (!props?.can_comment) return
144
+ const s = await inputComment(basename(rec.file.name), rec.comment)
145
+ if (s === undefined) return
146
+ rec.comment = s || undefined
147
+ },
148
+ async edit(rec) {
149
+ const s = await promptDialog(t('upload_name', "Upload with new name"), { def: rec.file.name })
150
+ if (!s) return
151
+ rec.name = s
152
+ },
153
},
154
}),
155
h(UploadStatus, { margin: '.5em 0' }),
@@ -165,7 +172,7 @@ export function showUpload() {
172
h('div', { key: q.to },
173
h(Link, { to: q.to, onClick: close }, t`Destination`, ' ', decodeURI(q.to)),
174
h(FilesList, {
168
- entries: Array.from(q.entries),
175
+ entries: uploadState.qs[idx].entries,
176
actions: {
177
delete: f => {
178
if (f === uploadState.uploading)
@@ -194,18 +201,20 @@ function path(f: File) {
201
return (f.webkitRelativePath || f.name).replaceAll('//','/')
202
}
203
197
-function FilesList({ entries, actions }: { entries: Readonly<ToUpload[]>, actions: { [icon:string]: null | ((rec :ToUpload) => any) } }) {
204
+function FilesList({ entries, actions }: { entries: ToUpload[], actions: { [icon:string]: null | ((rec :ToUpload) => any) } }) {
205
const { uploading, progress } = useSnapshot(uploadState)
199
- return !entries.length ? null : h('table', { className: 'upload-list', width: '100%' },
206
+ const snapEntries = useSnapshot(entries)
207
+ return !snapEntries.length ? null : h('table', { className: 'upload-list', width: '100%' },
208
h('tbody', {},
201
- entries.map((e, i) => {
209
+ snapEntries.map((e, i) => {
210
const working = e === uploading
211
return h(Fragment, { key: i },
212
h('tr', {},
205
- h('td', { className: 'nowrap '}, ..._.map(actions, (cb, icon) => cb && iconBtn(icon, () => cb(e))) ),
213
+ h('td', { className: 'nowrap '}, ..._.map(actions, (cb, icon) =>
214
+ cb && iconBtn(icon, () => cb(entries[i]))) ),
215
h('td', {}, formatBytes(e.file.size)),
216
h('td', { className: working ? 'ani-working' : undefined },
208
- path(e.file),
217
+ e.name || path(entries[i].file),
218
working && h('span', { className: 'upload-progress' }, formatPerc(progress))
219
),
220
),
@@ -299,7 +308,10 @@ async function startUpload(toUpload: ToUpload, to: string, resume=0) {
308
bytesSent += e.loaded - lastProgress
309
lastProgress = e.loaded
310
}
302
- req.open('PUT', to + encodeURIComponent(path(toUpload.file)) + '?' + new URLSearchParams({
311
+ let uploadPath = path(toUpload.file)
312
+ if (toUpload.name)
313
+ uploadPath = prefix('', dirname(uploadPath), '/') + toUpload.name
314
+ req.open('PUT', to + encodeURIComponent(uploadPath) + '?' + new URLSearchParams({
315
notificationChannel,
316
...resume && { resume: String(resume) },
317
...toUpload.comment && { comment: toUpload.comment },
@@ -314,7 +326,7 @@ async function startUpload(toUpload: ToUpload, to: string, resume=0) {
326
const {uploading} = uploadState
327
if (!uploading) return
328
if (name === 'upload.resumable') {
317
- const size = data?.[path(uploading.file)]
329
+ const size = data?.[path(uploading.file)] //TODO use toUpload?
330
if (!size || size > toUpload.file.size) return
331
const {expires} = data
332
const timeout = typeof expires !== 'number' ? 0
src/cross.ts
+1
-1
@@ -206,7 +206,7 @@ export function basename(path: string) {
206
}
207
208
export function dirname(path: string) {
209
- return path.slice(0, path.lastIndexOf('/', path.length - 1))
209
+ return path.slice(0, Math.max(0, path.lastIndexOf('/', path.length - 1)))
210
}
211
212
export function tryJson(s?: string, except?: (s?: string) => unknown) {