In this chapter, we're going to improve the UI of our workflows entity by adding a set of reusable components. Loading, Error, Empty, List, and Item. Let's go ahead and start with the three states first as they are the simplest to do and to demonstrate. Make sure you have your project running. I'm using npm run dev all to run both processes at the same time and make sure that you are on your workflows page.
Once you are in here let's go ahead inside of source components entity components. So the last thing we developed here was I believe entity pagination and now we're just going to add a couple of simpler ones. So all of these three states loading, error, and empty are going to share one common prop. So let's create a reusable interface called state view props with an optional message. And then let's extend state view props for each specific state.
For example, loading view props extends state view props and it adds the entity option. So let me show you how that looks like in one line. And then let's export const loading view. Now let's go ahead and destructure the props and assign them to this loading view props. So we're going to have the message and the entity.
Let's set the entity by default to be items if not passed and let's pass the message separately like this. Then let's go ahead and let's return a div element with a class name flex justify center items center full height flex1 flex column gap 4 and let's render the loader 2 icon inside from Lucid React. So just make sure you imported this from Lucid React. Let's go ahead and give the loader icon a class name of size 6, animate spin, and let's do text muted foreground. Then in here a paragraph with a class name text small text muted foreground.
And let's go ahead and either render a message or if the message was not passed let's just do loading and then entity whatever entity was passed three dots as simple as that that is our loading view now let's go ahead inside of features folder workflows components workflows.tsx and let's go ahead and export const workflows loading To very simply return loading view from our newly created entity components export and let's give it entity workflows like this. Now that we have workflows loading let's go inside of app folder, dashboard, rest, workflows, page.tsx. And let's go ahead and render it here instead in the suspense. Workflows loading. And make sure you add the import.
So, so far we've imported workflows container, workflows list and now workflows loading from features workflows components and then the workflows export file. There we go. So now if you go ahead and refresh, you should see a nicer loading state. It's very brief, but you can see it right there. And in fact, I want to modify it ever so slightly by going back inside of entity components and maybe changing the text of the loader here to text primary.
This way it will be an orange color. Let's see. Let me do a hard refresh here. It's cached so I can't see. Maybe if I try searching, really not helping my example here.
Okay, yeah, we were just able to see it for a second. I think it might look a little bit better with that orange color. I don't know. You choose for yourself. And now for the message.
You know what I think it might be easier to just send a message here instead. And just render this conditionally. If we have a message, then just go ahead and display it. And whenever you use this operator, also make sure to turn this into a Boolean. Otherwise, this will render if it's truthy.
So add double exclamation points here or you can use the Boolean operator or you can use the ternary operator with a question mark and the else, right? So I personally prefer this. I'm used to it but whatever is easier for you. And then inside of the workflows.tsx let's pass a message here loading workflows. I think this gives us kind of more explicit controls.
Again this agent mode. How do I close agent mode like that. And it should behave exactly the same now. Again I'm having trouble showing it. Let's go ahead and continue by focusing on some other entity components.
Now that we have the loading view, let's go ahead and copy the loading view and the interface and let's paste the entire thing here and let's change this loading view to now be error view and at this point we actually we don't... We no longer need the entity here. We can just reuse state view props because yeah we just remove the entity completely so we can do the same thing here just reuse the state view props everywhere it's the same And instead of this we're going to do alert triangle icon from Lucid React. So the same place where we imported a loader 2 icon a moment ago. And make sure to remove animate spin from here.
So how do we trigger an error? Well, very easy actually. First let's go inside of workflows.tsx and let's go ahead and do an export here. So this will be workflows error. Instead of loading view it will be error view and this will be error loading workflows.
Make sure to import the error view from components entity components and then inside of the page file also add workflows error and make sure to use it in the error boundary. It's a self-closing tag like this. In order to break it go inside of workflows list and just throw new error here. There we go. Error loading workflows and then remove this.
Perfect. So now that we have that, let's create an empty state. The empty state will be slightly different and we're actually going to be using some new Shatzian components which I'm very happy about. So instead of entity components folder here go ahead and add all the components from empty. So you can import it like this, or you can just use this because we are in the components folder.
Empty, empty content, description, header, media, and title. In order for this to exist in your UI empty you have to make sure that you use the proper npx chatcn 3.3.1 version because it's a very new component and I can actually quickly show you that uishatcn.com. If I go inside of the components here, you can see that empty is highlighted as new and it will give us this nice little, no projects yet example. So that's what I want to do here. So using this empty, let's go ahead and now build it.
I want to do it down here simply because all of my other states are down there. So my empty is going to be called interface empty view props and this one will actually extend it So extends state view props and it will add on new which is an optional component, I mean an optional prop here. So it makes sense for this one. Empty view like this and let's go ahead and assign empty view props. Let's extract the message and let's extract on new which is optional.
Inside of here we're going to return empty, which we just imported, and we're going to give it a class name of Border, BorderDashed, and background color of white. Then let's use empty header, So let me just quickly fix this component, empty header, empty media, and variant of icon. And inside of here, render package open icon from Lucid React. You can of course use whatever icon you think is best. I personally think this one kind of represents like an empty box.
So now let's close the empty header and let's add empty title here, which will simply say no items like this and empty description. Let's go ahead and render the message. And we should only do that if the message is available. So again, render it conditionally. The reason I rendered these things conditionally is so they don't take any weird UI space and remember to turn it into a boolean so you don't get any unexpected render results.
Same thing for on new. If it exists inside of empty content here, add a button, which you should already have imported here and give it on click on new and give it add item. Like this. If you want, you can also add props for like individual empty title label, add new label. I mean you absolutely can do that if you want to.
Let's go ahead inside of workflows now and just as we've created workflows error and workflows loading now let's create the workflows empty export const workflows empty this one will be slightly different so first Let's go ahead and let's return a fragment and empty view inside of here. Just make sure you input that empty view here. And let's go ahead and pass in message. You haven't created any workflows yet. Get started by creating your first workflow.
And let's go ahead and give it an on the new option by adding create workflow, use create workflow because we already have it imported here actually from hooks use workflows. And let's also make use of our premium model handle error and model from use upgrade model. We also have this imported because we used both of them instead of workflows header. And now where we simply, let's just add constant handle create, which calls the create workflow, mutates it, skips the first parameter because we don't have any options, but we need to pass something to access the second parameter which has the custom on error option, which allows us to handle error and open the model if needed which means that we also have to render the model just like that. And of course on new handle create.
There we go. And now that we have workflows empty we have to render that as well. The way we can do that is by actually not even leaving the workflows file. We can just go ahead and find workflows list here. And let's simply do if workflows.data.items.length is...
I think this will work. We can just return workflows empty Like this. So now if you go ahead and search something obscure you should see no items. You haven't created any workflows yet. Get started by creating your first workflow.
So yeah maybe not the best message to show for search empty, but you get the idea, right? This would appear if you had no workflows at all. So maybe you can change the message to something a bit more generic, like no workflows found, get started by creating a workflow. So it doesn't indicate like there are no workflows in this account. Maybe that's better by creating a workflows, no workflows found.
I don't know. I like the longer message simply because it fills the space in a nicer way. One problem though is that it expands a lot but we're going to fix that very soon by creating our new component which is going to be the entity list. So that's something that we actually don't have yet. You can see how instead of my workflows list here I manually added this and I still render this inside of a completely centered JSON string.
So now let's go ahead and let's focus on using the entity list. So for that, we go back inside of entity components where we just developed the empty view. And in here, we have to create an interface called entity list props and I'm going to write this one together with you because it's a little bit more complicated. Interface entity list props accepts a generic and then items will be a type of that generic. We're going to have a render item function which will accept the item which is the generic and it will of course have an index and it will return a React node.
This will allow us to basically have our own dot map done within this reusable component. And in order to have a dot map we also need to have a way to have a key for each object. So let's pass getKey, which is technically optional, but it's a good developer practice to have this as an option to pass if for whatever reason we don't want to use the index as the key. And here we are going to pass an option for empty view what if no items are found and an optional class name if we ever wish to modify how the entity list looks like. Let's export function entity list and pass in the generic here.
Go ahead and add items, render item, get key, empty view and class name. And for the class name, we don't have to pass anything in the class name actually. I mean, let's obviously add it. And let's assign entity list props and forward the generic from the entity list here. You're going to see how this generic will be used in a moment when we actually render it.
So first things first if items.length is equal to zero and if we have the empty view component passed, in that case we're going to return a div and this div we're going to have a class name to center that empty list so flex1 flex justify center and items center and it's going to render the empty view within a limited maximum width so make sure to add this div here. This way it will not expand like this. It will kind of be smaller. All right, after this, oh let me just expand a bit so you can see the code. After this we have to actually render our items.
For that we're going to use a div and a class name. And for this class name I'm going to use the CNUtil. So make sure to import CN from libutils. And I'm going to give it some default classes which will be flex, flex-column and gap-y4. And then I'm going to pass class name if we ever want to override that.
And then inside of here we can go ahead and iterate over our items. So each item will have an index accompanying it here and we're going to return a div like this and each div needs to have a key which will either be we at the get key function or we are going to manually call get key and pass in item index. This was an incorrect explanation. If we have the getKey function, we're going to call the getKey and pass the information of the item and the index, otherwise we're just going to use the index. And then let's use the renderItem function and pass in the item and the index.
This way we can easily change every component of this entity list to display whatever we want for either the render or for the empty state. Now we can head back to workflows.tsx inside of features, workflows, components, workflows. And now we can go inside of the workflows list and make this a little bit better. So I'm going to remove this entire thing and I'm just going to return the entity list from components entity components. And then let's go ahead and pass the items to be workflows.data.items.
GetKey is very simply going to get the individual workflow from each of this item list and it will use the workflow ID and you can see how we have type safety here. And you're probably wondering how does it know exactly what the workflow is because of the items prop, because instead of the entity list here, we decide that the items are the generic. So then we can reuse that generic type through all the other functions that we have and have type safety passed along. So if I change this to, I don't know, credentials or some other type, it will have that type here. So that's how this is working.
That's why we needed that a little bit complicated interface to allow us to adapt to any items list that we pass. This is how people actually build those reusable UI components, which render lists. If you ever wondered like data grids and things like that, they need a type safety like this. So it can accompany any scenario. Render item is the next prop which we need and for now let's go ahead and just get the workflow and render a paragraph which will say workflow.name because we don't actually have a component.
An empty view will simply be workflows empty. And now we should have a bit of a nicer experience. Here it is. And if I remove this, I should just get paragraphs of all of my workflows. And when I search for something obscure, I get my no items result.
And when I click add item, all it should do is just create a new workflow. And you can see how this one does not redirect me there. So that's what I was talking about, right? Sometimes we might not want to redirect the user. This action definitely tells the user to kind of immediately go there, but you can decide for yourself.
Do you want inside of your workflows empty here to also add an onSuccess, get the data and then do router which you need to import from next navigation, push workflows data ID. If you want to you can do that and then every time they create a new one they will be redirected. So perhaps that's a good idea to do here as well. Use router. We already have user out there imported.
Perfect. So now when you click add item from here it will also redirect there. There we go. Perfect. So let's go back inside of workflows now and now let's go ahead and render the workflow item component.
So the entity item component which we need to create now will be a little bit larger but it won't really be complicated in any way. So let's start by creating an interface EntityItemProps. In here let's start with a couple of easy ones. Href, title and subtitle which is a required, an optional react node. And then we will have optional image and actions and both of them will be react.react node also optional.
Then we're going to have an optional on remove which can be either a void or it can be a promise void. And then let's go ahead and add is removing and class name which are all optional. One is a Boolean and one is a string. Now let's go ahead and export entity item with all of those props destructured. Href, title, subtitle, image, actions, on remove, is removing, and class name.
And inside of here, what we are going to do is we're going to return a link which we already have. Make sure it's imported from next link. Give it an href of href and prefetch option. So automatically by clicking on the entity item you will redirect to its page. What we have to do now is we have to import all of the card components.
I'm not sure if we have that so let's go ahead and quickly check in our import with don't. So let's go ahead and add card, card content, description and title. You can also use this type of import since we are in the same folder. And we are also going to need all components from the dropdown menu. So same thing, dropdown menu, content item and dropdown menu trigger.
Now that we have that, let's go back to our entity item and let's start building the actual card here. So card will have a following class name. It will be dynamic so openCN. We already have it imported. By default padding 4, shadow none, hover, shadow and cursor pointer.
If is removing We're going to make sure that it has some kind of opacity to indicate that something is happening and that it should not be interacted with because it's being removed. And as the third, we're going to pass the class name, which can be whatever the user wants. I mean, the developer wants. So card content will have a class name of flex flex row items center justify between and padding zero. And then let's add a div with a class name flex items center and gap three.
And in here we're going to render an image. Then let's go ahead. So still inside of this div here let's open a new div and a card title. And in here we're just going to render the title of this item. The card title will have a class name of text base and font medium.
And then We are going to have a check if the subtitle exists. In that case, let's add card description and render the subtitle. And the class name will be text extra small. And now we have to develop the drop down menu action. So let's go ahead outside of these two divs, still inside of card content and let's check.
If we have actions or if we specifically have on remove, Let's go ahead and let's do div. Inside we can render the actions if the developers want to pass actions like this. Flex gap X4 items center suggests some simple styling here. And now let's specifically develop on remove here because this will be the one most commonly used in our case. We open the drop down menu, we open the drop down menu trigger, we make this trigger behave as child so it becomes the child element which in our case will be the button.
This way it won't be rendered as button within a button but it will use the styles of our chat CN button. That's why we are doing so size will be icon variant will be Ghost on click will be event stop propagation. The reason we need this is so that when you click on the drop down menu you don't trigger the link which redirects the user away. So that's why we are stopping propagation here. And inside we're going to render more vertical icon.
And let's go ahead and give it a class name size 4. Make sure you've imported this from Lucid React. Outside of the trigger, let's go ahead and add drop down menu content. And it needs to have a few props as well. First, align, which will be end.
And on click, which will again stop the propagation. So even when you click in the drop down content, it doesn't count as a link click. And finally, drop down menu item and trash icon from Lucid React with delete text and a class name to the icon of size 4. Again, make sure you've imported trash icon from Lucid React. And now we have to develop the handle remove method which will be very simple.
So it's just going to be a generic. We already have on remove here. So what we're going to do is const handle remove, make it asynchronous, event will be react.mouseEvent. First things first, prevent default, then stop propagation, check if you are already removing and then, my apologies, check if we have the onRemove method and then await onRemove. And you could perhaps check if is removing, just break the method.
This way you cannot trigger it twice. And then very simply on the drop down menu item add on click handle remove. There we go. Now we have the entity item and now that we have that we can go back inside of workflows.tsx and let's go ahead and let's develop the workflow item. So the way we're going to do that let's just do export const workflow item and the type will be workflow and you will be able to import the workflow type.
Let me see do we maybe have it already or do we not we don't have it. I think we can import it from Prisma schema. So let's import workflow from generated prisma like this workflow and let's import it as a type workflow so it's referring to our schema model workflow this way we have its exact types and then we're going to map that down here. So that's exactly what it will be used for. And let's destructure the, let me see, should it be data?
Oh, okay. We're going to pass it as data workflow like this. So we don't individually have to pass all the fields and in here let's simply return entity item from components entity components Make sure you've added this new import. Href is going to be forward slash workflows forward slash data dot ID. Title is going to be data dot name.
Subtitle is going to have a fragment here updated. And let's go ahead, say to do, then we're going to have an empty space, add bool as in bullet point, created empty space to do. So makes no sense now but we will fix this in a second. Now for the image let's go ahead And for now I think we can just pass an element like div class name size 8 flex items center justify center and the render workflow icon from Lucid React inside. Here's the tricky part again Lucid exports both workflow and workflow icon.
Both of these are valid. But make sure you don't use the workflow from Lucid React because it will conflict with the type here. So just make sure to use workflow icon. Honestly I'd also do this as workflow type maybe, but it's okay for now if we don't have any other workflow thingy named inside of this file. So workflow icon is rendered here.
Now let's go ahead and give this a class name. Size 5 and text muted foreground. Later this image will be completely dynamic and depending on the first trigger of the workflow it will display the image of that trigger. For example Stripe or Google Form or Webhook like things like that. But since we don't have that developed at all yet we're just going to fall back to this simple workflow icon.
All right, now that we have that, let's go ahead and just pass in on remove to be an empty arrow function is removing to be false. And I think that now we should be able to revisit our workflows list component. Here it is. So inside of the render item, now Instead of rendering this paragraph, let's render workflow item and pass in data to be workflow. And let's see, this now isn't compatible it seems.
So user ID, string, user ID. Let me see. Created at are not compatible. Ah, aha. I think I know exactly why this is happening it should work if you take a look it looks fine all right but why is the time why are the types wrong well as you can see here yeah things happen during the DRPC to server component, to hydration, to cache, types get lost, especially for the kinds of types which are in the database referred to as a date, but on the front end are referred to as a string.
For example, types of property created at are incompatible. String is not assignable to date. So yeah, that's a problem because one has a, I guess a timestamp of some sort in a string and the other one has an actual date, right? So how do we fix that? We fix that with super JSON.
So let's go ahead and do npm install superjson. And I will show you the exact version I have. Superjson. Usually I teach people to install superjson during the TRPC installation, but this actually might be better because you actually learn why we need it. So you can see, yes, now we have this problematic types, right, incompatible with our front end and our database because database has, we're using Postgres as the database And we are basically using schema to communicate to Postgres using those types, right?
But our JavaScript app on the front end doesn't have the same types as our Postgres database. So we need some way to transform that data to make it compatible and to make it, you know, able to communicate both ways from the database to the front end and from the front end to an API call which needs to create something in the database. And the way you fix this is super easy. You just have to revisit our trpc init file here and actually all of the files. So let's see.
In here we already have inside of inittrpc.create You can see we have data transformers and you can actually learn more about them by following this link. SuperJSON allows us to transparently use standard date, map, set over the wire between server and the client. Basically that was the problem. You can return any of these types from your AP resolver and use them in client without having to recreate the objects from JSON. So that's how that works.
So we can now finally enable the transformer superjson and we can import SuperJSON. Did I show you my SuperJSON version? I don't think it matters that much but it's 2.2.2. So SuperJSON as you can see now is added as a transformer here and now I think there are some other places where we need to add it. Let's check the server itself.
Okay, I think not needed here. Query client, in here we need it. So you can see it's already commented out. So import super JSON from super JSON and instead of make query client, uncomment serialized data and uncomment deserialized data. This way it will work both ways on the database and to the database and to the client.
And client.tsx let me check if we have something here. We do and we also have an error unless it is enabled. So make sure to uncomment transformer inside of HTTP batch link in client.tsx and let's also just import superjson from superjson and once you do that I think that's all. I think we so We added it inside of client, we added it inside of init, we added it inside of query client. The only place we didn't add it is inside of server.
And I think we don't need to add it here. If we've done this correctly and now revisit workflows.tsx, There should be no error. You can see that updated at and created at are both treated as date. If you are still getting errors, you can just reload your window or restart your TypeScript session or restart Visual Studio Code. So that's what super JSON does.
It's a data transformer. Yes, I'm actually happy this happened because this way you can learn why we need it rather than just blindly follow me with installing it. And we already saw that the UI is working just fine. So nothing much to really view here. It worked before as well, but now the types are correct as well.
So later, and yes, Clicking on it redirects to individual workflow. Later, each of these little icons will change depending on what is the first trigger node in each workflow. So if it is like a Stripe webhook, it will change to a Stripe icon. If it's a Google Form, it will change to Google Form. If it's manual execution, it will have a little arrow.
Things like that. Little details which are going to make this look better. Now let's fix the to-do thing and let's enable the deletion. So fixing to-do is quite simple. Let's go ahead and npm install datefns.
As always I'm going to show you the version that I installed simply in case you're wondering oh why don't I have the same API. It could be that you have a completely different major version depending on when you're watching the video. All right, now that we have this, let's go ahead and import a couple of things from DateFNS. So let's import, actually just one thing, format distance to now. That's the one we need.
And let's scroll all the way down to our workflow item. And let's change the to do here to instead be format distance to now, data dot updated at. Because that will be our primary order, right? Which was the most recently modified workflow to show first. So we are showing it first here as well like this and created also format distance to now data created at and this way you will see updated 21 minutes created 21 minutes.
We currently don't have an option to update so all of them will be exactly the same. And if you want to you can also add to both of these an option to add suffix add suffix true Like this. And when you add that to both of them, it will have the ago. So updated 21 minutes ago, created 21 minutes ago. Perfect.
One more thing left to do. And that is the on remove method. So let's start with our okay I just closed everything I didn't need to close. Let me go inside of routers.ts here. Do we have the remove method?
Let me check. We have. It's had protected procedure. Great. And it will delete a single unique file using the unique field and using permission user ID field.
Perfect. But what I think we don't have is use workflows hook for it. So we have it to create workflow. We never developed one to remove workflow. So let's do that first.
So hook to remove a workflow export const, use remove workflow. We're going to start usual by adding trpc and the use query client. I think we have both of them imported here already and let's return use mutation. In the first argument we are passing trpc.workflows.remove and we are opening the mutation options. Instead of the mutation options we are passing on success and if it succeeds let's go ahead and first do toast.success and let's go ahead and indicate which workflow was removed workflow data.name removed if you want to you can put it in quotes let me see what how do the other toast look like?
The other put it in quotes, so it would make sense for this one to do that as well. What I like to do here now is I like to invalidate my queries. The RPC workflows get many dot query options. Like this. It's also perfectly valid to not want to invalidate all queries and instead just want to manually go inside of cache and remove that one.
That's also, I mean, that's obviously a better solution. But the thing is, we have limits in place. So when you re-invalidate, it's not like you're going to refetch your entire workflow database. You will just refetch at max 5 or 10 items for that user. Right?
I find it easier, especially in tutorials, you know, to just invalidate the entire queries. Same thing goes when you create a new one. It's easier for me to just invalidate and load new ones rather than go inside of the workflows cache and then append one to the top or here filter out by the ID. But you can do that. Yeah, that would technically be even better.
But you don't have to worry about this being super unoptimized because we do have limits in place inside of our constants here. So we don't let just a billion records to be load every time we invalidate. Perfect. So now that we have use remove method here, we can go inside of workflows.tsx and instead of workflow item we can actually use it. So what I'm going to do is I'm going to do the following.
I'm going to import mutate async. I'm going to extract mutate async and I'm going to map it to remove workflow and I'm going to extract is spending and let me go ahead and use import use remove workflow here from all of the other ones and actually I don't need to complicate this that much. I can just do remove workflow. No reason to complicate it. Const handle remove like this.
Let's go ahead and just do remove workflow that mutate and in here passing I.D. Data I.D. And then inside of on remove we can pass in handle remove remove workflow is pending. There we go. So if we did this correctly I will try to remove harsh breezy Portugal by clicking delete.
You can see it's grayed out and it's re-invalidated and removed. Amazing job! So we just finished the entire UI for our handling workflows API thingy And what's cool about it is that we now have a set of components that we can reuse for credentials and for executions. So we no longer have to worry about building any of that from scratch. We can easily reuse it for anything.
We can reuse the pagination, we can reuse the header, the new buttons, everything. We can reuse the empty states in here. We can reuse the loading states. Let me try and demonstrate it. No luck.
Oh, okay. You saw it. Perfect. We can reuse the errors and the delete methods here as well. Perfect.
I believe that marks the end of this chapter. So let me see. We created the UI components for loading, error, empty, list, and for item. And Now let's go ahead and push this to GitHub. So 13 workflows UI.
Let me first review my files here. 9 files, package.json page, entity component, workflows, use workflows, client, init and query client where we initialized super JSON. So I'm going to go ahead and open a new branch here. 13 workflows UI. Then I'm going to stage all of my changes and I'm going to make a commit 13 workflows UI.
I'm going to commit and I'm going to publish the branch. Perfect. And now let's go ahead and do our GitHub pull request review. So compare pull request, create pull request, and let's have another set of eyes take a look at our code. And here we have the summary.
We enhanced workflows page with dedicated loading and error views. We added an empty state with a guided flow to create a new workflow. We introduced a consistent workflow list item with images and human readable time stamps. We enabled deleting workflows with user feedback on success. And of course, we integrated super json for more reliable data handling and compatibility across the app and added SuperJSON as a runtime dependency.
Exactly. In here as always file by file walkthrough. There wasn't too much business logic to review here really, but Perhaps some of you might be interested in this, which is the diagram explaining how super JSON serialization and deserialization happens. So feel free to pause the video and take a look here. Again, great catch by Code Rabbit explaining what seems a super simple thing we did which was actually very important for this project and to make it work all together.
And good thing is this project didn't include too much business logic so we actually have only one comment to add error handling for workflow removal. So we have on success but we forgot to do on error. So yeah, definitely something we can add. Other than that, no comments. Let's go ahead and merge this pull request And after we have merged it, let's go ahead back inside of our main branch.
Let's hit synchronize changes. And as always I like to confirm once the changes are synchronized inside of my graph here. 13 workflows UI merged back here in the main branch. I believe that marks the end of this chapter. Let me go ahead and mark it as finished, pushed to github, created a new branch, created apr and reviewed and merged.
Amazing, amazing job and see you in the next chapter.