fix: (regression beta) show "delete" button in top-bar when selecting in a folder with delete permission

Massimo Melina committed May 25, 2025 at 20:36 UTC f957f649317e7c7b8a4c9f209c71e0af7ce4d2be
4 files changed +26 -9
e2e/frontend.spec.ts
+13
@@ -27,6 +27,19 @@ test('around1', async ({ page }) => {
27 await page.getByRole('button', { name: 'Continue' }).click();
28 await page.locator('div').filter({ hasText: 'Logged in' }).nth(3).click();
29 await screenshot(page);
30 +
31 + // selecting in this folder should enable bulk delete button
32 + await page.getByRole('link', { name: 'for-admins, Folder' }).click();
33 + await page.getByRole('link', { name: 'upload, Folder' }).click();
34 + await page.getByRole('link', { name: 'alfa.txt' }).click();
35 + await expect(page.getByText('Delete')).toBeVisible(); // first check single-delete command
36 + await page.getByRole('button', { name: 'Close' }).click();
37 + await page.getByRole('button', { name: 'Select' }).click();
38 + await page.getByRole('checkbox', { name: 'alfa.txt' }).check();
39 + await expect(page.getByRole('button', { name: 'Delete' })).toBeEnabled();
40 + await page.getByRole('button', { name: 'Select' }).click();
41 + await page.getByRole('link', { name: 'home' }).click();
42 +
43 await page.getByRole('button', { name: username }).click();
44 await page.getByRole('button', { name: 'Logout' }).click();
45 await page.getByText('Logged out').click();
frontend/src/menu.ts
+5 -6
@@ -4,8 +4,7 @@ import { state, useSnapState } from './state'
4 import { createElement as h, Fragment, useEffect, useMemo, useState } from 'react'
5 import { alertDialog, confirmDialog, ConfirmOptions, formDialog, toast } from './dialog'
6 import {
7 - defaultPerms, err2msg, ErrorMsg, onlyTruthy, prefix, useStateMounted, VfsPerms, working,
8 - buildUrlQueryString, hIcon, WIKI_URL
7 + err2msg, ErrorMsg, onlyTruthy, prefix, useStateMounted, working, buildUrlQueryString, hIcon, WIKI_URL
8 } from './misc'
9 import { loginDialog } from './login'
10 import { showOptions } from './options'
@@ -24,7 +23,7 @@ const { t, useI18N } = i18n
23
24 export function MenuPanel() {
25 const { showFilter, remoteSearch, stopSearch, searchManuallyInterrupted, selected, props } = useSnapState()
27 - const { can_upload, can_delete, can_archive } = props ? { ...defaultPerms, ...props } : {} as VfsPerms
26 + const { can_upload, can_delete_children, can_archive } = props || {}
27 const { uploading, qs, uploadDialogIsOpen } = useSnapshot(uploadState)
28 useEffect(() => {
29 if (!showFilter)
@@ -48,8 +47,8 @@ export function MenuPanel() {
47 const [changingButton, setChangingButton] = useState<'' | 'upload' | 'delete'>('')
48 useEffect(() => {
49 if (can_upload !== undefined)
51 - setChangingButton(showFilter && can_delete ? 'delete' : (can_upload || qs.length > 0) ? 'upload' : '')
52 - }, [showFilter, can_delete, can_upload, qs.length])
50 + setChangingButton(showFilter && can_delete_children ? 'delete' : (can_upload || qs.length > 0) ? 'upload' : '')
51 + }, [showFilter, can_delete_children, can_upload, qs.length])
52 return h('div', { id: 'menu-panel' },
53 h('div', { id: 'menu-bar' },
54 h(LoginButton),
@@ -80,7 +79,7 @@ export function MenuPanel() {
79 className: 'sliding ' + (changingButton ? '' : 'hide-sliding') + (uploading && !uploadDialogIsOpen ? ' ani-working' : ''),
80 onClick: showUpload,
81 }),
83 - h(Btn, showFilter && can_delete ? {
82 + h(Btn, showFilter && can_delete_children ? {
83 id: 'cut-button',
84 icon: 'cut',
85 label: t`Cut`,
frontend/src/state.ts
+2 -1
@@ -29,6 +29,7 @@ export const state = proxy<typeof FRONTEND_OPTIONS & {
29 can_upload?: boolean
30 accept?: string
31 can_delete?: boolean
32 + can_delete_children?: boolean
33 can_archive?: boolean
34 can_comment?: boolean
35 can_overwrite?: boolean
@@ -156,7 +157,7 @@ export class DirEntry implements ServerDirEntry {
157 return this.p?.includes('A') || state.props?.can_archive && !this.p?.includes('a')
158 }
159 canDelete() {
159 - return !this.isRoot() && (this.p?.includes('D') || state.props?.can_delete && !this.p?.includes('d'))
160 + return !this.isRoot() && (this.p?.includes('D') || state.props?.can_delete_children && !this.p?.includes('d'))
161 }
162 canUpload() {
163 return this.isFolder && (this.p?.includes('U') || state.props?.can_upload && !this.p?.includes('u'))
src/api.get_file_list.ts
+6 -2
@@ -48,11 +48,13 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, c, onl
48 const onDirEntryHandlers = mapPlugins(plug => plug.onDirEntry)
49 const can_upload = admin || hasPermission(node, 'can_upload', ctx)
50 const can_delete = admin || hasPermission(node, 'can_delete', ctx)
51 + const fakeChild = await applyParentToChild({ source: 'dummy-file', original: undefined }, node) // used to check permission; simple but can produce false results; 'original' to simulate a non-vfs node
52 + const can_delete_children = admin || hasPermission(fakeChild, 'can_delete', ctx)
53 const can_archive = admin || hasPermission(node, 'can_archive', ctx)
54 const can_comment = can_upload && areCommentsEnabled()
55 const can_overwrite = can_upload && (can_delete || !dontOverwriteUploading.get())
56 const comment = node.comment ?? await getCommentFor(node.source)
55 - const props = { can_archive, can_upload, can_delete, can_overwrite, can_comment, comment, accept: node.accept, icon: getNodeIcon(node) }
57 + const props = { can_archive, can_upload, can_delete, can_delete_children, can_overwrite, can_comment, comment, accept: node.accept, icon: getNodeIcon(node) }
58 ctx.state.browsing = uri.replace(/\/{2,}/g, '/')
59 updateConnectionForCtx(ctx)
60 if (!list)
@@ -118,6 +120,7 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, c, onl
120 if (!isFolder || !node.children?.length) // folders with virtual children, keep them
121 throw e
122 }) : undefined
123 + // permissions of entries are sent as a difference with permissions of parent
124 const pl = node.can_list === WHO_NO_ONE ? 'l'
125 : !hasPermission(node, 'can_list', ctx) ? 'L'
126 : ''
@@ -125,7 +128,8 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, c, onl
128 const pr = node.can_read === WHO_NO_ONE && !(isFolder && filesInsideCould()) ? 'r'
129 : !hasPermission(node, 'can_read', ctx) ? 'R'
130 : ''
128 - const pd = Boolean(can_delete) === hasPermission(node, 'can_delete', ctx) ? '' : can_delete ? 'd' : 'D'
131 + // for delete, the diff is based on can_delete_children instead of can_delete, because it will produce fewer data
132 + const pd = Boolean(can_delete_children) === hasPermission(node, 'can_delete', ctx) ? '' : can_delete_children ? 'd' : 'D'
133 const pa = Boolean(can_archive) === hasPermission(node, 'can_archive', ctx) ? '' : can_archive ? 'a' : 'A'
134 const pu = !isFolder || Boolean(can_upload) === hasPermission(node, 'can_upload', ctx) ? '' : can_upload ? 'u' : 'U'
135 return {