So now that we have all the elements we need, we are ready to install the drag and drop package and implement the functionality. So head inside of your terminal and let's go ahead and run npm install at hello-pangea-dnd. So this is the same as React Beautiful DND but the old one has been deprecated and this one is up to date great So make sure you have your project running and let's go ahead inside of our ListContainer component which is in the platform, dashboard, board, components and in here we have the ListContainer and let's go ahead and let's import everything you need from the DND. So I'm going to import drag drop context and droppable from at hello.pangea.dnd like that. And now let's go ahead and let's drag this entire thing inside of drag drop context like that and let's go ahead and let's add an on drag end which for now is just going to be an empty arrow function.
Now let's go ahead and wrap this ordered list element inside of a droppable component. So droppable here, wrap the entire ordered list inside of that and go ahead and give it a droppable id of lists and give it a type of list and direction of horizontal and now go inside and destructure the provided prop and wrap the ordered list again inside of this new element. And now that we have the provided prop we can go ahead and pass it to the ordered list here. So let's go ahead above the class name and pass in provided.DropableProps so spread them like I did and give it a ref of provided.InnerRef like this and then go ahead above the list form and render provided dot placeholder like that. Now that we implemented this drag and drop context and droppable in the list container we have to do the same thing for the list item.
So let's go inside of this component list item and let's import everything we need from drag and drop so that's going to be draggable and droppable. From dnd like this and let's go ahead and let's wrap the entire component inside of draggable so draggable all the way to the end of the list element and let's give it a draggable id of data.id and an index of index and now we have to destructure the provided as we did so open curly brackets open normal brackets get the provided and wrap the entire list inside of that element. Go ahead and indent this a bit and now let's go ahead and pass everything we need to the list item here. So go ahead and spread provided.draggable props and ref is going to be provided.innerRef like this. And now inside of this div which wraps the list header Let's go ahead and give it another property Which is going to be a spread of provided dot drag handle props so when the user grabs on this div they're gonna activate the drag and drop.
Perfect! And now what we have to do is go below this list header here and wrap the ordered list of cards into droppable. So just go ahead and wrap that. Let's go ahead and give droppable the droppable ID of data.id and let's give it a type of card and then we have to destructure the element provided and wrap the entire ordered list inside of that. Now that we have the provided right here, let's give this ordered list a ref of provided.innerRef let's just not misspell innerRef and let's go ahead and spread provided.dropable props.
And now just at the end of this ordered list go ahead and render provided.placeholder like that and I think that already we should be able to at least visually see the drag and drop as you can see we can drag and drop of course it's not working because when it finishes the on drag end we don't actually do anything. And one more thing that we have to do to enable the cards from being draggable and droppable because right now if I grab on the card you can see that the entire list gets dragged. We have to go inside of the individual card item and let's go ahead and again import draggable from dnd. So import draggable from dnd like this and we have to wrap the entire div inside of draggable. So let's go ahead and do that.
Let's indent the element. Let's give the draggable id to be data.id and an index of index and then let's go ahead and destructure the provided element and wrap the entire div inside of that and then we can give this div a spread of provided draggable props and provided drag handle props like that and let's also give it a ref of provided inner ref like this and now I believe we should be able to drag the cards as well there we go we can now drag cards or we can grab on the list and then we can grab list but again it's not working right now You can see that it resets its position after we let go. That's because we haven't done anything inside of our onDragEnd. But as you can see, still it works if I try to rename something, I can click on the options here so all of those things are working perfect and now let's actually implement the functionality first to locally or optimistically mutate the state of orders and then we're gonna do the request on the back end. So we have to go back inside of the list container so let me just expand this and let's go inside of listcontainer.tsx inside of this board ID components here and now we have to modify this onDrag function.
Before we do that I just want to create a generic reorder function which we are going to reuse. So let's do function reorder which is accepting a generic t, has a list as a prop which is an array of the generic it has a start index, which is a number and an end index, which is a number as well let's go ahead and open this function let's create the result, which is going to be an array from the list we provide and then let's destructure the removed from result splice start index and one and now let's do result dot splice and index zero and removed and return the result So this is going to reorder them by indexes. And now we have to create a very big function onDragEnd so we're going to add comments inside so it's easier to follow exactly what it does so let's go ahead and let's create const onDragEnd() function and let's replace that with this empty function here so now onDragEnd() inside of our drag drop context should be handling this. So first let's go ahead and extract everything we need from our prop result which let's give it a type of any.
Let's go ahead and let's destructure the destination. Let's destructure the source and the type. First, we're gonna check if we can even do this. So if it happens that we don't have destination, we can already return the function. There is nothing else we can do with this.
Now let's go ahead and let's write a comment dropped in the same position or maybe if dropped in the same position. So if a user picks up a card or a list and drops them in the same position we don't have to do anything and we can check that with an if clause which is going to say if destination.DropableId is equal to sourceDropableId and if destination.index is equal to source.index in that case we can break the function meaning that there's nothing we have to do if the user picks up an item and drops them in the very same place. And now we're going to create what happens if user is moving a list. So let's write a comment. User moves a list.
Let's write if type is a list. So we know that because we gave this droppable a type of list. The same way that in a list item we have a droppable element right here with the type of card so we know what exactly we're dropping. So go back inside of the list container and if the user moves the list we're going to do the following. First we're going to reorder the items using our generic reorder function.
Inside we're gonna pass the ordered data which we have from right here so that's our initial data. We're passing the ordered data then the source dot index and then destination dot index and then let's do dot map let's get the item and the index and let's go ahead and return an immediate object where we spread the existing item but change the order to the new order using the index. And all we have to do then is set ordered data to be the items. Like that. And I'm going to write a to-do comment.
Well, trigger server action. So we have to create a server action which is also going to update this on the back end for now we're only gonna do the optimistic mutation here on the front end so I think we can already try it out if I drag and drop the original there we go the original now stays exactly where we place it Of course if we refresh it goes back to its original position that's because we don't do the server action for this yet. Great so now we have to do the same thing but for the card right so let's go outside of this if here and write the comment user moves a card. In that case let's do if type is card and let me try and expand the code even more. Alright so if type is card let's go ahead and just create new ordered data to be a copy of the existing order data so we can mutate on this and let's go ahead and first get the source and destination list so const source list is new data new order data dot find and we search through all the lists, we get the list ID and we compare it to sourceDropableID.
So let me zoom out so you can see that in one line. We get the source list, sorry we attempt to get the store list source list using the new order data and we use the find method to get the list and we compare the existing list ID with source droppable ID. And now let's go ahead and create the destination list. So const dest list is going to be again, new ordered data dot find. We get the individual list and we compare the list ID this time with destination dot droppable ID like that.
So let me zoom out so you can see both of those. So source list is comparing the list ID from the new order data with the drop with the source and the destination list is with destination droppable ID. All of those props you can find you can see that we have the droppable ID lists here and the list item we have different droppable IDs to be the data.id. All right and now let's go ahead and let's check in case that source list or destination list wasn't found inside of this iteration using find so if we don't have the source list or if we don't have the destination list there is nothing we can do so let's just break the function and now we have to handle the cases if the cards property doesn't exist on a specific list right if the list is empty it can happen that it doesn't have cards so check if cards exists on the source list we're gonna do if there is no source list.cards in that case let's go ahead and manually add them so source list.cards is an empty array now let's go ahead and check if cards exist on the destination list so in here if !destinationList.cards manually add cards to be an empty array like that.
Great, so now we can work with that. And now we have to handle the case if the user moves the card within the same list and if they move the card in a different list because we have to modify their list ID in that case so we can safely send that to the backend. So first let's do moving the card in the same list like this. So we can do that by checking if sourceDroppableId is identical to destinationDroppableId. Let's go ahead and let's reorder our cards using the generic reorder function.
So const reordered cards are reorder function and let's pass in the source list.cards source.index and destination.index and now let's change the order of each card reordered cards dot for each we have the car and the index and let's write card dot order to be the new index and let's just not misspell index all right and then let's assign that to the source list cards so source list cards is now reordered cards with the new indexes and now let's do set ordered data to be the new ordered data like that and let's go ahead and add a comment here to do and we're going to write well trigger server action so we can actually save this. So now if I'm not mistaken we should be able to locally keep the state of cards. There we go. So SDF is now on top and I can move it all the way to the back and it doesn't reset 123 is here I can move it all the way to the back and it doesn't reset but if I refresh of course it goes back because we have to do the server action Great! So now we have this and now we have to handle the case if the user moves the card to another list.
So let's go ahead and add a little comment here. User moves the card to another list. And let's open an else statement here because this is the end of our if clause as you can see right here so meaning that if the droppable ID is different from the destination's droppable ID it means that we are using a new list to drop this in So let's go ahead and let's remove card from the source list. So const movedCard is going to be sourceList.cards.splice, source.index and 1. Like that.
And then let's go ahead and assign the new list ID to the new card or actually moved card so we're gonna do that using move card list ID is gonna be destination.dropable ID And now let's add the card to the destination list. So we successfully just changed the card's list ID and now let's use the destination list.cards.splice so we can insert it in between the destination index and moved card like that and now we have to change the order of the cards as well because remember when we drop it inside another list we can also reorder the entire list we can put it on top or in the middle or on the bottom so let's write source list.cards.foreach get the card the index and card.order is the index oops, let me just go I somehow switched my components so back in the list container this is where we were sourceList.cards for each and apply the new index as the card order great and now we have to update the order for each card in the destination list. So let's go ahead and do destinationList.cards for each, get the card and the index again, and let's just write card.order is the new index.
And now we can do set ordered data to be new ordered data and let's write to do trigger server action And now this should be locally working as well. So let's try it out. If I drag 1, 2, 3 in the middle of this, it should stay. And it does. If I move this one as the first in this one, it works as well.
Perfect. So you can try with as many combinations as you want. You can try in between the same list and everything seems to be working. Perfect! So we are officially ready to now do this on the backend as well because locally it works.
Again if you're having any problems you can always visit my original github and look at the exact code from here. Perfect so now we have to create two server actions. One is to update the card order and the other one is to update the list order. So let's go ahead and do that. So as always let's copy an existing server action.
So I'm going to close everything, go into actions and I'm going to copy the create list and let's rename it to update list order like this and first let's go ahead and modify the schema so let's change this to update list order and it's no longer going to be having a title. So it needs a word ID and it accepts the item, which is z.array of z.object. And inside we expect the ID to be z.string, the title to be z.string as well. We expect the order to bz.number, the created at bz.date, and the updated at bz.date as well. All right, now let's go inside of types and let's use the update list order schema as our input type and instead of a single list we're going to be expecting an array of lists at the end so just make sure you add an array here.
And now let's go inside of the index right here and let's modify the update list order here as well. Let's move it all the way down to update list order and let's change this to update list order. Like that. Perfect. Now let's go inside of the actual handler the authentication can stay the same and let's go ahead and modify this so instead of title we are now extracting the items and it's not going to be list it's going to be lists like this let's go ahead and clear everything inside of our try function so make sure the try function is empty and let's go ahead and create a transaction so const transaction is items.map get the individual list from here and let's do db.list.update where ID is list.id and board matches the organization ID.
And let's pass in the data to be order list.order. Like that. So I just want to bring it to your attention that I did not add any curly brackets here. So this goes directly after the arrow function like this. I just moved it to a new line.
So I directly returned this function. So make sure you don't put any curly brackets. Otherwise, I think you have to return this function. Great, and now let's go ahead and write lists to be await db.$transaction and pass in the transaction inside. Like that.
And in the error, let's go ahead and let's say fail to reorder and then we're gonna go ahead and return the lists there we go and now we can use this inside of our list container so let's go back inside of the list container where we just wrote this big onDragEnd function and let's go ahead and import the useAction from hooks.useAction and let's also import updateListOrder from actions.updateListOrder and now inside of here the use action from hooks use action and let's also import update list order from actions update list order. And now inside of here, I'm going to go ahead and I'm going to get that well action. So const use action update list order and let's get the execute so execute is going to be execute update list order so we know exactly which one of those it is. Let's go ahead and open the callbacks and on success, we are not gonna work with data. Instead, we're just gonna call the toast from Sonar.
So just make sure you add this import and we're gonna say toast.success listreorder and if we have an error we're gonna go ahead and log that error in a toast. And now we can use the execute list order once we finish our list reordering. So go inside of on drag end and in here we have a clause if user moves a list and in here we have set order data for this new items and then we have a comment to do trigger server action let's remove the comment let's call execute update list order let's pass in the items and the board ID like this and now we should be able to move the original in the place of this one and it should be saved in the database so if I drag and drop it like this there we go list has been reordered and if I refresh the original stays in this place where we move it. Let's try that one more time. List has been reordered, if I refresh the original stays in the same place.
Perfect. So now we have to create the same thing but for updating the card order. So let's go ahead and copy the existing update list order because it's the closest to what we need. So I'm just gonna close this stuff I'm just gonna leave the list container open so we can quickly move back to it let's find the update list order copy it and paste it inside of the actions and rename it to update card order and inside focus on the schema first like this so let's rename this to update card order like that and we expect the board we don't expect the board ID we expect the list ID like this and then let's go ahead and confirm the object so we need an ID, title, order but we also need the list ID which is Z.string like this So let's go ahead now inside of types and let's import update card order and paste it here. And we are no longer working with lists instead of we're working with cards.
So import the card and place it here. So it's going to be an array of cards in our return type and now let's go inside of the index and let's change this schema to be import card order let's go all the way down and change this to import card order as well and the function should be update card order as well. Perfect! So now let's go ahead and modify this index here so this authorization can stay the same and we're no longer extracting the board ID instead we are extracting the list ID and the let is going to be updated cards like this so let's go ahead and let's remove everything inside of the try block and let's go ahead and write db.card.update where id is card.id. Oh, my apologies.
So we have to run a transaction actually. So const transaction is items.map. And then we have the individual card. And again, don't open the curly brackets, just immediately start writing a function. DbCardUpdate where id is card.id, list is board.
Organizational ID, so we ensure no foul plays can be done and then we pass in the data which is the new order from card.order and we pass in the list ID which is card list ID like that. Perfect so we have our transaction and now let's go ahead and write const updated cards to be await DB sorry not const but we just modify the updated cards from here. Await db.openTheTransaction and pass in the transaction inside. Great! And let's go ahead, okay this can stay the same and return the data to be updated cards and I just realized that we might actually need the board ID from here as well so let's go back inside of our schema here and let's request board ID to be Z.string as well.
And now let's go here inside of the index and let's destructure the board ID from it as well. And let's go ahead and yeah this can stay the same. Perfect. It actually seems to me like we don't need this list ID at all. So let's remove the list ID and let's also remove it from the schema.
So we only work with board ID here. Alright, and now let's go back inside of the list container and in the same way we created the execute update list order let's create the update card order. So I'm going to go ahead and I'm going to import update card order from update card order which we just created so let's just ensure that we did that properly the name is update card order correct great so we have the update card order and we can go ahead and we can copy this use action here. So let's just paste it below and let's give the alias to be execute update card order and let's use the update card order like that. And let's give the success message to be card reordered.
And now let's use the execute card order instead of the list order. Perfect. So let's go ahead and find where we have to do that. So we handled the case if the user moves the list. Now we are here where the user moves a card.
So let's go ahead and find where we have to do that. So we handled the case if the user moves the list. Now we are here where the user moves a card. So let's go ahead and find the first to do here. So moving the card in the same list.
Let's go ahead and change this to do trigger server action to call the execute update card order and let's pass in the board id do we do we have the board id in here let me just confirm we have the board ID in the props. All right, so we can use that. So you can just pass in the board ID, we have it in the props and let's give it items to be reordered cards. And that should be working. So this is a case if user moves the card in the same list.
Let's see if that saves in the database now. So I'm going to move 1, 2, 3 to the bottom of the list. And let's see if we get a success message. There we go. Card reordered.
And if I refresh 1, 2, 3 is still at the same place. Perfect. But if I try this for example, well, there's no success message because we don't handle that case. So if I refresh, it's gonna go back to here, Perfect. So reordering in between the same list seems to be working just fine.
Now we have to handle the case if the user drags it in the other one and we don't have to create any new server action for that one. We can instead just go ahead and find the case it's right here below it so where we wrote the execute update card order we have a comment user moves the card to another list so scroll here and we have a to do trigger the server action so let's add execute update card order Let's give it the board ID to be board ID and let's pass in the new items to be destination list cards. Like that. Perfect. Let's try it out for the moment of truth now.
I'm gonna drag and drop one two three in the middle of this board. I should be having a notification any second. The card is in the middle. If I refresh, the card stays in the middle and let's try it with this one moving it to the top. Let's wait, there we go, we have a notification, I refresh and it's still here.
Let's try if I can now move it in between. This seems to be working as well. I refresh and it is still here. Let's try with a new card, so new card. Let's see if it's gonna be created.
It is right here. I will immediately move it here and that seems to be working as well. I can refresh. Let's try and rotate this items. That seems to be working as well.
Let's move the new card to the top here and that seems to be working perfect. So I think we just did some good Q&A here. We handled all the cases perfect. What we have to do now is that when we click on a specific item or card it opens a model with more information about it. Great, great job!