Теперь давайте начнем работать с логикой, сначала определим наш рабочий процесс.
Рабочий процесс
После открытия файлового менеджера мы загрузим путь root
, затем выведем список его директорий на боковой панели, а его содержимое (Files And Directories) на правой стороне.
Таким образом, нам нужны следующие состояния:
На этом пока все, давайте посмотрим на это в действии.
// FileManager.tsx
export default function FileManager({
open,
onClose,
rootPath,
}: FileManagerProps) {
const [isLoading, setIsLoading] = useState(true);
const [currentDirectoryNode, setCurrentDirectoryNode] = useState<Node>();
return (
<>
<Modal size="xl" opened={open} onClose={onClose}>
<Toolbar />
<BodyWrapper>
<Grid>
<Grid.Col span={3}>
<Sidebar />
</Grid.Col>
<Grid.Col span={9}>
<Content />
</Grid.Col>
</Grid>
</BodyWrapper>
</Modal>
</>
);
}
FileManager.defaultProps = {
rootPath: "/",
};
Я добавил rootPath
в объект props по умолчанию и создал два состояния, как упоминалось ранее.
Теперь давайте создадим новый экземпляр файлового менеджера и сохраним его в ref
.
import { Grid, Modal } from "@mantine/core";
import BaseFileManager from "app/file-manager/utils/FileManager";
import { useRef, useState } from "react";
import Content from "./Content";
import { BodyWrapper } from "./FileManager.styles";
import { FileManagerProps } from "./FileManager.types";
import Sidebar from "./Sidebar";
import Toolbar from "./Toolbar";
import { Node } from "../../types/FileManager.types";
export default function FileManager({
open,
onClose,
rootPath,
}: FileManagerProps) {
const [isLoading, setIsLoading] = useState(true);
const [currentDirectoryNode, setCurrentDirectoryNode] = useState<Node>();
const fileManagerRef = useRef(new BaseFileManager());
return (
<>
<Modal size="xl" opened={open} onClose={onClose}>
<Toolbar />
<BodyWrapper>
<Grid>
<Grid.Col span={3}>
<Sidebar />
</Grid.Col>
<Grid.Col span={9}>
<Content />
</Grid.Col>
</Grid>
</BodyWrapper>
</Modal>
</>
);
}
FileManager.defaultProps = {
rootPath: "/",
};
Мы переименовали его в BaseFileManager
, чтобы не путать его с самим компонентом.
На самом деле мы можем улучшить ссылку, непосредственно уничтожив клавишу current
.
- const fileManagerRef = useRef(new BaseFileManager());
+ const { current: fileManager } = useRef(new BaseFileManager());
Далее создадим хук useEffect
, который будет загружать корневой путь.
import { Node } from "../../types/FileManager.types";
export default function FileManager({
open,
onClose,
rootPath,
}: FileManagerProps) {
const [isLoading, setIsLoading] = useState(true);
const [currentDirectoryNode, setCurrentDirectoryNode] = useState<Node>();
const { current: fileManager } = useRef(new BaseFileManager());
// load root directory
useEffect(() => {
if (!rootPath) return;
setIsLoading(true);
fileManager.load(rootPath).then(() => {
setIsLoading(false);
setCurrentDirectoryPath(rootPath);
});
}, [rootPath, fileManager]);
return (
<>
<Modal size="xl" opened={open} onClose={onClose}>
<Toolbar />
<BodyWrapper>
<Grid>
<Grid.Col span={3}>
<Sidebar />
</Grid.Col>
<Grid.Col span={9}>
<Content />
</Grid.Col>
</Grid>
</BodyWrapper>
</Modal>
</>
);
}
FileManager.defaultProps = {
rootPath: "/",
};
Теперь мы добавили rootPath
в массив зависимостей, поэтому useEffect
будет вызван, как только rootPath
будет изменен, также мы вызвали load
, поскольку он загрузит заданный путь в качестве текущей директории.
Прежде чем перейти к методу load
, давайте добавим еще одно условие, что если файловый менеджер не открыт, то загрузка будет проигнорирована.
// load root directory
useEffect(() => {
if (!rootPath || !open) return;
setIsLoading(true);
fileManager.load(rootPath).then(directoryNode => {
setIsLoading(false);
currentDirectoryNode(directoryNode);
});
}, [rootPath, fileManager, open]);
Создание метода load
Метод load
загрузит заданный путь и вернет обещание, которое будет разрешено, когда путь будет загружен, также он вернет Node
с загруженной директорией.
Также нам нужно определить корневой путь в файловом менеджере, поэтому мы создадим метод setRootPath
.
// file-manager/utils/FileManager.ts
import { Node } from "../types/FileManager.types";
export default class FileManager {
/**
* Root path
*/
protected rootPath = "/";
/**
* Current directory path
*/
protected currentDirectoryPath = "/";
/**
* Current directory node
*/
protected currentDirectoryNode?: Node;
/**
* Set root path
*/
public setRootPath(rootPath: string): FileManager {
this.rootPath = rootPath;
return this;
}
}
Перейдя к FileManagerService
, создадим из него новый экземпляр и экспортируем его, чтобы мы могли использовать его напрямую.
В конце файла мы экспортируем экземпляр.
// file-manager-service.ts
const fileManagerService = new FileManagerService();
export default fileManagerService;
Теперь давайте определим метод load
в FileManager
.
// FileManager.ts
import fileManagerService from "../services/file-manager-service";
import { Node } from "../types/FileManager.types";
export default class FileManager {
/**
* Root path
*/
protected rootPath = "/";
/**
* Current directory path
*/
protected currentDirectoryPath = "/";
/**
* Current directory node
*/
protected currentDirectoryNode?: Node;
/**
* Set root path
*/
public setRootPath(rootPath: string): FileManager {
this.rootPath = rootPath;
return this;
}
/**
* Load the given path
*/
public load(path: string): Promise<Node> {
return new Promise((resolve, reject) => {
fileManagerService
.list(path)
.then(response => {
this.currentDirectoryPath = path;
this.currentDirectoryNode = response.data.node;
resolve(this.currentDirectoryNode as Node);
})
.catch(reject);
});
}
}
Мы использовали метод list
из FileManagerService
для получения узла каталога, затем мы сохранили его в currentDirectoryNode
и вернули его.
Но нам нужно сделать небольшое изменение в методе list
, нам нужно вернуть один узел из бэкенда, который содержит все перечисленные дочерние узлы непосредственно внутри него, чтобы бэкенд обработал его.
import FileManagerServiceInterface from "../types/FileManagerServiceInterface";
import {
+ newNode,
- listNodes
} from "../utils/data";
export class FileManagerService implements FileManagerServiceInterface {
/**
* {@inheritDoc}
*/
public list(directoryPath: string): Promise<any> {
return new Promise(resolve => {
resolve({
data: {
- node: listNodes(),
+ node: newNode(),
},
});
});
}
}
const fileManagerService = new FileManagerService();
export default fileManagerService;
Возвращаясь к методу load
, мы также определили узел текущей директории, который будет разрешен для обещания загрузки.
Теперь давайте перейдем к нашему компоненту FileManager.tsx
и console.log currentDirectoryNode
, чтобы увидеть, что будет возвращено
...
const { current: fileManager } = useRef(new BaseFileManager());
console.log(currentDirectoryNode);
Поскольку он генерируется случайным образом, давайте сделаем еще один генерируемый узел для каталогов, чтобы убедиться, что у нас всегда есть каталог.
// utils/data.ts
export function newNode(): Node {
const isDirectory = faker.datatype.boolean();
const node: Node = {
name: isDirectory ? faker.system.directoryPath() : faker.system.fileName(),
path: faker.system.filePath(),
size: faker.datatype.number({ min: 1, max: 100000 }),
isDirectory,
};
if (node.isDirectory) {
node.children = listNodes(1, 3);
}
return node;
}
export function newDirectoryNode() {
const node = newNode();
node.children = listNodes(faker.datatype.number({ min: 3, max: 4 }), 5);
node.name = faker.system.directoryPath();
node.isDirectory = true;
return node;
}
Я также сделал небольшую модификацию listNodes
, чтобы он принимал значения min и max, чтобы убедиться, что у нас всегда есть каталог с как минимум тремя дочерними элементами.
Теперь давайте обновим наш сервисный класс, чтобы вызывать newDirectoryNode
вместо newNode
.
- import { newNode } from "../utils/data";
+ import { newDirectoryNode } from "../utils/data";
return new Promise(resolve => {
resolve({
data: {
- node: newNode(),
+ node: newDirectoryNode(),
},
});
});
Здесь мы сделаем паузу и продолжим в следующей статье, если вы запутались, прочитайте финальный код из репозитория ниже, а затем прочитайте статью еще раз.
Следующая статья будет о листинге каталогов в боковой панели.
Репозиторий статей
Вы можете посмотреть файлы главы в репозитории Github
Не забывайте, что ветка
main
содержит последний обновленный код.
Салам.