Skip to content

Commit c8349d8

Browse files
authored
Sort header (#39)
ADDED: new FsSort.ts with different sort methods ADDED: headerRenderer method that displays sort indicator and resize indicator ADDED: localized header names UPDATED: added bDate (Stat.birthtime) property to Fs.File CLEANED-UP: moved node creation into separate method IMPROVED: Parent element is now added from the UI (FileTable) instead of Fs, this allows sorting files without using hacks for the ".." parent dir CLEANED-UP: removed ugly hacks for ".." in sort methods ADDED: new FSAPI.getParent() which returns the parent File WIPE: click on headerColumn to sort FIXED: headerRenderer didn't use columnData for sortMethod ADDED: new File.id property with inode/dev, this allows to keep track of selected file even if name/path has changed
1 parent 5f60796 commit c8349d8

File tree

15 files changed

+340
-92
lines changed

15 files changed

+340
-92
lines changed

src/components/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ class App extends React.Component<WithNamespaces, IState> {
424424
onDebugCache = () => {
425425
let i = 0;
426426
for (let cache of this.appState.views[0].caches) {
427-
console.log('cache', cache.selected.length, cache.selected, cache.position);
427+
console.log('cache', cache.selected.length, cache.selected, cache.position, cache.selectedId);
428428
}
429429
}
430430

src/components/FileTable.tsx

Lines changed: 131 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import * as React from 'react';
22
import { IconName, Icon, Classes, HotkeysTarget, Hotkeys, Hotkey } from '@blueprintjs/core';
3-
import { Column, Table, AutoSizer, Index } from 'react-virtualized';
3+
import { Column, Table, AutoSizer, Index, HeaderMouseEventHandlerParams } from 'react-virtualized';
44
import { AppState } from '../state/appState';
55
import { WithNamespaces, withNamespaces } from 'react-i18next';
66
import { inject } from 'mobx-react';
77
import i18next from 'i18next';
88
import { IReactionDisposer, reaction, toJS } from 'mobx';
9-
import { File } from '../services/Fs';
9+
import { File, FileID } from '../services/Fs';
1010
import { formatBytes } from '../utils/formatBytes';
1111
import { shouldCatchEvent, isEditable } from '../utils/dom';
1212
import { AppAlert } from './AppAlert';
@@ -18,6 +18,7 @@ import { RowRenderer } from './RowRenderer';
1818
import { SettingsState } from '../state/settingsState';
1919
import { ViewState } from '../state/viewState';
2020
import { debounce } from '../utils/debounce';
21+
import { TSORT_METHOD_NAME, TSORT_ORDER, getSortMethod } from '../services/FsSort';
2122

2223
require('react-virtualized/styles.css');
2324
require('../css/filetable.css');
@@ -106,14 +107,14 @@ export class FileTableClass extends React.Component<IProps, IState> {
106107
constructor(props: IProps) {
107108
super(props);
108109

109-
this.viewState = this.injected.viewState;
110+
const cache = this.cache;
110111

111112
this.state = {
112113
nodes: [],// this.buildNodes(this.cache.files, false),
113114
selected: 0,
114115
type: 'local',
115-
position: this.cache.position,
116-
path: this.cache.path
116+
position: cache.position,
117+
path: cache.path
117118
};
118119

119120
this.installReaction();
@@ -126,7 +127,8 @@ export class FileTableClass extends React.Component<IProps, IState> {
126127
}
127128

128129
get cache() {
129-
return this.viewState.getVisibleCache();
130+
const viewState = this.injected.viewState;
131+
return viewState.getVisibleCache();
130132
}
131133

132134
private bindLanguageChange = () => {
@@ -157,7 +159,7 @@ export class FileTableClass extends React.Component<IProps, IState> {
157159
this.tableRef.current.scrollToPosition(scrollTop);
158160
}
159161

160-
this.cache.position = this.state.position;
162+
// this.cache.position = this.state.position;
161163
}
162164

163165
renderMenuAccelerators() {
@@ -226,35 +228,45 @@ export class FileTableClass extends React.Component<IProps, IState> {
226228
return !!cache.selected.find(file => file.fullname === name);
227229
}
228230

229-
private buildNodes = (files: File[], keepSelection = false): ITableRow[] => {
230-
// console.log('** building nodes', files.length, 'cmd=', this.cache.cmd, this.injected.viewState.getVisibleCacheIndex(), this.cache.selected.length, this.cache.selected);
231-
// console.log(this.injected.viewState.getVisibleCacheIndex());
231+
buildNodeFromFile(file: File, keepSelection: boolean) {
232+
const filetype = file.type;
233+
let isSelected = keepSelection && this.getSelectedState(file.fullname) || false;
232234

233-
return files
234-
.sort((file1, file2) => {
235-
if ((file2.isDir && !file1.isDir)) {
236-
return 1;
237-
} else if (!file1.name.length || (file1.isDir && !file2.isDir)) {
238-
return -1;
239-
} else {
240-
return file1.fullname.localeCompare(file2.fullname);
241-
}
242-
})
243-
.map((file, i) => {
244-
const filetype = file.type;
245-
let isSelected = keepSelection && this.getSelectedState(file.fullname) || false;
246-
247-
const res: ITableRow = {
248-
icon: file.isDir && "folder-close" || (filetype && TYPE_ICONS[filetype] || TYPE_ICONS['any']),
249-
name: file.fullname,
250-
nodeData: file,
251-
className: file.fullname !== '..' && file.fullname.startsWith('.') && 'isHidden' || '',
252-
isSelected: isSelected,
253-
size: !file.isDir && formatBytes(file.length) || '--'
254-
};
255-
256-
return res;
257-
});
235+
const res: ITableRow = {
236+
icon: file.isDir && "folder-close" || (filetype && TYPE_ICONS[filetype] || TYPE_ICONS['any']),
237+
name: file.fullname,
238+
nodeData: file,
239+
className: file.fullname !== '..' && file.fullname.startsWith('.') && 'isHidden' || '',
240+
isSelected: isSelected,
241+
size: !file.isDir && formatBytes(file.length) || '--'
242+
};
243+
244+
return res;
245+
}
246+
247+
private buildNodes = (list: File[], keepSelection = false): ITableRow[] => {
248+
console.time('buildingNodes');
249+
const { sortMethod, sortOrder } = this.cache;
250+
const SortFn = getSortMethod(sortMethod, sortOrder);
251+
const dirs = list.filter(file => file.isDir);
252+
const files = list.filter(file => !file.isDir);
253+
254+
const nodes = dirs.sort(SortFn)
255+
.concat(files.sort(SortFn))
256+
.map((file, i) => this.buildNodeFromFile(file, keepSelection));
257+
258+
// append parent element
259+
const path = this.cache.path;
260+
261+
// TODO: when enabling ftp again, there may be something wrong
262+
// with the dir of the parent element added here
263+
if (!this.cache.isRoot(path)) {
264+
const node = this.buildNodeFromFile(this.cache.getParent(path), keepSelection);
265+
nodes.unshift(node);
266+
}
267+
console.timeEnd('buildingNodes');
268+
269+
return nodes;
258270
}
259271

260272
_noRowsRenderer = () => {
@@ -279,7 +291,18 @@ export class FileTableClass extends React.Component<IProps, IState> {
279291
private updateState(nodes: ITableRow[], keepSelection = false) {
280292
const cache = this.cache;
281293
const newPath = nodes.length && nodes[0].nodeData.dir || '';
282-
this.setState({ nodes, selected: keepSelection ? this.state.selected : 0, position: keepSelection ? cache.position : -1, path: newPath });
294+
// TODO: retrieve cursor selection: this may have changed if:
295+
// - cache have change (new files, files renamed, ...)
296+
// - sort method/order has changed
297+
const position = keepSelection && this.getFilePosition(nodes, cache.selectedId) || -1;
298+
this.setState({ nodes, selected: keepSelection ? this.state.selected : 0, position, path: newPath });
299+
}
300+
301+
getFilePosition(nodes: ITableRow[], id: FileID): number {
302+
return nodes.findIndex(node => {
303+
const fileId = node.nodeData.id;
304+
return fileId && fileId.ino === id.ino && fileId.dev === id.dev
305+
});
283306
}
284307

285308
getRow(index: number): ITableRow {
@@ -292,11 +315,41 @@ export class FileTableClass extends React.Component<IProps, IState> {
292315
return (<div className="name"><Icon icon={iconName}></Icon><span title={data.cellData} className="file-label">{data.cellData}</span></div>);
293316
}
294317

318+
/*
319+
{
320+
columnData,
321+
dataKey,
322+
disableSort,
323+
label,
324+
sortBy,
325+
sortDirection
326+
}
327+
*/
328+
headerRenderer = (data: any) => {
329+
// TOOD: hardcoded for now, should store the column size/list
330+
// and use it here instead
331+
const hasResize = data.columnData.index < 1;
332+
const { sortMethod, sortOrder } = this.cache;
333+
const isSort = data.columnData.sortMethod === sortMethod;
334+
const classes = classnames("sort", sortOrder);
335+
336+
return (<React.Fragment key={data.dataKey}>
337+
<div className="ReactVirtualized__Table__headerTruncatedText">
338+
{data.label}
339+
</div>
340+
{isSort && (<div className={classes}>^</div>)}
341+
{hasResize && (
342+
<Icon className="resizeHandle" icon="drag-handle-vertical"></Icon>
343+
)}
344+
</React.Fragment>);
345+
}
346+
295347
rowClassName = (data: any) => {
296348
const file = this.state.nodes[data.index];
297349
const error = file && file.nodeData.mode === -1;
350+
const mainClass = data.index === - 1 ? 'headerRow' : 'tableRow';
298351

299-
return classnames('tableRow', file && file.className, { selected: file && file.isSelected, error: error });
352+
return classnames(mainClass, file && file.className, { selected: file && file.isSelected, error: error, headerRow: data.index === -1 });
300353
}
301354

302355
clearClickTimeout() {
@@ -306,6 +359,22 @@ export class FileTableClass extends React.Component<IProps, IState> {
306359
}
307360
}
308361

362+
setSort(newMethod: TSORT_METHOD_NAME, newOrder: TSORT_ORDER) {
363+
this.cache.setSort(newMethod, newOrder);
364+
}
365+
366+
/*
367+
{ columnData: any, dataKey: string, event: Event }
368+
*/
369+
onHeaderClick = ({ columnData, dataKey }: HeaderMouseEventHandlerParams) => {
370+
console.log('column click', columnData, dataKey);
371+
const { sortMethod, sortOrder } = this.cache;
372+
const newMethod = columnData.sortMethod as TSORT_METHOD_NAME;
373+
const newOrder = sortMethod !== newMethod ? 'asc' : (sortOrder === 'asc' && 'desc' || 'asc') as TSORT_ORDER;
374+
this.setSort(newMethod, newOrder);
375+
this.updateNodes(this.cache.files);
376+
}
377+
309378
onRowClick = (data: any) => {
310379
console.log('nodeclick');
311380
const { rowData, event, index } = data;
@@ -358,8 +427,9 @@ export class FileTableClass extends React.Component<IProps, IState> {
358427
newSelected--;
359428
}
360429

361-
this.setState({ nodes, selected: newSelected, position });
362-
this.updateSelection();
430+
this.setState({ nodes, selected: newSelected, position }, () => {
431+
this.updateSelection();
432+
});
363433
}
364434

365435
private onInlineEdit(cancel: boolean) {
@@ -401,9 +471,14 @@ export class FileTableClass extends React.Component<IProps, IState> {
401471
updateSelection() {
402472
const { appState } = this.injected;
403473
const fileCache = this.cache;
404-
const { nodes } = this.state;
474+
const { nodes, position } = this.state;
405475

406-
const selection = nodes.filter((node) => node.isSelected).map((node) => node.nodeData) as File[];
476+
const selection = nodes.filter((node, i) => i !== position && node.isSelected).map((node) => node.nodeData) as File[];
477+
478+
if (position > -1) {
479+
const cursorFile = nodes[position].nodeData as File;
480+
selection.push(cursorFile);
481+
}
407482

408483
appState.updateSelection(fileCache, selection);
409484
}
@@ -422,6 +497,7 @@ export class FileTableClass extends React.Component<IProps, IState> {
422497
}
423498

424499
toggleInlineRename(element: HTMLElement, originallySelected: boolean, file: File) {
500+
425501
console.log('toggle inlinerename');
426502
if (!file.readonly) {
427503
if (originallySelected) {
@@ -503,9 +579,9 @@ export class FileTableClass extends React.Component<IProps, IState> {
503579
i++;
504580
}
505581

506-
this.setState({ nodes, selected, position });
507-
508-
this.updateSelection();
582+
this.setState({ nodes, selected, position }, () => {
583+
this.updateSelection();
584+
});
509585
}
510586
}
511587

@@ -639,9 +715,9 @@ export class FileTableClass extends React.Component<IProps, IState> {
639715
nodes[position].isSelected = true;
640716

641717
// move in method to reuse
642-
this.setState({ nodes, selected, position });
643-
644-
this.updateSelection();
718+
this.setState({ nodes, selected, position }, () => {
719+
this.updateSelection();
720+
});
645721
}
646722
}
647723

@@ -669,6 +745,7 @@ export class FileTableClass extends React.Component<IProps, IState> {
669745
rowGetter = (index: Index) => this.getRow(index.index);
670746

671747
render() {
748+
const { t } = this.injected;
672749
const { position } = this.state;
673750
const rowCount = this.state.nodes.length;
674751
const scrollTop = position === -1 && this.cache.scrollTop || undefined;
@@ -678,12 +755,12 @@ export class FileTableClass extends React.Component<IProps, IState> {
678755
<AutoSizer>
679756
{({ width, height }) => (
680757
<Table
681-
disableHeader={false}
682758
headerClassName="tableHeader"
683759
headerHeight={ROW_HEIGHT}
684760
height={height}
685761
onRowClick={this.onRowClick}
686762
onRowDoubleClick={this.onRowDoubleClick}
763+
onHeaderClick={this.onHeaderClick}
687764
noRowsRenderer={this._noRowsRenderer}
688765
rowClassName={this.rowClassName}
689766
rowHeight={ROW_HEIGHT}
@@ -696,18 +773,21 @@ export class FileTableClass extends React.Component<IProps, IState> {
696773
width={width}>
697774
<Column
698775
dataKey="name"
699-
label="Name"
776+
label={t('FILETABLE.COL_NAME')}
700777
cellRenderer={this.nameRenderer}
778+
headerRenderer={this.headerRenderer}
701779
width={NAME_COLUMN_WIDTH}
702780
flexGrow={1}
781+
columnData={{ 'index': 0, sortMethod: 'name' }}
703782
/>
704783
<Column
705784
className="size bp3-text-small"
706785
width={SIZE_COLUMN_WITDH}
707-
disableSort
708-
label="Size"
786+
label={t('FILETABLE.COL_SIZE')}
787+
headerRenderer={this.headerRenderer}
709788
dataKey="size"
710789
flexShrink={1}
790+
columnData={{ 'index': 1, sortMethod: 'ctime' }}
711791
/>
712792
</Table>
713793
)

src/css/filetable.css

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,11 @@ body.bp3-dark .tableRow .bp3-icon{
104104
font-size:12px;
105105
}
106106

107-
.ReactVirtualized__Table__headerRow{
108-
-webkit-box-shadow: inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
109-
box-shadow: inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
110-
/* background-color: #f5f8fa; */
107+
.headerRow{
108+
-webkit-box-shadow: inset 0 -1px 0 rgba(16, 22, 26, 0.1);
109+
box-shadow: inset 0 -1px 0 rgba(16, 22, 26, 0.1);
111110
background-color:rgb(231, 236, 239);
112-
/* background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.8)), to(rgba(255, 255, 255, 0)));
113-
background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)); */
111+
border-right: 1px solid rgba(16, 22, 26, 0.2);
114112
color: #182026;
115113
font-weight:normal;
116114
text-transform:none;
@@ -120,10 +118,37 @@ body.bp3-dark .tableRow .bp3-icon{
120118
vertical-align:bottom;
121119
}
122120

121+
.ReactVirtualized__Table__headerTruncatedText:first-child{
122+
flex-grow:1;
123+
}
124+
125+
.tableHeader .resizeHandle, .tableHeader .sort {
126+
color:#ABAFB3;
127+
}
128+
123129
.tableHeader{
124-
border-right: 1px solid #ccc;
130+
display:flex;
125131
}
126132

127133
.tableHeader:last-child{
128134
border:none;
129-
}
135+
}
136+
137+
.bp3-dark .headerRow{
138+
background-color:rgb(66, 84, 97);
139+
color:#BFCCD6;
140+
}
141+
142+
.tableHeader .sort{
143+
align-self:flex-end;
144+
font-family:verdana;
145+
}
146+
147+
.tableHeader .sort.desc{
148+
align-self:flex-start;
149+
transform:rotateX(180deg);
150+
}
151+
152+
.bp3-dark .tableHeader .resizeHandle, .bp3-dark .tableHeader .sort{
153+
color: rgba(163, 188, 207, 0.589);
154+
}

0 commit comments

Comments
 (0)