Skip to content

Commit 5da9824

Browse files
committed
Continue with file manager code cleanup
1 parent 2692e98 commit 5da9824

File tree

4 files changed

+89
-150
lines changed

4 files changed

+89
-150
lines changed
Lines changed: 69 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { createRef, useEffect, useState } from 'react';
1+
import React, { useState } from 'react';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
33
import {
44
faCopy,
@@ -7,189 +7,118 @@ import {
77
faLevelUpAlt,
88
faPencilAlt,
99
faTrashAlt,
10+
IconDefinition,
1011
} from '@fortawesome/free-solid-svg-icons';
1112
import RenameFileModal from '@/components/server/files/RenameFileModal';
1213
import { ServerContext } from '@/state/server';
1314
import { join } from 'path';
1415
import deleteFile from '@/api/server/files/deleteFile';
1516
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
1617
import copyFile from '@/api/server/files/copyFile';
17-
import { httpErrorToHuman } from '@/api/http';
1818
import Can from '@/components/elements/Can';
1919
import getFileDownloadUrl from '@/api/server/files/getFileDownloadUrl';
2020
import useServer from '@/plugins/useServer';
2121
import useFlash from '@/plugins/useFlash';
2222
import 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

2528
type 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
};

resources/scripts/components/server/files/FileManagerContainer.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,29 @@ import { CSSTransition } from 'react-transition-group';
44
import Spinner from '@/components/elements/Spinner';
55
import FileObjectRow from '@/components/server/files/FileObjectRow';
66
import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs';
7-
import loadDirectory, { FileObject } from '@/api/server/files/loadDirectory';
7+
import { FileObject } from '@/api/server/files/loadDirectory';
88
import NewDirectoryButton from '@/components/server/files/NewDirectoryButton';
99
import { Link, useLocation } from 'react-router-dom';
1010
import Can from '@/components/elements/Can';
1111
import PageContentBlock from '@/components/elements/PageContentBlock';
1212
import ServerError from '@/components/screens/ServerError';
1313
import tw from 'twin.macro';
1414
import Button from '@/components/elements/Button';
15-
import useSWR from 'swr';
1615
import useServer from '@/plugins/useServer';
17-
import { cleanDirectoryPath } from '@/helpers';
1816
import { ServerContext } from '@/state/server';
17+
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
1918

2019
const sortFiles = (files: FileObject[]): FileObject[] => {
2120
return files.sort((a, b) => a.name.localeCompare(b.name))
2221
.sort((a, b) => a.isFile === b.isFile ? 0 : (a.isFile ? 1 : -1));
2322
};
2423

2524
export default () => {
25+
const { id } = useServer();
2626
const { hash } = useLocation();
27-
const { id, uuid } = useServer();
27+
const { data: files, error, mutate } = useFileManagerSwr();
2828
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
2929

30-
const { data: files, error, mutate } = useSWR(
31-
`${uuid}:files:${hash}`,
32-
() => loadDirectory(uuid, cleanDirectoryPath(window.location.hash)),
33-
);
34-
3530
useEffect(() => {
3631
setDirectory(hash.length > 0 ? hash : '/');
3732
}, [ hash ]);

resources/scripts/components/server/files/FileObjectRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => {
6969
}
7070
</div>
7171
</NavLink>
72-
<FileDropdownMenu uuid={file.uuid}/>
72+
<FileDropdownMenu file={file}/>
7373
</Row>
7474
);
7575
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import useSWR from 'swr';
2+
import loadDirectory, { FileObject } from '@/api/server/files/loadDirectory';
3+
import { cleanDirectoryPath } from '@/helpers';
4+
import useServer from '@/plugins/useServer';
5+
import { useLocation } from 'react-router';
6+
7+
export default () => {
8+
const { uuid } = useServer();
9+
const { hash } = useLocation();
10+
11+
return useSWR<FileObject[]>(
12+
`${uuid}:files:${hash}`,
13+
() => loadDirectory(uuid, cleanDirectoryPath(hash)),
14+
);
15+
};

0 commit comments

Comments
 (0)