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 {