admin: "create folder" button in file-picker
Massimo Melina committed
Oct 27, 2023 at 20:09 UTC
bb8aed74b1516c67ed477fdbd90e4a70d5271330
3 files changed
+35
-13
admin/src/FilePicker.ts
+18
-4
@@ -14,11 +14,12 @@ import {
14
TextField,
15
Typography
16
} from '@mui/material'
17
-import { enforceFinal, formatBytes, isWindowsDrive, spinner, Center, err2msg, basename } from './misc'
18
-import { ArrowUpward, VerticalAlignTop } from '@mui/icons-material'
17
+import { enforceFinal, formatBytes, isWindowsDrive, spinner, Center, err2msg, basename, IconBtn, Flex } from './misc'
18
+import { ArrowUpward, CreateNewFolder, VerticalAlignTop } from '@mui/icons-material'
19
import { StringField } from '@hfs/mui-grid-form'
20
import { FileIcon, FolderIcon } from './VfsTree'
21
import { FixedSizeList } from 'react-window'
22
+import { promptDialog } from './dialog'
23
24
export interface DirEntry { n:string, s?:number, m?:string, c?:string, k?:'d' }
25
@@ -42,7 +43,7 @@ export default function FilePicker({ onSelect, multiple=true, files=true, folder
43
}
44
}).finally(() => setReady(true))
45
}, [from])
45
- const { list, error, connecting } = useApiList<DirEntry>(ready && 'get_ls', { path: cwd, files, fileMask })
46
+ const { list, error, connecting, reload } = useApiList<DirEntry>(ready && 'get_ls', { path: cwd, files, fileMask })
47
useEffect(() => {
48
setSel([])
49
setFilter('')
@@ -143,7 +144,7 @@ export default function FilePicker({ onSelect, multiple=true, files=true, folder
144
}
145
})
146
),
146
- h(Box, { display:'flex', gap: 1 },
147
+ h(Flex, {},
148
(multiple || folders || !files) && h(Button, {
149
variant: 'contained',
150
disabled: !cwd || !folders && !sel.length && files,
@@ -153,6 +154,7 @@ export default function FilePicker({ onSelect, multiple=true, files=true, folder
154
}
155
}, files && (sel.length || !folders) ? `Select (${sel.length})` : `Select this folder`),
156
h(TextField, {
157
+ size: 'small',
158
value: filter,
159
label: `Filter results (${filteredList.length}${filteredList.length < list.length ? '/'+list.length : ''})`,
160
onChange(ev) {
@@ -160,6 +162,18 @@ export default function FilePicker({ onSelect, multiple=true, files=true, folder
162
},
163
sx: { flex: 1 },
164
}),
165
+ h(IconBtn, {
166
+ icon: CreateNewFolder,
167
+ doneMessage: true,
168
+ title: "Create folder",
169
+ sx: { mt: '5px' },
170
+ async onClick() {
171
+ const s = await promptDialog("New folder name")
172
+ if (!s) return false
173
+ await apiCall('mkdir', { path: `${cwd}/${s}` })
174
+ reload()
175
+ }
176
+ }),
177
),
178
)
179
)
admin/src/mui.ts
+11
-8
@@ -93,11 +93,12 @@ interface IconBtnProps extends Omit<IconButtonProps, 'disabled'|'title'|'onClick
93
progress?: boolean | number
94
link?: string
95
confirm?: string
96
+ doneMessage?: boolean | string // displayed only if the result of onClick !== false
97
tooltipProps?: Partial<TooltipProps>
98
onClick: (...args: Parameters<NonNullable<IconButtonProps['onClick']>>) => Promisable<any>
99
}
100
100
-export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, link, tooltipProps, confirm, ...rest }: IconBtnProps, ref: any) => {
101
+export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, link, tooltipProps, confirm, doneMessage, sx, ...rest }: IconBtnProps, ref: any) => {
102
const [loading, setLoading] = useStateMounted(false)
103
if (typeof disabled === 'string')
104
title = disabled
@@ -107,12 +108,13 @@ export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, l
108
ref,
109
disabled: Boolean(loading || progress || disabled),
110
...rest,
111
+ sx: { height: 'fit-content', ...sx },
112
async onClick(...args) {
113
if (confirm && !await confirmDialog(confirm)) return
114
const ret = onClick?.apply(this,args)
115
if (ret && ret instanceof Promise) {
116
setLoading(true)
115
- ret.catch(alertDialog).finally(()=> setLoading(false))
117
+ ret.then(x => x !== false && execDoneMessage(doneMessage), alertDialog).finally(()=> setLoading(false))
118
}
119
}
120
},
@@ -135,7 +137,7 @@ interface BtnProps extends Omit<LoadingButtonProps,'disabled'|'title'|'onClick'>
137
progress?: boolean | number
138
link?: string
139
confirm?: boolean | string
138
- doneMessage?: boolean | string
140
+ doneMessage?: boolean | string // displayed only if the result of onClick !== false
141
tooltipProps?: TooltipProps
142
onClick: (...args: Parameters<NonNullable<ButtonProps['onClick']>>) => Promisable<any>
143
}
@@ -161,11 +163,7 @@ export function Btn({ icon, title, onClick, disabled, progress, link, tooltipPro
163
const ret = onClick?.apply(this,args)
164
if (ret && ret instanceof Promise) {
165
setLoading(true)
164
- ret.then(async res => {
165
- if (doneMessage)
166
- toast(doneMessage === true ? "Operation completed" : doneMessage, 'success')
167
- return res
168
- }, alertDialog)
166
+ ret.then(x => x !== false && execDoneMessage(doneMessage), alertDialog)
167
.finally(()=> setLoading(false))
168
}
169
}
@@ -175,6 +173,11 @@ export function Btn({ icon, title, onClick, disabled, progress, link, tooltipPro
173
return ret
174
}
175
176
+function execDoneMessage(msg: boolean | string | undefined) {
177
+ if (msg)
178
+ toast(msg === true ? "Operation completed" : msg, 'success')
179
+}
180
+
181
export function iconTooltip(icon: SvgIconComponent, tooltip: ReactNode, sx?: SxProps) {
182
return h(Tooltip, { title: tooltip, children: h(icon, { sx: { verticalAlign: 'bottom', ...sx } }) })
183
}
src/api.vfs.ts
+6
-1
@@ -3,7 +3,7 @@
3
import { getNodeName, isSameFilenameAs, nodeIsDirectory, saveVfs, urlToNode, vfs, VfsNode, applyParentToChild,
4
permsFromParent } from './vfs'
5
import _ from 'lodash'
6
-import { stat } from 'fs/promises'
6
+import { mkdir, stat } from 'fs/promises'
7
import { ApiError, ApiHandlers, SendListReadable } from './apiMiddleware'
8
import { dirname, extname, join, resolve } from 'path'
9
import { dirStream, enforceFinal, isDirectory, isWindowsDrive, makeMatcher, PERM_KEYS, VfsNodeAdminSend } from './misc'
@@ -168,6 +168,11 @@ const apis: ApiHandlers = {
168
return { path, isFolder: await isDirectory(path) }
169
},
170
171
+ async mkdir({ path }) {
172
+ await mkdir(path, { recursive: true })
173
+ return {}
174
+ },
175
+
176
get_ls({ path, files=true, fileMask }, ctx) {
177
return new SendListReadable({
178
async doAtStart(list) {