So now that we have the ability to bulk delete our accounts, I want to wrap up the accounts entity by giving us the option to edit them and to also, well, open them exactly like this as we would a new account but instead I want it pre-filled with the data we have. So let's go ahead inside of our app folder, API and accounts API. Inside of here we have a get route and two post routes. So just below this first get route I want to go ahead and I want to chain another get route here and this one is going to be slash colon ID like this and inside of here we're going to add a z validator to validate the param which is going to be z.object specifically it's going to be a string but it's going to be optional and you're gonna see why later. Don't worry we are still going to validate it here.
So it's also gonna have the clerk middleware and then finally let's get our controller here. So just make sure that adding this get route didn't mess up the chaining below. So just ensure that this still has no errors. Inside of here, we're going to do what we always do. So let's go ahead and get the out using get out C.
And let's also the structure ID from C request valid. And we are specifically looking for params. If there is no ID we can return C.json and we can throw an error here with bad request. And let's write 400. Or we can be more specific and write missing ID.
And now let's write if there is no out question mark user ID. In that case we can copy and paste this error but with a different error message. So this one will say unauthorized and a status code will be 401. And now finally we can go ahead and we can destructure data immediately from this query. Await database.select.
So we are only going to get account from accounts.id and name from accounts.name. So remember this accounts variable is the schema in case you forgot about that. And all right so we chose what we select now from where? Well from the accounts schema but where specifically? And now we're gonna go ahead and use our chaining here first to ensure that what user is trying to fetch is truly theirs by comparing the user ID and next we are ensuring that we are only fetching the user account that was passed through the params.
Which params? The id params right here. So this should take care of that. We don't need returning because this is select, right? So it's automatically doing that.
But what we are going to check is if there is no data, right? So if we couldn't get anything in the array, In that case we can return c.jsonError not bound with a 404. And finally let's return the data. There we go. So now if you hover over your get here you can see alright so it looks like it can only load errors for now so we're gonna see a better response in the hook.
Let's go ahead and Let's create the hook now that we have a working API. And we can already try out the API. So go ahead and do the following. Make sure that you have like at least one or two accounts created and go inside of localhost 3000 slash API slash accounts. And now copy the ID and change your URL to slash API slash account slash ID.
And there we go. So this is that exact account and if I change it to this ID, then it's test two. If I slightly modify the ID, I get the error not found. Perfect. Now let's go ahead and turn that into a reusable hook here.
So we're gonna go ahead and do the following. We're gonna go inside of our features, accounts, API and let's copy useGetAccounts and let's rename it to useGetAccount. So individual. Make sure this is accounts and this is just a singular account. Let's rename the hook to useGetAccount as well and this one will have an optional ID which is a string.
This entire query is only going to be fetched if we actually have the ID. And the query key will be account and an object with ID. And now instead of calling accounts.get we're gonna call accounts.id.get but we also have to pass in a param which just happens to be this id. So this error will say fail to fetch individual account and this can stay exactly the same. So the reason I made it so that inside of accounts, ID is optional is simply because it's easier to get the types right, right?
So later you can see when we use this use get account, there is simply no way for us to ensure that the ID always exists. So we will know because of our code logic, we know that almost impossible will it be for it to happen that ID is passed as undefined and even if it is this query won't even run because of this enabled property right here but it's just easier for the types, right? So let's go ahead and leave it like that and now what I want to do is I want to go ahead and I want to create a ZustandController here. So instead of Features, Accounts, Hooks, let's create useOpenAccount.ts. So I don't want to call it useEditAccount because we're going to have that as a name here.
So let's call this useOpenAccount and we can copy everything from useNewAccount and paste it here. Let's rename this state to openAccountState. Let's rename the hook to useOpenAccount. Let's extend the state to accept an optional ID which is a string and let's modify the onOpen to require an ID to be passed if we are opening this drawer. So then we have to modify this as well.
ID by default will be undefined and on open, we'll have that ID. And besides setting is open to true, it will also assign the ID. And on close, we'll bring back the ID to undefined. Like that. There we go.
And now let's go ahead and do the following. Let's go inside of components and let's copy the new account sheet and let's rename this to edit account. There we go. Make sure you are inside of edit account and inside of here we're gonna rename this to edit account as well like that and let's change this and let's remove use new account completely and instead we're gonna use use open account like that so add use open account I'm gonna add the features accounts like this And then let's see what else we are going to need. So this can stay the same use create account.
It doesn't matter. What matters is that we have the ability to fetch our account here. So this is what I'm going to do. I'm going to go ahead here and I'm going to add const account query is going to be use get account. So make sure it's not accounts, just the single account.
And let me go ahead and change this to Features, Accounts, and let me move it up with all the other features here. So inside of the account query, we're gonna pass in use get account and remember we have to pass in the ID and we can now destructure the ID from here as well because we just changed this to this use open account hook where we defined the ID and we can now pass that inside of here, like that. Perfect. So we now have the account query now. So let's go ahead and do this.
Let's define const default values. Let's make that if we have accountQuery.data in that case go ahead and make it name account query dot data dot name otherwise make it name an empty string and now go ahead and simply pass in the default values here like this there we go So I think this already might be enough for us to attempt and do something. So we need to add this inside of our providers. Sheet provider here. So I'm going to remove this import, we don't need it.
And let's import edit account sheet from features accounts components edit account and simply add it here. Like that. There we go. And now we have to find a way to open it. So for that, I want to go back inside of the columns of my account page.
So let's go inside of app, inside of dashboard accounts columns right here. And we now have the checkbox for selection, we have the name field. So inside of this columns let's add the last field here which is going to have an id of actions and cell is going to be row and it's going to return the actions component which at the moment does not exist. So let's go ahead and do the following. In order to create it, let's go inside of the terminal and let's shut down the app and let's write bunx chat-cnui.atlatest and let's add drop-down menu.
So that's the component that we need for this. So Bonnerun-dev again. Now we're gonna go inside of the accounts on the same level as columns and we're gonna write actions.psx. Let's mark this as use client and let's export const actions and let's return a div saying actions. Let's quickly just give it some props id of a string and let's destructure the same props here.
Now let's go back inside of columns and let's add the actions from slash actions like that and let's pass in the ID to be row.original.id. There we go. Let's go ahead and let's check out our localhost 3000 and more specifically the actions page the accounts page right here let's see there we go so now we have this third column here which just simply says actions. We're not going to change that to be a drop-down menu. That's why we've added that.
So let's go ahead and import everything we need from components UI drop-down menu here. We're going to need the drop-down menu itself, the drop-down menu content, the item, and we're going to need the drop-down menu trigger like this. Great. And now let's go ahead and change this entire thing to be a fragment. Let's change this to be a drop-down menu.
Let's add a drop-down menu trigger. Let's give it the property as child. So it looks like it's child, which is going to be a button component. Make sure you add the import for the button as I did here and it's going to use an icon more horizontal from Lucid React so ensure that you have this as well. And this button here is going to have a variant of Ghost and a class name size 8 and padding of 0.
Like this. Just below the trigger add a drop down menu content with an align of end. Inside of here, add a drop-down menu item, which is going to say edit. Before the paragraph edit, we're going to have the icon edit. So I import icon again from Lucida React.
And I'm going to give the edit a class name of size four and margin right of two. And for the dropdown menu item, I'm going to give it a disabled of false and on click also just an empty void for now. Let's go ahead and check this out. There we go. We can now click and we can click edit right here.
So let's go ahead and let's go back inside of the actions here and let's give this more horizontal a class name size 4 just so it doesn't look so enormous. There we go. So now it looks better And now what I want is that when we click on edit, is that we call a hook just like this, but our new use open account hook. So let's add import use open account from features account hooks. Use open account.
I'm going to separate the features as always. So inside of here, I'm gonna go ahead and I'm going to call this use open account. And this will be on open. We can actually just destructure on open very simply. And inside of here, you're gonna call onOpen and you're gonna pass in the ID as the prop.
So what's the flow here? OnOpen is going to very simply, let's go ahead and open useOpenAccount. So onOpen is going to set the id and isOpen to true. IsOpen in return is going to make the EditAccountSheet component rendered and then the moment it renders is it's going to destruct from this ID and it's going to enable this query which is going to fetch by ID and then using that it's going to fill the default values right here. So let's see if that is working.
So I'm going to go ahead and refresh the entire thing now. I'm going to press here and click edit and there we go. So something is opening but it looks like it's not filling out my data. That's fine we're gonna go ahead back inside of the edit account sheet anyway now and let's go ahead and continue developing here. So what I want us to do is I want us to destructure the loading or we can just do this.
So how about we just write const isLoading to be accountQuery isLoading. Let's just keep it simple. And then inside of here, we are conditionally going to render the account form. So if we are loading, we're going to render our usual loader like this. So this is going to be a div with a class name of absolute inset zero flex item center and justify center And it's going to use a loader2 icon from Lucid React with a size 4 text, muted foreground and animate spin.
Make sure to import Loader2 from Lucid React. So I'm just going to add that to the top here in the Edit Account Sheet component. And let's add the alternative here which will be our account form. So let's see if this conditional rendering automatically enables the default values or if we have a bug somewhere else. So if I click edit, there we go.
It says test. If I click here, it says test 2. Perfect. So what we have to modify now is that this account form now accepts ID as well. And now what I have to do is I'm noticing a couple of bugs here.
So first of all it's showing it changed this save changes and it changed delete account but our title and description are still showing a new account instead of edit. So let's go ahead and change that here and that's actually right here. So edit account and in the description here let's pass in edit an existing account there we go so now it makes more sense when we click add new it's completely blank but when I click on some of these it opens up this. Perfect! So now I want to go ahead and I want to create a patch function.
So before we can build the patch route we first have to visit app API route route.ts and remember inside of here we only added two handlers for get and for post. So let's go ahead and enable it for patch now as well because that's the one we are going to need. If you don't add this, you're gonna get 405 methods. So let's go inside of accounts now. Whoops I opened it twice and let's go ahead and do the following.
So at the bottom here I'm gonna change patch and it's gonna do the same thing. It's gonna go to slash ID. Let's go ahead and collapse this. So ID, we're gonna have the clerk middleware and a Z validator here which is going to validate the param which is going to be an object, id, z is going to be an optional string. Like this.
And now let's go ahead and let's pass in our asynchronous controller here. So first things first, let's get the out get out context then let's destructure the ID to be CRequestValidParam but we also need the JSON object here so remember we can chain validators as well ZValidator In this one we're going to check for JSON and what we are going to check is insertAccountSchema.pickNameTrue. So the same thing that we actually did in here. ZValidator.json.insertCountSchema.pickName. But in this case we are chaining two validators.
First to validate the ID we are trying to patch and the second one the actual JSON object. Great! So now what we can do is we can also get the values from cRequestValidJSON like that. If there is no id, let's return c.json error missing ID with a 400. If there is no Auth user ID with a question mark between them, return c.json error unauthorized with a status of 401.
And then finally let's find and edit our account. So const the structure the single item await database dot update accounts dot set values database.updateAccounts.setValues where let's use AND to ensure that this is this user's account and let's also ensure that it's the account that was passed in as the ID and let's add dot returning as well. There we go. If there is no data returned that means that we could not have found that account. Otherwise, we are returning the data itself.
There we go. So now we have a working edit request. So what we have to do is we have to create use edit account and we can thankfully copy some stuff here. So features, API, we can copy the same thing as create account, just rename it, use edit account like this. And let me go ahead and expand this as much as I can.
So we are changing to not target just the accounts but we are targeting, well not even post, right? We are targeting accounts, id and then patch. So this is how it looks in one line, right? So let me copy this and replace this and the JSON is still the same, right? We are accepting only the name.
Let's rename this to use edit account like this and let's change the JSON. So there is no error here because the JSON is the same for creating the accounts and for our ID, patch JSON. So it's the same thing, right? Let's see, is it the same thing or did I make a mistake? Oh, well, it's not the same thing because it accepts JSON.
Yes, that's true. JSON is correct, but it also needs to accept a param ID. Where do we get the ID from? Well, from useEditAccount() we will extend it by adding an optional ID here as well. There we go.
So ensure that you are passing the param ID in the JSON of the patch function here. Instead of account created is going to be account updated. We are going to update accounts but we are primarily going to update account with that ID as the key. I'm going to add to do invalidate summary and transactions later in the future. And this will be failed to edit account.
Now that we have this hook, we can head back inside of our edit account sheet component. So let me go ahead and add this. Import, use edit account. I'm going to change this to be features, accounts, like this. And then we're going to go ahead and add some more mutations here so I'm going to add const edit mutation to be used edit account and I'm going to pass in the id as well and then in here I'm going to go ahead and create const is pending to be edit mutation dot pending is pending for now it's only going to be this so that's why I added a space because later we're gonna have another one here so isPending here and let's change the onSubmit here so we are no longer gonna have this useCreateAccount we can delete the mutation and use createAccount and we can remove the import useCreateAccount no need for that so that's going to break here but now we have the edit mutation.
So let's add the edit mutation with the values. That's going to work just fine. Great. And I think that this shouldn't... All right, so instead of here, isPending is no longer gonna be controlled directly by mutation isPending, but we're gonna use that constant isPending, which for now holds the exact same thing but later is going to be extended with another mutation which is going to be the delete mutation let's try this out, my account is named test, I'm going to click edit and I'm going to call this edited.
Let's click save changes. And there we go. It is edited. Let's try again. Perfect.
Let's refresh to ensure that data is preserved and it is. Perfect. I can now save this right here. Great. What I want to do now is I want to create a separate delete endpoint, right?
So we have the bulk delete. Let's go ahead and let's quickly create the individual delete endpoint. So the same way we needed to enable patch requests, we also need to do that for delete requests. So let's copy and paste this and add delete. There we go.
Inside of app, API, route, route.cs. To speed things up, we can copy the entire patch id method because it's going to be very similar. So I'm going to copy the entire thing and I'm going to chain it together like this. So I have twice patch and I'm gonna change this to delete now. And I'm going to remove the validator for JSON because it's not needed.
And no, I no longer need the values, right? But these rules can stay the same. If there is no ID, I don't know what to delete. If there is no user, you are unauthorized to do this. And now instead of update, it's going to be delete accounts and there's not going to be the set chain here.
The where rule will stay the same and for returning, we can just say which ID we remove, no need for the entire data. And here is the same rule, not found, 404. That is it. That is our delete route. So just make sure you've chained it with .delete, make sure that your existing patch, which you've copied, is intact, so you didn't accidentally cut it, and make sure you've removed the validator for JSON.
We don't need it for the delete requests and of course ensure you've enabled the delete requests. So now what we can do is we can go ahead and copy the use edit account features accounts API, use edit account, copy and paste it and change it to use delete account. Rename this to use delete account. And all we're gonna have to change is these patch methods here. So instead of patch, these two are going to be delete.
And in fact, we're not even gonna have the request type for this one. So you can remove the request type entirely and in place of it, you're simply going to remove it and remove the trailing comma as well. Go ahead and replace patch with delete in the mutation function as well and simply rely on the param and no need to add json in the props here either and we're going to write account deleted here, failed to delete account. We are going to update account and the ID. The accounts, we are going to update the summary and transactions as well later in the future when we have those things.
Great, let's go ahead and let's do the same thing now. Inside of edit account, go ahead and copy and paste this use delete account from use delete account like this so just make sure you've added that go ahead and add it here so edit mutation const delete mutation from use delete account pass in the ID and now we can chain these two together so either edit mutation or delete mutation is pending we are going to disable the form right so the user cannot spam with API requests there we go and let's go ahead now and let's attach that right here So we need to pass in the onDelete method which is optional so far. So in here it can be the deleteMutation.Mutate Like this I believe. We are not passing anything inside. Like that.
Let's just ensure inside of the account form that we are actually using on Delete. So we are using it, it's optional, it's right here. And it should be used, let's see, right here in handle delete. We have a safety check here and we are using it inside of this button which is only enabled if we pass in the ID. So make sure you've passed in the ID.
So if we try this now, for example, I'm going to open the test 2 and if I click delete account, there we go, it says account deleted. Perfect, and it's actually deleted but there are a couple of things we could improve here. First of all, well, it could close the drawer, right? But it could also go ahead and do the confirmation for us so let's go ahead and do that I'm gonna go ahead now I'm going to import our very cool use confirm hook from hooks use confirm and then I'm gonna go ahead and define it here. Use confirm for the title we're gonna say are you sure as well as our previous one and we're gonna write you are about to delete this transaction.
Let's extract the confirm dialog and the confirm method. Let's make sure that we are rendering the confirm dialog somewhere. So what we can do is we can wrap the entire sheet component in a fragment, indent it and simply add the confirm dialog here. It is a self-closing tag. And then let's define our onDelete method here.
Let's make sure it is an asynchronous method. Const ok is going to be await confirm so we know whether the user pressed yes or no. And if we are okay we are going to call delete mutation dot mutate but for the variables we're gonna pass undefined because there is there are none and then we're gonna open the options object and write on success and in here on close because that's what we wanted to do right and then let's go ahead and use this on delete right here instead there we go So now we should have a much better experience with deleting accounts. So if I go into edit and if I click delete are you sure you are about to delete this transaction? Cancel, does nothing, confirm, closes it and deletes it.
Perfect. So one more thing I want to do is add a shorthand delete here alongside edit. So for that we go back inside of our columns, inside of app accounts columns right here, inside of actions more specifically. So let's go ahead and let's I believe we have to import the handle. We have to define const handle delete right?
It is an asynchronous method and we're gonna do the following let's add const delete mutation to be use delete account and passing the ID so make sure you've imported this from the features like this alongside use open account and let's also add our use confirm from hooks from shared hooks right So we have the delete mutation here, but we also need to add our const useConfirm. Are you sure? And description, you are about to delete this transaction. Let's get the confirm dialog and the confirm method. Let's render the confirm dialog right here.
So we already prepared the fragment. And we're going to call const OK await confirm if OK delete mutation dot mutate like that and let's go ahead and use handle delete now. We can copy and paste the dropdown menu item and this one will simply call the handle delete. It's gonna be disabled if the delete mutation is pending. And the edit is also gonna be closed if delete mutation is pending and instead of edit this will say delete and we're going to use the trash icon from Lucid React like this.
There we go. And I have a dot here but I don't have it in my edit account sheet so let me just add the dot here. Alright now I am at peace. Let's try it out now. So this should be it for our accounts entity, I believe.
Delete, cancel does nothing. Delete, confirm. There we go. Perfect. So we just enabled a bunch of things for our accounts.
We can edit them, we can load them, we can individually delete them, but we can also bulk delete them. Amazing. Great, great job. You're gonna see now how easier it's going to be for us to create the categories entity, which is practically gonna be the same thing as accounts variables wise and size wise right obviously it's going to serve a different purpose but for code it's going to be almost exactly the same so you're going to see how much these components that we've created and these methods that we've established are going to speed up this process. So it's going to take I want to say three times less than it took us to complete the accounts page.
Nevertheless Great, great job.