@@ -27,6 +27,7 @@ import dayjs from "dayjs";
27
27
import { PrimitiveAtom , atom , useAtom , useAtomValue , useSetAtom } from "jotai" ;
28
28
import { OverlayScrollbarsComponent , OverlayScrollbarsComponentRef } from "overlayscrollbars-react" ;
29
29
import React , { Fragment , memo , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
30
+ import { useDrag , useDrop } from "react-dnd" ;
30
31
import { quote as shellQuote } from "shell-quote" ;
31
32
import { debounce } from "throttle-debounce" ;
32
33
import "./directorypreview.scss" ;
@@ -657,34 +658,6 @@ function TableBody({
657
658
[ setRefreshVersion , conn ]
658
659
) ;
659
660
660
- const displayRow = useCallback (
661
- ( row : Row < FileInfo > , idx : number ) => (
662
- < div
663
- ref = { ( el ) => ( rowRefs . current [ idx ] = el ) }
664
- className = { clsx ( "dir-table-body-row" , { focused : focusIndex === idx } ) }
665
- key = { row . id }
666
- onDoubleClick = { ( ) => {
667
- const newFileName = row . getValue ( "path" ) as string ;
668
- model . goHistory ( newFileName ) ;
669
- setSearch ( "" ) ;
670
- } }
671
- onClick = { ( ) => setFocusIndex ( idx ) }
672
- onContextMenu = { ( e ) => handleFileContextMenu ( e , row . original ) }
673
- >
674
- { row . getVisibleCells ( ) . map ( ( cell ) => (
675
- < div
676
- className = { clsx ( "dir-table-body-cell" , "col-" + cell . column . id ) }
677
- key = { cell . id }
678
- style = { { width : `calc(var(--col-${ cell . column . id } -size) * 1px)` } }
679
- >
680
- { flexRender ( cell . column . columnDef . cell , cell . getContext ( ) ) }
681
- </ div >
682
- ) ) }
683
- </ div >
684
- ) ,
685
- [ setSearch , handleFileContextMenu , setFocusIndex , focusIndex ]
686
- ) ;
687
-
688
661
return (
689
662
< div className = "dir-table-body" ref = { bodyRef } >
690
663
{ search !== "" && (
@@ -700,13 +673,110 @@ function TableBody({
700
673
< div className = "dummy dir-table-body-row" ref = { dummyLineRef } >
701
674
< div className = "dir-table-body-cell" > dummy-data</ div >
702
675
</ div >
703
- { table . getTopRows ( ) . map ( displayRow ) }
704
- { table . getCenterRows ( ) . map ( ( row , idx ) => displayRow ( row , idx + table . getTopRows ( ) . length ) ) }
676
+ { table . getTopRows ( ) . map ( ( row , idx ) => (
677
+ < TableRow
678
+ model = { model }
679
+ row = { row }
680
+ focusIndex = { focusIndex }
681
+ setFocusIndex = { setFocusIndex }
682
+ setSearch = { setSearch }
683
+ idx = { idx }
684
+ handleFileContextMenu = { handleFileContextMenu }
685
+ ref = { ( el ) => ( rowRefs . current [ idx ] = el ) }
686
+ key = { idx }
687
+ />
688
+ ) ) }
689
+ { table . getCenterRows ( ) . map ( ( row , idx ) => (
690
+ < TableRow
691
+ model = { model }
692
+ row = { row }
693
+ focusIndex = { focusIndex }
694
+ setFocusIndex = { setFocusIndex }
695
+ setSearch = { setSearch }
696
+ idx = { idx + table . getTopRows ( ) . length }
697
+ handleFileContextMenu = { handleFileContextMenu }
698
+ ref = { ( el ) => ( rowRefs . current [ idx ] = el ) }
699
+ key = { idx }
700
+ />
701
+ ) ) }
705
702
</ div >
706
703
</ div >
707
704
) ;
708
705
}
709
706
707
+ type TableRowProps = {
708
+ model : PreviewModel ;
709
+ row : Row < FileInfo > ;
710
+ focusIndex : number ;
711
+ setFocusIndex : ( _ : number ) => void ;
712
+ setSearch : ( _ : string ) => void ;
713
+ idx : number ;
714
+ handleFileContextMenu : ( e : any , finfo : FileInfo ) => Promise < void > ;
715
+ } ;
716
+
717
+ const TableRow = React . forwardRef ( function (
718
+ { model, row, focusIndex, setFocusIndex, setSearch, idx, handleFileContextMenu } : TableRowProps ,
719
+ ref : React . RefObject < HTMLDivElement >
720
+ ) {
721
+ const dirPath = useAtomValue ( model . normFilePath ) ;
722
+ const connection = useAtomValue ( model . connection ) ;
723
+ const formatRemoteUri = useCallback (
724
+ ( path : string ) => {
725
+ let conn : string ;
726
+ if ( ! connection ) {
727
+ conn = "local" ;
728
+ } else {
729
+ conn = connection ;
730
+ }
731
+ return `wsh://${ conn } /${ path } ` ;
732
+ } ,
733
+ [ connection ]
734
+ ) ;
735
+
736
+ const dragItem : DraggedFile = {
737
+ relName : row . getValue ( "name" ) as string ,
738
+ absParent : dirPath ,
739
+ uri : formatRemoteUri ( row . getValue ( "path" ) as string ) ,
740
+ } ;
741
+ const [ { isDragging } , drag , dragPreview ] = useDrag (
742
+ ( ) => ( {
743
+ type : "FILE_ITEM" ,
744
+ canDrag : true ,
745
+ item : ( ) => dragItem ,
746
+ collect : ( monitor ) => {
747
+ return {
748
+ isDragging : monitor . isDragging ( ) ,
749
+ } ;
750
+ } ,
751
+ } ) ,
752
+ [ dragItem ]
753
+ ) ;
754
+
755
+ return (
756
+ < div
757
+ className = { clsx ( "dir-table-body-row" , { focused : focusIndex === idx } ) }
758
+ onDoubleClick = { ( ) => {
759
+ const newFileName = row . getValue ( "path" ) as string ;
760
+ model . goHistory ( newFileName ) ;
761
+ setSearch ( "" ) ;
762
+ } }
763
+ onClick = { ( ) => setFocusIndex ( idx ) }
764
+ onContextMenu = { ( e ) => handleFileContextMenu ( e , row . original ) }
765
+ ref = { drag }
766
+ >
767
+ { row . getVisibleCells ( ) . map ( ( cell ) => (
768
+ < div
769
+ className = { clsx ( "dir-table-body-cell" , "col-" + cell . column . id ) }
770
+ key = { cell . id }
771
+ style = { { width : `calc(var(--col-${ cell . column . id } -size) * 1px)` } }
772
+ >
773
+ { flexRender ( cell . column . columnDef . cell , cell . getContext ( ) ) }
774
+ </ div >
775
+ ) ) }
776
+ </ div >
777
+ ) ;
778
+ } ) ;
779
+
710
780
const MemoizedTableBody = React . memo (
711
781
TableBody ,
712
782
( prev , next ) => prev . table . options . data == next . table . options . data
@@ -837,6 +907,48 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
837
907
middleware : [ offset ( ( { rects } ) => - rects . reference . height / 2 - rects . floating . height / 2 ) ] ,
838
908
} ) ;
839
909
910
+ const [ , drop ] = useDrop (
911
+ ( ) => ( {
912
+ accept : "FILE_ITEM" , //a name of file drop type
913
+ canDrop : ( _ , monitor ) => {
914
+ const dragItem = monitor . getItem < DraggedFile > ( ) ;
915
+ // drop if not current dir is the parent directory of the dragged item
916
+ // requires absolute path
917
+ if ( monitor . isOver ( { shallow : false } ) && dragItem . absParent !== dirPath ) {
918
+ return true ;
919
+ }
920
+ return false ;
921
+ } ,
922
+ drop : async ( draggedFile : DraggedFile , monitor ) => {
923
+ if ( ! monitor . didDrop ( ) ) {
924
+ const timeoutYear = 31536000000 ; // one year
925
+ const opts : FileCopyOpts = {
926
+ timeout : timeoutYear ,
927
+ recursive : true ,
928
+ } ;
929
+ const desturi = await model . formatRemoteUri ( dirPath , globalStore . get ) ;
930
+ const data : CommandFileCopyData = {
931
+ srcuri : draggedFile . uri ,
932
+ desturi,
933
+ opts,
934
+ } ;
935
+ try {
936
+ await RpcApi . FileCopyCommand ( TabRpcClient , data , { timeout : timeoutYear } ) ;
937
+ } catch ( e ) {
938
+ console . log ( "copy failed:" , e ) ;
939
+ }
940
+ model . refreshCallback ( ) ;
941
+ }
942
+ } ,
943
+ // TODO: mabe add a hover option?
944
+ } ) ,
945
+ [ dirPath , model . formatRemoteUri , model . refreshCallback ]
946
+ ) ;
947
+
948
+ useEffect ( ( ) => {
949
+ drop ( refs . reference ) ;
950
+ } , [ refs . reference ] ) ;
951
+
840
952
const dismiss = useDismiss ( context ) ;
841
953
const { getReferenceProps, getFloatingProps } = useInteractions ( [ dismiss ] ) ;
842
954
0 commit comments