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) {