Skip to content

Commit 237c5ea

Browse files
authored
Hide and opt-in file and directory delete buttons (#4689)
1 parent 0a2edea commit 237c5ea

File tree

20 files changed

+508
-292
lines changed

20 files changed

+508
-292
lines changed

catalog/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ where verb is one of
1818

1919
## Changes
2020

21+
- [Changed] Hide file and directory delete buttons in Bucket tab by default; enable with `ui.actions.deleteObject` ([#4689](https://github.com/quiltdata/quilt/pull/4689))
2122
- [Added] Display detailed permission error messages when bucket add/update fails due to insufficient S3 permissions ([#4670](https://github.com/quiltdata/quilt/pull/4670))
2223
- [Added] Support `prefixes` field in bucket configuration for prefix-scoped bucket access ([#4670](https://github.com/quiltdata/quilt/pull/4670))
2324
- [Fixed] Catch error and don't show Perspective table if Perspective failed to render ([#4669](https://github.com/quiltdata/quilt/pull/4669))

catalog/app/containers/Bucket/Dir/Toolbar/Get/Options.spec.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,21 @@ describe('containers/Bucket/Dir/Toolbar/Get/Options', () => {
3232
}
3333

3434
it('should render CodeSamples components with correct dest for nested path', () => {
35-
render(<Options handle={DirHandleCreate('test-bucket', 'folder/subfolder/')} />)
35+
render(
36+
<Options
37+
handle={DirHandleCreate('test-bucket', 'folder/subfolder/')}
38+
features={{ code: true }}
39+
/>,
40+
)
3641

3742
expect(Quilt3Fetch).toHaveBeenCalledWith({ ...props, dest: 'subfolder' }, {})
3843
expect(CliFetch).toHaveBeenCalledWith({ ...props, dest: 'subfolder' }, {})
3944
})
4045

4146
it('should render CodeSamples components with bucket as dest for root path', () => {
42-
render(<Options handle={DirHandleCreate('test-bucket', '')} />)
47+
render(
48+
<Options handle={DirHandleCreate('test-bucket', '')} features={{ code: true }} />,
49+
)
4350

4451
expect(Quilt3Fetch).toHaveBeenCalledWith({ ...props, dest: 'test-bucket' }, {})
4552
expect(CliFetch).toHaveBeenCalledWith({ ...props, dest: 'test-bucket' }, {})

catalog/app/containers/Bucket/Dir/Toolbar/Get/Options.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as CodeSamples from 'containers/Bucket/CodeSamples'
77
import * as Buttons from 'containers/Bucket/Download/Buttons'
88
import GetOptions from 'containers/Bucket/Toolbar/GetOptions'
99
import type * as Toolbar from 'containers/Bucket/Toolbar'
10+
import type { Features } from '../useFeatures'
1011

1112
const useCodeSamplesStyles = M.makeStyles((t) => ({
1213
code: {
@@ -61,14 +62,14 @@ function DownloadDir({ dirHandle }: DownloadDirProps) {
6162

6263
interface OptionsProps {
6364
handle: Toolbar.DirHandle
64-
hideCode?: boolean
65+
features: Exclude<Features['get'], false>
6566
}
6667

67-
export default function Options({ handle, hideCode }: OptionsProps) {
68+
export default function Options({ handle, features }: OptionsProps) {
6869
const download = <DownloadDir dirHandle={handle} />
69-
const code = hideCode ? undefined : (
70+
const code = features.code ? (
7071
<DirCodeSamples bucket={handle.bucket} path={handle.path} />
71-
)
72+
) : undefined
7273

7374
return <GetOptions download={download} code={code} />
7475
}

catalog/app/containers/Bucket/Dir/Toolbar/Organize/Options.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as Icons from '@material-ui/icons'
55
import * as Format from 'utils/format'
66
import assertNever from 'utils/assertNever'
77

8+
import type { Features } from '../useFeatures'
89
import * as Context from './Context'
910

1011
const LIST_ITEM_TYPOGRAPHY_PROPS = { noWrap: true }
@@ -34,7 +35,11 @@ const useStyles = M.makeStyles((t) => ({
3435
},
3536
}))
3637

37-
export default function OrganizeOptions() {
38+
interface OrganizeOptionsProps {
39+
features: Exclude<Features['organize'], false>
40+
}
41+
42+
export default function OrganizeOptions({ features }: OrganizeOptionsProps) {
3843
const classes = useStyles()
3944
const {
4045
toggleBookmarks,
@@ -102,16 +107,20 @@ export default function OrganizeOptions() {
102107
/>
103108
</M.List>
104109

105-
<M.Divider />
106-
107-
<M.List dense>
108-
<MenuItem
109-
className={classes.error}
110-
icon={<Icons.DeleteOutlined color="error" />}
111-
onClick={confirmDeleteSelected}
112-
primary="Delete selected items"
113-
/>
114-
</M.List>
110+
{features.delete && (
111+
<>
112+
<M.Divider />
113+
114+
<M.List dense>
115+
<MenuItem
116+
className={classes.error}
117+
icon={<Icons.DeleteOutlined color="error" />}
118+
onClick={confirmDeleteSelected}
119+
primary="Delete selected items"
120+
/>
121+
</M.List>
122+
</>
123+
)}
115124
</>
116125
)
117126
}
Lines changed: 57 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import * as React from 'react'
2-
import { beforeEach, describe, it, expect, vi, afterEach, type Mock } from 'vitest'
2+
import { describe, it, expect, vi, afterEach, type Mock } from 'vitest'
33
import { render, cleanup } from '@testing-library/react'
4-
import { renderHook } from '@testing-library/react-hooks'
54

65
import * as BucketPreferences from 'utils/BucketPreferences'
7-
import { extendDefaults } from 'utils/BucketPreferences/BucketPreferences'
86
import noop from 'utils/noop'
97

108
import * as DirToolbar from './Toolbar'
@@ -19,14 +17,24 @@ vi.mock('./Add', () => ({
1917
}))
2018

2119
vi.mock('./Get', () => ({
22-
Options: () => <div>"Get" popover</div>,
20+
Options: ({ features }: { features?: { code: boolean } }) => (
21+
<div>
22+
"Get" popover
23+
{features?.code && <div data-testid="code-samples" />}
24+
</div>
25+
),
2326
}))
2427

2528
vi.mock('./Organize', () => ({
2629
Context: {
2730
Provider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
2831
},
29-
Options: () => <>"Organize" popover</>,
32+
Options: ({ features }: { features?: { delete: boolean } }) => (
33+
<div>
34+
"Organize" popover
35+
{features?.delete && <div data-testid="delete-button" />}
36+
</div>
37+
),
3038
}))
3139

3240
vi.mock('./CreatePackage', () => ({
@@ -70,78 +78,6 @@ vi.mock('utils/BucketPreferences', async () => ({
7078
use: () => prefsHook(),
7179
}))
7280

73-
describe('useFeatures', () => {
74-
beforeEach(() => {
75-
vi.clearAllMocks()
76-
})
77-
78-
it('should return null when preferences are loading', () => {
79-
prefsHook.mockImplementationOnce(() => ({
80-
prefs: BucketPreferences.Result.Pending(),
81-
}))
82-
83-
const { result } = renderHook(() => DirToolbar.useFeatures())
84-
85-
expect(result.current).toBeNull()
86-
})
87-
88-
it('should return all features disabled when all permissions are false', () => {
89-
prefsHook.mockImplementationOnce(() => ({
90-
prefs: BucketPreferences.Result.Ok(
91-
extendDefaults({
92-
ui: {
93-
actions: {
94-
writeFile: false,
95-
downloadObject: false,
96-
createPackage: false,
97-
},
98-
blocks: {
99-
code: false,
100-
},
101-
},
102-
}),
103-
),
104-
}))
105-
106-
const { result } = renderHook(() => DirToolbar.useFeatures())
107-
108-
expect(result.current).toEqual({
109-
add: false,
110-
get: false,
111-
organize: true, // Always true in the implementation
112-
createPackage: false,
113-
})
114-
})
115-
116-
it('should return all features enabled when all permissions are true', () => {
117-
prefsHook.mockImplementationOnce(() => ({
118-
prefs: BucketPreferences.Result.Ok(
119-
extendDefaults({
120-
ui: {
121-
actions: {
122-
writeFile: true,
123-
downloadObject: true,
124-
createPackage: true,
125-
},
126-
blocks: {
127-
code: true,
128-
},
129-
},
130-
}),
131-
),
132-
}))
133-
134-
const { result } = renderHook(() => DirToolbar.useFeatures())
135-
136-
expect(result.current).toEqual({
137-
add: true,
138-
get: { code: true },
139-
organize: true,
140-
createPackage: true,
141-
})
142-
})
143-
})
144-
14581
const handle = DirToolbar.CreateHandle('test-bucket', 'test/path')
14682

14783
describe('Toolbar', () => {
@@ -158,7 +94,12 @@ describe('Toolbar', () => {
15894
it('should render all buttons when all features are enabled', () => {
15995
const { getByTitle } = render(
16096
<DirToolbar.Toolbar
161-
features={{ add: true, get: { code: true }, organize: true, createPackage: true }}
97+
features={{
98+
add: true,
99+
get: { code: true },
100+
organize: { delete: true },
101+
createPackage: true,
102+
}}
162103
handle={handle}
163104
onReload={vi.fn()}
164105
/>,
@@ -186,7 +127,12 @@ describe('Toolbar', () => {
186127
it('should render buttons for enabled features: add, organize', () => {
187128
const { getByTitle, queryByTitle } = render(
188129
<DirToolbar.Toolbar
189-
features={{ add: true, get: false, organize: true, createPackage: false }}
130+
features={{
131+
add: true,
132+
get: false,
133+
organize: { delete: true },
134+
createPackage: false,
135+
}}
190136
handle={handle}
191137
onReload={vi.fn()}
192138
/>,
@@ -196,4 +142,36 @@ describe('Toolbar', () => {
196142
expect(queryByTitle('Get files')).toBeFalsy()
197143
expect(queryByTitle('Create package')).toBeFalsy()
198144
})
145+
146+
it('should show delete button in Organize.Options when delete feature is enabled', () => {
147+
const { getByTestId } = render(
148+
<DirToolbar.Toolbar
149+
features={{
150+
add: false,
151+
get: false,
152+
organize: { delete: true },
153+
createPackage: false,
154+
}}
155+
handle={handle}
156+
onReload={vi.fn()}
157+
/>,
158+
)
159+
expect(getByTestId('delete-button')).toBeTruthy()
160+
})
161+
162+
it('should hide delete button in Organize.Options when delete feature is disabled', () => {
163+
const { queryByTestId } = render(
164+
<DirToolbar.Toolbar
165+
features={{
166+
add: false,
167+
get: false,
168+
organize: { delete: false },
169+
createPackage: false,
170+
}}
171+
handle={handle}
172+
onReload={vi.fn()}
173+
/>,
174+
)
175+
expect(queryByTestId('delete-button')).toBeFalsy()
176+
})
199177
})

catalog/app/containers/Bucket/Dir/Toolbar/Toolbar.tsx

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import * as React from 'react'
33
import * as M from '@material-ui/core'
44
import * as Lab from '@material-ui/lab'
55

6-
import cfg from 'constants/config'
7-
import * as BucketPreferences from 'utils/BucketPreferences'
8-
96
import * as Toolbar from 'containers/Bucket/Toolbar'
107
import { FromHandles, useCreateDialog } from 'containers/Bucket/PackageDialog'
118
import * as Selection from 'containers/Bucket/Selection'
@@ -15,9 +12,10 @@ import * as Add from './Add'
1512
import * as CreatePackage from './CreatePackage'
1613
import * as Get from './Get'
1714
import * as Organize from './Organize'
15+
import { useFeatures, type Features } from './useFeatures'
1816

1917
export { DirHandleCreate as CreateHandle } from 'containers/Bucket/Toolbar'
20-
export { Add, CreatePackage, Get, Organize }
18+
export { Add, CreatePackage, Get, Organize, useFeatures, type Features }
2119

2220
const useStyles = M.makeStyles((t) => ({
2321
root: {
@@ -27,34 +25,6 @@ const useStyles = M.makeStyles((t) => ({
2725
},
2826
}))
2927

30-
interface Features {
31-
add: boolean | null
32-
get: false | { code: boolean } | null
33-
organize: boolean | null
34-
createPackage: boolean | null
35-
}
36-
37-
export function useFeatures(): Features | null {
38-
const { prefs } = BucketPreferences.use()
39-
return React.useMemo(
40-
() =>
41-
BucketPreferences.Result.match(
42-
{
43-
Ok: ({ ui: { actions, blocks } }) => ({
44-
add: actions.writeFile,
45-
get:
46-
!cfg.noDownload && actions.downloadObject ? { code: blocks.code } : false,
47-
organize: true,
48-
createPackage: actions.createPackage,
49-
}),
50-
_: () => null,
51-
},
52-
prefs,
53-
),
54-
[prefs],
55-
)
56-
}
57-
5828
interface DirToolbarProps {
5929
className?: string
6030
features: Features | null
@@ -123,14 +93,14 @@ function DirToolbar({ className, features, handle, onReload }: DirToolbarProps)
12393

12494
{features.get && (
12595
<Toolbar.Get>
126-
<Get.Options handle={handle} hideCode={!features.get.code} />
96+
<Get.Options handle={handle} features={features.get} />
12797
</Toolbar.Get>
12898
)}
12999

130100
{features.organize && (
131101
<Organize.Context.Provider onReload={onReload}>
132102
<Toolbar.Organize>
133-
<Organize.Options />
103+
<Organize.Options features={features.organize} />
134104
</Toolbar.Organize>
135105
</Organize.Context.Provider>
136106
)}

0 commit comments

Comments
 (0)