In this chapter we're going to be working on the editor state which will include adding the save functionality, delete functionality for each node and the settings functionality for each node. We're also going to fix some of the bugs which CodeRabbit pull request review noted last chapter. So let's start with that. We have an issue of empty on click handlers and a specific typecast which we might not need. So make sure you have your app running and let's take a look at one of the examples here.
So right now if I click on this button, a node selector opens and if I click on the one in the upper right corner, same thing happens. Let's go ahead and actually look at the definition. So if I go inside of my initial node in source components here you will see that I use the set selector open both here and the node selector but I also do it here in the on click. Now, the reason we need it in the on click here is because we specifically want the placeholder node click to trigger the node selector and not the workflow node which is essentially as you can see just some node toolbar which renders the children. So because of that reason we specifically need an on click of this component to open it.
But an interesting case happens in our add node button component. In here we actually have an empty on click. So why is the node selector opening? Why is this working? That's what CodeRabbit proposed as a fix like hey you are missing setSelectorOpen setToTrue here.
If you change this nothing will change it just works but it also worked before. Why? It's because of the way node selector works. If you go inside of node selector you will see that we render the children inside of sheet trigger. Now this sheet trigger basically acts as a button which has set selector toggle.
Right? So I believe that even if we completely remove the on click we can just rely on the fact that we passed on open change here. So if I try now, you can see it works just as well. So if for whatever reason, yours doesn't work or if you just prefer being explicit, you can go ahead and do set select or open here and set it to true if that's what you want right but basically this sheet can communicate with the trigger and once the trigger has been clicked it will call the on open change that we pass here and it will execute it again the reason that doesn't work here is because it registers this as the trigger because that's its nearest child. But we want it to be active on the placeholder node.
So because of that, it's not working as expected. If you disable the onClick on the placeholder node in the initial node component, you can see that nothing really happens. Because it's expecting a click on the workflow node, not the placeholder node. Now you could potentially move the node selector here. And I think that then maybe it will work.
I'm not sure. I haven't tried. Let's see. Yes, then it does work, but I'm not sure what are the implications of node selector being wrapped inside of the workflow node. I'm not sure, but yes, I built the entire project like this.
So I'm going to stay true to my source code, but yes, you could just move node selector to wrap the placeholder node and then users click on the placeholder node will trigger this the same way the individual on click does. Alright once we resolve those issues which okay we resolved one of them so empty on click handlers Now let's do type casts. So I think that's just a unneeded cast we added. If we go inside of HTTP request forward slash node. So inside of features, executions, components, HTTP request folder, for some reason, we define no data constant and we give it a cast.
I think we can just remove the cast and when I hover over this it is HTTP request node data which is exactly what we casted it as. So this is obviously working as expected. No errors are being shown which means this works just fine. So we can go ahead and mark this as fixed as well. Now let's go ahead and let's implement the save functionality.
So I want to start that by going inside of the workflows feature router. Basically the TRPC router. And just as we have create and just as we have update name, let's copy update name because it's the most similar one and let's change it to be update. It's going to be a protected procedure and the input will be a little bit different. So I'm just going to collapse this because the object will be quite larger than before.
So we are still going to need the id but we are not going to be working with name. Instead we will receive a bunch of nodes in fact it's going to be an array of nodes inside of that array we're going to have some objects each object will have an id a type which is a Z.string or it can be nullish then we're going to have a position for each node which will be an object again with X and Y After position we're also going to have some data which will be a record and then we're going to set the key to be a type of string and the value can be a type of any because the data can literally be any and it's also optional and besides receiving nodes we're also going to be receiving edges so we are receiving this from react flow right so don't confuse these values with our database of nodes and connections. We are now going to have to transform this value into database compatible values. So the objects that will be stored inside of edges input will be source, which is a type of string, target, which is a type of string, and source handle, which is string nullish, and target handle, which is the same.
All right, that is our input here. So now let's go ahead and start by extracting the fields here. ID nodes and edges from the input, Make sure to mark this as an asynchronous mutation. You can remove this for now. Let's go ahead and see if we find the workflow or not by using ID and user ID.
We can change this to find unique or throw. So this will always be an existing workflow because if you just use find unique then it can be null and then you have to manually fix that so just make sure you throw otherwise because we will handle that error. And now let's go ahead and let's make sure to open a transaction so we ensure consistency. So let's return await Prisma.$transaction asynchronous, open a transaction and let's go ahead and start doing some things. First things first we're going to delete all existing nodes and by deleting nodes for this workflow that's also going to delete connections because we are using cascade instead of prisma schema.
If you take a look at nodes, my apologies, I think we need to take a look at the connection. Yes. So if from node or to node is deleted for that connection it will automatically remove itself from the database. So what we have to do to clean this up is just go ahead and use the transaction to delete all nodes associated with this workflow id. There we go, just like this.
And then we have to create new nodes. So now we're going to use this input here to create nodes compatible with our database. So create many. Data, nodes.map, get the individual node and return an immediate object. Now let's pass in the id.
Workflow id will simply be id. Name will be node type or unknown, type will be node.type and now we have to fix this by manually casting as node type. Looks like we already have node type imported from Prisma. Perfect. Add position to be node.position, data to be node data or an empty array.
And now let me go ahead and fix the this to be dot node and I believe that's all we need to use the transaction to transform whatever input we received from react flow editor into our database compatible nodes. And now we also need to create connections. So let's go ahead and wait tx.connection.createMany, data, edges.map, get the individual edge and return an immediate object. Now inside of here workflow id will be id from node id will be edge.source to node id will be edge.target from output will be edge source handle or main to input will be edge target handle or main. Great!
And since this transaction doesn't really modify the workflow directly, just its relations, it will never really appear as modified. So we have to end this by also updating the workflows update at timestamp. Otherwise, it will be bad user experience, right? You just created some nodes in a workflow, you clicked save, and in the list, it doesn't appear as updated. So let's just do await TX workflow.update, where we found the matching ID, data updated at new date.
Just like this. And finally return workflow. And just like that we now have an equivalent update procedure which will transform React form value, React flow values into our database compatible values the same way we just did the opposite, right? Inside of get one, we transform server database nodes into react flow compatible nodes. Inside of our update we do the opposite, we transform react flow nodes into database compatible nodes.
Perfect. So now that we have that let's go ahead and create use update workflow. So instead of use workflows where we develop use suspense workflow we use update workflow name Let's go ahead and in fact copy that specific one. Let's paste it here. So this will be a hook to update a workflow.
So this is located in here. Let me show you. Features, workflows, hooks, use workflow. Use update workflow will have query client ERPC and it will call workflows.update. And this will say updated or let's actually call save because that's kind of the action that will happen.
It will save, right? So what do we want to invalidate? We do want to invalidate get many and we want to invalidate this one. Yes, perfect. And this will be failed to save workflow excellent so not much changing needed in this case perfect now that we have that we have to go ahead and implement some kind of state management why do we need that well take a look at what we currently have.
So right now the save button is in a component called workflow id header or how did I call it header my apologies I called it editor header as you can see. Now the editor header doesn't really have access to my editor component state. So in order to pass that around, I can either create some kind of context, I can do some prop drilling, or I can just install one of the state management libraries. Now, the one that I found myself using more and more is Yotai, so I'm gonna go ahead and do npm install Yotai. The reason I like using it is because the syntax is almost the same as just using normal use state except it's global and it works everywhere.
So I kind of really like that. Let me go inside of package.json, search for Yotai so you can see the exact version I'm using just in case you're wondering and now let's go ahead and let's create a store for this so I'm gonna go ahead right here inside of source features editor I'm going to create store and I will create atoms.ts and then in here I'm going to import type react flow instance from XY flow react I'm going to import atom from your type and I will export const editor atom to be atom which is a type of react flow instance or null if it wasn't initialized and then I will be able to share this atom I will assign it inside of the editor component and I will fetch it inside of my editor header component. Now, there is just a quick note here. If we go to Yotai documentation here, you can use the link on the screen to visit it. And if I go inside of docs here, We usually have a provider here, but I never, I don't think I really understood when we are supposed to use it and that just crossed my mind here.
So let me go ahead and find next JS guide. Okay, so this works, this works, yes, but because the problem is I can use the provider or I don't have to use it. So let's see. Okay, so to limit the lifetime of the store to the scope of one request, you need to use a provider at the root of your app or a subtree if you're using Yotai only for a part of your application. Let's go ahead and just add this.
I think it will make our app behave better. So instead of layout, instead of the app source app, I'm just going to import this. Let me move globalist all the way down here. I think that should be down. So provider, Yotai.
And let's go ahead and just add provider like this. There we go. And now let's go ahead inside of our editor.tsx component so where we actually render the React flow and what we have to do now is we have to find a way to, well, you know, initialize that editor instance. So what I'm gonna do here is I'm gonna do const set editor instance, actually just, let's just do set editor, use set atom from Yotai. And let's pass in editor atom from store atoms which we've just created so we are working inside of features editor components editor.tsx and we've created a neighboring store atoms so that's why I was able to import the editor atom from dot dot forward slash store atoms Now that we have this what we can do is very simply pass a new prop here in the react flow called onInit and pass in setEditor.
Simple as that. So now the moment editor is initialized it will initialize the React flow instance inside of this atom. And now what we should do here is we should load that inside of the editor header. So let's go inside of editor header.tsx right here and in here as you can see we have a completely unused save button. Here it is, editor save button.
So what we have to do inside of the editor save button is load that instance. So const editor will be useAtomValue from Yotai, editor store, my apologies, editor atom from store atoms. And now we can pass the value around so let's also go ahead and do update workflow let's call it save workflow to be use update workflow make sure you didn't import the name one so we are using the new one we just created which uses TRPC workflows update which has all of those big inputs and now let's go ahead and do const handle save we're going to do the following so if there is no editor let's just break the function right no point in doing anything further now let's get the nodes by using editor.getNodes Then let's get the edges by using editor.getEdges. And finally let's do updateWorkflow.mutate, passing the id to be workflow.id and passing the nodes and the edges. And my apologies, this is save workflow.mudate.
And as you can see, no errors because the type of nodes that I receive here is fully compatible with the type of TRPC workflow input that we've created. Perfect. Now that we have that, let's go ahead and change this to be handle save. And let's go ahead and change this to be our save workflow is bending. There we go.
Let's see, I'm using handle save. I think this should all be working now. So let's go ahead and try it out. So I think it's best that we just start a new workflow completely. So like a clean working slide here.
And I'm going to add a manual trigger. And I'm going to add an HTTP request. And I'm going to connect the two. And I'm going to position them in some way I can recognize like this and I'm going to click save and let's see workflow then the name of the workload saved. Now I'm going to go back inside of my workflows list.
I'm going to refresh here so I clear any cache or any you know store and once I load this there we go we can now officially save our editor state and we didn't even have to do anything special to load it because that's already finished, right? Instead of editor.tsx we already do that. When we load it here using the useSuspense workflow we already populate the nodes and the edges from it. So we are now successfully transforming between server values for connections and nodes and react flow values for nodes and edges. They are one and the same now.
So that's one part of our job finished. Let's go ahead and mark it as finished. So we added the save functionality. Now let's go ahead and implement the delete functionality. Now this one shouldn't be too hard either.
So what I want to do is go to the place where we actually call these functions. So I believe that is currently inside of components. I think I have, it's not, instead of features. So we have to modify two components. One is for the trigger and one is for the execution.
Let's go inside of the trigger one first and let's find the base trigger node which we developed last time. And in here we have handled the lead but we never really implemented it. So let's go ahead and do that now. So I'm pretty sure there is an easy way to do that by using set nodes and set edges from use react flow. So as long as this component is initialized within the editor, which I think is definitely the case, we should be able to modify set nodes and edges from here directly.
We have the ID which is great because we need the ID to be able to manipulate and filter this node. So instead of handle delete Let's go ahead and try set nodes and get the nodes. And now what we need to do here is let's call this current nodes so it's easier to understand. I'm going to open a function like this and I will do const updated nodes to be current nodes dot filter get the single node and compare that node's id against the id of the node here. So if it is not id and let's simply return updated nodes there we go but we also have to delete all the edges so let's go ahead and do set edges current edges here and I'm gonna do the same thing const updated edges are going to be current edges dot filter get the individual edge and check if edge whoops edge dot source is not equal to node to ID and if edge.target is not equal to ID and return updated edges.
And I think that this should now be enough for us to, well, handle delete. So we already have handle delete here, So that seems to be working. Now this is just for the trigger node. So the only way you can test this is by this. I'm not even sure if we connected the button.
Let me try and click here. It looks like it works. And if I click save, looks like that works. And if I refresh, that works too. And don't worry about the position change.
The node didn't change its position. We just always do this, right? Every time the editor loads, we focus on it. Perfect. So that seems to be working.
That was very simple, wasn't it? Let's go ahead and copy this entire Implementation here and let's copy it inside of the base execution node and that will be the only other place where we are going to need this I think. So instead of executions, base execution node and let's do the same thing. So I can remove this, paste all of this, fix the indentation and import use react flow from XY flow react. And now this should work as well.
So if I go ahead and select this and delete it, there we go. Works amazing. Obviously if you don't save and refresh it will still be here. Perfect. So that's one more issue taken care of.
Let's go ahead and mark that as finished as well. But now we have the settings functionality and this one is a little bit more complicated. But I do want to work on it because it's the only way we can move forward by actually executing these things. Just before we do that there is one more thing that I want to show you and I want to confirm if it works or not because of the changes we just did. It is inside of features, editor components, editor.tsx.
So it's quite annoying that right now we can only delete one node at one time, right? So if I have like two nodes here, I can't select them. Well, maybe I can, I don't know, but I'm not really seeing that indicator is quite well? So let's go ahead and add some more props to react flow in order to make it better. So I'm going to give this snap grid 10 and 10 inside of an array like this.
And now it's kind of having to follow the grid. So if you snap it around now, let's also do snapped to grid. And when you try moving it around now you will see how it kind of sticks. Right? I think this is a slightly better experience now.
Simply because it can't go anywhere so you will be able to center them because of the snap to grid. Then let's do pan on scroll. Now pan on scroll will basically change the controls here. They are going to work on the opposite directions. This will make it much easier to work with this with a touch bar.
And if you enable pan on scroll, basically it's just the way you modify your canvas. It will change the controls and it's actually more familiar to WhatsApp year and N8N use in my experience. And let's do pan on drag and let's go ahead and actually disable it. And instead, let's add selection on drag. So what will happen now is that you can see you can now only move this by using your scroll mouse right.
But or if you're in a touch bar, it's much easier actually. But what you can do now is this. You can now finally select them. Okay. And I think that's actually all I wanted to do.
But now that you actually are able to select your nodes here, you can go ahead and delete them. There we go. I just wanted to test if that is working. So yes, we just added this three things here. If for whatever reason you don't like that behavior, you can remove it, right?
So I tried to replicate the behavior of popular platforms and this is mostly how they work. The thing that's hard with this is moving the canvas around. I haven't really found any other way to do this other than using scroll bars now, like you can't drag because that is selecting. If you have a laptop touch bar, then you can easily do it. The way I'm able to scroll left and right using my mouse is because I use a mouse that has to scroll bars.
So I don't know how you will be able to do it. So for that reason, you can play around with this, this settings and see if you like them or not and then just remove or leave them. It doesn't matter, right? It's just I just wanted to show you that there is a way to like select things and then delete them using the backspace. All right.
So once we have this, Let's go ahead and let's work on implementing the settings. So let's start with super simple one by going inside of features, triggers, manual trigger node.tsx. The reason I say super simple is because the settings for manual trigger literally don't even exist, but we are going to create them anyway simply to be consistent with user experience of all of our nodes. So instead of manual trigger, create a new file called dialogue.tsx. Mark it as use client and then import everything we need from the dialog components which come from chat.cnui.
After that go ahead and create props like this and let's go ahead, actually we can just do like interface props, export const, manual trigger dialogue, assign the props here and execute like this. Then get the open on open change here. And now all we have to do is just create the dialogue. So that's fairly easy to do. We render the dialogue, we give it open and we give it on open change on open change and then we render the content inside we render the header then we render the title which will say manual trigger We go ahead and copy this and render the description.
Inside of the description here, well, we can just say configure the settings for manual trigger nodes and the dialog header, Create a div with a class name of py4 inside a paragraph manual trigger because that's the only thing we can really say to the user now, right? This is a manual trigger. What else can we say. Obviously I'm doing this to show you how this will look like in a more complicated node. Now that we have this dialogue we can go back inside of this node and Then we can finally make sense of this fragment by also adding a manual trigger dialogue and let's render it above.
I think that's semantically correct. Manual trigger dialogue. Did I forget something? Why is it not? Why is this an error?
Oh, it's missing properties. Oh, yeah. Okay. So now let's go ahead and just add a simple state here from React. Dialogue open.
Set dialogue open. And all we have to do is just pass those two props. And that's it. That's all we need. So this is a super simple example because there is no really any settings, but at least we have the UI for those settings.
And now we can enable these. And handle open settings can just be a super simple function like this. The reason I'm separating it in a function because in other components this can be obviously more complicated. So let's go ahead and just get a new workflow going on here and go ahead and add a trigger node. And Now when you click on the settings button it will open it.
Manual trigger. Configure settings for the manual trigger node. Now in here I've just left this small comment and you can also do double click on it by the way. I've left this small comment like manual trigger but in some other node this wouldn't be as simple. This would be a whole form here.
So for example, I don't know, in order to make this better, maybe you can say, used to manually execute a workflow. No configuration available. I don't know, something like that. But I think it's a good idea to have it here simply so users always expect to see more information about a node when they double click or when they click on the settings. So they will try that on like other nodes here because in here, obviously we need it.
Like what HTTP request, what method, any authorization keys, right? That's what users will expect. So we have to teach our users that they can always access the settings to see more information. Great. Now while we are here, I actually have an idea of also enabling the status because I told you we can't do the status, but we actually can, and it's not that difficult.
And if we do it now, we won't have to remind ourselves to do it later. And it's not too difficult to do. I think that the number one thing we have to do is install a package from React flow. So let's go back here. Let's go inside of UI and here we have it, the status indicator.
So this is it, right? And let's go ahead and do this. So NPM here. Oh, you also have manual here. Yeah, you can also just copy it if you don't want it to be added, sure.
Or you can use, you know, my public assets folder and in there you can find all of these components that I added this way. So, NPX, ShadCN, okay. Base handle, change this to node status indicator. Okay. And then Immediately after I get this, I'm going to move it into its own folder.
So instead of components, here it is, node status indicator. Let me just confirm it's been finished. It is. I'm going to drag it and I'm going to drop it inside of React flow. So I know all of those have been added by them and not by me.
All right now let's go ahead and do the following. We have to go inside of base trigger node so let's go inside of base trigger node here and here we have a to do wrap within node status indicator. So let's do that. I'm going to add node status indicator and I'm going to wrap the entire base node in it. And I'm going to indent it like this.
Now in here let's go ahead and give it a status. Which for now can just be set to initial. Let's give it a variant of border and let's give it a class name of roundedL2Excel looks like it does not accept class name alright so that's something we're going to have to modify the node status indicator for but let's go ahead and just take a look at our app now. So nothing is happening but if I change the status here to loading there we go. You can see something is happening.
If I do success you can see it's green. If I do error you can see it's red. So what we have to do obviously is make it match the look of our current node. So first things first, let's make sure loading is turned on. I think that one might be the most complicated one to match.
So now we're going to go inside of node status indicator instead of the react flow folder and we're going to modify it so that it works for our use case. So first things first, Let's go all the way to the top here instead of the node status indicator props and let's add class name to be an optional string like this. Then inside of let's see. So this is a spinner loading indicator. That's fine.
Let's go inside of a border loading indicator here and let me add class name here and let me add class name here and it's going to be a optional type of string. There we go. OK. So we have that. And now we have to find a way to render it.
So I'm just thinking what is the best way to render it. I think it should be in this class name specifically. So let's go ahead and open this instead of curly brackets like so, and let's import CN. Looks like we already have it in use here. Perfect.
So I can just drop this as the first value and then extend it with class name. As simple as that. Perfect. I think that for now, yes, let's also do it in one more place which is this one node status indicator. So this is one that actually has to pass the class name further down.
OK. So we have the loading we have the overlay we are not interested in that The border is the one we are interested in. So let's pass in the class name here. Class name. There we go.
You can see no errors. Perfect. For the success one, for the other one, let's not modify anything now. Let's just go back instead of the base trigger node and let's try and changing the class name now there we go no more type errors and when I click Save looks much better doesn't it try changing this to error not good okay something seems broken success oh yeah okay So head back to node status indicator. We have to modify the success and the error ones very simply by adding CN here like this.
So CN and then pass class name as the second argument. And same thing here. CN, wrap it and pass class name as the second argument. Perfect, There we go. Now let's go ahead and see if we can slightly modify this.
So for the error, I want to use border red, 700 with 50% opacity. For success, I want to use emerald. I want to use green, 700 with 50% opacity. So exactly like this. Yes.
All right. Now let's go ahead inside of the status border here. And let me see if I can slightly modify this. So I'm going to modify minus left to be two pixels minus stop to be two pixels and I will do this calculation to be plus four pixels and the width calculation will be plus 4 pixels as well. So this will make the border look thicker.
For a rounded, I'm just going to change this to rounded MD and I'm going to change the border to 3. And now it will just be like a thicker border. All right. Now let's go ahead up here and see what can we change here for this spinner. Let's see.
We have rounded seven pixels. Let's change this from rounded seven pixels to rounded SM. And let's change this to loading so we can see what we are doing. Rounded SM. All right.
And let me go ahead and change the colors a bit. So this will be, we're going to turn this into RGBA. And I'm going to pass 0.5 here. And the second one can stay the same. Yes I don't think it needs any modification.
But we will be modifying something here above. Right here. So we're going to make this thicker as well. So minus left two pixels minus top two pixels. Height calculation will be four pixels and width calculation will be four pixels pixels as well.
So now all of these have thicker values. I mean thicker look. OK. Obviously this is just my personal decision. You can of course modify this however you prefer.
Now let's go ahead and Let's just use a better way of passing the class name. I mean passing the status. So right now instead of base node, my apologies not base node, instead of features triggers base trigger node, We just pass the status as loading. So instead, what I want to do is I want to develop like the, I want to use the type from our node status indicator component. So I think that we can now do this.
I commented this out inside of the base trigger node props. Let me see. Yes I can now import node status from my node status indicator. There we go. And now that I have the status I can actually use it here status and by default we are going to set it to initial and now we can just pass the status here.
There we go. So right now by default nothing is happening. Now we're not yet finished here because we also have to now modify the base node. Now that we can accept the status. So let's just do status here.
Status like this and that will obviously throw an error because base node itself does not accept the status. So just make sure you have that opened instead of source app components react flow base node. So this was added here using the CLI. I will go back inside of my base trigger node here to make sure this error is resolved. And I will also find manual trigger node.tsx because in here This is also the place where we actually define the node status.
So what I'm going to do is just const node status. And in here, I'm just going to modify it. So now I can send this this way. There we go. It's now controlled directly from manual trigger node instead of from the reusable components.
Perfect. And now here we're going to change it as needed. So what we have to focus on now is the base node. So let's go ahead and do that. We're going to start by creating an interface base node props.
Extends HTML attributes. HTML div element. And passing the optional status to be a type of node status from node status indicator component. Perfect. So now we have the base node props which accept the status prop.
And in the forward draft, whoops, We now have to modify this slightly. So instead of just HTML attributes, we can now use base node props, which will extend both the HTML attributes as well as our status, which we should be able to pass here now. And you can see immediately in the base trigger node, we now no longer get the error for passing that status here. So I think that now we can close the base trigger node and just focus on developing the base node. Perfect.
So some things I want to modify in here. I want to remove all of these comments. They're just confusing and I want to remove all of these react flow features here. And now let's go ahead and do this. So I'm going to change this from rounded medium to rounded small.
That's the first thing I'm going to do and immediately it looks a little bit better because the border is now matching. Then I'm going to change this to be border and I'm going to do border-muted-foreground. Immediately a little bit darker now as you can see. I can leave the BG card as it is and I can leave the text card foreground and let me add hover BG accent like this. And I can remove this hover ring 1.
We don't need it. There we go so that is the behavior I am expecting here and now we have to do something in regards to the status that we are passing. So let's go ahead and do and check if status is equal to error In that case we're going to render X circle icon from Lucid React. And we also have to extract the status from here. Is that not how I do it?
Let's see status. What am I doing incorrectly here? Status, status is here. Three dots expected. Not sure.
Let me just debug for a second. Oh, oh, my apologies. I didn't realize this is a self-closing tag. Change this. It's no longer a self-closing tag.
And now you have to make sure to add props.children here like that. And then let's do status equals to error. My apologies. I wasn't aware that it was. That it was a self-closing tag And now we can do X circle icon render in here.
And let's go inside of manual trick. Oh, it's already open here. And let's go ahead and just change this from no status to loading to error. And there we go. We can now see a very ugly X circle icon.
In order to make it pretty, let's add a class name, absolute. And let's add right, 0.5. Let's do bottom, 0.5. Let's do size 2. Text, red, 700 and stroke, 3.
There we go. Let's copy this and change the error to success. Change the icon to be check circle 2 icon. All icons are from Lucid React. Let's go inside of node change this to success and let's change this to be green 700 And the last one will be if the status is loading.
So let me change this to loading. And now this will be a loader to icon from Lucid React. And let's see. So absolute minus right 0.5 minus bottom 0.5. Size to this will be text blue seven hundred stroke three and animate spin.
All right so now I believe there is a slight problem here because you can see how the spin when we add the spin animation it kind of moves further here. So let me try and doing right one. Actually, Let's see. What if I do minus right 0.5 and minus bottom 0.5? I think now it's exactly positioned as for example, error.
Yes. That is the exact position. Perfect. So we can now bring this back to initial. And now we have our look and feel as we expected it.
Amazing. Now, the only problem here is let's remove the trailing comma. Now, the only problem here is that this currently does not work. If you go inside of the HTTP request node inside of executions components HTTP request node so if I go ahead and do const node status and set it to loading and if I try to pass the status here you can see that node status will not even be accepted here so now we just have to do the same thing to the base execution node as we did to the base trigger node so there we go, We can uncomment this inside of a base execution node props import the node status from here and also import node status indicator component itself. Let's see.
Do I have to do I do so wrap within node status indicator. Here we go. So the entire base node is now wrapped in that. Let's go ahead and let's pass the status to be the status prop. Let me just see if I actually use it.
I don't. So here I have to use it after I added it as a type of course. And I also have to give it a default value of initial in case it's not passed. You can see how immediately this starts working. Let's also give it here.
So status, status to the base node. And you can see how we now just reuse that entire code for this specific thing. And let me just check if there is something that we've missed. I think we just have to specify variant border here, just in case. There we go.
Let's go ahead and set up the node in the HTTP request, change this to error, works, success, works. Amazing. Perfect. So we just fixed all of the to do things when it comes to our status, which I think it was very easy to demonstrate. I thought there was going to be a problem with demonstrating.
So for that reason, I didn't want to do it, but looks like it works just fine. And now that we added the settings, let's wrap this chapter up by implementing the settings for the HTTP request. So you can see how we are going to preserve some actual know the data because this is super simple, right? Just the manual trigger. But what about this one?
This one is more complex. This one requires some kind of form, right? Now, let's go ahead and focus on that. Let me see is there something I can check off from here. So we added well we almost finished the settings functionality.
I don't want to do it. I don't want to check it just yet. So let's focus on that now. So let's start by copying the existing dialog inside of source features triggers manual trigger and copy the dialog and paste it in the HTTP request folder. Now let's go inside of that dialog and let's rename it.
So instead of manual trigger dialog, this is going to be HTTP request dialog. And let's change this to be HTTP request like this. And let's change the description to be configure the settings for the HTTP request node. And inside of here let's remove this entire div just leave an empty space for the form that is about to be here. Now let's go inside of the node of the HTTP request here And let's go ahead and add a simple state control here.
So I'm gonna do that right here. Basically a simple use state from React for dialogue open and set dialogue open with a default value of false. And I'm going to move the node status here simply because I wanted to be consistent to my manual trigger. And that comes right after this. It's easy.
It's easier for you if you make your code consistent. It will be like easier to understand what's going on. At least that's the case for me. All right. So now that we have that, let's go ahead and do const handleOpenSettings setDialogOpen to true.
And do const handle open settings set dialog open to true. Great. And now we can go ahead and we can render HTTP request dialog from dot slash dialog And we can go ahead and pass those two values open and on open change. There we go. Perfect.
And now let's go ahead and pass in the on settings and on double click to be handle open settings. And now it should behave exactly the same. So if I click here, there we go. HTTP request. Same as this.
If I double click, it opens exactly the same way. Perfect. So now we have to configure our HTTP request dialog to accept some more elements here. So instead of this dialog, let's go ahead and extend the props. So I'm going to add on submit here to be an optional of, actually it's gonna be required.
So it will accept the values, which are going to infer type of form schema, which we don't have yet, but we will in a second. And then we're going to add a default endpoint to be a type of string, default method to be a type of get or post or put or patch or delete. And default body will be a type of string. Now let's go ahead and let's import Z from Zod. And let's do const form schema Z.object endpoint Z.url and an error message please enter a valid URL.
Method is going to be z.enum with options of get, post, put, patch and delete. The body will be a string. It's going to be optional. Not every HTTP request needs a body, right? It can just be a simple get method.
And for now I'm going to leave it like this. I will prepare a refine and I will comment it out. We will need refine later but it makes no sense for me to do that now because it would be hard to explain why. So I'm just leaving this as a to do to not forget that I have to do that. It's basically to accept a specific template language that we are going to implement later on.
All right. So now we have all of those. We have default endpoint, default method, and default body. And now our HTTP request is actually expecting that. So after onOpenChange let's do onSubmit here for now just this.
Let's do default endpoint to be node data dot endpoint like this. Default method to be node data dot method. Default body to be node data dot body. And now when I'm writing this, I'm realizing that I could have technically just sent initial values as the node data object. And I think that is a much better solution actually especially when it comes to extending this in the future.
So for the next chapter I will make sure to look into improving this. I will add a little comment to do check if it can be improved by just sending initial values, no data. Because this is kind of pointless writing every single thing here and it will save time in defining this as well. All right, now let me just check something so props.data always exist. Okay, Let's just go ahead and do this.
And I think that's OK. Yeah. No errors. Perfect. But still nothing really happens here.
So now we have to develop the actual form. So in order to do that, let's go ahead and let's import everything we will need to create that form. So that will include all the form elements, form, form control, description field, form item label and form message. We're going to need the input from components UI input. We're going to need select from components UI select and we're going to need text area from components UI text area.
Now for some other components like Zod, we're going to need the Zod resolver from hook form resolvers Zod. We already have this installed and we used it already all right good and we're going to need use form from react hook form and we're going to need use effect from React to update if any new values are received in the meantime. All right, I think that's all for now. Later we are going to need, I'm just going to add JSON5, Simply like a reference for me so I know what I have to explain here. But for now we don't have to concern ourselves with that.
Now inside of the HTTP request dialog it would be a good idea to extract all of those new values here. OnSubmitDefaultEndpoint, DefaultMethod and DefaultBody. I assigned this as the default value to get. Again, this might change later. I will explore for the next chapter to see if we can improve this by sending just the initial values.
Let's do form, use form, and inside of here, add default values here. Endpoint, default endpoint, method, default method, body, default body, like this. Now let's go ahead and improve the type safety of the form by adding Z dot infer type of form schema. And let's also add resolver here ZodResolver form schema. There we go.
Now, let's go ahead and do this. So const watch method will be form.watch method. The reason we are doing this is so that we can do some dynamic field hide and show depending on the method that the user selects. So basically we are going to show the body field if either post or put or patch are in the watch method, which we are looking at above. At above.
In the handle submit let's define the values as z.infer type of form schema. Let's call on submit and on open change to false so we close the model alright now we are ready to actually build the form so let's add a form and let's spread the form in it Now let's use a normal HTML form element and let's give it an on submit prop of form dot handle submit handle submit inside. So the same way we built the form for the out screens. Space Y eight, margin top four. The reason I'm comparing this to out screens is because I won't be doing as in-depth explanation as I did during that part where it was the first time we encountered building forms.
So let's now add form field, which is a self-closing tag. Let's give it control, form.control. The first field will be used to control the method of this HTTP request. Let's go ahead and execute render here like this and it's going to render form item with a form label method. And then we're going to add select.
Let's give this select on a value change field on change default value field dot value. And then let's open form control select trigger. Let's give it a class name full width. And let's give it a value. Which is a self closing tag with a placeholder select a method.
Alright. And now outside of the trigger, outside of form control, we're going to add select content with all the possible options so let me expand my screen select content with multiple select items with matching value here and the value prop. This value prop is very important so make sure you don't misspell it. Basically add all the options that we write here and here in the enum. And now we have our first field.
We can now select basically what is this method's mode of request. And now let's also add a form description here. The HTTP method to use for this request and let's render form message which is a self-closing tag which will appear if there is an error in this field specifically. Perfect. So now let's go ahead and let's copy this entire form field.
Let's paste it here. And now let's go ahead and modify it. So this one will be used for the endpoint. So let's go ahead and change the form label to endpoint URL. And luckily this will be a little bit simpler.
You can remove the entire select thing. And after the label add form control. Render the input which is a self-closing tag. Spread all the field properties. And just give it a placeholder here.
And this is the one I'm going to use simply because it will be a cool way of showing your users what you can do. For example, you can query api.example.com users and then you can actually do like a dynamic thing here. Of course, you could manually write 1, 2, 3, but if you want to reuse the data from the previous node you could use our templating language. Now this makes no sense to you right now but this is what we are going to implement. We will basically allow users to reuse the data from previous nodes.
Right now it makes no sense because we just have this combination but imagine the HTTP request was connecting to some third node which already has some data in it so that's how we're going to be able to do that. What if it was three HTTP requests connected one after another, right? That's kind of what I'm trying to show here. And because of that we're going to add an advanced form description here like this static url or use and then I'm using this kind of thing so I can display double brackets like use variables for simple values or use json and then the name of the variable to stringify objects because you will be able to pass objects, you will be able to pass individual values. So this is just some instructions for the users how they will be able to use this.
Don't worry, this will make more sense later once we actually do this, which I commented out, JSON5 refine, you will see how cool it is. For now, let's just, you know, write the explanation for it. And one more field left, which is only if show body field is enabled. So only then are we going to render this form field. So let's go ahead and just copy the render for this one.
Paste it here. Let's go ahead and give this control form dot control. Let's go ahead and give it a name of body. There we go. So this will be the request body.
Instead of using an input, it's going to be using a text area. And let's give it a class name minimum height of one hundred and twenty pixels font mono and text small. So now when you switch to post, you should see a request body in here. Because it only shows if either post, put or patch are selected because otherwise you don't need the body field. And now what I'm going to do is I'm just going to improve the placeholder for this.
So the placeholder will be a little specific. You don't obviously don't have to do it the same as me. So this will be the placeholder, just explaining to the user that they can do like dynamic values in here. So this is how it looks like in here. You can pass an object, define user ID, name and items and you can use the dynamic templates language here to show variables, right?
So I'm going to update the form description to kind of resemble that as well. I mean it already kind of resembles it but let's go ahead and do this. JSON with template variables. You can use variables for simple values or JSON variable to stringify objects. And basically you are now explaining to your users like hey you can do advanced things here you can use the value from your previous node and add it here so obviously this is just placeholder it's not important code so you can just write whatever you want here if you can't bother with you know writing this it's obviously a long and annoying line of code but we will implement this later and in order to wrap this up let's just end it by adding a dialog footer which we forgot to import and button which we also forgot to import.
Make sure it has a type submit save and this a margin top of 4. There we go. Perfect. So now what we have to do to end the chapter is find a way to preserve the value. So let's go back inside of the node here.
And let's actually do that. So what we're going to do here is we are going to add set nodes from use react flow like this from xy flow react And then I'm gonna go ahead here and do const handle submit. The values which I'm going to accept are endpoint which is a string, method which is a string as well, and body which is an optional string. And then in here, I'm going to do set nodes. And then I'm going to go through my current nodes.
And I will do nodes.map, find the individual node and I'm going to find the node which matches the prompts.id which means okay I found the node that I have to add this data to and let's simply return by spreading the existing value of the node, opening the data object, preserving whatever is currently in that data and then just adding those new values like this. And then I also have to do return node here. There we go. So very simple handle submit. I will take a look if we can somehow reuse the, well, we can definitely, you know, reuse the values from the dialogue.
We can just export a Z inferred type of form schema and then we can use that here so it doesn't have to be like this. All right. And now that we have the handle submit let's just go ahead and pass in the handle submit here. There we go. I get no errors simply because I my types are exactly the same here but we can improve this definitely.
Let me just go ahead and do export type. I don't know form type. And then I can import this here. From dot slash dialogue. There we go.
And now the values have the exact same thing and I don't have to manually type that. Perfect. So now on submit we'll go through the state of the React flow and it will save those values. There is just one more thing we have to do before we test this out and that is instead of dialogue, instead of here, we need to add a use effect to reset the form values if the dialog opens with new defaults. So let's add use effect like this.
If open form reset endpoint default endpoint method default method body default body like this and let's go ahead and add open default endpoint default, method, default, body, and form. All right. And I think that now this should be working fine. So let's just do a refresh here. And let's add an HTTP request.
Let me select a post here. Let me do HTTPS code with Antonio.com and in here I'm going to do I don't know we're just pretending here user ID 123 and let's click save. And you can see how immediately we have post method targeting code with Antonio.com. Perfect! So if I go inside of my node.tsx you can see that that's exactly what happens here.
We are reading the node data and we can now construct the description here. Let me just quickly go instead of base execution node. I think I made a typo here for truncating. Let me see. Oh, workflow node.
Let's go inside of workflow node. So that is located inside of source components, workflow node. Am I missing? Yes. Truncate.
I just had to fix that typo. There we go. So now it's all in one line. Perfect. And you can see when I open it's preserved and if I click save here.
And if I do a hard refresh here there we go it is all preserved the data is saved successfully amazing amazing job so we can now mark that as completed amazing That's it for this chapter. So 17 editor state. We have 18 files. Let me close this. So these are all of my files.
18. 17 editor state. Let me go ahead and open a new branch. 17 editor state. I'm going to stage all of my changes here.
17 editor state, I'm going to commit, I'm going to publish the branch and then I'm going to go ahead and open a pull request to review all of these changes and see if there are some improvements or critical mistakes that we've made. And here we have the summary by CodeRabbit. Some new features. We added visual status indicators for nodes, loading success and error. HTTP request node is now configurable via dialog.
We can configure the method, endpoint and the body. A manual trigger node now also includes a setting dialog for consistency. Add node button opens the node selector. So this actually worked before but we now explicitly gave it an on click so it's no longer an empty on click as per CodeRabbit's previous comments. Editor gains save action for workflows with feedback and data persistence.
Exactly. And we changed the editor interactions with grid snapping, scroll to pan, and refined selection drag behavior. Obviously, I told you to adjust this to whatever you like that you can work with. And we also added functionality to delete the nodes and its edges and another bug fix to fix the text truncation that at least I had. Now, I don't think there was anything like too complicated happening here but we can definitely take a look at how our save function works.
So once we save using the editor header we read the editor atom which was initialized when it read the editor instance and we send a mutation with nodes and edges. And now what we do is we have a transaction which will verify the workflow ownership, delete existing nodes, and also cascade all connections and then create new nodes and new node data according to that. Same goes for the connections and finally it will update the workflow and that will result in a success message for the user. In here we have a super simple dialogue explaining how we populated the values of the HTTP request dialogue and how we update it using React flow set nodes. As per some comments here, we have some TypeScript issues it appears, but I think this is incorrect.
It's telling me Border3 is not a Tailwind utility. That's because most AI models are not familiar with Tailwind version 4 which I'm pretty sure introduced Border3. In here it's telling me to not use the default value and instead to use value which could be a good point. I'm not sure. I will take a look at that.
And in here, yes, it's basically it doesn't like the fact that we are casting the value here. So for the type I just defined the string. So in here it's telling me to use a native enum and pass the node type. We could do that but then on the client I would have to do casting. So we'll see.
I will take a look if there is a better solution for this. Other than that all good so not as bad as I thought it would be. We did write a lot of code but looks like most of it was good. Let's go back to the main branch and as always let's make sure to synchronize our changes. Once you synchronize your changes I always recommend just going inside of the graph so you can see that everything here is fine.
Perfect. So now that we've done that let's go ahead and wrap this chapter up by confirming that we pushed to GitHub, we created a new branch, a new PR and we reviewed and merged. Amazing amazing job you just finished with the editor state and see you in the next chapter.