In this chapter we are going to develop the editor component. By adding React flow dependency we will be able to drag nodes around within a canvas and establish connections between them. We are then also going to update our Prisma schema by adding a node and the connection table. This will allow us to preserve whatever we create within the editor canvas such that we will be able to store it in the database. And once we have that established, we will be able to end the chapter by loading the default editor state from the database.
Let's go ahead and start by adding React Flow into our project. Using the link on the screen, you can visit their landing page. And clicking on Quick Start, you can visit the documentation. Since we already have a project we can just follow the second step which installs XY flow react package. So I'm gonna go ahead and install this package.
And once it's been installed, I'm going to show you the exact version that I am using. Here it is, 12.8.6. You don't have to use the exact version, but I just want you to be aware which one I'm working with in case something doesn't work for you. So now let's go ahead and make sure that we have our app running and once we have our app running let's go ahead and go into an individual workflow page. Right now the only thing that should be visible there is a stringified json of the workflow data.
So if I click here in Breezy early after I load the editor, I should only see this. And now we're going to follow the usage here and modify our editor.tsx. So editor.tsx. It is inside of features editor components. Let's start by adding use state and use callback from React and then let's import all of these from our newly installed xyflow react.
React flow, apply node changes, apply edge changes and add edge. Then let's also import xyflow styles and I like to do that here at the end after all other imports. Then let's go ahead and establish the initial nodes. We're going to do that above the editor constant since that's the one we are interested in. So as you can see the initial nodes will be two objects.
Both of them will have their position coordinates and their data object which establishes their label. So, both nodes have an id, position and data. Now that we have the initial nodes established let's also add the initial edges. The initial edges will be a very simple array with a single object inside and that object will have a source referring to the id of the first node that we have here and the target referring to the second node that we have here So make sure that source n1 matches the id n1 and target n2 matches the id n2. And we also give the edge an id simply because every element needs to have its own id.
And for the position it's pretty self-explanatory. Node 1 is strictly in the center, whereas node 2 will be initialized slightly below on the Y axis. So now that we have the initial edges, let's go ahead and set up the state here. So after use a suspense workflow, let me go ahead and collapse this. There we go.
So after use suspense workflow, make sure to add nodes and edges as well as their setters using use state and set the default value respectively for nodes and for edges to the constants we defined above. Now what I like to do here is immediately create the types for this. So this will be a type of node which you can import from XYFlow React. And it's going to be an array of nodes. And this will be a type of edge.
Again, you can import both node and edge from XY flow react. And you can even specifically say that this is just a type. So now you will have the exact types in here. When you hover over nodes you can see it will become node. Now let's go ahead and let's add these three methods.
OnNodesChange, OnEdgesChange and OnConnect. All of them are exactly the same as you can see. Let me go ahead and expand this as much as I can so it's easy for you to look at. Use callback receives new changes and immediately returns and calls set nodes. Inside of set nodes we can open a callback if we want to receive the snapshot or basically, in other words, the current value of nodes.
So technically, you could also just call applyNodeChanges and instead of using node snapshots you could just pass nodes but you shouldn't really do that if you want to get the most update value of the state you should use the callback parameter like this. So I will zoom out just once more so you can see how all of this looks in one line because it's a long line and I don't want you to be confused. So use callback receives changes and immediately calls set nodes. Set nodes calls a callback and immediately calls apply node changes. And all of the other ones are the same except they are using set edges and call apply edge changes in onEdgeChange or add edge in onConnect.
All of these apply node changes, apply edge changes and add edge can be found in the import above. Now in order to fix the changes type we also have to specify them here so this would be a type of node change and then an array. You can also import the node change type let me just go ahead and space specified from here So type node change and let's also go ahead and prepare the type edge change and type connection. Now that we have those let's go ahead and call this edge change and an array and this a connection. There we go.
Now we no longer have any type errors here as you can see. So once we've established these three functions, we can finally render a React flow. So let me clean up my return here We're going to create a div with a class name of size full and then we're going to render React flow now let's go ahead and pass in the nodes, edges and those three methods we've just created. Nodes, edges, onNodesChange, onEdgeChange and onConnect and let's add fitView. FitView will automatically zoom in on the nodes that we have.
And now, as you can see, when you refresh in an individual workflow ID, you will be able to drag these nodes around and they will already be connected. The reason they are connected is because we established the initial edges, right? So initial edges show that node one is connected to target node two. Perfect. Now let's go ahead and learn how to improve the react flow look so if you change this from a self-closed component to this one and add a component called the background which you can also import from XY flow react and save you can see that now we get a background grid which is already looking much better.
Let's enhance it even further by adding controls again from XY flow react and now in this corner you will be able to zoom out, zoom in or fit view. Let's also add a mini-map. Again, from XY flow react. And now in here you will be able to see a mini-map of what's going on. And since I know some of you will be asking me this, down here you can see the text React flow.
And I think they fully deserve to have that text here. And I'm going to show you exactly how you can support them as a company. If you have the resources or if your company does, do consider supporting ReactFlow package. This is as far as I'm aware the only way they maintain this and ReactFlow is open source and MIT licensed software, which means that you are allowed to use it without supporting them. But as you can see, with your subscriptions, you are ensuring the sustainable maintenance and development of the React Flow library.
The reason I'm telling you this is because now I'm going to show you how you can remove their logo if you want to. So you can add pro options here. Hide attribution set to true. I would recommend that if you do this do consider supporting them because if you don't do it the minimum they deserve is recognition. And you can see that when you add pro options you hide their attribution here.
And when you don't have it you have a little text react flow. Again it's not required but I think it would be very nice to support open source MIT licensed projects like this one. They are absolutely amazing. Great! Now that we have that established, let's see what's next on our list here.
So we just created the editor components, we added the React flow and the initial nodes. Now let's go ahead and update the schema and add the node and the connection table. In order to do that, we have to go inside of Prisma schema. So now I'm going to expand this so we can focus exclusively on that. So below the workflow table create a model called node.
Now you can copy the type of id because it's going to be exactly the same and now you have to create a relation with a workflow. So each node will have a workflow ID which is a type of string and now we have to create a workflow type of relation. So workflow relation is referring to field workflow ID and it is referencing the ID field in the workflow model and when we delete the workflow let's also delete this node we can ensure that happens by adding on delete cascade and once you've established that you also have to go back inside of the node inside of the workflow here and add the nodes relation like this node and then you will have no errors perfect Now let's go ahead and give this a name. Let's go ahead and give this a type. Now the type will be a specific custom node type.
So this does not exist yet. We are now going to create it. So I'm going to go here and I'm going to do enum node type. For now let's go ahead and create initial as that's the only one we will be working with. So now that we have the type node type here Let's also add position which will be a type of JSON data which will be a type of JSON with the default of a stringified object like this.
Then let's go ahead and copy created at and updated at timestamps here. And let's go ahead and see. I think this is enough. I think that's all we need right now. And now let's also create the connection model.
Model connection will represent the edges. So model connection. Go ahead and you can copy these three lines here. So each will have an ID and workflow, which means that we have to go back to workflow and add connection, connection, and change this to connections, like this. Inside of connection here, let's do from node ID and make it a type of string.
From node make it a type of node relation called from node. Fields from node ID references ID on delete cascade. So if the nodes get deleted we can also remove this connection. And now let's go ahead and let's copy these two and change this to be from node ID to be to node ID. So from what node to what node and change this to be to node.
Change the name to be to node and change the to node ID to be used as the field here like this. Let's also add from output to be a type of string default main to input type of string default main. This might come in handy. I'm going to see if we're actually going to need this or not, but let's leave it here for now. And let's copy the timestamps as usual.
And let's go ahead and set the unique field here to be from node ID to node ID, from output and to input. Once we have that, let's go ahead and create the relations back in the node here. Output connections will be connection with a relation name from node. Input connections will be a connection, a connection array, let me fix the typo, with a relation called to node. There we go.
Once we have that, I think that we are ready to create a migration. So We've created the node, we've created the initial node type and connection. Perfect. So let me go ahead now and do npx prisma migrate dev and let's call this React flow tables. So React flow tables and once we do that, we have applied the migration.
I now recommend restarting Next.js. So everything works as expected. Perfect. So now that we have that what we can do and what we should do is go inside of source features workflows server routers dot ts and let's specifically focus on the create. Now what we are going to add is that every time a new workflow is created let's also create a set of nodes.
So nodes create type will be node type which we can now import from generated prisma dot initial position let's go ahead and make x zero y zero and name node type dot initial. We're going to see if this name field is actually useful or not. I think that most of the time we're just going to use the type. So maybe the name won't be all that useful. For now add it here so you don't get any errors.
And now every time you create a new workflow you will also create a new initial node in that workflow. The problem is in Editor TSX, we don't ever use that, right? Because first we don't even include nodes. You can see our workflow type doesn't even have any nodes here. So we can't pass them here.
So what we have to do while we are here is revisit our get one protected procedure. So let's go ahead and start by doing const workflow to be await Prisma workflow find unique or throw make the query async like this. Now this workflow will always exist. You don't have to do if workflow doesn't exist because we are continuing to use find unique or throw. And instead let's find all the nodes.
Now let's go ahead and import the node type from generated Prisma like this. Okay and let's do my apologies we also have to include the nodes. Include nodes, true connections, true. And I think that maybe I'm telling you something incorrect here. Yes, it makes no sense that this is a node.
So what are we actually doing here? Why am I getting confused? What I thought that we are doing now is just, you know, fetching the nodes from the server. But here's the thing, the structure that React Flow expects and the structure that we save in our Prisma schema are not one-to-one mappings, which means that now what we are doing here is transforming the server nodes to React flow compatible nodes. That's what we are doing.
Which means that this node type should more specifically come from React Flow. So type node from XY flow react and this will be a type of edge the other one. Great so make sure you have added both node and edge head back instead of get one and now in here what we are going to do is we are going to transform the nodes so workflow dot nodes which now exists because we include them. .Map get the individual node here. Give each node an ID which we have.
Give each node a type which we also have. And give each node a position. Let's go ahead and also wrap it up by giving each node its data. Now in here we have some type issues I believe. The position is incompatible That's because the position is a type of JSON value, which is technically correct.
But let's be a little more specific. It's going to be a type of number and a type of number, the coordinates. And this one is the second one that's problematic so this one is yes a little bit problematic so let's go ahead and do node data as record of string or unknown because literally anything can be data or just an empty object. Perfect. Now let's go ahead and let's transform the server connections to react flow compatible edges.
So const edges is a type of edge which is an array. Workflow dot connections dot map. Get the individual connection and let's go ahead and add the following things. ID connection ID source connection from node ID target connection to node ID source handle from output target handle to input. This way we can always transform from the server to the client.
So now we have nodes and edges which are ready to be passed down and rendered in React flow. So let's go ahead and modify this by doing return id workflow.id name workflow.name nodes edges. There we go. Now that we have that, instead of editor.tsx, we can modify the default values here. So instead of using initial nodes and initial edges, let's do workflow.nodes.
And let's do workflow.edges, like this. And we can now remove the unused variables from here. What I want to do now is also start npx prisma studio. Let's go ahead and open up the studio here and let's remove all of the existing workflows. This way we won't have any outdated workflows which don't have initial nodes.
So just delete all of them. Once you have deleted all of them, let's go inside of workflows and make sure to refresh this because it might cause some errors. So there we go. No items. And when I click add item, What should happen now is that I should have an initial node and you can see that I do have it but it's weird.
It doesn't exactly look as expected. Nothing is really written here but something was loaded here. So what we have to do now is we have to create something called node components registry. So let's go ahead and do the following. I'm gonna go ahead and extend this and I'm gonna go inside of source config and in here I'm going to create node components.ds and I will export node components as a factory map And in here I'm going to do when we use node type from Prisma initial, let's render initial node.
And let's go ahead and do as const satisfies node types from React flow. So make sure to import this as type and I think, okay, no, this is not import as type. So obviously we're now going to have to create the initial node and let me just go ahead yeah let's also just do a quick type export which we're going to need later. Registered node type to be key of type of node components. So in order to create the initial node we're going to need a little help.
Specifically help from React flow and chat CNUI. That's right. So you could go on to React flow and click inside of UI. In here you can go ahead and you can find where is it custom nodes and you can find the placeholder. And this is how it looks like.
And that's exactly what I want. I want this to be our initial node, just the big plus button. In order to do that, we have to run npx chat-cn-latest and then add the placeholder node. So let's go ahead and do that. So I will specifically continue to use my version of chat-cn, which is 3.3.1.
I would suggest that you also kind of try and use the same chat CN version, even though I think this will continue to work even if you use an updated version now. So even using at latest should work. And you can see how we specifically use this placeholder node. Now, if this fails for you, I will try and put a link on the screen where you can find exact code of this component, right? Because that's all this is.
It will just add one file to our projects. If I press enter here let's go ahead and see this should now add placeholder node. There we go. It added two things. It added base node and place holder node.
Now I want to make sure that we know this was added with a CLI that we didn't write this. So inside of components I will create something called react-flow like this and I will select both base node and placeholder node and I will paste it here simply so I don't confuse them and select yes for updating the imports so let's go inside of base node and let's just see okay imports are fine placeholder node you can see it's having problems so let's just change this to its neighbor like this because we just moved both of them here I feel more comfortable this way so I know exactly these are the ones that I made myself, these are the ones from ShatCN and these are the ones from React flow using a ShatCN registry. Now that we have base node and placeholder node inside of components I'm going to create the initial node. What is the initial node? The thing we just specified here which doesn't exist yet.
So inside of components let's create initial node.tsx. Let's mark it as use client. Let's import type node props from xyflow react let's import plus icon from lucid react Let's go ahead and import memo use state from React. Let's go ahead and import placeholder node from dot forward slash React flow placeholder node like this. Let's export initial node to be memo.
Props to be a type of node props. Like this. Let's go ahead and define initial node.displayname. Initial node. And let's go ahead and return inside of here placeholder node and inside let's add a div and a plus icon give this a class name of size 4 Give this a class name of size four.
Give this a class name of cursor pointer, flex items center and justify center. Go ahead and spread all the past props to the placeholder node. Description will be at a first step here. Let me just see, do we need description? No, let's remove it.
No need for a description here. So just a big plus icon and I think that actually might be enough. Now that we have the initial node, we can import it from components initial node. Now that we have the registry node components, which is mapping Prisma type initial to a component initial node, and we use the placeholder node from the chat CN installation you can go ahead inside of source you can go inside of features editor editor dot TSX and let's go ahead and add node. Let me just go ahead and find what the name of the prop is.
So it's called node types and passing node components config here. So just the thing we've created where we map the Prisma type to the initial node here. And now if we've done this correctly, there we go. You can see how the initial node looks now. So now every single time you create a new workflow, you will have this new placeholder node, which we are going to use that when clicked on, simply opens the sidebar node selector, which will give the user much more options, right?
But that's what I want for the initial node and basically what we've learned here is how easy it is to add custom nodes to React flow which is what we are going to be heavily using in this tutorial. So we are going to create a registry of node components and then inside of Prisma, you know, later in node types when we add OpenAI, we are very simply going to copy this, change this to OpenAI and this will be OpenAI node. That's how it's going to work. And then React flow will know exactly what to render if the type is set to OpenAI. I purposely did this with initial, so it's the simplest possible example because this one literally displays nothing, has no data.
It's just a different looking node than the other ones. And I just want to show you like one more thing because I'm not sure if all of you completely understood what we did here. We are no longer using the initial nodes constant or the initial edges constant, we are literally fetching from the database about our current node state, right? So if I go inside of useSuspenseWorkflow, inside of get one and then find my create here, right? Where I create these.
Let me just check. How do I create many? Okay, create many and then like an array. Is that how you use Prisma? Okay, let me just quickly find how I do this.
Okay, I think that create many is also an object and then uses data and then you open an array. Yes, So like this. So instead of just adding like one initial node, if you were to copy this, paste it and created another initial node, but gave it a position of 200 and Then went back to workflows and if you click new workflow That should give you two initial nodes every time you create So that's what I'm trying to explain, right? We have successfully connected our database state with our canvas state. So we will now work from that principle and it will be very easy for us to translate whatever we make within this canvas into nodes that we understand on the server that we can easily turn into background jobs that we can easily update, remove, delete, right?
It's all gonna be in sync. So let me revert this quickly back to just a normal create. So just create with a single initial node. Perfect. Now before we wrap up, let's just enhance the look of our nodes even more because what we're gonna do now, we're going to reuse in every other node.
Inside of components, let's go ahead and create workflow-node.tsx and let's go ahead and mark it as use client. Let's go ahead and let's import node toolbar from xyflow react and position from xyflow react. Let's import settings icon from Lucid React and trash icon from Lucid React. Then let's import type react node from react and let's import button from UI button in here let's go ahead and create an interface workflow node props Let me just fix the way I did this. So interface workflow node props, which will accept children, which are a type of react node.
And then let's go ahead and do show toolbar to be an optional boolean, on delete to be an optional function, then the same for on settings, name optional string, and description optional string as well. And then expert function workflow node. Let's assign workflow node props here. Let's go ahead and destructure all of the props and set the default show toolbar to be true. And then in here, let's go ahead and let's return an empty fragment.
And let's check if show toolbar is set to true in that case, render node toolbar and inside of here add a button which renders the settings icon give it a size of small variant of ghost on click on settings and the settings icon shall have a class name of size 4. Duplicate this exact button and change this to be on delete and trash icon. After the toolbar render the children and then if a name is present render node toolbar again. Give it a position prop of position dot bottom is visible class name maximum width of 200 pixels and text center Then a paragraph here which will render the name which we pass and a class name font medium. Then let's check if we have a description and if we do a paragraph which will render the description.
This will have a class name of text muted foreground, truncate and text small. This will be very useful when we for example have a node named OpenAI and then a description for example not configured or if it is configured we will show a brief description of what we entered as the system prompt, for example. And the toolbar above will be used to quickly access the settings of that node. So now that we have this, Let's go ahead and go back inside of our initial node here and let's wrap it inside of workflow node from .slash workflow node which we've just created. So wrap the entire thing in it like so.
And now. When it's rendered here. Whoops. I have to find a way to turn off that agent thingy. Let me refresh to see if we can maybe already see something.
All right, so now on click this just becomes a node. Yes, not exactly what I wanted to demonstrate. Yes, I think placeholder node is like the worst node to demonstrate this into. But let's just for now end the chapter by adding show toolbar and set it to false just so I don't forget to do it later. I know this makes no sense.
I made a mistake. I should have maybe introduced this in another chapter, but the good thing is we are going to use this as soon as the next node is created. And it wasn't really too complicated. I'm just disappointed that you can't see what we did because this way it's hard to develop. You don't even know what you did, right?
So for now in the on placeholder node, let's see if I add on click. Can I maybe just like override it? So it cannot accept on click and let me see. Let's go inside of this React flow placeholder node. I think we might need to create some adjustments here.
So what I'm going to do here is I'm going to add on click to be this. And now on click should be passed further down. Let me see, I guess. So this is the base node. We have handle, we have source.
Okay. We have handle click here. Ah, yes. But you can see that handle click what it does is it creates this new node so we're actually going to remove that functionality yes, let's go inside of placeholder node and we can remove everything here really everything we don't need a single thing The only thing we should do is add on click here. And paste it here directly.
Let me see. The handle is fine. This handle is fine too. This is alright. Let's go ahead and change this from width 150 pixels to be width auto and height auto as well.
Border dashed, border gray 400, BG card, padding 2. Let's change this to padding 4. Text center text gray 400 shadow none let's do cursor pointer let's go ahead and give it that. Hover border gray 500 hover BG gray 50. Let's go ahead and add this.
Let's remove the unused use react flow and unused use node ID and let's remove use callback. And now instead of the initial node you will see that the placeholder node now accepts its own on click. So if I go inside of my workflows here now and just create a new workflow. I should now get the one with the normal initial node in here and there we go. Looks much better and clicking on it does nothing which is exactly what we want.
What it's going to do later is it's going to open a sidebar. So we have overridden the initial functionality of the placeholder node that was added in here with the command line interface so that we can take care of the onclick from the outside. And I think that was kind of the problem why we were not able to demonstrate workflow node. Let me try and remove this and there we go you can see that now when you click on it so if you click outside, it doesn't exist. When you click on it, you can see the settings and you can see the delete option, right?
And if I were to give this a name, initial node, you can see it's displayed here. And if I do description, click to add a node, you will see it has a description. There we go. So that is what I wanted to demonstrate. Now I personally think the big plus button is descriptive enough.
So for that reason, I chose to do show toolbar set to false here. And because I mean, this seems pretty self-explanatory, right? When we click here, it's just going to open a big sidebar node selector. Perfect. So that is what I wanted to show you and I'm so happy that I was able to do that.
Let me remove use state from react here it seems to be an unused import we're going to leave this as it is and I'm going to wrap the chapter up by adding just one more thing here instead of features editor editor.tsx let's add one more thing here. So we're going to learn how to add a custom element inside of the canvas because all of these three elements were built in. So what if you just want to add like a random plus button here at the top? Why would we need that? Well, once we add something here, this initial node will no longer exist.
So we won't have any way of adding new nodes. Because of that we need one more way to add new nodes. So let's go ahead and go inside of editor components and create add node button.tsx mark this as use client. Import plus icon from Lucid React. Import memo and use state from React.
And import button from components UI button export const add node button make it a memo component and return button with a plus icon here and go ahead and give this button an on click for now to just be an empty arrow function size of icon variant of outline class name BG background like this And let's do add node button dot display name add node button. Now that we have this you can remove use state. We will have it later when we actually enable this. Now let's go ahead and actually add this. We can do that by using panel from react flow react.
I mean XY flow react. And go ahead and render add node button in here from dot slash add node button. And to define the position all you have to do is define it. And you can see that once I do that I have a new button here in the corner. So now even if the initial node goes away I still have the plus icon here which is always fixed on the screen as you can see.
Perfect! So I believe that that is a good place to end this chapter. We got familiar with React flow, we learned how to connect nodes using code, right? Using those initial constants. But we also learned how to load the React canvas state from our server and how to render completely custom node components which didn't exist initially.
We also created some reusable toolbars which will be very helpful later. And we also started separating, you know, what was added from ShadCN and what we built ourselves from scratch. Amazing, amazing job. I think we've done more than enough now. So in the next chapter, we are actually going to learn on how to create the node selector sidebar, which will allow us to select some trigger nodes like manual trigger and then some execution nodes like open AI, Gemini, Slack, Discord.
So let's go ahead now and see what was the plan here. We updated the schema, node table, connection table, and we loaded the default editor state. Let's go ahead and push this to GitHub. So 15 editor, I have 12 changes here, some of them were migration schemas here. Alright so I'm gonna head and create a new branch.
15 editor. I'm going to click stage all changes and then 15 editor. Let's hit commit and let's hit publish branch. Now, as always, I'm going to go ahead and just create a new pull request here and since this was a lot of new things it would be a good idea to have another pair of eyes take a look at it. And here we have the summary.
New features. We introduced an interactive workflow editor with a node based canvas including pan and zoom controls, background grid, as well as a mini-map. New workflows now start with an initial node displayed on the canvas. Nodes feature a toolbar with settings and delete actions. Optional name and description are shown in a bottom label.
We added a add node button in the editor panel, UI only at this time, meaning no real functionality here. As well as some visual refinements for node structure and placeholders to support future node types. Exactly. As always, file by file walkthrough here and some interesting sequence diagrams. So the first one is explaining how when we fetch one workflow we attempt to find it or we throw an error and then if we do find it, we make sure to include workflows, nodes, and connections.
And then we map that to React flow type of node and edges. And when we return that we can properly render the react flow canvas and get exactly what the user expects to get. And in here we have a simple diagram explaining how when we create a new workflow we also create the initial node with it. And for the comments not too much actually. I made a typo in my workflow node.
I misspelled truncate and down here it is telling me that I should find a way to persist node edge state to the server. That is correct. Remember, we do have a save button in the workflow header but we never actually enabled it. We will work on that later but yes this is a good comment but we are on our way to fix that. So let's go ahead and merge this pull request.
And once it's been merged, let's go ahead and go back instead of our main branch. Make sure to synchronize your changes. And once your changes have been synchronized as always confirm with the graph. 15 editor. Perfect.
Merged into main. That marks the end of this chapter. Amazing amazing job and see you in the next one.