admin/fs: add > virtual folder
Massimo Melina committed
Feb 12, 2022 at 22:13 UTC
85d775893b15e2aa5f3f6abeec1df7b246fd4fcf
10 files changed
+93
-21
admin/package-lock.json
+2
-2
@@ -1,12 +1,12 @@
1
{
2
"name": "admin",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
"lockfileVersion": 2,
5
"requires": true,
6
"packages": {
7
"": {
8
"name": "admin",
9
- "version": "0.10.0",
9
+ "version": "0.11.0",
10
"dependencies": {
11
"@emotion/react": "^11.7.1",
12
"@emotion/styled": "^11.6.0",
admin/package.json
+9
-1
@@ -1,6 +1,6 @@
1
{
2
"name": "admin",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
"private": true,
5
"proxy": "http://localhost:63636",
6
"scripts": {
@@ -40,6 +40,14 @@
40
"extends": [
41
"react-app",
42
"react-app/jest"
43
+ ],
44
+ "overrides": [
45
+ {
46
+ "files": ["*.ts"],
47
+ "rules": {
48
+ "no-mixed-operators": "off"
49
+ }
50
+ }
51
]
52
},
53
"browserslist": {
admin/src/MenuButton.ts
new
+37
@@ -0,0 +1,37 @@
1
+import React, { createElement as h, useCallback } from 'react'
2
+import { Button, Menu, MenuItem } from '@mui/material'
3
+
4
+interface Props { items: any[], [rest:string]:any }
5
+
6
+export default function MenuButton({ items, ...rest }: Props) {
7
+ const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
8
+ const open = Boolean(anchorEl)
9
+ const onClose = useCallback(() => setAnchorEl(null), [])
10
+ return h(React.Fragment, {},
11
+ h(Button, {
12
+ 'aria-controls': open ? 'basic-menu' : undefined,
13
+ 'aria-haspopup': 'true',
14
+ 'aria-expanded': open ? 'true' : undefined,
15
+ onClick: (event: React.MouseEvent<HTMLButtonElement>) => {
16
+ setAnchorEl(event.currentTarget)
17
+ },
18
+ ...rest,
19
+ }),
20
+ h(Menu, {
21
+ anchorEl,
22
+ open,
23
+ onClose,
24
+ MenuListProps: { 'aria-labelledby': 'basic-button' },
25
+ },
26
+ items.map((it,idx) =>
27
+ h(MenuItem, {
28
+ key: idx,
29
+ ...it,
30
+ onClick() {
31
+ onClose()
32
+ it.onClick?.apply(this, arguments)
33
+ }
34
+ }) )
35
+ )
36
+ )
37
+}
admin/src/VfsMenuBar.ts
+11
-3
@@ -5,7 +5,8 @@ import { Add, Delete, Refresh } from '@mui/icons-material'
5
import { alertDialog, confirmDialog } from './dialog'
6
import { apiCall } from './api'
7
import { reloadVfs } from './VfsPage'
8
-import addFiles from './addFiles'
8
+import addFiles, { addVirtual } from './addFiles'
9
+import MenuButton from './MenuButton'
10
11
export default function VfsMenuBar() {
12
const { selectedFiles } = useSnapState()
@@ -21,9 +22,16 @@ export default function VfsMenuBar() {
22
width: 'fit-content',
23
},
24
},
24
- h(Button, { variant:'contained', onClick: addFiles, startIcon: h(Add) }, 'Add'),
25
+ h(MenuButton, {
26
+ variant: 'contained',
27
+ startIcon: h(Add),
28
+ items: [
29
+ { children: 'from disk', onClick: addFiles },
30
+ { children: 'virtual folder', onClick: addVirtual }
31
+ ]
32
+ }, 'Add'),
33
h(Button, { onClick: removeFiles, disabled: !selectedFiles.length, startIcon: h(Delete) }, 'Remove'),
26
- h(Button, { onClick: reloadVfs, startIcon: h(Refresh) }, 'Reload'),
34
+ h(Button, { onClick(){ reloadVfs() }, startIcon: h(Refresh) }, 'Reload'),
35
)
36
}
37
admin/src/VfsPage.ts
+9
-5
@@ -7,6 +7,8 @@ import VfsMenuBar from './VfsMenuBar'
7
import VfsTree from './VfsTree'
8
import { onlyTruthy } from './misc'
9
10
+let selectOnReload: string[] | undefined
11
+
12
export default function VfsPage() {
13
const [id2node] = useState(() => new Map<string, Node>())
14
const snap = useSnapState()
@@ -22,18 +24,19 @@ export default function VfsPage() {
24
recur(root) // this must be done before state change that would cause Tree to render and expecting id2node
25
state.vfs = root
26
// refresh objects of selectedFiles
25
- const ids = state.selectedFiles.map(x => x.id)
27
+ const ids = selectOnReload || state.selectedFiles.map(x => x.id)
28
+ selectOnReload = undefined
29
state.selectedFiles = onlyTruthy(ids.map(id =>
30
id2node.get(id)))
31
32
// calculate id and parent fields, and builds the map id2node
30
- function recur(node: Node, prefix='', parent: Node|undefined=undefined) {
33
+ function recur(node: Node, pre='', parent: Node|undefined=undefined) {
34
node.parent = parent
32
- node.id = prefix + node.name || '/' // root
35
+ node.id = (pre + node.name) || '/' // root
36
id2node.set(node.id, node)
37
if (!node.children) return
38
for (const n of node.children)
36
- recur(n, (prefix && node.id) + '/', node)
39
+ recur(n, (pre && node.id) + '/', node)
40
}
41
42
}, [res, id2node])
@@ -49,7 +52,8 @@ export default function VfsPage() {
52
h(FileCard)))
53
}
54
52
-export function reloadVfs() {
55
+export function reloadVfs(pleaseSelect?: string[]) {
56
+ selectOnReload = pleaseSelect
57
state.vfs = undefined
58
}
59
admin/src/addFiles.ts
+21
-5
@@ -15,10 +15,7 @@ export default function addFiles() {
15
})
16
17
function Content() {
18
- let f: Node | undefined = state.selectedFiles[0]
19
- if (f && f.type !== 'folder')
20
- f = f.parent
21
- const under = f?.id
18
+ const under = getUnder()
19
return h('div', {},
20
h(Box, { sx:{ typography: 'body1', px: 1, py: 2 } }, "Selected elements will be added to " + (under || '(home)')),
21
h(FilePicker, {
@@ -27,7 +24,7 @@ export default function addFiles() {
24
apiCall('add_vfs', { under, source }).then(() => '', () => source) ))
25
failed = onlyTruthy(failed)
26
if (failed.length)
30
- await alertDialog('Some elements have been rejected: '+failed.join(', '), 'error')
27
+ await alertDialog("Some elements have been rejected: "+failed.join(', '), 'error')
28
reloadVfs()
29
close()
30
}
@@ -37,3 +34,22 @@ export default function addFiles() {
34
35
}
36
37
+export async function addVirtual() {
38
+ try {
39
+ const name = "Folder"+String(Date.now()).slice(8)
40
+ const under = getUnder()
41
+ await apiCall('add_vfs', { under, name })
42
+ reloadVfs([ (under||'') + '/' + name ])
43
+ await alertDialog(name + " created")
44
+ }
45
+ catch(e) {
46
+ await alertDialog(e as Error)
47
+ }
48
+}
49
+
50
+function getUnder() {
51
+ let f: Node | undefined = state.selectedFiles[0]
52
+ if (f && f.type !== 'folder')
53
+ f = f.parent
54
+ return f?.id
55
+}
package-lock.json
+2
-2
@@ -1,12 +1,12 @@
1
{
2
"name": "hfs",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
"lockfileVersion": 2,
5
"requires": true,
6
"packages": {
7
"": {
8
"name": "hfs",
9
- "version": "0.10.0",
9
+ "version": "0.11.0",
10
"license": "GPL-3.0",
11
"dependencies": {
12
"@koa/router": "^10.1.1",
package.json
+1
-1
@@ -1,6 +1,6 @@
1
{
2
"name": "hfs",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
"description": "HTTP File Server",
5
"keywords": [
6
"file server",
src/api.vfs.ts
+1
-1
@@ -61,7 +61,7 @@ const apis: ApiHandlers = {
61
if (n.isTemp || !await nodeIsDirectory(n))
62
return new ApiError(403, 'invalid under')
63
const a = n.children || (n.children = [])
64
- if (a.find(x => x.source === source))
64
+ if (source && a.find(x => x.source === source))
65
return new ApiError(409, 'already present')
66
a.unshift({ source, name })
67
await saveVfs()
todo.md
-1
@@ -1,5 +1,4 @@
1
# To do
2
-- admin: add virtual folders
2
- admin: warn in case of items with same name
3
- password protect admin
4
- allowed referer