Skip to content

Commit 5ded022

Browse files
authored
Statusbar add hidden files toggle (#332)
* StatusBar: replace paste button with show hidden files toggle Only UI is ready: need to add hidden files filter now. * FileTable: respect cache.showHiddenFiles & reload view when it has changed TODO: IIRC when a file is selected & is renamed outside of react-explorer, it's automatically re-selected on cache reload. We should not re-select it if it becomes hidden and showHiddenFiles is false :) * StatusBar: use showHiddenFiles when counting the number of files/folders Also do not re-select a file that is hidden when refreshing file cache. * Statusbar: fixed tests Also removed native-ext-loader from webpack+package.json since it's not needed anymore. * Statusbar: don't keep a local showHiddenFiles state, rely on filecache instead This will make it easier to modify showHiddenFiles from another component, for example a native menu, shortcut,... * Electron: added native menu to toggle hidden files Menu can be activated using mod + h accelerator. This combo was previously binded to the debug history command: I removed it this was not used anymore.
1 parent 1929d1d commit 5ded022

15 files changed

Lines changed: 152 additions & 145 deletions

File tree

package-lock.json

Lines changed: 0 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@
9797
"jest-environment-jsdom": "^29.3.1",
9898
"lint-staged": "^10.4.2",
9999
"mock-fs": "git+https://git@github.com/warpdesign/mock-fs.git",
100-
"native-ext-loader": "^2.3.0",
101100
"pm2": "^5.2.2",
102101
"prettier": "^2.7.1",
103102
"source-map-loader": "^4.0.1",

src/components/Statusbar.tsx

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
import * as React from 'react'
2-
import { InputGroup, ControlGroup, Button, Intent, IconName } from '@blueprintjs/core'
2+
import { useState, useEffect } from 'react'
3+
import { InputGroup, ControlGroup, Button, Intent } from '@blueprintjs/core'
4+
import { IconNames } from '@blueprintjs/icons'
35
import { Tooltip2 } from '@blueprintjs/popover2'
46
import { observer } from 'mobx-react'
57
import { useTranslation } from 'react-i18next'
6-
import classNames from 'classnames'
7-
88
import { useStores } from '$src/hooks/useStores'
9+
import { filterDirs, filterFiles } from '$src/utils/fileUtils'
910

1011
const Statusbar = observer(() => {
11-
const { appState, viewState } = useStores('appState', 'viewState')
12+
const { viewState } = useStores('viewState')
1213
const { t } = useTranslation()
1314
const fileCache = viewState.getVisibleCache()
14-
const disabled = !fileCache.selected.length
15-
const numDirs = fileCache.files.filter((file) => file.fullname !== '..' && file.isDir).length
16-
const numFiles = fileCache.files.filter((file) => !file.isDir).length
17-
const numSelected = fileCache.selected.length
18-
const iconName = ((fileCache.getFS() && fileCache.getFS().icon) || 'offline') as IconName
19-
const offline = classNames('status-bar', { offline: fileCache.status === 'offline' })
15+
const { files, showHiddenFiles, error, status } = fileCache
2016

21-
const onClipboardCopy = () => {
22-
appState.clipboard.setClipboard(viewState.getVisibleCache())
23-
}
17+
const numDirs = filterDirs(files, showHiddenFiles).length
18+
const numFiles = filterFiles(files, showHiddenFiles).length
19+
const isDisabled = error || status !== 'ok'
20+
const hiddenToggleIcon = showHiddenFiles ? IconNames.EYE_OPEN : IconNames.EYE_OFF
2421

25-
const copyButton = (
26-
<Tooltip2 content={t('STATUS.CPTOOLTIP', { count: numSelected })} disabled={disabled}>
22+
const toggleHiddenFilesButton = (
23+
<Tooltip2
24+
content={showHiddenFiles ? t('STATUS.HIDE_HIDDEN_FILES') : t('STATUS.SHOW_HIDDEN_FILES')}
25+
disabled={isDisabled}
26+
>
2727
<Button
2828
data-cy-paste-bt
29-
disabled={disabled}
30-
icon="clipboard"
31-
intent={(!disabled && Intent.PRIMARY) || Intent.NONE}
32-
onClick={onClipboardCopy}
29+
disabled={isDisabled}
30+
icon={hiddenToggleIcon}
31+
intent={(!isDisabled && showHiddenFiles && Intent.PRIMARY) || Intent.NONE}
32+
onClick={() => fileCache.setShowHiddenFiles(!showHiddenFiles)}
3333
minimal={true}
3434
/>
3535
</Tooltip2>
@@ -39,12 +39,11 @@ const Statusbar = observer(() => {
3939
<ControlGroup>
4040
<InputGroup
4141
disabled
42-
leftIcon={iconName}
43-
rightElement={copyButton}
42+
rightElement={!isDisabled && toggleHiddenFilesButton}
4443
value={`${t('STATUS.FILES', { count: numFiles })}, ${t('STATUS.FOLDERS', {
4544
count: numDirs,
4645
})}`}
47-
className={offline}
46+
className="status-bar"
4847
/>
4948
</ControlGroup>
5049
)
Lines changed: 67 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,89 @@
11
/**
22
* @jest-environment jsdom
33
*/
4-
import React, { PropsWithChildren } from 'react'
5-
import { screen, render, LOCALE_EN, userEvent, t } from 'rtl'
6-
import { Provider } from 'mobx-react'
4+
import React from 'react'
5+
import { screen, setup, render, t, waitFor } from 'rtl'
76
import { Statusbar } from '../Statusbar'
7+
import { filterFiles, filterDirs } from '$src/utils/fileUtils'
8+
import { File } from '$src/services/Fs'
9+
import { ViewState } from '$src/state/viewState'
10+
import { FileState } from '$src/state/fileState'
11+
import { action, makeObservable, observable, runInAction } from 'mobx'
812

913
describe('Statusbar', () => {
10-
const appState = {
11-
clipboard: {
12-
setClipboard: jest.fn(),
14+
const cache = makeObservable(
15+
{
16+
status: 'ok',
17+
files: observable<File>([]),
18+
setShowHiddenFiles: jest.fn((show: boolean) => {
19+
cache.showHiddenFiles = show
20+
}),
21+
showHiddenFiles: false,
22+
path: '/tmp',
23+
} as unknown as FileState,
24+
{
25+
path: observable,
26+
showHiddenFiles: observable,
27+
setShowHiddenFiles: action,
1328
},
14-
}
15-
16-
const cache = {
17-
selected: [],
18-
getFS: (): undefined => undefined,
19-
files: [],
20-
} as { [x: string]: any }
29+
)
2130

22-
const viewState = {
23-
getVisibleCache: () => cache,
31+
const options = {
32+
providerProps: {
33+
viewState: {
34+
getVisibleCache: () => cache,
35+
} as unknown as ViewState,
36+
},
2437
}
2538

26-
const Wrapper = () => (
27-
<Provider appState={appState} viewState={viewState}>
28-
<Statusbar />
29-
</Provider>
30-
)
39+
const buildStatusBarText = () => {
40+
const files = filterFiles(cache.files, cache.showHiddenFiles).length
41+
const folders = filterDirs(cache.files, cache.showHiddenFiles).length
42+
return `${t('STATUS.FILES', { count: files })}, ${t('STATUS.FOLDERS', {
43+
count: folders,
44+
})}`
45+
}
3146

3247
beforeEach(() => {
33-
cache.selected = []
34-
cache.files = []
35-
jest.resetAllMocks()
36-
})
48+
cache.status = 'ok'
49+
cache.showHiddenFiles = false
50+
cache.files.replace([
51+
{
52+
fullname: 'dir1',
53+
isDir: true,
54+
} as unknown as File,
55+
{
56+
fullname: 'foo1',
57+
isDir: false,
58+
} as unknown as File,
59+
{
60+
fullname: '.foo2',
61+
isDir: false,
62+
} as unknown as File,
63+
])
3764

38-
it('should display statusbar', () => {
39-
render(<Wrapper />)
40-
expect(screen.getByRole('textbox')).toHaveValue(
41-
`${t('STATUS.FILES', { count: 0 })}, ${t('STATUS.FOLDERS', {
42-
count: 0,
43-
})}`,
44-
)
65+
jest.clearAllMocks()
4566
})
4667

47-
it('should display the number of files & folders', () => {
48-
const files = 10,
49-
folders = 5
68+
it('should display statusbar text and toggle hidden files button', () => {
69+
render(<Statusbar />, options)
5070

51-
cache.files = Array(files + folders)
52-
.fill({ isDir: false })
53-
.fill({ isDir: true }, files)
54-
render(<Wrapper />)
55-
expect(screen.getByRole('textbox')).toHaveValue(
56-
`${t('STATUS.FILES', { count: files })}, ${t('STATUS.FOLDERS', {
57-
count: folders,
58-
})}`,
59-
)
71+
expect(screen.getByRole('textbox')).toHaveValue(buildStatusBarText())
72+
expect(screen.getByRole('button')).toBeInTheDocument()
6073
})
6174

62-
it('clipboard button should be disabled if selection is empty', () => {
63-
render(<Wrapper />)
64-
expect(screen.getByRole('button')).toBeDisabled()
75+
it('should show hidden files when clicking on toggle hidden files button', async () => {
76+
const { user } = setup(<Statusbar />, options)
77+
78+
await user.click(screen.getByRole('button'))
79+
80+
expect(screen.getByRole('textbox')).toHaveValue(buildStatusBarText())
6581
})
6682

67-
it('clicking on clipboard button should set clipboard', async () => {
68-
// add a fake file to the selcted files
69-
cache.selected = [undefined]
70-
render(<Wrapper />)
71-
await userEvent.click(screen.getByRole('button'))
72-
expect(appState.clipboard.setClipboard).toHaveBeenCalledWith(cache)
73-
expect(appState.clipboard.setClipboard).toHaveBeenCalledTimes(1)
83+
it('toggle hidden files button should be hidden if file cache is not valid', () => {
84+
cache.status = 'busy'
85+
render(<Statusbar />, options)
86+
87+
expect(screen.queryByRole('button')).not.toBeInTheDocument()
7488
})
7589
})

src/components/dialogs/ShortcutsDialog.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const buildShortcuts = (t: TFunction<'translation', undefined>) => ({
4545
{ combo: 'mod + d', label: t('SHORTCUT.ACTIVE_VIEW.DELETE') },
4646
{ combo: 'mod + k', label: t('SHORTCUT.ACTIVE_VIEW.OPEN_TERMINAL') },
4747
{ combo: 'backspace', label: t('SHORTCUT.ACTIVE_VIEW.PARENT_DIRECTORY') },
48+
{ combo: 'mod + u', label: t('APP_MENUS.TOGGLE_HIDDEN_FILES') },
4849
],
4950
[t('SHORTCUT.GROUP.TABS')]: [
5051
{ combo: 'ctrl + tab', label: t('APP_MENUS.SELECT_NEXT_TAB') },

src/components/filetable/index.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { RowRenderer, RowRendererProps } from '$src/components/filetable/RowRend
3232
import { SettingsState } from '$src/state/settingsState'
3333
import { ViewState } from '$src/state/viewState'
3434
import { debounce } from '$src/utils/debounce'
35-
import { getSelectionRange } from '$src/utils/fileUtils'
35+
import { filterDirs, filterFiles, getSelectionRange } from '$src/utils/fileUtils'
3636
import { throttle } from '$src/utils/throttle'
3737
import { FileState } from '$src/state/fileState'
3838
import { FileContextMenu } from '$src/components/menus/FileContextMenu'
@@ -194,7 +194,7 @@ export class FileTableClass extends React.Component<Props, State> {
194194
if (cache) {
195195
// when cache is being (re)loaded, cache.files is empty:
196196
// we don't want to show "empty folder" placeholder
197-
// that case, only when cache is loaded and there are no files
197+
// in that case, only when cache is loaded and there are no files
198198
if (cache.cmd === 'cwd' || cache.history.length) {
199199
this.updateNodes(files)
200200
}
@@ -208,6 +208,15 @@ export class FileTableClass extends React.Component<Props, State> {
208208
},
209209
),
210210
)
211+
212+
this.disposers.push(
213+
reaction(
214+
(): boolean => {
215+
return !!this.cache?.showHiddenFiles
216+
},
217+
(): void => this.cache && this.updateNodes(this.cache.files),
218+
),
219+
)
211220
}
212221

213222
private getSelectedState(name: string): boolean {
@@ -238,11 +247,10 @@ export class FileTableClass extends React.Component<Props, State> {
238247
}
239248

240249
private buildNodes = (list: File[], keepSelection = false): TableRow[] => {
241-
// console.time('buildingNodes');
242-
const { sortMethod, sortOrder } = this.cache
250+
const { sortMethod, sortOrder, showHiddenFiles } = this.cache
243251
const SortFn = getSortMethod(sortMethod, sortOrder)
244-
const dirs = list.filter((file) => file.isDir)
245-
const files = list.filter((file) => !file.isDir)
252+
const dirs = filterDirs(list, showHiddenFiles)
253+
const files = filterFiles(list, showHiddenFiles)
246254

247255
// if we sort by size, we only sort files by size: folders should still be sorted
248256
// alphabetically
@@ -251,8 +259,6 @@ export class FileTableClass extends React.Component<Props, State> {
251259
.concat(files.sort(SortFn))
252260
.map((file) => this.buildNodeFromFile(file, keepSelection))
253261

254-
// console.timeEnd('buildingNodes');
255-
256262
return nodes
257263
}
258264

@@ -277,7 +283,6 @@ export class FileTableClass extends React.Component<Props, State> {
277283
}
278284

279285
private updateNodes(files: File[]): void {
280-
// reselect previously selected file in case of reload/change tab
281286
const keepSelection = !!this.cache.selected.length
282287

283288
const nodes = this.buildNodes(files, keepSelection)
@@ -395,7 +400,6 @@ export class FileTableClass extends React.Component<Props, State> {
395400
}
396401

397402
onRowClick = (data: RowMouseEventHandlerParams): void => {
398-
console.log('onRowClick')
399403
const { rowData, event, index } = data
400404
const { nodes, selected } = this.state
401405
const originallySelected = rowData.isSelected
@@ -782,7 +786,6 @@ export class FileTableClass extends React.Component<Props, State> {
782786

783787
onScroll = debounce(({ scrollTop }: ScrollParams): void => {
784788
this.cache.scrollTop = scrollTop
785-
// console.log('onScroll: updating scrollTop', scrollTop, this.cache.path);
786789
}, SCROLL_DEBOUNCE)
787790

788791
rowGetter = (index: Index): TableRow => this.getRow(index.index)
@@ -829,7 +832,6 @@ export class FileTableClass extends React.Component<Props, State> {
829832
]
830833

831834
renderFileContextMenu = (props: ContextMenu2ContentProps): JSX.Element => {
832-
console.log('file under mouse', this.state.rightClickFile, props.isOpen)
833835
return props.isOpen ? <FileContextMenu fileUnderMouse={this.state.rightClickFile} /> : null
834836
}
835837

src/components/shortcuts/KeyboardHotkeys.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,6 @@ class KeyboardHotkeysClass extends React.Component<WithTranslation> {
8686
ipcRenderer.invoke('openDevTools')
8787
}
8888

89-
onShowHistory = (): void => {
90-
const fileCache: FileState = this.getActiveFileCache(true)
91-
92-
if (fileCache && fileCache.status === 'ok') {
93-
console.log('showHistory')
94-
fileCache.history.forEach((path, i) => {
95-
const str = (fileCache.current === i && path + ' *') || path
96-
Logger.log(str)
97-
})
98-
}
99-
}
100-
10189
onDebugCache = (): void => {
10290
// let i = 0;
10391
// for (let cache of this.appState.views[0].caches) {
@@ -184,14 +172,6 @@ class KeyboardHotkeysClass extends React.Component<WithTranslation> {
184172
onKeyDown: this.onOpenDevTools,
185173
},
186174
/* debug only shortcuts */
187-
{
188-
global: true,
189-
combo: 'mod + h',
190-
label: this.injected.t('SHORTCUT.ACTIVE_VIEW.VIEW_HISTORY'),
191-
preventDefault: true,
192-
onKeyDown: this.onShowHistory,
193-
group: this.injected.t('SHORTCUT.GROUP.ACTIVE_VIEW'),
194-
},
195175
{
196176
global: true,
197177
combo: 'mod + p',

0 commit comments

Comments
 (0)