11import * as React from 'react' ;
22import { 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' ;
44import { AppState } from '../state/appState' ;
55import { WithNamespaces , withNamespaces } from 'react-i18next' ;
66import { inject } from 'mobx-react' ;
77import i18next from 'i18next' ;
88import { IReactionDisposer , reaction , toJS } from 'mobx' ;
9- import { File } from '../services/Fs' ;
9+ import { File , FileID } from '../services/Fs' ;
1010import { formatBytes } from '../utils/formatBytes' ;
1111import { shouldCatchEvent , isEditable } from '../utils/dom' ;
1212import { AppAlert } from './AppAlert' ;
@@ -18,6 +18,7 @@ import { RowRenderer } from './RowRenderer';
1818import { SettingsState } from '../state/settingsState' ;
1919import { ViewState } from '../state/viewState' ;
2020import { debounce } from '../utils/debounce' ;
21+ import { TSORT_METHOD_NAME , TSORT_ORDER , getSortMethod } from '../services/FsSort' ;
2122
2223require ( 'react-virtualized/styles.css' ) ;
2324require ( '../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 )
0 commit comments