Skip to content

Commit 5f59210

Browse files
committed
Use easy-peasy to store file state data
1 parent 918e0e2 commit 5f59210

File tree

11 files changed

+110
-34
lines changed

11 files changed

+110
-34
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"sockette": "^2.0.6",
2828
"styled-components": "^4.3.2",
2929
"use-react-router": "^1.0.7",
30+
"uuid": "^3.3.2",
3031
"ws-wrapper": "^2.0.0",
3132
"xterm": "^3.14.4",
3233
"xterm-addon-attach": "^0.1.0",
@@ -50,6 +51,7 @@
5051
"@types/react-router-dom": "^4.3.3",
5152
"@types/react-transition-group": "^2.9.2",
5253
"@types/styled-components": "^4.1.18",
54+
"@types/uuid": "^3.4.5",
5355
"@types/webpack-env": "^1.13.6",
5456
"@types/yup": "^0.26.17",
5557
"@typescript-eslint/eslint-plugin": "^1.10.1",

resources/scripts/api/server/files/loadDirectory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import http from '@/api/http';
2+
import v4 from 'uuid/v4';
23

34
export interface FileObject {
5+
uuid: string;
46
name: string;
57
mode: string;
68
size: number;
@@ -18,6 +20,7 @@ export default (uuid: string, directory?: string): Promise<FileObject[]> => {
1820
params: { directory },
1921
})
2022
.then(response => resolve((response.data.data || []).map((item: any): FileObject => ({
23+
uuid: v4(),
2124
name: item.attributes.name,
2225
mode: item.attributes.mode,
2326
size: Number(item.attributes.size),

resources/scripts/components/elements/Field.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ interface Props {
88
name: string;
99
label?: string;
1010
description?: string;
11+
autoFocus?: boolean;
1112
validate?: (value: any) => undefined | string | Promise<any>;
1213
}
1314

14-
export default ({ id, type, name, label, description, validate }: Props) => (
15+
export default ({ id, type, name, label, description, autoFocus, validate }: Props) => (
1516
<Field name={name} validate={validate}>
1617
{
1718
({ field, form: { errors, touched } }: FieldProps) => (
@@ -23,6 +24,7 @@ export default ({ id, type, name, label, description, validate }: Props) => (
2324
id={id}
2425
type={type}
2526
{...field}
27+
autoFocus={autoFocus}
2628
className={classNames('input-dark', {
2729
error: touched[field.name] && errors[field.name],
2830
})}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
import React, { createRef, useEffect, useState } from 'react';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
33
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons/faEllipsisH';
4-
import { FileObject } from '@/api/server/files/loadDirectory';
54
import { CSSTransition } from 'react-transition-group';
65
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt';
76
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
87
import { faFileDownload } from '@fortawesome/free-solid-svg-icons/faFileDownload';
98
import { faCopy } from '@fortawesome/free-solid-svg-icons/faCopy';
109
import { faLevelUpAlt } from '@fortawesome/free-solid-svg-icons/faLevelUpAlt';
1110
import RenameFileModal from '@/components/server/files/RenameFileModal';
11+
import { ServerContext } from '@/state/server';
1212

1313
type ModalType = 'rename' | 'move';
1414

15-
export default ({ file }: { file: FileObject }) => {
15+
export default ({ uuid }: { uuid: string }) => {
1616
const menu = createRef<HTMLDivElement>();
1717
const [ visible, setVisible ] = useState(false);
1818
const [ modal, setModal ] = useState<ModalType | null>(null);
1919
const [ posX, setPosX ] = useState(0);
2020

21+
const file = ServerContext.useStoreState(state => state.files.contents.find(file => file.uuid === uuid));
22+
if (!file) {
23+
return null;
24+
}
25+
2126
const windowListener = (e: MouseEvent) => {
2227
if (e.button === 2 || !visible || !menu.current) {
2328
return;

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
11
import React, { useEffect, useState } from 'react';
22
import FlashMessageRender from '@/components/FlashMessageRender';
33
import { ServerContext } from '@/state/server';
4-
import loadDirectory, { FileObject } from '@/api/server/files/loadDirectory';
54
import { Actions, useStoreActions } from 'easy-peasy';
65
import { ApplicationStore } from '@/state';
76
import { httpErrorToHuman } from '@/api/http';
87
import { CSSTransition } from 'react-transition-group';
98
import { Link } from 'react-router-dom';
109
import Spinner from '@/components/elements/Spinner';
1110
import FileObjectRow from '@/components/server/files/FileObjectRow';
12-
import { getDirectoryFromHash } from '@/helpers';
1311

1412
export default () => {
1513
const [ loading, setLoading ] = useState(true);
16-
const [ files, setFiles ] = useState<FileObject[]>([]);
17-
const server = ServerContext.useStoreState(state => state.server.data!);
1814
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
15+
const { contents: files } = ServerContext.useStoreState(state => state.files);
16+
const getDirectoryContents = ServerContext.useStoreActions(actions => actions.files.getDirectoryContents);
17+
18+
const urlDirectory = window.location.hash.replace(/^#(\/)+/, '/');
1919

2020
const load = () => {
2121
setLoading(true);
2222
clearFlashes();
23-
loadDirectory(server.uuid, getDirectoryFromHash())
24-
.then(files => {
25-
setFiles(files);
26-
setLoading(false);
27-
})
23+
24+
getDirectoryContents(urlDirectory)
25+
.then(() => setLoading(false))
2826
.catch(error => {
2927
if (error.response && error.response.status === 404) {
3028
window.location.hash = '#/';
@@ -36,7 +34,7 @@ export default () => {
3634
});
3735
};
3836

39-
const breadcrumbs = (): { name: string; path?: string }[] => getDirectoryFromHash().split('/')
37+
const breadcrumbs = (): { name: string; path?: string }[] => urlDirectory.split('/')
4038
.filter(directory => !!directory)
4139
.map((directory, index, dirs) => {
4240
if (index === dirs.length - 1) {
@@ -86,7 +84,7 @@ export default () => {
8684
<div>
8785
{
8886
files.map(file => (
89-
<FileObjectRow key={file.name} directory={getDirectoryFromHash()} file={file}/>
87+
<FileObjectRow key={file.name} file={file}/>
9088
))
9189
}
9290
</div>

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
99
import React from 'react';
1010
import { FileObject } from '@/api/server/files/loadDirectory';
1111
import FileDropdownMenu from '@/components/server/files/FileDropdownMenu';
12+
import { ServerContext } from '@/state/server';
13+
14+
export default ({ file }: { file: FileObject }) => {
15+
const directory = ServerContext.useStoreState(state => state.files.directory);
1216

13-
export default ({ file, directory }: { file: FileObject; directory: string }) => {
1417
return (
1518
<a
1619
key={file.name}
@@ -50,7 +53,7 @@ export default ({ file, directory }: { file: FileObject; directory: string }) =>
5053
distanceInWordsToNow(file.modifiedAt, { includeSeconds: true })
5154
}
5255
</div>
53-
<FileDropdownMenu file={file}/>
56+
<FileDropdownMenu uuid={file.uuid}/>
5457
</a>
5558
);
5659
};

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
11
import React from 'react';
22
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
33
import { Form, Formik, FormikActions } from 'formik';
4-
import { FileObject } from '@/api/server/files/loadDirectory';
54
import Field from '@/components/elements/Field';
6-
import { getDirectoryFromHash } from '@/helpers';
75
import { join } from 'path';
86
import renameFile from '@/api/server/files/renameFile';
97
import { ServerContext } from '@/state/server';
8+
import { FileObject } from '@/api/server/files/loadDirectory';
109

1110
interface FormikValues {
1211
name: string;
1312
}
1413

15-
export default ({ file, ...props }: RequiredModalProps & { file: FileObject }) => {
16-
const server = ServerContext.useStoreState(state => state.server.data!);
14+
type Props = RequiredModalProps & { file: FileObject };
15+
16+
export default ({ file, ...props }: Props) => {
17+
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
18+
const directory = ServerContext.useStoreState(state => state.files.directory);
19+
const pushFile = ServerContext.useStoreActions(actions => actions.files.pushFile);
1720

1821
const submit = (values: FormikValues, { setSubmitting }: FormikActions<FormikValues>) => {
19-
const renameFrom = join(getDirectoryFromHash(), file.name);
20-
const renameTo = join(getDirectoryFromHash(), values.name);
22+
const renameFrom = join(directory, file.name);
23+
const renameTo = join(directory, values.name);
2124

22-
renameFile(server.uuid, { renameFrom, renameTo })
23-
.then(() => props.onDismissed())
25+
renameFile(uuid, { renameFrom, renameTo })
26+
.then(() => {
27+
pushFile({ ...file, name: values.name });
28+
props.onDismissed();
29+
})
2430
.catch(error => {
2531
setSubmitting(false);
2632
console.error(error);
@@ -41,6 +47,7 @@ export default ({ file, ...props }: RequiredModalProps & { file: FileObject }) =
4147
name={'name'}
4248
label={'File Name'}
4349
description={'Enter the new name of this file or folder.'}
50+
autoFocus={true}
4451
/>
4552
<div className={'mt-6 text-right'}>
4653
<button className={'btn btn-sm btn-primary'}>

resources/scripts/helpers.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,3 @@ export function bytesToHuman (bytes: number): string {
44
// @ts-ignore
55
return `${(bytes / Math.pow(1000, i)).toFixed(2) * 1} ${['Bytes', 'kB', 'MB', 'GB', 'TB'][i]}`;
66
}
7-
8-
/**
9-
* Returns the current directory for the given window.
10-
*/
11-
export function getDirectoryFromHash (): string {
12-
return window.location.hash.replace(/^#(\/)+/, '/');
13-
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import loadDirectory, { FileObject } from '@/api/server/files/loadDirectory';
2+
import { action, Action, thunk, Thunk } from 'easy-peasy';
3+
import { ServerStore } from '@/state/server/index';
4+
5+
export interface ServerFileStore {
6+
directory: string;
7+
contents: FileObject[];
8+
getDirectoryContents: Thunk<ServerFileStore, string, {}, ServerStore, Promise<void>>;
9+
setContents: Action<ServerFileStore, FileObject[]>;
10+
pushFile: Action<ServerFileStore, FileObject>;
11+
removeFile: Action<ServerFileStore, string>;
12+
setDirectory: Action<ServerFileStore, string>;
13+
}
14+
15+
const files: ServerFileStore = {
16+
directory: '',
17+
contents: [],
18+
19+
getDirectoryContents: thunk(async (actions, payload, { getStoreState }) => {
20+
const server = getStoreState().server.data;
21+
if (!server) {
22+
return;
23+
}
24+
25+
const contents = await loadDirectory(server.uuid, payload);
26+
27+
actions.setDirectory(payload);
28+
actions.setContents(contents);
29+
}),
30+
31+
setContents: action((state, payload) => {
32+
state.contents = payload;
33+
}),
34+
35+
pushFile: action((state, payload) => {
36+
const matchIndex = state.contents.findIndex(file => file.uuid === payload.uuid);
37+
if (matchIndex < 0) {
38+
state.contents = state.contents.concat(payload);
39+
return;
40+
}
41+
42+
state.contents[matchIndex] = payload;
43+
}),
44+
45+
removeFile: action((state, payload) => {
46+
state.contents = state.contents.filter(file => file.uuid !== payload);
47+
}),
48+
49+
setDirectory: action((state, payload) => {
50+
state.directory = payload;
51+
}),
52+
};
53+
54+
export default files;

resources/scripts/state/server/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import getServer, { Server } from '@/api/server/getServer';
22
import { action, Action, createContextStore, thunk, Thunk } from 'easy-peasy';
33
import socket, { SocketStore } from './socket';
44
import { ServerDatabase } from '@/api/server/getServerDatabases';
5+
import files, { ServerFileStore } from '@/state/server/files';
56

67
export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running';
78

89
interface ServerDataStore {
910
data?: Server;
10-
getServer: Thunk<ServerDataStore, string, {}, any, Promise<void>>;
11+
getServer: Thunk<ServerDataStore, string, {}, ServerStore, Promise<void>>;
1112
setServer: Action<ServerDataStore, Server>;
1213
}
1314

@@ -49,13 +50,14 @@ const databases: ServerDatabaseStore = {
4950
state.items = state.items.filter(item => item.id !== payload.id).concat(payload);
5051
}),
5152
removeDatabase: action((state, payload) => {
52-
state.items = state.items.filter(item => item.id !== payload.id);
53+
state.items = state.items.filter(item => item.id !== payload.id);
5354
}),
5455
};
5556

5657
export interface ServerStore {
5758
server: ServerDataStore;
5859
databases: ServerDatabaseStore;
60+
files: ServerFileStore;
5961
socket: SocketStore;
6062
status: ServerStatusStore;
6163
clearServerState: Action<ServerStore>;
@@ -66,6 +68,7 @@ export const ServerContext = createContextStore<ServerStore>({
6668
socket,
6769
status,
6870
databases,
71+
files,
6972
clearServerState: action(state => {
7073
state.server.data = undefined;
7174
state.databases.items = [];

0 commit comments

Comments
 (0)