So we stopped at creating a new directory in the server successfully, now let's make a tree based kernel to inject all of our nodes inside it.
But before that let's clean our code a little bit
Cleaning Our Code
Navigate to FileManager.tsx
the main component function, and update it with the following code:
import { Grid } from "@mantine/core";
import Content from "app/file-manager/components/Content";
import LoadingProgressBar from "app/file-manager/components/LoadingProgressBar";
import Sidebar from "app/file-manager/components/Sidebar";
import Toolbar from "app/file-manager/components/Toolbar";
import { KernelContext } from "app/file-manager/contexts";
import Kernel from "app/file-manager/Kernel";
import { useEffect, useRef } from "react";
import { BodyWrapper } from "./FileManager.styles";
import { FileManagerProps } from "./FileManager.types";
export default function FileManager({ rootPath }: FileManagerProps) {
const { current: kernel } = useRef(new Kernel(rootPath as string));
// load root directory
useEffect(() => {
if (!rootPath) return;
kernel.load(rootPath);
}, [rootPath, kernel]);
return (
<KernelContext.Provider value={kernel}>
<LoadingProgressBar />
<Toolbar />
<BodyWrapper>
<Grid>
<Grid.Col span={3}>
<Sidebar />
</Grid.Col>
<Grid.Col span={9}>
<Content />
</Grid.Col>
</Grid>
</BodyWrapper>
</KernelContext.Provider>
);
}
FileManager.defaultProps = {
rootPath: "/",
};
What we did here is we removed all defined states
as it is not needed here anymore and all of our states are now inside the Kernel
class.
Also we passed the rootPath
to the kernel class and we're loading the root directory inside the useEffect
hook.
Kernel Tree
Now let's add the constructor method to accept the rootPath
and initialize the tree
with it.
// app/file-manager/Kernel/Kernel.ts
import { Node } from "app/file-manager/Kernel/Node";
...
/**
* Constructor
*/
public constructor(rootPath: string) {
this.rootPath = rootPath;
}
Now let's define also our KernelTree
class beside the Kernel
class.
// app/file-manager/Kernel/KernelTree.ts
import Kernel from "./Kernel";
import { Node } from "./Kernel.types";
export default class KernelTree {
/**
* Root node
*/
public root?: Node;
/**
* Constructor
*/
constructor(public kernel: Kernel) {}
}
We injected the Kernel to the constructor so we can use any method/property from the kernel directly.
Now let's update our Kernel class to use the KernelTree
class.
// app/file-manager/Kernel/Kernel.ts
import events, { EventSubscription } from "@mongez/events";
import { createDirectory } from "../actions";
import fileManagerService from "../services/file-manager-service";
import { KernelEvents, Node } from "./Kernel.types";
import KernelTree from "./KernelTree";
export default class Kernel {
...
/**
* Kernel nodes tree
*/
public tree: KernelTree;
/**
* Root node
*/
public rootNode?: Node;
/**
* Constructor
*/
public constructor(rootPath: string) {
this.rootPath = rootPath;
this.tree = new KernelTree(this);
}
...
}
The concept of Kernel Tree
The concept of the kernel tree is to inject all of our nodes inside it, so we can easily access any node directly, update it, delete it or update its children list.
So Our KernelTree
class will have the following features:
- Set Node: Add the given node to the tree, if the node already exists, then update it.
- Get Node: Get the node by its path or the node itself.
- Delete Node: Delete the node by its path or the node itself.
- Get Parent Node: Get the parent node of the given node.
- Order Node Children: Order the children of the given node by the given order.
- Define node children as as directories and files.
Now let's start implementing the KernelTree
class.
// app/file-manager/Kernel/KernelTree.ts
import Kernel from "./Kernel";
import { Node } from "./Kernel.types";
export default class KernelTree {
/**
* Root node
*/
public root?: Node;
/**
* Constructor
*/
constructor(public kernel: Kernel) {}
/**
* Set root node
*/
public setRootNode(root: Node) {
this.root = root;
this.kernel.trigger("nodeChange", this.root);
this.prepareNode(this.root);
}
}
Here we added setRootNode
to define the top node that will have everything inside it, also we called prepareNode
which will do two things, to split the node children as directories and files and also order the children by the name alphabetically.
Preparing Node
As stated earlier, we'll split the node children as directories and files and also order the children by the name alphabetically.
// app/file-manager/Kernel/KernelTree.ts
...
/**
* Prepare the given node
*/
public prepareNode(node: Node) {
if (!node.children) return;
this.reorderChildren(node);
// set children directories
node.directories = node.children.filter(child => child.isDirectory);
// set children files
node.files = node.children.filter(child => !child.isDirectory);
}
/**
* Reorder node children by child name
*/
public reorderChildren(node: Node) {
node.children?.sort((a, b) => {
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) return 1;
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) return -1;
return 0;
});
}
But the Node
type does not have directories
and files
properties, so let's add them.
// app/file-manager/Kernel/Kernel.types.tsx
/**
* File Manager node is the primary data structure for the File Manager.
* It can be a directory or a file.
* It contains the following properties:
*/
export type Node = {
/**
* Node Name
*/
name: string;
/**
* Node full path to root
*/
path: string;
/**
* Node size in bits
*/
size: number;
/**
* Is node directory
*/
isDirectory: boolean;
/**
* Node children
* This should be present (event with empty array) if the node is directory
*/
children?: Node[];
/**
* Get children directories
*/
ππ» directories?: Node[];
/**
* Get children files
*/
ππ» files?: Node[];
};
Updating the Kernel tree
Now we need to update the tree once the node is loaded from the server so let's jump into the load
method.
/**
* Load the given path
*/
public load(path: string): Promise<Node> {
// trigger loading event
this.trigger("loading");
return new Promise((resolve, reject) => {
fileManagerService
.list(path)
.then(response => {
this.currentDirectoryPath = path;
if (response.data.node.path === this.rootPath) {
ππ» this.tree.setRootNode(response.data.node);
this.rootNode = response.data.node;
} else {
ππ» this.tree.setNode(response.data.node);
}
// trigger load event as the directory has been loaded successfully.
this.trigger("load", response.data.node);
// if the current directory is not as the same loaded directory path,
// then we'll trigger directory changed event.
if (response.data.node.path !== this.currentDirectoryNode?.path) {
this.trigger("directoryChange", response.data.node);
}
this.currentDirectoryNode = response.data.node;
resolve(this.currentDirectoryNode as Node);
})
.catch(reject);
});
}
Now we can update the tree node as we passed the node inside it, so once the node is loaded we check if its the root node, then we update the root node otherwise update the loaded node only.
Let's create setNode
method inside the KernelTree
class.
// app/file-manager/Kernel/KernelTree.ts
/**
* Add the given node to the tree
*/
public setNode(node: Node) {
// first find the parent node
let parentNode = this.parentNode(node);
// if it has no parent, which should not happen, then mark the root as parent
if (!parentNode) {
parentNode = this.root;
}
// if there is no parent, then do nothing and just return
if (!parentNode) return;
// a flag to determine if the given node was already existing but has been changed
let nodeHasChanged = false;
// a flag to determine if the parent node is changed
let parentHasChanged = false;
// now check if the node already exists in its parent
if (this.parentHas(parentNode, node)) {
// if it exists, replace it
parentNode.children = parentNode.children?.map(child => {
if (child.path === node.path) {
if (this.nodeHasChanged(child, node)) {
nodeHasChanged = true;
parentHasChanged = true;
}
return node;
}
return child;
});
} else {
// it means the node does not exist in the parent, then push it to the parent's children
parentNode?.children?.push(node);
this.kernel.trigger("newNode", node);
parentHasChanged = true;
// prepare the node
this.prepareNode(node);
}
// this will be only triggered if the node has changed
if (nodeHasChanged) {
this.prepareNode(node);
this.kernel.trigger("nodeChange", node);
}
// this will be only triggered if the parent node has changed
if (parentHasChanged) {
this.prepareNode(parentNode);
// as the parent node has changed thus the root node will be marked as changed as well
// we may later make it recursive to mark all the parent nodes as changed
this.prepareNode(this.root as Node);
this.kernel.trigger("nodeChange", parentNode);
this.kernel.trigger("nodeChange", this.root);
}
}
The code is self explanatory, we check if the node already exists in the parent, if it does then we replace it, otherwise we push it to the parent's children.
Let's add also the following methods:
-
parentNode
to get the parent node of the given node -
parentHas
to check if the parent node has the given node -
nodeHasChanged
to check if the given node has changed
// app/file-manager/Kernel/KernelTree.ts
/**
* Check if the given parent has the given node
*/
public parentHas(parent: Node, node: Node): boolean {
return parent.children?.some(child => child.path === node.path) ?? false;
}
/**
* Get parent node
*/
public parentNode(node: Node): Node | undefined {
return this.findNode(this.getParentPath(node.path));
}
/**
* Find node for the given path recursively in the tree
*/
public findNode(path: string): Node | undefined {
// loop starting from the tree root
const currentNode = this.root;
const findNode = (node?: Node): Node | undefined => {
if (node?.path === path) {
return node;
}
if (!node?.children) return undefined;
for (const child of node.children) {
const foundNode = findNode(child);
if (foundNode) return foundNode;
}
};
return findNode(currentNode);
}
/**
* Check if the given node has been changed
*/
public nodeHasChanged(oldNode: Node, newNode: Node): boolean {
return JSON.stringify(oldNode) !== JSON.stringify(newNode);
}
/**
* Get the parent path of the given path
*/
protected getParentPath(path: string): string {
if (!path) return "/";
// get the parent path by splitting the path and removing the last item
return path.split("/").slice(0, -1).join("/");
}
One last thing to do is to update the kernel events
// Kernel.types
/**
* Kernel events
*/
export type KernelEvents =
| "loading"
| "load"
| "directoryChange"
| "nodeChange"
| "nodeDestroy"
| "newNode";
We added nodeChange
that will be triggered when a node is changed, nodeDestroy
that will be triggered when a node is destroyed, and newNode
that will be triggered when a new node is added.
Next Chapter
In the next chapter we'll enhance the create directory, the sidebar and the content to watch for node changes.
Article Repository
You can see chapter files in Github Repository
Don't forget the
main
branch has the latest updated code.
Tell me where you are now
If you're following up with me this series, tell me where are you now and what you're struggling with, i'll try to help you as much as i can.
Salam.
Top comments (0)