In this chapter, we're going to develop the file explorer. We're going to design the file folder data model inside of the convex schema. We're going to build a recursive tree component. We're going to create the collapsible folder behavior. We're going to implement file and folder icons using VS Code icons JS package.
And we're going to add file selection state management. Let's take a look at how that actually looks like when it's finished. So this is what we are supposed to have at the end of this chapter. Basically a file explorer that let us, well, explore files, folders, recursively. And we will also have the logic which decides what folder or file is currently open.
You can see that we even have this advanced temporary open file logic. So when I click on something else, it overrides that tab. But if I double click, it kind of makes it persistent. And then the next one I click goes in that other depth, similarly to how the actual VS Code works. And I think that's a better solution than just to open a bunch of tabs whenever you click.
So let's go ahead and focus on building that. I'm going to go ahead and make sure we have our app running and make sure you have NPX convex dev running as well since we are going to be updating our schema and creating some new functions here. So in our project, the localhost 3000 here, what we finished last time is the ability to click on a project. And inside of here we have allotment panes which we can resize. And we've also prepared the code and preview feature.
So now we're going to focus on the code feature. So make sure you have that opened here and let's start by defining the schema. The schema will basically define how this file explorer will actually be able to work. So I'm going to go ahead and create a new files table here. Files, let's use define table and let's go ahead and give it some properties.
So first things first, each file has to belong to a project. So each file needs to have a project ID with a type of ID, specifically project ID. Next, each file can technically be a folder or it can be inside of another folder. Because of that, a file may or may not have a parent ID. So it's optional and its ID is a reference to itself, another file.
Each file will also have a name and now we're going to add what I was talking about here for the parent ID. Each file can either be a file or a folder. Technically, we could separate those into two different tables, but I think the complexity is not necessary at this level of project. And more so, they're going to have a lot of similar fields, so it will mostly just be maintaining two almost identical tables. Because of that, giving it a type, which is a union, which can either be a file or folder, in my opinion, makes more sense.
So now we're going to add an optional field called content. Content will be optional and it's going to be a type of string and you can see I have a comment here text files only. Basically if our file is text based like majority of the files in a code editor, we will store that inside of content. But if we attempt to add PNG, JPEG, a GIF or anything like that, which is basically a binary file, and basically anything that is not a text file which is a binary file we have a couple of options to store that we technically could store that in content by converting it to base64 but as far as I know that really isn't optimized And I'm pretty sure it will break some limits in convex. So because of that, we're going to use convex storage.
So because of that, we're going to have storage ID, which again is optional. And it's basically going to have a reference to a storage property. So, VID underscore storage. So that's going to be used for binary files and it's optional. And the last property we're going to have is updated at.
And now let's go ahead and let's add some indexes here. So, the first index we're going to add is going to be by project and it's going to be referencing to the project ID. The second one will be by parent referencing to the parent ID and the third one will be a combination of the two if we ever need to do that. Great. So make sure that you don't have any errors here, you should have successful indexes, you should have everything successful here.
If you want to you can do npx convex dev again just to confirm that you don't have any errors. Great. So now that we have this done, let's go and inside of convex folder create a new files.ts. So this will be very similar to projects.ts. In fact, you can keep them both open because we are going to kind of copy and paste some things.
So how about we actually copy the get query since it will be quite similar. So I'm going to go ahead and paste this and I'm going to go ahead and I'm going to copy all the imports here. So we save some time. So make sure you have convex values, mutation query and verify out. So for the get one, let's call this get files and the arguments is going to accept will actually be project ID, the ID projects like that.
Same things as the usual, we need to verify our identity. And then we need to first fetch the project and make sure to extract the arguments. So why do we need to fetch the project if we are just getting files? Well first things first we need to check if this project still exists because if it doesn't we need to throw an error and not do any computation further. But there is another thing we have to check and that is the ownership.
So we shouldn't just allow a user to fetch files if they know a project id. We need to make sure that the user who is logged in actually has access to that project. And only then, let's go ahead and return await context database query files with index by project and then inside of here make sure to query for equals project ID arguments project ID. I'm going to try and expand this so you can see how it looks like in one line like this and make sure to execute collect. In the previous chapter I believe I made this mistake.
So make sure you don't do it. Great! That is our first one finished. So now I'm gonna go ahead and copy it and I'm going to create another one but this one will be called get files as in individual one and in here the argument will simply be an ID. ID of files.
So let's go ahead and first things first after we get the identity let's attempt to get the file and then if there is no file let's go ahead and throw an error, file not found. And now once we have the file, we can actually do file.projectid to do the exact same thing as above. And then finally, if all of the checks pass, we don't have to fetch anything new, we can just return the file. There we go. So that's our code to get the file as an individual one.
Now let's go ahead and let's build get folder contents. So again I'm going to copy get files because it's actually quite similar and I'm going to rename it to get folder contents and besides accepting the project ID we're also going to accept parent ID and that's a type of well it's not required so let's go ahead and give it optional. An ID of files. So, same things. We're going to verify our identity.
We're going to fetch the project. If it's not found, throw an error. If we don't have access to it, throw an error again. And then let's go ahead and just modify this again. So we're not returning early.
We're just going to define files as await context.database query files with index by project and by parent get the query and immediately return query equals project ID to arguments project ID and then go ahead and just chain maybe this will be easier to read like this chain parent ID arguments dot parent ID and collect them Once we have the files let's go ahead and sort them. So how do we want to sort them? Folders first, then files and alphabetically within each group. At least this is the pattern I've noticed in all code editors. All of them show folders first and then files below them so they don't actually mix folders and files.
I don't know if you've noticed that. You can see that all the folders are at the top and only then files come and same is true for individual inside of the folders. So I'm gonna go ahead and develop that here. Return files.sort we receive an argument a and argument B. First things first is we do folders before files.
So if a.type is folder and b.type is file return minus one as in the reverse order. And if a.type is file and b.type is folder, we just do the reverse. So reverse logic for a reverse return. And in here, within the same type, so if this is not the case, if all of these files are the same type, we just sort alphabetically by name. So return a.name, locale compare, b.name.
Like that. Great. And I'm actually kind of not sure just by looking at this. I think we can actually use getFolderContents without using getFiles because getFiles seems like a primitive version of get folder contents. So I'm going to see, except if we're going to use this later in some other area.
But I think we might not even need this, but still it's okay because we copied and pasted it here. So it's mostly the same, except this has proper sorting logic for the file explorer. But let's go ahead and continue developing all the mutations that we're gonna need here. So again, I'm going to copy the last one here, even though this one will be a little bit different because this one will be called create file and it's gonna be a mutation. So it will accept a project ID, it will accept an optional parent ID, it will accept a required name, and it will accept a required content.
So now let's go ahead and do what we usually do. Identity check. Check if the project doesn't exist. Check if we are the owner of that project. And then let's go ahead and check if the same file exists within the parent folder.
Because we cannot have the same named files, right? So that's why I'm adding this comment here. Let me show you that. If I go inside of convex and create a new file, files.ts, you can see I have an error. So we need the same behavior here.
So let's see, can we maybe reuse this? Files, query files by project and by parent. This stays exactly the same and collect. Great. And then let's go ahead and just check if an existing file is present.
So constant existing files.find file file.name is equal to arguments.name and file.type is equal to file because this method right here will be used to create file specifically not folder that's why we don't even accept the type here we're going to make the type be file so if this existing one is true we're going to throw an error. File already exists within this folder. And at the end here, we actually do not need this at all. Instead, we can just await context database insert into files, project ID, arguments, project ID, name, arguments, name, content, arguments, content type, we hard code it to file. Parent ID arguments, parent ID, updated at date dot now, and that's it.
That's how we create a file. So we just make sure that it doesn't already exist within the same project parent situation. And now let's go ahead and copy it and do the identical one for folder. We could probably create some abstraction to make this make more sense, but it's only two scenarios and I feel more confident having them separated so they don't mix or so I don't have to take care of things, right? So the only thing that's different here is folders do not accept any content.
So we can remove that. Other than that, everything should be exactly the same. So we do identity, we do the project. If it doesn't exist, we throw an error. If we don't have access to it, we throw an error.
And now we do the same thing here. So check if and change this if folder within the same name already exists in the parent folder. And then in the existing logic, simply look for file type folder and change this to folder already exists and then inside of here We're going to modify this to not pass any content and this be a folder like that. I think everything else is the same. Now we're going to implement the rename file logic.
So this one will be a little bit different and you're gonna see why. So let's go ahead and let me just fix this. This should be a curly bracket, not a square one. This too. The arguments it's going to accept is just the ID of the file we are going to rename and the new name.
Then let's define the handler. Let me fix this to be context. So first things first, we do the identity check as always. After we do a successful identity check, let's see if the file arguments ID exists or not. And if it doesn't, we can just go ahead and throw an error.
Then let's go ahead and using that file dot project ID, let's get the project this file is located in. And then we do our usual check. If there is no project, throw an error. If we don't belong to that project, throw an error as well. What we have to do now is we have to check if a file with the new name already exists in the same parent folder.
So what does that mean? Well, let me try and show you somewhere. I have to rename something. So if I want to rename projects to schema, same thing. Same behavior as creating a new file.
So that's what we're doing here. We have to check if a file with the new name already exists in the parent folder. So let's go ahead and let's fetch all the siblings. Siblings await context.database query files with index by project and by parent. And I'm going to do the same thing here so it's easier to understand.
So we are querying both by project ID using file.projectID and parent ID using file.parentID. And now we're going to check if we have an existing sibling. But this time we're going to check for both file and folder type at the same time. So existing siblings.find sibling if sibling.name is equal to arguments.newname and if sibling.type is equal to file type and if sibling id is not the same as the arguments id. So the only thing that should be allowed to have a new name is the very same file.
That's why this is not true because the point is if this results to true, we throw an error. So the only way this will not resolve to true and still have the same name and the same type if it's the ID is the same. Other than that, it's obviously a different file. If we detect that the name is the same, we are getting ready to throw an error, but not just yet, because you are allowed to have the same file and the same folder. Those are two different things, right?
That's why we also do a strict check on the type. And now if we have the existing, let's throw a new error and it will basically throw a folder or a file with this name already exists. So that's the difference. And then we just have to update the file's name. So let's go ahead and do that, update the file's name, like this.
Whoops, we don't need this one. So context.database.patch, files, arguments ID, new name and updated at. So yes, a lot of logic for a very simple result, but this is, you know, to create a good project, a high quality one. We didn't want this bugs to happen. We have authorization, we have authentication, and we even have this logic to make sure we don't have the two same named files, because if we do, that's an invalid file three structure, and this project cannot run.
Great, and we actually don't have to update the descendants right because of the way our schema is structured. We simply refer to the parent ID. So it doesn't matter if the name gets changed. Even if you change the name of the folder with a billion things inside, the parent ID is exactly the same to all of those inner files. So because of that, this is a quite of a simple update.
The one that will be a little bit different is delete file. And that's because we're gonna have to recursively delete things. And let's export const rename file. Let's not forget that. And let's copy the entire thing.
Let's paste it here. Let me find which is the new one. Here it is. This will be called delete file. So let's see.
We don't need any new name here, so we can remove this. We need the identity, file, file error. We get the project, we throw if project doesn't exist, we throw if we don't have access to it. And now we don't need to do the sibling check. In fact, I'm going to remove everything down here because it will be a little bit more complex than that.
So what do we actually have to do? Well, we have to recursively delete file or folder and all of its descendants. So basically we have to traverse to find the most, how do I call it, the lowest file in the file tree structure and make sure that when a parent is deleted all the things down to the last file are deleted as well. So let's go ahead and develop a constant delete recursive. Delete recursive is an asynchronous function which accepts a file ID which is basically a type of arguments.id or we can maybe ourselves say it's going to be this because that's what it is.
You can import id from generated data model. So let me scroll all the way down to delete recursive and let's go ahead and build it. First things first, we're going to check if the file actually exists. If it doesn't, we break the method. So, let's go ahead and check the scenario.
If it's a folder, we need to delete all children first. So I'm going to go ahead and open an if clause. And what I'm going to do is I'm first going to get all the children inside of this folder. So children, I'm going to use await context database, query all files with index by project and by parent. Again, I'm going to make this look a little bit prettier.
I find it easier to understand this way. So the way we are fetching all children of a folder is by using our project parent index and we are using item which we fetched right here dot project ID and The current file ID as the parent ID because that file itself is a folder. So we need to find all of its children, right? And now that we have that, we have to basically call this function again because each of these children can be a folder itself. So basically, for each child of the children defined above, we need to call delete recursive once more.
And then If that child is a folder, same thing will happen. That's how we're going to find every single child when we delete a folder. Great. And then outside of this if clause, if type is folder, let's go ahead and do delete storage file if it exists. So that's for files which are binary if you remember.
We are going to have this scenario later in the project but I think it makes sense to implement it now. So if we have storage id we should also clean up the storage, right? No need to have that populate our storage usage. So if we have item storage ID, go ahead and delete it. And then finally, delete the file or folder itself.
So it's safely gonna go ahead through all of the recursive things it needs to do and then in the end it's going to delete itself. So we are actually not calling this method right now, you can see it's just defined. So after you define it right here, await, delete recursive, and the first file we're going to pass inside will be the one we passed in the arguments here. There we go. And since we're finishing all the functions for files, I want to add one more.
Even though we are not exactly going to be using this inside of the file explorer, it's going to be the one that is going to be used later. And that is the general update file. Why do I say we're not going to use this right now? Well, because I remind you, we're just building the file explorer now. So we are only going to be able to rename, delete and create.
The update file is basically referring to modifying the content of the file, the actual code inside. So we're not going to see that now because we don't have the editor set up yet. But still, let's get ready. So the arguments it will accept is the ID and the new content that it's going to accept. And then As usual, let's go ahead and get the handler here.
So we can actually copy a lot of things from the lead file here. We can copy from identity all the way to here. So let's go ahead and just add that here. Basically we get the identity, we get the file, if file doesn't exist, we throw, if project doesn't exist, we throw, if we don't belong to a project, we throw. And now let's go ahead and define a variable called now to be date.now.
And then we're just gonna go ahead and await context.database.patch files arguments ID, content arguments.content and update it at to now. And one cool thing we're also gonna do, so you see this right here, which says saved two days ago. It would be fun for this to update every time we do some modifications for the files table. Because right now even though we modify updated at individually in the files, you can see here for example, This doesn't update the project table last updated at. So let's start with the last one, the last function we developed, the new one, updateFile.
At the end here, How about we add and also update the projects? So context database patch projects file project ID updated at now. And I feel like this is a cool thing that we can now add into other places too. So I'm going to copy this. I'm going to find the lead file.
How about we do the same thing. After you do the recursive delete. Let's also you know, update the project here. They thought now like this, right so we kind of say hey, some files were just deleted in this project. That probably means that when the user hovers over, we should say, yeah, we just saved those changes.
I think that makes sense. So I'm going to copy this again and let me add it to rename file as well. All of that makes sense if you ask me. If you want to use the exact one you can do const now and then replace both of this to use the same one. So that's the rename file.
We have create folder. Looks like this one doesn't have project ID but it does have arguments.projectId. I'm just gonna go ahead and make sure I have this. So now I'm gonna use now here and here. Okay so that was create folder.
Now let's do create file const now new whoops date dot now remove this and use the constant and let's see what else create file get folder contents shouldn't update it get file should not update it and get files shouldn't either so I believe those are all the changes we need for our files functions Let's check convex and let's run npx convex dev just to confirm we have no bugs when it comes to uploading these functions. There we go. All functions ready. So we are now ready to build the UI. So I'm going to go back inside of source, features, projects, components, and I'm going to go inside of project I.
D. View and inside of project I. D. View we have the placeholder for an editor. So let's go ahead and actually make this have its own allotment panes.
We're going to start by defining the constants. The minimum sidebar width, the maximum sidebar width, default, and default main size. And then let's go ahead and let's import allotment, our package that we used in the previous chapter, and let's go ahead and prepare the layout of the editor. So inside of here, instead of rendering just an empty div with a text editor. Let's render allotment.
The allotment will have default sizes, default sidebar width, and the default main size. The children are going to be an allotment.pane and this allotment pane will be our file explorer. And another allotment pane will be the editor view. And now let's go ahead and give this allotment pane for the file explorer, snap, minimum size, maximum size and preferred size. So now you should be able to see another split pane right here.
In the preview it shouldn't exist. It should only exist when the user selects they want to see the code. So basically The file explorer is where they are going to see well, the list of files, right? And the editor view you can see is larger and in here is where they are going to write the actual code. And they will be able to expand this or they will be able to collapse it and they can always bring it back.
So now we can go ahead and develop the file explorer. So I'm going to keep the project ID open And inside of project components I will create a new file, File Explorer. Inside of File Explorer, let's add an index.tsx and I will do export const File Explorer, return a div with a class name full height, background color of sidebar, scroll area from components UI, scroll area, a div with a row of button. Let's give it on click for now to just be an empty row function. A class name of group forward slash project.
Cursor pointer. Full width, text left, flex, items center, gap 0.5, height of 22 pixels, background color of accent, and font bold. Let's see how we can write this. So we can write this as height 5.5. There we go.
Inside of here, we're going to render a Chevron right icon from Lucid React. We're going to give the Chevron right icon a dynamic class name. So make sure to import the CNUtil. And I'm going to add the following. I'm also going to add a state is open set is open here use state with a default one set to false and I think that's the only one I'm going to need right now.
So then I can modify this div with a row of button to use set is open and set it to whatever is the opposite of the current value like this. So like a toggle. And then inside of here the default class of this Chevron right icon will be size 4, shrink 0 and text muted foreground. But if is open, we're going to rotate it by 90 degrees. Let's take a look at how this actually looks like.
The only thing I have to personally do is rearrange my imports because I prefer it this way. Great. So we now have the file explorer and I've purposely created it inside of a folder because we're going to have many subcomponents here. So let's go inside of project ID view here and let's replace the placeholder file explorer with the actual file explorer. Let's click save.
There we go. So you should now have this and you can see how it kind of opens, right? When you click on it, the chevron rotates by 90 degrees. Okay. Now what we have to do is we have to bring the project information from file explorer, I mean from project ID view to the file explorer.
We're going to do that using the project ID. So inside of here, just go ahead and pass it. Now inside of here, we have to define the props that it accepts. But that's quite easy. So it's going to be the only prop.
So project ID is a type of project. It is a type of project ID. Yeah. So just import this. And now in order to load a project using this project ID, we have to call our use project hook.
So const project, use project from hooks use projects and pass in project ID. So just make sure you've imported hooks use project. Great, and now that we have the project, we can actually go down here, right after the Chevron right icon. Open a paragraph and just render project question mark name or fallback to loading. And let's give this a text extra small, uppercase and line clamp one.
And just like that, you should now have your project's name printed out right here. Great. So now what we have to do is we have to add a button that will allow us to create new files or folders. So I'm gonna go ahead after this paragraph and I'm going to create a div. Now this div will have a following class name.
Opacity 0, on group hover let me go ahead and expand this so you can see it in one line. On group hover, but specifically on the project group, we are going to change the opacity to 100. Transition will be none, duration will be none. We're going to have flex, items center, Gap 0.5 and margin left of auto. In here I'm going to render chat CN button.
So I'm just going to move it here. Now let's go down to the button. The button in here is going to have an on click. And the first thing we're going to do is stop propagation. We're also going to prevent default.
Set is open to true and let's just add set creating to true comment here. Let's go ahead and give it a variant of highlight. Let's go ahead and give it a size of icon extra small. Okay, so what exactly is not working here? Button from components UI button Okay, I think we're gonna have to create both of those variants actually.
So let's go ahead and command click instead of button or go inside of source components UI and find the button here and what I'm gonna do is I'm gonna change the sizes here so I need a size smaller than this so I'm going to add size by 5.5 and I'm gonna make it rounded like this. And for the variants, I'm also gonna create my own variant called highlight. It's gonna have a transparent background. On hover, it's gonna use BGX in the foreground, but only on 5% opacity, and save the file. Close the button and go back.
And you can see that now, these are fully supported. So that's the power of ShadC and UI. We can now modify our button source code in real time. All right? So inside of this button, go ahead and create file plus corner icon from Lucid React.
And I think you can already see how this is gonna look like. When you hover, you should see a button. Let me go ahead and zoom in a bit. When you hover, you should see a button here. Maybe I can snap this.
There we go. You should see a button which will be used to create new files. And while we are here, we can also copy this button. So this one will be used set creating folder to true and the icon will be folder plus icon from Lucid React. So just make sure you've imported that and let's go ahead and copy this one one more time and then this one this one will be a little bit different so this one will just be reset collapse this one will be used to like collapse or expand again So let me show you copy minus icon from Lucid React again and give it a class name size 3.5 Let me fix the typo in the class name.
So let's see all the three icons that we have now So in here you can create new file, new folder, and you can kind of collapse the entire thing. So the easiest one to implement right away might actually be the collapse one. So we're going to go ahead and just add a new state here. Collapse key and set collapse key. And make the use state be at 0.
And while we are here, let's also implement creating and set creating from use state. So we will only be able to create a file or a folder and by default it's gonna be null. So let's set it to null. Great. Now that we have this, let's go ahead and check out the last button here, which is basically reset collapse.
So what we're gonna do is we're just going to increase the current or whatever it was the previous collapse key by one. And the way we're going to be using this is so that later when we implement the recursive tree, when the user clicks on this minus thing, it will kind of re-collapse the entire thing and user will have to load everything again. It serves as a kind of hard refresh of the entire thing. Not much purpose right now if you click on it nothing's happening but you will see how it works later. Perhaps I've implemented it too early so you're just confused now but you're going to see it's not too complicated.
It's like we're purposely going to change the key property that we're going to pass down here to some elements. So that's how it's going to work. And we could also set isOpen to false at that point, I think. Or maybe not, actually. Oh, well, we are implementing this.
I can show you exactly what we're doing. This. That's what we're doing. So yeah, it actually doesn't have to close the entire thing. It will literally just collapse all the folders that were opened.
Okay, so now let's take a brief pause at developing the file explorer and instead let's go ahead inside of project hooks and let's create use files. And inside of here let's go ahead and add our well let's see I'm trying to see what would be the most useful one for us to have I think the most useful one would be to list files and to create files. So let's go ahead and do export const use create file and we're going to do use create folder. They're going to be quite similar so we're just going to copy and paste them. So we're going to pass project ID and parent ID.
The types are going to be project ID, a type of ID, projects, parent ID is going to be optional and it's going to be a type of files. So just make sure you set the parent ID to optional because it's not required, right? And let's return useMutation API. We need to import API.files.createFile. So for now, that's all we have to do.
In fact, yeah, I don't think we even need to pass the project ID and the parent ID. And that reminds me, we also have to fix that problem from the previous chapter that Code Rabbit told us about. And I think we're doing the same mistake here. You know what? Let's do this.
Don't pass anything. Just very simply do this. And then in here let's do use create folder. Create folder. Just keep it simple.
Now let's go back instead of index here, basically our file explorer. Let's do const handle create and in here the name will be string. Now above this, let's define create file, use create file. Create folder, use create folder. So I've just imported from my hooks those two new hooks we've just created.
Now that I have those, I'm going to first set creating back to null. And then I'm going to do if creating is equal to file I'm gonna call create file I'm going to pass in the project ID the name the content to be empty and the parent ID to be undefined. Creating. Else, create folder with project ID, name and parent ID explicitly to be undefined. Alright, So now we have our handle create method right here.
And now, in these buttons here, Let's go ahead and make this one change set creating to be file. And this one to be set creating to be folder. Like that. And now I'm gonna go ahead outside of these two divs, but still inside of the scroll area. And I'm going to do, if it's open, let's go ahead and render a fragment.
And if we are creating something, let's go ahead and do the create input here. So the create input will have a type of creating, so what are we creating? A file or a folder? It's going to have a level, which is going to be 0 right now because this is kind of the root. OnSubmit which will be handleCreate.
OnCancel which is just going to set creating back to null. And that's the only thing we're gonna do now because in order to fetch any file we first need to create it right? Now let's go ahead inside of the file explorer new file create input.dsx ok so I'm gonna go ahead and import Chevron right icon from Lucid React I'm going to import file icon and I'm going to import folder icon from a package we have to install react symbols so react dash symbols forward slash icons forward slash utils let's go ahead and let's import reacts symbols npm install react dash symbols and let me actually check I think the full package name is forward slash icons So I'm going to show you what is my latest version here. We should no longer have an error here. There we go.
That's resolved. So I'm using 1.3.0. And from here I can import file icon and folder icon from react symbols icons utils like this let's export const create input Let's go ahead and define the types. So type will be either file or folder. Level is going to be a number.
OnSubmit will be a function which accepts a new name. OnCancel will very simply be a void. Now we can destructure all of those above so type level on submit and on cancel Now let's start by defining The value right Let's import use state from React. Let me move this at the top. So value and setValue.
Let's develop the handleSubmit method. Inside of here, we're going to trim the value so the user isn't able to pass like a blank space. And if trimmed value still exists, onSubmit trimmed value. Otherwise, just cancel. Great.
Now let's go ahead and let's return a div here with a class name, full width flex items center gap one height of 22 pixels or if I remember correctly 5.5 and the BG accent of 30. And in here another div with a class name flex-items-center and the gap 0.5 if type is equal to folder render a chevron right icon give it a class name size 4 shrink 0 and text muted foreground. If type is equal to file, go ahead and render file icon, give it a file name of value. Auto assign property and the class name of size 4. And let's go ahead and do one more type folder here.
Folder icon with a class name size 4, file name, actually folder name, value. Like this and then below this div an input and native HTML one give it autofocus type of text value of value on change event set value event target value. Now let's go ahead and pass it the following class name. Flex1 background transparent text small outline none focus ring one focus ring inset and focus ring ring. If the user blurs, we're going to consider that a submit.
And then a very simple onKeyDown method, which checks if the event.key is enter and submits and it also checks if event.key is escape and cancels. There we go. So there is actually one thing missing here but it's the level thing, right? But I just want to make sure that you can see what we're doing before we implement that so import the create input file and now if we've done this correctly first things first make sure you have this kind of open, right? And click on this.
And you should now be able to see an input to create a new file. But if you click this, you should see that it's kind of ready to create a new folder. And the cool thing is, if I go ahead and type, you know, test.jsx, you can see it changes the icon. If I do app.ts, it changes the icon. Tsx changes the icon again.
Same thing is true for folder. If I call this source, it will change the source. Test, same thing. I can't think of anyone right now, but I don't know images. There we go, changes to an appropriate images file.
So this package that does that magic is called react-symbols and it's actually made by the same person who developed SVGL app if you've heard about it where you can basically find a bunch of SVG. It's made by this guy Pablo Hernandez. I hope I pronounced it correctly. And icons crafted by Miguel. So shout out to those guys.
Amazing, amazing work. And you can see 229 file icons and 93 folder icons. And all of these, you can see we just installed a package and we just used their React symbols, icons, utils, file and folder icon, which allow us to pass the file name and auto assign which icon should be used, right? So every single thing that you see here can be rendered as a type of file. So If I use Docker, or maybe it's test.docker, actually I have no idea how to do it.
Docker file, there we go, it's Docker file. Even cloud it works, right? So every single thing you see here can be used in your file explorer. And I just wanna tell you one more thing. In case you're having troubles with this icons, for whatever reason, you can still continue with the project, right?
I mean, icons were kind of the last thing I implemented in this project. The reason I'm telling you is just in case you're having trouble, I don't know, I've had only the great things to say about this package, but you never know, you know, if something's not working for you or images are not loading for whatever reason, you can always, you know, not have this logic at all and just use the input. Right. But I think it should work just fine for you. Great.
So yes, I'm using React symbols, icon utils, and again, I'm using version 1.3.0 if you want to use the same one as me. Perfect. So even if we actually created something right now, test.jsx, we don't really know what happened until we go to our convex dashboard and we go inside of the project. And here in the database, Let's go inside of files and here we go. Test.jsx.
And I even have a one from before called images and I believe this one is a folder. It is. Great. So we can now officially create folders and files. The only thing that's kind of missing is this level thing.
This level thing will be used, let me show you exactly how. So this is the finished project. You can see that if I want to create a new file here, I kind of have to, oh, This is a bad example because it moves everything for some reason. But yeah, you can see how it's indented inside, right? That's the level, the level of indentation because the same component will be used here but also in here and you can see the level of indentation is different, right?
So that's what we're going to do now. So in order to develop that thing, what we have to do is develop something called getItemPadding and it's actually super simple to implement. So inside of file explorer create constants.ts and let's define base padding and level padding. So base padding for root level items and then additional padding for per nesting level. So the further down we go, it's going to move by 12.
And now a very simple function here. My apologies. GetItemPadding, which accepts a level and isFile boolean. Files need extra padding since they don't have the chevron. So if it's file, 16.
Otherwise, 0. And then a simple function to create the new padding. What do I mean by file doesn't have a chevron? Well, take a look. You can see it's just a file icon, but a folder has a chevron icon.
So we need to offset by the width of the chevron icon, which is 16 pixels, basically to make these two look the same. Because right now there's a visible size difference in width. Right? So that's what we're gonna do. Alright, so just make sure you've implemented getItemPadding.
Let's go back instead of createInput here. And now, what I'm gonna do is I'm going to give this a style property because Tailwind is kind of tricky when it comes to computed values because of its just-in-time compiler. So I believe you cannot do things like this using Tailwind. So we're going to pass in level and type equal to file. I mean we're going to do like a type check, right?
If the type is equal to file, then the second argument is file is going to be true. So now let's go ahead and test this out and there we go. You can see that now they are briefly at this same width level and there's an overall indentation here happening. Now I want to create a function or a hook more specifically that's actually going to help us see the files in our file explorer. So let's go inside of source, features, projects, hooks And let's go inside of our use files.
And now let's do export const use folder contents. And in here we are going to accept project ID, parent ID, enabled and hard-coded to true. And now in here let's go ahead and give it the types. ProjectId is an ID type of projects, parentId is optional, an ID of files, and enabled is an optional boolean and let's just return useQuery. Api.files.getFolderContents as the first argument.
If this function is enabled, pass in the project ID and the parent ID, otherwise skip the query. And import useQuery from convex react. There we go. Now we have useFolderContents. Now that we have useFolderContents, we can go back inside of our fileExplorerIndex.ts file.
And in here I'm going to define root files and my root files will be used folder contents from here and I'm going to pass in project ID and enabled only if is open. So if we close, no need to fetch because user isn't seeing anything anyways. Alright, now let me also move the use project together with it here. It kind of makes sense. Now that we have all of that, Let's go ahead and do a very simple loading indicator.
So if root files are undefined, it means they're loading. So let's render a loading row and let's give it a level of zero. So you're gonna see this behavior a lot of times. This level thing is basically our level of children indentation. And since this is the root level, all of it is zero.
And then later, as we defined in our constants, each extra child or nesting level will increase by 12. So that's why all of these are 0. So all of this calculation will result to 0. Because whatever you multiply right is 0 and then plus the offset. So only the offset will actually be calculated.
So loading row. Let's go inside of file explorer and create loading-row.tsx. And in here we import CN and we import spinner, we import get item padding from constants. Let's go ahead and export loading row with class name and level. Both of them are optional as you can see.
And then in here, let's go ahead and let's return. My apologies, what am I doing wrong here? Okay. Let's return a div with a class name, CN. First argument height 5.5, flex, items center and text muted foreground and passing the class name if we ever want to modify it from outside.
:12 Now let's pass in the style attribute here. Padding left, get item padding level and hardcode this to true. So this isn't exactly a folder loading level but we want to use the same offset as if it were a file. So I'm hardcoding the isFile property to true. Just so it looks good.
:33 That's the only reason. And in here we render the spinner and give it a class name size for TextRing ML 0.5. There we go. And now in here let's import the loading row. There we go.
:50 So now if you refresh here for a brief second, oh when you click open you should see loading, right? And then it's supposed to display all of the files. So that's what we're going to display now. Okay, let's go down here. So root files.
:09 Okay, let me expand this more. This is the creating scenario. And now we're going to have the actual root files dot map scenario. So question mark dot map, get the individual item, and for each item we're going to render something called a tree. And then each tree is going to have a key item underscore id combined with what collapse key remember we are using that so we easily reset the entire tree structure so that's why we're doing that great let's give it a item a level So this one by default will be zero.
:57 This is the root one. And you're going to see when these levels will start to increase in a moment. And we pass in the project ID. There we go. Now let's go ahead and let's develop the tree component.
:12 So let me go ahead and close everything actually. Go inside of features, projects, components, file explorer and let's do 3.tsx and in here again Let's import Chevron right icon, file icon and folder icon from React symbols, icon, utils. Let's go ahead and import CN from lib, utils. And let's go ahead and import use create file, use create folder, use folder contents. From features, actually I think can we just do...
:59 Yeah, we can just do hooks, use files like this. Or if you prefer you can use forward slash features projects hooks like that. Either will work. And Now let's go ahead and also import get item padding from constants, import loading row, import create input from create input, and import document from data model from convex ID as well and let's start building the tree. So the tree will be a very powerful component It will be able to do everything that our index file explorer can, but at an indented, nested level.
:54 Infinitely. Right? Recursively, should I say. Maybe that's a better word for it. So let's first define the types.
:02 It will accept an ID, my apologies, an item, which is a type of file, level, which is an optional number, and the project ID. Let's hard code the level to zero when we extract it. Now in here, we're going to start the same. Is open, set is open. Use state, false.
:26 Make sure to import use state from React. I'm just gonna move it to the top here. Okay. Now that we have that, let's go ahead and add is renaming, set is renaming, use date, false. And let's do creating, set creating, use state, and by default null.
:55 And now we simply go ahead and we give it those three possible types. File folder or null. Great. So now let's actually go ahead and pause And let's go back instead of hooks, use file, and I want to add use delete file, delete file. And as far as I know, those are the only ones we need, I think.
:37 We have create folder, we have create file, we have rename file, we have delete file. I think that should be okay. So now, back inside of the tree here, let's import useRenameFile and useDeleteFile from hooks. And now let's prepare them right here because we are going to kind of use all of them here. So const RenameFile, useRenameFile.
:07 Let me fix the typo. So const rename file. Use rename file. Let me fix the typo. Then this one will be delete file.
:13 So use delete file. Create file. Use create file. This one will be create folder use create folder. Alright perfect.
:29 Then let's do const folder contents, use folder contents passing the project ID, parent ID to be item, underscore ID, enabled to be item.type is equal to folder and only in that scenario we check for isOpen. Great. Now let's go ahead and well let's check. So actually yeah, here's the thing. We can start rendering things but we are missing a wrapper component.
:11 So I'm trying to think of how do I code this. So you don't because we've coded a lot right now but we aren't seeing anything and I don't like that since you know you're just getting confused like what are you coding here. So let me try and do something. How about we do this. If item.type is equal to file let's just return a div which says I am a file and then down here let's just return a div I am a folder then let's go back inside of a file Explorer Index, and let's import tree from ./.tree.
:57 And you should now see I am a folder and I am a file. If I click on a new file, I should see a new one. If I click on a new file, I should see a new one. If I click on a new folder, I should see a new folder, right? But in order to display them properly, we have to develop something called a tree item wrapper.
:14 So inside of the components right here, inside of File Explorer, let's create Tree-Item-Wrapper.tsx Let's go ahead and add all the imports, so CN from libutils, and then let's add all the components from context menu, so the menu, menu item, menu content, trigger, shortcuts, and separator. Then let's also import the constants, meaning get item padding, and document from convex generated data model. And now let's export const tree item wrapper. Now this is a component that's going to have a lot of props. So let's go ahead and extract all of of them.
:03 Item, children, level, isActive, onClick, onDoubleClick, onRename, onDelete, onCreateFile, and onCreateFolder. And inside of its props, We're going to define each of them. Item is a type of document file. Children are react.reactNode. Level is a required number.
:24 IsActive is an optional boolean. OnClick is an optional function. As well as all the other functions. So all of them are optional. Great.
:34 Now in here let's go ahead and let's return a context menu and let's add context menu trigger. Let's give it an as child property inside of here let's render a button and this button will simply render the children the button will have an onclick of onclick on double click On double click, so same named props. On key down, event. If event.key is equal to enter, we're going to prevent default and attempt to call on rename. And let's give it a class name which is going to be dynamic.
:22 So the classes that it's going to have is actually very similar to the createInput one. Group, flex, items center, gap one, full width height of 22 pixels or 5.5 hover BG accent with 30% opacity outline none and then all the focus ones and if is active BG dash accent with active bg-accent with forward slash 30 and then a style padding left get item padding level item dot type is equal to file This will return true or false and give our isFile boolean that value. Great. So that's for the context menu trigger. Now let's go inside of the context menu content.
:16 OnCloseAutoFocus here should simply do a prevent default. Class name should be W64. And then inside of here let's check. If item.type is equal to folder, we should have a fragment rendered, and then we should have some items inside. The first item will be a context menu item, which has a prop onClick and class name text small with a label new file.
:52 Then let's copy this, paste it, change this to new folder, on create folder, new folder. And then below that a context menu separator. Otherwise, outside of this right here, still inside of the context menu content, Render another context menu item with a prop onClickOnRename, classNameText small, a rename label, a context menu shortcut element next to its label which simply says enter. And then last item, we can actually duplicate this one if it's easier that way. This one should say delete permanently.
:51 And it should have an onClick on delete. And its context menu shortcut will be the command icon and backspace. We're going to implement the shortcuts later. For now, let's just have them visually. Some of you have already guessed what this is.
:09 We've created this tree item wrapper so that every time we right click on something here, we have these options, regardless how deep they are. We're always going to have either rename or delete so let's go ahead and continue developing now because the tree item is now finished so we can now go back to tree.tsx so I'm gonna go ahead in here item type file and instead of rendering a div we're going to render tree item wrapper so the tree item wrapper can now be imported from here And let's go ahead and give it some props. Item will be item, level will be level, isActive will be isActive. Looks like isActive is... We're not currently...
:09 Okay for now hard-coded to false on click for now shouldn't do anything we just have to remember to add it later same thing with on double click on rename should simply trigger the set is renaming to true on delete should let's add a comment close tab and then delete file and passing the id to be item underscore id. What does close tab mean? Basically later when we have the logic to have open and closed tabs, when we delete a file, we should also close if it's opened in the code editor, right? And what do we render inside? Well, either a file icon with filename, filename.
:02 Let me just see. So this file name, let's do const file name, item.name. The reason I'm defining it in a constant is because later when we add the rename functionality we're going to pass it through this constant as well. Let's go ahead and give this auto assign and class name size 4 and a span here file name and a class name truncate and text small. Let me fix this.
:39 Truncate. There we go. So now your files should display properly. Perfect. Now let's go ahead and do the same for folders.
:55 So I'm going to go ahead down here and I'm going to define folder name to be item.name and then let's go ahead and define const folder-content open a fragment, open a div give this a class name flex-items-center and gap 0.5 open chevron-right-icon give it a class name cn-size name flex, items center and gap 0.5, open chevron right icon, give it a class name cn size, oops, size dash 4, oops, shrink 0, text muted foreground is open, rotate 90 degrees, then render a folder icon from react symbols give it a folder name folder name like this give it a class name size 4 and outside of the div render a span rendering the folder name and give this a class name truncate text small and then Let's go ahead down here, replace this with tree item wrapper. Go ahead and render this within a fragment. Inside of tree item wrapper, render folder content. And in here, give it an item of item, give it a level of level, and then give it onClick to be just an empty arrow function. Same thing for onDoubleClick.
:47 OnRename, let's give it setIsRenaming to true. On double click, on rename, let's give it set is renaming to true, on delete, a comment to close the tab and then delete file, id item underscore id and I'm just going to change this to be to do, close tab and I'm going to change that here as well so I remember to search for that later So I don't forget. And in here let's also add onCreateFile startCreating, set it to file and onCreateFolder startCreating and set it to folder. So we don't have start creating. We can very easily implement that up here.
:41 Const start creating accepts either a file or a folder set is open to true and set creating to the type of past. So we don't have to do the same thing twice here. Great. And now here's the thing. We're not done yet when it comes to folder, because folder can be opened.
:05 And in that case, if the folder is opened, guess what? We have to render the entire thing again. So let's go ahead and do if folder contents are undefined, render the loading row with level level plus one. So we are finally indenting now. And if folder contents question mark map, get the individual sub item here.
:33 And guess what? We rendered the tree itself. Give it a key of sub-item, underscore ID, item of sub-item, and a level of level plus one. And finally, the project ID. There we go.
:55 So now, we should have a very basic file explorer working. Let's try and see that. So I'm going to collapse this and open this and there we go. So now when I create a new file and call it, I don't know, image.tsx, It's right here. When I create a new folder and I call it source, there we go.
:20 It's right here. The only thing that I don't see happening is that when I click on it, it should expand. So let's go ahead and just see how do we do that. Oh, okay. So down, down here, tree item wrapper, make sure it's the one which actually is the folder one, right?
:44 Because we have two instances of tree item wrappers. One is here, inside of an if clause, if item type is file, not that one, down here. And find it's on click, and simply do set is open, and then whatever is the opposite of the current value. And you can actually remove on double click. Nothing will happen if you double click on the folder.
:10 You can of course modify that for yourself, but there we go. Since we don't have any nested ones, oh yeah, I forgot to show you that. If you right click, you can see the delete works, yes, but none of the other ones are actually working. So yeah, you can try out to delete. It should be working just fine.
:32 The only thing you cannot test is kind of nested files. We can see I managed to delete all of my files, so that's working fine. Right. But the one we should test out, and the one I hope to, I hope to add a few more features and that is to rename a file and to create a new file and folder inside. Because I think that shouldn't be too complicated simply because we can just copy and paste the create input.
:06 In fact, for creating itself, we already have everything we need. We just have to find the folder content here. So right after we define it, before we return this, let's check if is creating and do the following. Return a fragment, open a button here, a native HTML button element, not chat-cn1. Let's give it an on click, set is open and then just do whatever is the opposite of its current value.
:43 That's the first one. Then Let's go ahead and give it a class name. Its class name will have group flex items center, gap one, height of 5.5, hover BG accent with 30% opacity, cursor pointed, pointer, actually we don't need cursor pointer since this is already a button and full width. Then let's go ahead and give it a style padding left, get item padding level and false since we know this is a folder and inside, render the folder content and then, isOpen should go ahead and render a fragment and then render folder contents, check if it's undefined and render the loading row. Give it level, level plus one, prop.
:42 Be very careful here. So not folder content, folder contents, right? You should probably go through the code and confirm you aren't doing that anywhere or maybe we can rename this to something better. Folder contents, I don't know, folder contents files. For now, I'm just going to leave it to be this.
:06 But make sure you aren't mistaking this variable right here. Alright so folder content is rendered inside of the button, but folder contents are all the files which belong to that folder. And then in here let's render the create input, which we already have imported. Give it a type of creating. Give it a level of level plus one, give it on submit to be handle create.
:39 I'm not sure we have that, we will implement it. And let's have set creating to null here. We're not done yet. We also have to check if folder contents has its children open. If it does, we have to render them using a tree.
:57 And then let's simply pass all the props the tree expects. Key, item, level, which is level plus one, and project ID. So the only one we don't have is the handle create method. So let's go up here and let's add it. Const handle create.
:21 We'll accept a name, which is a string. We're going to set creating to null if we are creating a file, we're going to call createFile() with projectID, name, emptyContent and parentID of item.underscoreID. Else we are going to create a folder with project ID, name and parent ID. Great! We shouldn't have any errors and now if you make sure you have a folder right click on it and new file and you should be able to create inner file.
:06 There we go. You should also be able to do inner folder just like that. And guess what? You should also be able to do inner inner file and you should also be able to do inner inner file and you should also be able to do inner inner folder and I think you get the point I know this was a lot of work but you can see that now we can create infinite nested files and folders We can also delete them and here's the thing. So I have one, two, three, let me see.
:39 One, two, three, four, five items here. If I delete app, I should have just one left. There we go. So our delete method is working correctly because all the nested files and folders were deleted as well. Very, very good.
:57 One more thing left to do since we are that close to kind of wrapping up this chapter and it kind of makes sense to do this in this chapter and that is the rename functionality. And I just can't get over how We are calling this thing folder content. Let's call it folder render because it's so close to being called folder contents which is something completely different and I have a feeling this will just confuse people. So let's rename this to folder render and let's see all the places where that is supposed to be rendered. Inside of if creating as the child of this button we should use folder render.
:49 Down here in the main return tree wrapper should use folder render. Everywhere else we are using folder contents. So you can see I have seven instances. Okay, well, not all of them, but five actually constant names, folder contents, but yeah, technically seven instances if we also count the name of the hook. So make sure you have the same numbers and that you didn't accidentally use folder render somewhere else.
:24 All right, one more thing to do, we can do it. Let's start by implementing the function handleRename the same way we have this handleCreate. So handleRename accepts a new name, set is renaming to false, if newName is equal to item.name we return and let's go ahead and simply call from this rename file. Great. That's the handle rename.
:50 And now let's go ahead and we have to do it in two places now. And we actually have to create it first. So let's go ahead and do the following. We're gonna go inside of source, features, projects, components, file explorer, copy the create input and rename it to rename input. Like so.
:15 Inside of the rename input, make sure to immediately change this and let's see the props so most of this will be the same but we're also gonna have default value and is open so let's make sure to add those two. Besides that, on cancel. Yeah, we should also have on cancel. So yeah, let's leave it as is. Value, set value will be the same.
:47 Trim the value. Yeah, I want the exact same logic as in my create input. I think that kind of makes sense. Right? So let's see.
:57 Width full, flex item center, gap 1, height 5.5, which is 22 pixels, BG accent, padding left. All of this looks perfectly fine. Chevron right icon. The only thing we should do here. So we have is open here.
:16 So perhaps what we should do if type is folder. How about we go ahead and wrap this instead of CN. So make sure you import CN. And if is open rotate by 90 degrees. So even.
:34 Okay. Just confirm you have this. So even in the rename input, if the folder is opened, let's rotate the Chevron right icon, let's be consistent, right? Because I think currently that's a bug, right? If I open a new folder and call it app Actually, I can't test it now.
:54 Yeah, okay, maybe it's not a bug. Yeah, because basically I was thinking should this chevron rotate? It shouldn't because we never open a non-created folder. So this is actually perfectly fine. We should only have isOpen here in the rename input.
:13 Okay. Now if it's type file, it's super simple. No changes here. And if it's type folder, same thing here. I think that folder icon here doesn't accept is open, so no need for that.
:28 And now for the input, I think Most of it is exactly the same. Out of focus, type is text, value is value, on change, flex 1, BG transparent, text small, outline none, focus is exactly the same, on blur, handle submit, on key down. Alright, the only thing I think I want to change is the following. I want to do on focus. If type is equal to folder, let's go ahead and simply select the entire thing but else if it's file let's go ahead and do the following let's get the value let's get last dot index value last index of dot if last dot index is larger than zero value, let's get last.index, value lastIndexOf.
:26 If last.index is larger than 0, let's go ahead and Set selection range to start from zero and go to the last dot index. Else select the entire thing. You're probably wondering what does that mean? You're going to see in a second. It's a small implementation which kind of improves the quality of life.
:50 So, okay, we have onBlur, onKeyDown, we have onCancel. So I think all of this should be fine. Okay, let's go ahead and render it. So I'm going to go inside of file explorer tree right here and the first place we should render this is down here. If item type is file and now if is renaming we have is renaming stored right here.
:26 So if is renaming we're going to return rename input. So make sure you import that because this is the first time we are adding it. The rename input should have a type of file because we know it's a type of of file, a default value of file name, a level of level, and on submit handle rename. And we are missing on cancel here, which should very simply set is renaming back to false. So let me just check how does handle rename work.
:14 If new name is item.name and instead of the rename input here... Okay. And I mean if it's new name should it also close? I don't know. We'll see.
:27 So now make sure you have a file. Right click, rename. Okay. Some... Kind of works but not really.
:36 Let's try this again. Right click rename. Okay, so it offers me to rename. Does it work? Let's see.
:43 Something new.tsx. It seems to work but not as I want it to. So it kind of completely resets the current state. Why is that happening? Let's take a look.
:55 So, if isRenaming here... Oh, we are missing something. We have the default value, which is filename. Let's go inside of renameInput here. We are never using the rename input's default value.
:10 So the default value should first of all be here. That's the first thing. And then in here, for the trimmed value, let's do either trimmed value or fallback to the default value. And then in that case, we can just call onSubmit, like that. Let's see if that improves it.
:36 So right click, rename, there we go. Do you see what this focus thing did? Basically it made it behave the same way as it does in a real code editor. When you focus on a file, it's not going to highlight the entire thing. It's going to carefully highlight only until the extension.
:59 So you can safely rename it without changing the extension of the file, which is actually exactly how it behaves in this. Take a look. If I want to change this, if I right click and rename, you can see it only highlights up to the extension. So I kind of wanted it to behave the same way. I want this to be a cool project, right?
:22 So I'm focusing on those details. Great. So we have that inside of the rename input. I think this behaves quite well. I'm not sure if we have to change anything.
:36 Let's see what if I rename and escape. So it will just kind of cancel, right? It won't do anything. I think that's fine. I think that's perfectly fine behavior.
:46 And one more place where we have to do this now is down here. So let's just copy the if clause, if creating and let's go ahead and now do if is renaming like that And then instead of a button here we will have a rename input and the rename input will have a type of folder default value folder name is open is open level level onSubmit handle rename and onCancel set isRenaming to false. There we go. And let's see Type null is not assignable. How did that happen?
:40 It's not assignable type of folder. That makes sense, yes. I mean I think all of this is still working just fine. How about we go inside of the Create Input and we allow it to be null? I don't think it's causing any problem.
:01 I think it works fine. I'm not sure if maybe it ruins something else here. Nope. Let me see. I think we can now rename folders.
:13 Yes, we can. So if I change this to tests, There we go. If I change this to something else, that works too. One thing I don't like is the flash of a previous one. We can fix this with optimistic mutation, But since this chapter is already an hour and 40 minutes long, I'm going to pause here.
:36 I think this will be the last thing we're going to implement. So basically you can see in tree.tsx, we successfully used every single thing. We no longer have any warnings about unused files or folders here. I guess if there was one thing I would have to do before we end, I just want to make sure I kind of abruptly ended the isRenaming here. I'm not sure I correctly checked if that's really all we have to do but I think it is, yeah.
:05 We have to render the rename input and then the rest is just the same, folder contents, loading row, level plus one, oh, create input. This is where it was throwing me the error. In isRenaming we do not need the createInput. We just need to check for folder contents. No need for the renameInput.
:32 And I think that now, instead of create input, I can remove this. Because we will never render the create input if we don't explicitly give it a type, I think. Okay, I just lied because we do that here very clearly but you can see that here it is already kind of solved that it's not gonna be null because we check that right here. Okay, I understand now. Okay, I guess there's only like one possible race condition if you at the same time create and rename, but I'm not sure how that would happen.
:17 So what if you do this and then right click? Yeah, you see, it's not possible because that resets it. So yeah, it can only be creating or renaming, not both at the same time. Great, so quite powerful file explorer from this session. We can do so many things inside of here.
:39 Let's see, can we rename? We can, amazing, amazing. I'm going to test this thoroughly before the next chapter if I can detect any bugs, but I think we did a very good job here and you can test it too, you know. Alright, one thing I want to do before we wrap up is just fix the bugs from the previous chapter. So The first thing is the invalid project ID label in our layout.tsx.
:07 So if we go inside of source app folder, projects, project ID, layout, here it is. Let's change this to be a type of ID from convex. Projects. Okay, that's one fixed. Now let's go down here.
:25 Okay, we don't have to fix this one. This is just telling us that it's an incomplete implementation, which it is. But in here, we do have something that's wrong. And it's the optimistic mutation. So let's go inside of our features, projects, hooks, use projects.
:45 And let's find this, use rename project. There is no reason this hook should be accepting the project ID. Why? Because we have it in the arguments. We can just do arguments.id.
:59 You can see we even have auto-complete. So we definitely know that this is the one and I think that now works perfectly fine just in case we can check let's go back here if I create a new one immediately created working just fine awesome So that all seems to be working. Let's close this and let's go ahead and merge our changes, right? So I'm going to shut down all of my terminals now. This is chapter 9, so git add and then a dot git commit 9 file explorer perfect git checkout dash b 09 file explorer git push u origin 09 file explorer perfect git checkout-b09-file-explorer git push uorigin09-file-explorer Perfect!
:58 Once this is pushed, You will see in your IDE the file explorer. And now let's go inside of our repository and let's open a new pull request. This was a lot of new files, 17, so definitely a good idea to get it checked by CodeRabbit. And here is the summary. New features.
:23 We introduced a File Explorer with collapsible tree view for managing project files and folders. We can create files and folders with inline editing. We can rename and delete files or folders via context menu options. We've added split pane layout with the file explorer sidebar and the editor pane. Folder contents has sorted with folders first and then files alphabetically.
:51 Project timestamps automatically updates when file changes. And now in here we have some comments. First things first, it commented on my usage of convex as comtex.database.batch. So as I told you, in the past, convex actually didn't allow passing the table name, right? They just accepted the ID, right?
:19 This didn't exist like a few weeks ago. So that's why AI is still outdated on that. Usually you would just pass in the arguments ID. So this is actually an update, a very recent one. So, So I gave it information about that, that the Convex's API has updated, and you can see that it read the documentation and it updated its learnings.
:43 So very cool to see how even these production-grade applications are using something like Firecrawl to update their learnings, which is exactly what we implemented a few chapters ago. So we keep seeing how useful tools like Firecrawl are. This is another comment on that same thing, so I made sure it knows that going forward and now it will not correct us anymore. Perfect. And in here, yes, there is kind of a problem here.
:12 It's what I told you in the beginning. We could technically create a separate files, folders and binary table. But I just found it to be, at least for a tutorial, very simple to keep it all together. Later, if you want to, you can keep all three separated because yes, there is technically a scenario here where we could have a storage id content and a type of folder which is something that shouldn't exist a folder cannot have content and it cannot have storage id A file cannot have both content and storage ID. Basically, it's telling us that we are not enforcing this at all.
:54 So right now, the only thing that's preventing this from not breaking is the fact that we know what's supposed to exist and what isn't. But yeah, we could either consider making strict checks in our create file and create folder so that storage can under no circumstances be added or we could look into some schema level enforcement in convex. But right now, I think we are completely okay going this way further. But I'm still very happy that CodeRabbit commented on this because yes there is kind of you know some odd behavior here. I mean it's not odd behavior.
:31 It's the fact that nothing is preventing us from creating a state which shouldn't exist. That's what CodeRabbit is commenting on. So good comment here, yes. In here it's commenting about the fact that we modified our layout.tsx project ID type from string to ID layouts. In fact, it's not telling us that we shouldn't use ID projects.
:55 It knows it should be ID projects. But it is warning us that Next.js route parameters are always a type of string. We are only using this so we get rid of the type errors. So here is the suggestion it gave me. Keep them as a string and then cast it as an ID.
:16 The reason this might be a better solution is because you never know that Next.js version might update, whereas if you define the wrong type in its params, because these are Next.js specific things, It might break the build. So I might actually lean towards doing this change. Because for long term, that might make more sense. You can see it completely understands what we're doing. So this is a branded convex type that requires validation, right?
:43 But it's suggesting casting that, rather than changing what is not true. Yes, Next.js project ID is 100% a string. That's simply how the framework works. It's never going to be this. But Technically, in our case, we know it is, so we're kind of using a hacky solution to fix our types.
:07 So perhaps we should do this in a proper way. We can even add runtime validation if we really want to, right? But convex usually takes care of that. And in here you can see it's telling us to double check if size 5.5 actually exists. And I've told it that it does.
:28 But you can see that even I, if you probably noticed, I didn't use size 5.5 in my code. I used size 22 pixels. And then Tailwind extension CSS told me, hey, you can use size 5.5. So it's obviously a very new thing that just came out and I couldn't even find public documentation about it. But you can see I just said that to CodeRabbit and it learned the same way I learned this chapter, right?
:56 The extension taught me and I just taught my AI reviewer here and you can see that it now knows that and it's not going to mess with that anymore. It here it suggests not using both default sizes and preferred size for the allotment paints. That's probably true. Yeah, I'm going to take a look if we can remove one or the another, or if it causes any problems. I think as long as it doesn't cause any errors or problems, we're good to go.
:23 Awesome. Amazing, amazing check by CodeRabbit here. So we're going to merge this. And now we should have this branch visible right here. Here it is.
:34 Let's go ahead and do git checkout main, git pull origin main. There we go. I think that marks the end of the chapter. Let's make sure we are on the main branch right here. And inside of here, I always like to confirm I've checked out number nine and merged it back to main.
:57 Amazing. So we've designed the file folder data model in context, we built a recursive tree component, we created collapsive folder behavior, we implemented file icons, and we did not implement this, but we did implement create, rename, and the context menu. So we actually did way more. And then in the next chapter, we're just going to have to implement this, which is way simpler than what we just did. Amazing, amazing job and see you in the next chapter.