@samitouri / QOSami-HFS / commits / bb8aed74

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