In this chapter we're going to focus on creating the individual workflow page. By using the workflow ID we will be able to pre-fetch on the server component, use suspense query in the client component and display loading and error states with our suspense and error boundary components. We are then going to create a useful workflow header component which we are going to use to display and update workflow name And we're going to end the chapter by creating the editor component which will display react flow where we are going to finally start adding some nodes. Let's start by doing the prefetch use suspense query loading and error because we have just done that for the entire workflows list page. One thing I want to bring your attention to is that when we initialized this project here we used biome as our linter, But you've probably noticed that during this entire tutorial I never really got any biome warnings.
That is because I never installed the Biome extension. So if you actually want to see the benefit of using Biome, you should probably use the extension. But keep in mind that Biome is pretty strict. For example, if I go instead of source trpc server.tsx, you can see that now I have all kinds of warnings and infos here. For example, biome is telling me that I need to sort my imports and exports.
It's also telling me that I should make sure to only import things which are types separately as types. It's also telling me not to use any. Now these warnings shouldn't really break my build. The only thing that can break the build are TypeScript errors. So I do know that there is an option to fix all of this at once by using a formatter and you can set that formatter to apply every time you save the file or you can manually format your document and then configure the default formatter to be biome and you can see how that kind of fixes the issue.
But in this tutorial I'm actually not going to do that and the reason why is I don't feel comfortable with you not seeing exactly every single line that I change because if I use the formatter sure we can see that it modified some files here but I'm pretty sure you didn't notice that it also modified the files down there. And I don't want to leave you in a situation where you have different code than I do. For that reason and for that reason only, because this is a tutorial, I will not be using the Biome extension at all. If you want to, you can of course use it. Linters are overall a good thing and if you use the format on save, it will be obviously a better quality code.
But just for tutorial purposes to make this easier for you to follow, I will not be using Biome at all. I still recommend using Biome. I did use it while I was developing to catch some issues, but for tutorial sake, I'm not going to use it because it has a bit too strict rules that I will waste my time on. So let's go ahead now and let's focus on doing load workflow page by ID. And yes, you can always format your entire project when you finish it.
So just like format all files and fix them. But keep in mind that almost all files from Shazzy and UI will be broken according to biome. So it will be quite a lot of changes. So decide for yourself maybe while following this tutorial don't fix every single biome issue. You will just spend a lot of time doing that.
Maybe later when you finish the project. Alright now finally I can go into building this. So I want to start by seeing do I even have my features workflows server routers.ts do I have get one looks like I do it is protected and it is queried by ID. Perfect. And it's fetching for this user permission.
Great. Exactly what we need. So now what we should do here inside of workflows, let's go inside of prefetch here and let's create a prefetch for individual workflow. So that's actually quite simple. Prefetch a single workflow, export const prefetch workflow, accept an id, which is a type of string and just as above prefetch, drpc, workflows, this time using get1 and query options are just that single id and save the file.
Now we have prefetch workflow. Now let's also do the equivalent in the hooks. Instead of use workflows here, we have use remove workflow, we have use create workflow and use suspense workflows. So down here At the bottom I'm going to add a use suspense workflow. A hook to fetch a single workflow using suspense.
So use suspense workflow also accepts an ID, initializes DRPC and simply returns use suspense query DRPC workflows get one query options and passes in the id this is how it looks like in one line there we go let's go ahead and continue with this for now once we have those two we can go ahead inside of app folder, dashboard, editor, workflows, workflow ID, page.dsx. And we already have something prepared here. So we are protecting this page, great. And we are even extracting the workflow ID from the params, which means that if I go to localhost 3000 and just click on a random workflow, I should be seeing the workflow ID here, and I do, which means that this is working as expected. And since I do that, it also means that I can now safely prefetch here so prefetch workflow not workflows just workflow and pass in workflow id make sure you have imported prefetch workflow A quick reminder you do not need to await this so the prefetch which we have defined in server.tsx is using a void for that specific reason.
It's not returning any value it is simply populating the cache. So now that we have prefetched this let's go ahead and let's save some time by going inside of rest workflows page.tsx and let's copy hydrate client and suspense and error boundary like this paste all of them here let's go ahead and close error boundary and let's close hydrate client. Now let's go ahead and indent these things. There we go. Let's import hydrate client from trpc server let's import error boundary from react error boundary let's import suspense from react all of these components.
Great! Now let's go ahead and temporarily just set the error boundary to be error in a paragraph and let's go ahead and set the loading to be a similar solution like this now let's go ahead and create the editor component so inside of our features folder I want to create a new folder called editor and in here I'm going to create a folder called components and the file editor.tsx. Let's mark it as use client and let's import use suspense workflow from features workflow hooks and let's export const editor. Let's go ahead and give it a simple prop type, workflow ID, which is a type of string. Workflow ID.
And then in here const data, Remap it to workflow, use suspense workflow and pass in the workflow ID. And let's go ahead and return a paragraph, make sure to close it, JSON, Stringify, workflow and some settings to format that stringify in a nicer way. Make sure you added use client at the top. Now let's also make sure to add editor loading and editor error. Loading and editor error.
So for this we can reuse entity loading view from components entity components so just make sure to return it and the message here will be loading editor like this and export const editor error return error view from same and message will be error loading editor. So from entity components. Perfect. So now that we have those two we can go ahead and go back inside of the page. We can replace the error with editor error and we can replace the suspense with editor loading.
And inside of suspense we can render the editor from features editor components editor and make sure to pass the workflow ID which is workflow ID. There we go. Now this will be prefetched and it will be loaded or it will be thrown if there is an error. Let's quickly take a look. Looks amazing!
There we go. Now that we have that you've probably noticed that we are missing a header. For example, my workflows has a header which I can use to collapse the sidebar but if I go instead of an individual workflow that header no longer exists. That's because as explained here we're going to have a workflow header which will have the ability to update workflow name. Since it has that complex feature I made the architectural decision to separate it in its entire organizational folder here.
So let's go ahead and work on that workflow header now. The first thing we have to do is go inside of source, app folder, dashboard, editor, workflow id page and wrap the children inside of suspense inside of main like this and give it a class name of flex1 and above that name create editor header render which doesn't exist yet. We're going to create it in a moment and pass in the workflow id like this. The reason we are doing this exact structure is because we are replicating what other routes have in their layout. You can see it's exactly that.
The children are wrapped in main and above main is the app header. We're doing that exact thing but directly inside of page.tsx because we don't really need the layout and also because instead of editor header we're also going to use use suspense workflow. So because of that we have to wrap it instead of suspense and error boundary and hydrate client and I think it makes more sense to do that instead of this page then to purposely use a layout file because technically there is no need for a layout in a single route folder So let's focus on creating the editor header component We can speed things up by copying the existing app header component. So let me just copy this entire file content and let me go inside of features editor components and create a new file called editor header.tsx and paste the content here. Let's rename this to editor header and let's modify the props so that they accept workflow id.
There we go let me just extract the workflow id perfect and Let's go ahead and go inside of our page.tsx and let's import editor header. So, so far we've imported the editor component, editor error and editor loading. And we've now added editor header from components editor header. You can see that now I have that same collapse button. Now it doesn't make too much sense why we did this because it looks identical to the one in the list but now we're going to extend it by adding breadcrumbs so we can see exactly what workflow we are on and the ability to modify it.
So after sidebar trigger let's create a div and a class name. Make sure you're doing this inside of the editor header file. Don't accidentally overwrite your app header, you can close it. Let's give this a class name of flex flex row items center justify between gap x 4 and full width. Inside let's first create editor breadcrumbs and pass in the workflow id as the prop and then let's do editor save button and you can copy the prop from above.
So editor the breadcrumbs and editor save button. Let's go ahead and develop them one by one. So editor, save button, and we can copy the exact thing like this. Now the editor save button will just return a div with a class name, ML Auto to push it all the way to the right side by giving it a margin on the left side and it will render the button from components UI button Save icon from Lucid React with a class name size4 and a text which says save. Size will be small, onClick for now will be empty and disabled will be explicitly false.
So we remember to modify it later. All right that's one down now let's copy editor save button and let's go ahead and create editor breadcrumbs component and once you save and refresh the errors should no longer appear Let me see why they are appearing here. Aha, okay, it is because we forgot to add use client to the editor header. So use client. And once we do that and refresh, I'm fairly confident we should no longer have any errors.
We should just have two instances of save buttons. It's obviously because we didn't change the content of the editor breadcrumbs. So let's go ahead and do that. In order to do that, we are going to have to import all the components from breadcrumb. We already have it when we added Chatsy and UI.
Breadcrumb, item, link, list, and separator. And while we are here, let's also import input. And let's also import useEffect, useRef, useState from React. And I think for now this is ok, let's just add link from next link. We will add some more imports later let's for now focus on making the breadcrumbs look like something.
So instead of returning a div, it will return breadcrumb. So let's go ahead and close it. Inside it will return breadcrumb list, then breadcrumb item, then breadcrumb link, give it an as child property so it becomes the next JS link which we separately imported and it doesn't use because otherwise if you don't do it, it will just use the normal anchor. So we can now prefetch and we can just redirect the user to workflows and this will say workflows. Great.
Outside of the item, add the breadcrumb separator and then in here just leave it empty. And you will see how this looks now. So in here obviously we are going to render the individual workflow name. But when we click on the workflows we get a redirected back to workflows. Right?
We have this smooth navigation here. Now what this will allow us to do is that when we click on what's going to be the name of the workflow it's going to switch to an input and we will be able to change the name. In order to save the name, we have to make sure we have two things ready. Let's go inside of routers.ts, inside of workflows-server-routers.ts and let's find update name. Just make sure you have it.
The only thing we should accept is the name and the ID. It should be a protected procedure and in the where clause, you make sure that you query by user ID as well as the ID. Perfect. Once you have that, let's go ahead inside of workflows, hooks, use workflows. So we have used remove workflow, we have used create workflow.
Let's go ahead and copy use create workflow and let's go ahead and paste it here and let's rename it to use update workflow name. So hook to update a workflow name. We need query client and trpc. Instead of calling workflows.create we are calling update name. On success It will be workflow data name updated.
We are going to refetch this, and we're also going to query client invalidate queries, TRPC workflows get one query options, ID data dot ID. This way the individual one will be refetched as well. So it has a new name. And this will be failed to update workflow. Error message.
Great. Now that we have used update workflow name, let's go back inside of the editor header. And let's go ahead and let's export const editor name editor. Or maybe editor name inputs. Maybe that one is better so we don't repeat the word editor so much.
So let's repeat this type here. You can copy of course. And in here we're first going to get the data, remap it to workflow from useSuspense workflow and pass in the workflow id. Now don't worry just because inside of page.tsx here we render both the editor header which now uses useSuspense workflow and the editor which uses useSuspense workflow it doesn't mean that there will be two requests. It just means that one of them will be cached.
So yes, that's the cool thing about using React Query. And let's go ahead and let's do update workflow name or maybe we can just call it update workflow use update workflow name. Perfect. Now let's go ahead and let's return breadcrumb item and render workflow.name It says that workflow is possibly null. Let me check how that is exactly possible because we are using use suspense workflow.
So that should not happen. Let me check inside of page here. Use a suspense workflow. We are returning use suspense query. This should guarantee that the workflow exists.
I think it might be due to the way instead of useSuspenseWorkflow and specifically instead of the getOne here. Yes, because of this. Let's do it like this instead. Const workflow is a wait Prisma workflow. Turn the query into an asynchronous component and if there is no workflow let's throw new trpc error instead because usually this will not throw or maybe I can use find or throw.
My apologies, maybe that's easier. Find unique or throw, exactly. If I use this, I think that then my header here will always have this. Yes, So we have to throw the error if it didn't find one. There's no point in returning null because we don't handle that.
Right. We can't do anything with something that's null. And now we no longer have that issue. So just make sure you have modified this get one to find unique or throw. I will go through the rest of the code to see if there is an opportunity to fix that anywhere else here.
For example, delete. No, it's just delete. Create is just create. Okay, I think there are no other places to check other than this. But yes, the get one needs find unique or throw because we will display an error if it doesn't exist.
And now that we have a update workflow here, we just have it ready. We have this workflow name, we have editor name input and now in here after the separator let's render editor name input and now also passing the workflow ID. Here it is. And now you will see the name of your workflow. So if you go ahead and change to this one, Breezy Early Pencil, you will see the name Breezy Early Pencil right here.
Now let's go ahead and make it so that it it visually appears clickable. We're going to do that using CSS. So let's go ahead and give the breadcrumb item a class name of cursor pointer hover text foreground and transition colors. And now when you hover over this you will see that you can visually click on it, right? And when you click on it something should happen.
What will happen is that we're going to switch a state from is editing from false to true. So is editing from use state. Give it a default value of false is editing and set is editing and let's prepare name set name use state the default value is workflow.name which will always be some form of data because otherwise we throw an error. We fix that just a moment ago and let's prepare the input ref so we can always focus on this when we switch. Input element as the type, HTML input element as the type and let's go ahead and do use use effect like this.
If we have workflow.name set name to workflow.name This will update if any new name is received which will happen when we revalidate. And another use effect here which will do the following if is editing and if input ref dot current in that case let's go ahead and do input ref dot current focus and let's also do select and in the dependency array passing is editing. You don't have to pass the ref. Let's develop handle save method and in here if name is equal to workflow.name what we are going to do is set isEditing to false and break the method. We didn't modify the name.
Otherwise, let's go ahead and let's do update workflow. So what we can do is we can actually open try and catch here. So turn this into an asynchronous and instead of try do a wait update workflow dot mutate asynchronous passing the ID to be workflow ID and the name like this. If that fails, make sure to reset the name. And in finally, so regardless if it fails or succeeds, make sure to reset setIsEditing back to false.
This way we handled most of the use cases that can happen. And let's add one more method const handleKeyDown which accepts the value of react.keyboardEvent if event.key is enter, handle, save. Else if event.key is escape, set name to whatever was the current value and reset the editing status. So just some hotkeys for easier editing. And finally, if isEditing instead of returning breadcrumbItem we are going to return an input component.
It will have a ref of input ref. It will have value of name on change which accepts the event and passes it to set name as event target value. On blur automatically triggers handle save. If you don't like that you can of course remove it. On key down handle key down.
This is a listener for hotkeys. And class name height 7 with auto minimum width of a hundred pixels and the PX of 2 and now it should be ready right now it's called Breezy Early Pencil and let me go ahead and yes of course one thing we forgot was to actually add on click to the breadcrumb item so on click set is editing to true So let's go ahead and try it now. When I try to click here, there we go. It automatically selects. If I click escape, nothing happens.
But if I change from pencil to rename and press enter. And did it? It did. Workflow Breezy Early Name updated. And it should also change it here in the data right so if I refresh it should fully stay renamed I just don't like that when I pressed enter it didn't immediately close.
So when we press enter handle save happens Perhaps we should immediately do set is editing to false. Set is editing to false. Like this. And maybe remove the finally. And you know, maybe you can improve it by not allowing to change back to this mode if it's pending so test 2 immediately closes and then you can see it renames maybe that's a little bit of a better experience and you can also do disabled if let's go ahead and do create update workflow dot is pending And then maybe you don't have to do it here.
Maybe you can then revert to finally. Just experimenting now. So click here, remove something. Yeah it's kind of disabled. Okay.
Yeah, that looks fine. That's good. I like this solution. Perfect. So you can see now it's renamed to Breezy Early.
Let's confirm by going back to workflows. You can see updated less than a minute ago, but created two days ago. So obviously works. Great, so there's one thing left inside of the editor header and that is the save button. But we can't really do anything here.
We will develop this later because the save button will be used to save the current workflow editor state, the nodes, the connections, executions, those kinds of things. This is a fairly simple developer experience. We can just save on blur, but the save button will somehow need to have the state from the editor. So I know that I've said that I want to add workflow in this chapter but it kind of makes no sense because we are already half an hour here and I'm fairly satisfied with what we did here. So I want to call it a day here and skip this part for now and instead dedicate the next chapter to it so I can properly explain it and go a bit more in depth.
So let's wrap this up. So 14 workflow page. I'm going to go ahead and create a new branch. 14 workflow page. And then I'm going to go ahead and make sure to commit all six changes.
So stage all changes 14 workflow page commit and publish branch. Once it's been published, as always, let's go ahead and let's review the pull request. So I'm creating a new pull request and now we're going to review it. And here we have the review by CodeRabbit. New features.
We introduced a full page workflow editor with header, breadcrumbs, and inline name editing. We added save action in the editor header with clear disabled state during saving. We provided loading and error views for smoother editing experience and we improved handling for missing workflows. Users now see a clear error when a workflow doesn't exist. This is referring to our find unique or throw change in Prisma.
As always file by file summary here and as per the sequence diagrams this isn't really anything we haven't seen before. This specific one is referring to how prefetch is working and what happens in the seed cache here. I'm pretty sure you understand this by now since we did have a couple of examples of this already. But if you want to take a look at it you can of course pause the video. I will you know slowly go over it here so you can see exactly what is going on.
Basically prefetch seeds the cache and then our client component, our UI component immediately has access because it hits the cache. And it's pretty clear how our editor name input works as well. We actually have no actionable comments. There are some nitpick comments. So for that reason I told it to summarize what we've learned here.
And yes, CodeRabbit can do that as well. You can talk to it like you would normally to a actual person. And what I think is important here is this. We changed from find unique to find unique or throw in the router to explicitly handle missing records paired with try catch blocks in the UI that revert optimistic updates on failure. Exactly what they did and I'm pretty sure we knew all the other ones already.
Overall, pretty good pull request, not much to comment on. It was pretty simple. Amazing job. After you've merged that, go back inside of your main branch and make sure to synchronize your changes. And once you've done that, As always, I like to end by going inside of graph and making sure that I can see my newest merge right here.
Excellent. And let's go ahead now and wrap it all up by marking the GitHub part as finished. Amazing, amazing job and see you in the next chapter where we are going to start to implement the editor using React Flow.