Давайте создадим файловый менеджер React Глава VI: Получение каталогов и файлов

Теперь давайте начнем работать с логикой, сначала определим наш рабочий процесс.

Рабочий процесс

После открытия файлового менеджера мы загрузим путь 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 содержит последний обновленный код.

Салам.

Оцените статью
devanswers.ru
Добавить комментарий