-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(dashboard): workflow editor autosave #6718
Conversation
✅ Deploy Preview for novu-stg-vite-dashboard-poc ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
apps/dashboard/package.json
Outdated
@@ -41,17 +41,21 @@ | |||
"class-variance-authority": "^0.7.0", | |||
"clsx": "^2.1.1", | |||
"date-fns": "^4.1.0", | |||
"debounce": "^2.2.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the debounce library used to debounce save workflow requests
"tailwind-merge": "^2.4.0", | ||
"tailwindcss-animate": "^1.0.7", | ||
"use-deep-compare-effect": "^1.8.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
used in the autosave hook, more details below
); | ||
}; | ||
|
||
const Toaster = ({ ...props }: ToasterProps) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
autogenerated
|
||
type ToasterProps = React.ComponentProps<typeof Sonner>; | ||
|
||
const SmallToast = ({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small sonner implementation shown in the workflow editor I will explain to you why later in the comments
name: z.string(), | ||
type: z.nativeEnum(StepTypeEnum), | ||
}) | ||
.passthrough() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
passthrough
is passing all the other fields from the persisted step apart from what is defined in the schema, for example stepUuid, controls, controlValues
, etc. Otherwise during the save we will be missing these and the BE will treat it as a new step.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh so this just doesn't strip out the unspecified fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
return; | ||
} | ||
|
||
updateWorkflow({ id: workflow._id, workflow: { ...workflow, ...data } as any }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to satisfy TS until the BE types are fixed
const id = toast( | ||
<SmallToast> | ||
<RiProgress1Line className="size-6" /> | ||
<span className="text-sm">Saved</span> | ||
</SmallToast>, | ||
{ | ||
duration: 5000, | ||
position: 'bottom-left', | ||
unstyled: true, | ||
classNames: { | ||
toast: 'ml-10', | ||
}, | ||
onAutoClose: () => { | ||
changesSavedToastIdRef.current = undefined; | ||
}, | ||
} | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how the sonner toast is used. Opened for the discussion.
Basically, in the Workflow Editor, we might want to show different types of toasts, like a small version when the workflow is saved or a bigger one when the step is updated. And I haven't seen a way of saying which variant you want to show, this can only be defined when Toaster
is used. That's why the only solution I saw was to use unstyled
property and provide the component to the toast
function call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you checked this out? shadcn-ui/ui#2254 (comment)
We can probably create our own functions with predefined css here. Or just a function that is just toast()
with a custom component and use that across the app. Unless we are not planning to reuse this specific one somewhere else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh nice, I didn't see that... seems to be a better approach, I'll update it then 🙌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahh I thought you could create your custom function...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two options:
- create a variant on the
Toaster
- use
unstyled
prop and provide a custom component <--- this is what I did
I didn't go with the first approach as we might want to show different toast types on the same page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll move the code into the custom function so that it will be more reusable
<span className="text-sm">Saved</span> | ||
</SmallToast>, | ||
{ | ||
duration: 5000, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tbd
}, | ||
}); | ||
|
||
useFormAutoSave({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
form autosave hook
const watchedData = useWatch<T>({ | ||
control, | ||
}); | ||
|
||
const debouncedSave = useCallback<() => void>( | ||
debounce(() => { | ||
handleSubmit(onSubmitRef.current)(); | ||
}, 1000), | ||
[handleSubmit] | ||
); | ||
|
||
useDeepCompareEffect(() => { | ||
if (formState.isDirty) { | ||
debouncedSave(); | ||
} | ||
}, [watchedData]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically, we watch all the form data changes and compare it deeply with the previous form-watched data between re-renders. If the form data was changed and the form is dirty, we call debounced save, which calls the react hook form handleSubmit
that triggers validation. If the data is valid, then the onSubmit
will be called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe additionally we can show the sonner when there are validation errors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! Just some minor comments/suggestions.
name: z.string(), | ||
type: z.nativeEnum(StepTypeEnum), | ||
}) | ||
.passthrough() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh so this just doesn't strip out the unspecified fields.
const id = toast( | ||
<SmallToast> | ||
<RiProgress1Line className="size-6" /> | ||
<span className="text-sm">Saved</span> | ||
</SmallToast>, | ||
{ | ||
duration: 5000, | ||
position: 'bottom-left', | ||
unstyled: true, | ||
classNames: { | ||
toast: 'ml-10', | ||
}, | ||
onAutoClose: () => { | ||
changesSavedToastIdRef.current = undefined; | ||
}, | ||
} | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you checked this out? shadcn-ui/ui#2254 (comment)
We can probably create our own functions with predefined css here. Or just a function that is just toast()
with a custom component and use that across the app. Unless we are not planning to reuse this specific one somewhere else.
novu
@novu/client
@novu/framework
@novu/headless
@novu/nest
@novu/nextjs
@novu/js
@novu/notification-center
@novu/node
@novu/providers
@novu/react-native
@novu/react
@novu/shared
@novu/stateless
commit: |
What changed? Why was the change needed?
Workflow Editor autosave functionality and sonner toast for successfully saved action.
Because there are a few issues with the BE implementation on the video you can see that I only added one step.
Screenshots
Screen.Recording.2024-10-17.at.23.05.22.mov