1- import React , { createRef , useEffect , useState } from 'react' ;
1+ import React , { useState } from 'react' ;
22import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' ;
33import {
44 faCopy ,
@@ -7,189 +7,118 @@ import {
77 faLevelUpAlt ,
88 faPencilAlt ,
99 faTrashAlt ,
10+ IconDefinition ,
1011} from '@fortawesome/free-solid-svg-icons' ;
1112import RenameFileModal from '@/components/server/files/RenameFileModal' ;
1213import { ServerContext } from '@/state/server' ;
1314import { join } from 'path' ;
1415import deleteFile from '@/api/server/files/deleteFile' ;
1516import SpinnerOverlay from '@/components/elements/SpinnerOverlay' ;
1617import copyFile from '@/api/server/files/copyFile' ;
17- import { httpErrorToHuman } from '@/api/http' ;
1818import Can from '@/components/elements/Can' ;
1919import getFileDownloadUrl from '@/api/server/files/getFileDownloadUrl' ;
2020import useServer from '@/plugins/useServer' ;
2121import useFlash from '@/plugins/useFlash' ;
2222import tw from 'twin.macro' ;
23- import Fade from '@/components/elements/Fade' ;
23+ import { FileObject } from '@/api/server/files/loadDirectory' ;
24+ import useFileManagerSwr from '@/plugins/useFileManagerSwr' ;
25+ import DropdownMenu from '@/components/elements/DropdownMenu' ;
26+ import styled from 'styled-components/macro' ;
2427
2528type ModalType = 'rename' | 'move' ;
2629
27- export default ( { uuid } : { uuid : string } ) => {
28- const menu = createRef < HTMLDivElement > ( ) ;
29- const menuButton = createRef < HTMLDivElement > ( ) ;
30- const [ menuVisible , setMenuVisible ] = useState ( false ) ;
31- const [ showSpinner , setShowSpinner ] = useState ( false ) ;
32- const [ modal , setModal ] = useState < ModalType | null > ( null ) ;
33- const [ posX , setPosX ] = useState ( 0 ) ;
34-
35- const server = useServer ( ) ;
36- const { addError, clearFlashes } = useFlash ( ) ;
30+ const StyledRow = styled . div < { $danger ?: boolean } > `
31+ ${ tw `p-2 flex items-center rounded` } ;
32+ ${ props => props . $danger ? tw `hover:bg-red-100 hover:text-red-700` : tw `hover:bg-neutral-100 hover:text-neutral-700` } ;
33+ ` ;
3734
38- const file = ServerContext . useStoreState ( state => state . files . contents . find ( file => file . uuid === uuid ) ) ;
39- const directory = ServerContext . useStoreState ( state => state . files . directory ) ;
40- const { removeFile, getDirectoryContents } = ServerContext . useStoreActions ( actions => actions . files ) ;
35+ interface RowProps extends React . HTMLAttributes < HTMLDivElement > {
36+ icon : IconDefinition ;
37+ title : string ;
38+ $danger ?: boolean ;
39+ }
4140
42- if ( ! file ) {
43- return null ;
44- }
41+ const Row = ( { icon, title, ...props } : RowProps ) => (
42+ < StyledRow { ...props } >
43+ < FontAwesomeIcon icon = { icon } css = { tw `text-xs` } />
44+ < span css = { tw `ml-2` } > { title } </ span >
45+ </ StyledRow >
46+ ) ;
4547
46- const windowListener = ( e : MouseEvent ) => {
47- if ( e . button === 2 || ! menuVisible || ! menu . current ) {
48- return ;
49- }
48+ export default ( { file } : { file : FileObject } ) => {
49+ const [ showSpinner , setShowSpinner ] = useState ( false ) ;
50+ const [ modal , setModal ] = useState < ModalType | null > ( null ) ;
5051
51- if ( e . target === menu . current || menu . current . contains ( e . target as Node ) ) {
52- return ;
53- }
52+ const { uuid } = useServer ( ) ;
53+ const { mutate } = useFileManagerSwr ( ) ;
54+ const { clearAndAddHttpError , clearFlashes } = useFlash ( ) ;
5455
55- if ( e . target !== menu . current && ! menu . current . contains ( e . target as Node ) ) {
56- setMenuVisible ( false ) ;
57- }
58- } ;
56+ const directory = ServerContext . useStoreState ( state => state . files . directory ) ;
5957
6058 const doDeletion = ( ) => {
61- setShowSpinner ( true ) ;
6259 clearFlashes ( 'files' ) ;
63- deleteFile ( server . uuid , join ( directory , file . name ) )
64- . then ( ( ) => removeFile ( uuid ) )
65- . catch ( error => {
66- console . error ( 'Error while attempting to delete a file.' , error ) ;
67- addError ( { key : 'files' , message : httpErrorToHuman ( error ) } ) ;
68- setShowSpinner ( false ) ;
69- } ) ;
60+
61+ // For UI speed, immediately remove the file from the listing before calling the deletion function.
62+ // If the delete actually fails, we'll fetch the current directory contents again automatically.
63+ mutate ( files => files . filter ( f => f . uuid !== file . uuid ) , false ) ;
64+
65+ deleteFile ( uuid , join ( directory , file . name ) ) . catch ( error => {
66+ mutate ( ) ;
67+ clearAndAddHttpError ( { key : 'files' , error } ) ;
68+ } ) ;
7069 } ;
7170
7271 const doCopy = ( ) => {
7372 setShowSpinner ( true ) ;
7473 clearFlashes ( 'files' ) ;
75- copyFile ( server . uuid , join ( directory , file . name ) )
76- . then ( ( ) => getDirectoryContents ( directory ) )
74+
75+ copyFile ( uuid , join ( directory , file . name ) )
76+ . then ( ( ) => mutate ( ) )
7777 . catch ( error => {
78- console . error ( 'Error while attempting to copy file.' , error ) ;
79- addError ( { key : 'files' , message : httpErrorToHuman ( error ) } ) ;
8078 setShowSpinner ( false ) ;
79+ clearAndAddHttpError ( { key : 'files' , error } ) ;
8180 } ) ;
8281 } ;
8382
8483 const doDownload = ( ) => {
8584 setShowSpinner ( true ) ;
8685 clearFlashes ( 'files' ) ;
87- getFileDownloadUrl ( server . uuid , join ( directory , file . name ) )
86+
87+ getFileDownloadUrl ( uuid , join ( directory , file . name ) )
8888 . then ( url => {
8989 // @ts -ignore
9090 window . location = url ;
9191 } )
92- . catch ( error => {
93- console . error ( error ) ;
94- addError ( { key : 'files' , message : httpErrorToHuman ( error ) } ) ;
95- } )
92+ . catch ( error => clearAndAddHttpError ( { key : 'files' , error } ) )
9693 . then ( ( ) => setShowSpinner ( false ) ) ;
9794 } ;
9895
99- useEffect ( ( ) => {
100- menuVisible
101- ? document . addEventListener ( 'click' , windowListener )
102- : document . removeEventListener ( 'click' , windowListener ) ;
103-
104- if ( menuVisible && menu . current ) {
105- menu . current . setAttribute (
106- 'style' , `margin-top: -0.35rem; left: ${ Math . round ( posX - menu . current . clientWidth ) } px` ,
107- ) ;
108- }
109- } , [ menuVisible ] ) ;
110-
111- useEffect ( ( ) => ( ) => {
112- document . removeEventListener ( 'click' , windowListener ) ;
113- } , [ ] ) ;
114-
11596 return (
116- < div key = { `dropdown:${ file . uuid } ` } >
117- < div
118- ref = { menuButton }
119- css = { tw `p-3 hover:text-white` }
120- onClick = { e => {
121- e . preventDefault ( ) ;
122- if ( ! menuVisible ) {
123- setPosX ( e . clientX ) ;
124- }
125- setModal ( null ) ;
126- setMenuVisible ( ! menuVisible ) ;
127- } }
128- >
129- < FontAwesomeIcon icon = { faEllipsisH } />
130- < RenameFileModal
131- file = { file }
132- visible = { modal === 'rename' || modal === 'move' }
133- useMoveTerminology = { modal === 'move' }
134- onDismissed = { ( ) => {
135- setModal ( null ) ;
136- setMenuVisible ( false ) ;
137- } }
138- />
139- < SpinnerOverlay visible = { showSpinner } fixed size = { 'large' } />
140- </ div >
141- < Fade timeout = { 150 } in = { menuVisible } unmountOnExit classNames = { 'fade' } >
142- < div
143- ref = { menu }
144- onClick = { e => {
145- e . stopPropagation ( ) ;
146- setMenuVisible ( false ) ;
147- } }
148- css = { tw `absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48` }
149- >
150- < Can action = { 'file.update' } >
151- < div
152- onClick = { ( ) => setModal ( 'rename' ) }
153- css = { tw `hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded` }
154- >
155- < FontAwesomeIcon icon = { faPencilAlt } css = { tw `text-xs` } />
156- < span css = { tw `ml-2` } > Rename</ span >
157- </ div >
158- < div
159- onClick = { ( ) => setModal ( 'move' ) }
160- css = { tw `hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded` }
161- >
162- < FontAwesomeIcon icon = { faLevelUpAlt } css = { tw `text-xs` } />
163- < span css = { tw `ml-2` } > Move</ span >
164- </ div >
165- </ Can >
166- < Can action = { 'file.create' } >
167- < div
168- onClick = { ( ) => doCopy ( ) }
169- css = { tw `hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded` }
170- >
171- < FontAwesomeIcon icon = { faCopy } css = { tw `text-xs` } />
172- < span css = { tw `ml-2` } > Copy</ span >
173- </ div >
174- </ Can >
175- < div
176- css = { tw `hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded` }
177- onClick = { ( ) => doDownload ( ) }
178- >
179- < FontAwesomeIcon icon = { faFileDownload } css = { tw `text-xs` } />
180- < span css = { tw `ml-2` } > Download</ span >
181- </ div >
182- < Can action = { 'file.delete' } >
183- < div
184- onClick = { ( ) => doDeletion ( ) }
185- css = { tw `hover:text-red-700 p-2 flex items-center hover:bg-red-100 rounded` }
186- >
187- < FontAwesomeIcon icon = { faTrashAlt } css = { tw `text-xs` } />
188- < span css = { tw `ml-2` } > Delete</ span >
189- </ div >
190- </ Can >
97+ < DropdownMenu
98+ renderToggle = { onClick => (
99+ < div onClick = { onClick } >
100+ < FontAwesomeIcon icon = { faEllipsisH } />
101+ < RenameFileModal
102+ file = { file }
103+ visible = { ! ! modal }
104+ useMoveTerminology = { modal === 'move' }
105+ onDismissed = { ( ) => setModal ( null ) }
106+ />
107+ < SpinnerOverlay visible = { showSpinner } fixed size = { 'large' } />
191108 </ div >
192- </ Fade >
193- </ div >
109+ ) }
110+ >
111+ < Can action = { 'file.update' } >
112+ < Row onClick = { ( ) => setModal ( 'rename' ) } icon = { faPencilAlt } title = { 'Rename' } />
113+ < Row onClick = { ( ) => setModal ( 'move' ) } icon = { faLevelUpAlt } title = { 'Move' } />
114+ </ Can >
115+ < Can action = { 'file.create' } >
116+ < Row onClick = { doCopy } icon = { faCopy } title = { 'Copy' } />
117+ </ Can >
118+ < Row onClick = { doDownload } icon = { faFileDownload } title = { 'Download' } />
119+ < Can action = { 'file.delete' } >
120+ < Row onClick = { doDeletion } icon = { faTrashAlt } title = { 'Delete' } $danger />
121+ </ Can >
122+ </ DropdownMenu >
194123 ) ;
195124} ;
0 commit comments