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