disable zip button when missing permission

Massimo Melina committed Oct 4, 2023 at 21:28 UTC c7348b8552dcf575baf7212d2a2c1e4a308cf541
8 files changed +31 -24
frontend/src/BrowseFiles.ts
+2 -2
@@ -40,7 +40,7 @@ export function BrowseFiles() {
40 }
41
42 function FilesList() {
43 - const { filteredList, list, loading, stoppedSearch, can_upload } = useSnapState()
43 + const { filteredList, list, loading, stoppedSearch, props } = useSnapState()
44 const midnight = useMidnight() // as an optimization we calculate this only once per list and pass it down
45 const pageSize = 100
46 const [page, setPage] = useState(0)
@@ -102,7 +102,7 @@ function FilesList() {
102 h('ul', {
103 ref,
104 className: 'dir',
105 - ...acceptDropFiles(files => can_upload ? enqueue(files.map(file => ({ file })))
105 + ...acceptDropFiles(files => props?.can_upload ? enqueue(files.map(file => ({ file })))
106 : alertDialog(t("Upload not available"), 'warning') )
107 },
108 msgInstead ? h('p', {}, msgInstead)
frontend/src/fileMenu.ts
+3 -3
@@ -26,7 +26,7 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (FileMe
26 const cantDownload = entry.cantOpen || isFolder && entry.p?.includes('r') // folders needs both list and read
27 const menu = [
28 !cantDownload && { id: 'download', label: t`Download`, href: uri + (isFolder ? '?get=zip' : '?dl'), icon: 'download' },
29 - state.can_upload && { id: 'comment', label: t`Comment`, icon: 'comment', onClick: () => editComment(entry) },
29 + state.props?.can_upload && { id: 'comment', label: t`Comment`, icon: 'comment', onClick: () => editComment(entry) },
30 ...addToMenu.map(x => {
31 if (x === 'open') {
32 if (entry.cantOpen) return
@@ -34,7 +34,7 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (FileMe
34 return !isFolder ? open : h(Link, { to: uri, onClick: () => close() }, hIcon(open.icon), open.label)
35 }
36 if (x === 'delete')
37 - return (state.can_delete || entry.p?.includes('d')) && {
37 + return (state.props?.can_delete || entry.p?.includes('d')) && {
38 id: 'delete',
39 label: t`Delete`,
40 icon: 'delete',
@@ -49,7 +49,7 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (FileMe
49 }
50 return x
51 }),
52 - state.can_delete && { id: 'rename', label: t`Rename`, icon: 'edit', onClick: () => rename(entry) },
52 + state.props?.can_delete && { id: 'rename', label: t`Rename`, icon: 'edit', onClick: () => rename(entry) },
53 isFolder && { id: 'list', label: t`Get list`, href: uri + '?get=list&folders=*', icon: 'list' },
54 ]
55 const props = [
frontend/src/index.scss
+4
@@ -111,6 +111,10 @@ button {
111 vertical-align: middle;
112 cursor: pointer;
113 &:hover { outline: 1px solid var(--mild-contrast); }
114 + transition: background-color .5s;
115 + &[disabled] {
116 + background-color: var(--faint-contrast);
117 + }
118 }
119 button.toggled {
120 color: #fff;
frontend/src/menu.ts
+4 -2
@@ -3,7 +3,7 @@
3 import { state, useSnapState } from './state'
4 import { ComponentPropsWithoutRef, createElement as h, Fragment, useEffect, useMemo, useState } from 'react'
5 import { alertDialog, confirmDialog, ConfirmOptions, promptDialog } from './dialog'
6 -import { err2msg, ErrorMsg, hIcon, onlyTruthy, prefix, useStateMounted, working } from './misc'
6 +import { defaultPerms, err2msg, ErrorMsg, hIcon, onlyTruthy, prefix, useStateMounted, VfsPerms, working } from './misc'
7 import { loginDialog } from './login'
8 import { showOptions } from './options'
9 import showUserPanel from './UserPanel'
@@ -17,7 +17,8 @@ import { reloadList } from './useFetchList'
17 import { t, useI18N } from './i18n'
18
19 export function MenuPanel() {
20 - const { showFilter, remoteSearch, stopSearch, stoppedSearch, selected, can_upload, can_delete } = useSnapState()
20 + const { showFilter, remoteSearch, stopSearch, stoppedSearch, selected, props } = useSnapState()
21 + const { can_upload, can_delete, can_archive } = props ? { ...defaultPerms, ...props } : {} as VfsPerms
22 const { uploading, qs } = useSnapshot(uploadState)
23 useEffect(() => {
24 if (!showFilter)
@@ -87,6 +88,7 @@ export function MenuPanel() {
88 id: 'zip-button',
89 icon: 'archive',
90 label: t`Zip`,
91 + disabled: !can_archive,
92 tooltip: list ? t('zip_tooltip_selected', "Download selected elements as a single zip file")
93 : t('zip_tooltip_whole', "Download whole list (unfiltered) as a single zip file. If you select some elements, only those will be downloaded."),
94 href: '?'+String(new URLSearchParams(_.pickBy({
frontend/src/state.ts
+7 -3
@@ -27,11 +27,15 @@ export const state = proxy<{
27 adminUrl?: string,
28 loginRequired?: boolean, // force user to login before proceeding
29 messageOnly?: string, // no gui, just show this message
30 - can_upload?: boolean
31 - can_delete?: boolean
32 - accept?: string
30 + props?: {
31 + can_upload?: boolean
32 + accept?: string
33 + can_delete?: boolean
34 + can_archive?: boolean
35 + }
36 tilesSize: number
37 }>({
38 + props: {},
39 tilesSize: getHFS().tilesSize || 0,
40 iconsReady: false,
41 username: '',
frontend/src/upload.ts
+6 -6
@@ -100,7 +100,7 @@ export function showUpload() {
100 function Content(){
101 const adding = useSnapshot(uploadState.adding)
102 const { qs, paused, eta, skipExisting } = useSnapshot(uploadState)
103 - const { can_upload, accept } = useSnapState()
103 + const { props } = useSnapState()
104 const etaStr = useMemo(() => !eta ? '' : formatTime(eta*1000, 0, 2), [eta])
105 const inQ = _.sumBy(qs, q => q.entries.length) - (uploadState.uploading ? 1 : 0)
106 const queueStr = inQ && t('in_queue', { n: inQ }, "{n} in queue")
@@ -108,12 +108,12 @@ export function showUpload() {
108
109 return h(FlexV, { gap: 0, props: acceptDropFiles(more => uploadState.adding.push(...more.map(f => ({ file: ref(f) })))) },
110 h(FlexV, { className: 'upload-toolbar' },
111 - !can_upload ? t('no_upload_here', "No upload permission for the current folder")
111 + !props?.can_upload ? t('no_upload_here', "No upload permission for the current folder")
112 : h(FlexV, {},
113 h(Flex, { justifyContent: 'center', flexWrap: 'wrap', alignItems: 'center' },
114 h('button', {
115 className: 'upload-files',
116 - onClick: () => pickFiles({ accept: normalizeAccept(accept) })
116 + onClick: () => pickFiles({ accept: normalizeAccept(props?.accept) })
117 }, t`Pick files`),
118 !isMobile() && h('button', {
119 className: 'upload-folder',
@@ -252,9 +252,9 @@ export async function enqueue(entries: ToUpload[]) {
252 }
253
254 function simulateBrowserAccept(f: File) {
255 - const { accept } = state
256 - if (!accept) return true
257 - return normalizeAccept(accept)!.split(/ *[|,] */).some(pattern =>
255 + const { props } = state
256 + if (!props?.accept) return true
257 + return normalizeAccept(props?.accept)!.split(/ *[|,] */).some(pattern =>
258 pattern.startsWith('.') ? f.name.endsWith(pattern)
259 : f.type.match(pattern.replace('.','\\.').replace('*', '.*')) // '.' for .ext and '*' for 'image/*'
260 )
frontend/src/useFetchList.ts
+2 -5
@@ -47,8 +47,7 @@ export default function useFetchList() {
47 state.selected = {}
48 state.loading = true
49 state.error = undefined
50 - state.can_upload = undefined
51 - state.can_delete = undefined
50 + state.props = undefined
51 // buffering entries is necessary against burst of events that will hang the browser
52 const buffer: DirList = []
53 const flush = () => {
@@ -94,11 +93,9 @@ export default function useFetchList() {
93 if (uri && !uri.endsWith('/')) // now we know it was a folder for sure
94 return navigate(uri + '/')
95 if (op === 'props') {
97 - Object.assign(state, _.pick(par, ['can_upload', 'can_delete', 'accept']))
96 + state.props = par
97 continue
98 }
100 - state.can_upload ??= false
101 - state.can_delete ??= false
99 if (op === 'add')
100 buffer.push(new DirEntry(par.n, par))
101 }
src/api.file_list.ts
+3 -3
@@ -37,12 +37,12 @@ export const get_file_list: ApiHandler = async ({ uri, offset, limit, search, c
37 const can_upload = hasPermission(node, 'can_upload', ctx)
38 const fakeChild = applyParentToChild({}, node) // we want to know if we want to delete children
39 const can_delete = hasPermission(fakeChild, 'can_delete', ctx)
40 - const props = { can_upload, can_delete, accept: node.accept }
40 + const can_archive = hasPermission(fakeChild, 'can_archive', ctx)
41 + const props = { can_archive, can_upload, can_delete, accept: node.accept }
42 if (!list)
43 return { ...props, list: await asyncGeneratorToArray(produceEntries()) }
44 setTimeout(async () => {
44 - if (can_upload || can_delete)
45 - list.props(props)
45 + list.props(props)
46 for await (const entry of produceEntries())
47 list.add(entry)
48 list.close()