Repository: novuhq/novu
Pull request #8134: feat(dashboard): Step conditions autosave
Author: login: desiprisg
Labels: name: @novu/dashboard
Created: 2025-04-15T10:54:40Z | Merged: 2025-04-22T12:46:54Z

### What changed? Why was the change needed?

* Added autosave functionality to step conditions.
* Fixed a bug where editing step conditions first when creating a step caused an error on trying to edit the template later on.



Diff:
diff --git a/apps/dashboard/src/components/conditions-editor/add-condition-action.tsx b/apps/dashboard/src/components/conditions-editor/add-condition-action.tsx
index dc26c32d657..efbbc65f955 100644
--- a/apps/dashboard/src/components/conditions-editor/add-condition-action.tsx
+++ b/apps/dashboard/src/components/conditions-editor/add-condition-action.tsx
@@ -3,7 +3,7 @@ import { ActionWithRulesAndAddersProps } from 'react-querybuilder';
 
 import { Button } from '@/components/primitives/button';
 
-export const AddConditionAction = ({ label, title, rules, handleOnClick }: ActionWithRulesAndAddersProps) => {
+export const AddConditionAction = ({ label, title, rules, handleOnClick, context }: ActionWithRulesAndAddersProps) => {
   if (rules && rules.length >= 10) {
     return null;
   }
@@ -14,7 +14,10 @@ export const AddConditionAction = ({ label, title, rules, handleOnClick }: Actio
       variant="secondary"
       size="2xs"
       className="bg-transparent"
-      onClick={handleOnClick}
+      onClick={(e) => {
+        handleOnClick(e);
+        context?.saveForm();
+      }}
       leadingIcon={RiAddFill}
       title={title}
     >
diff --git a/apps/dashboard/src/components/conditions-editor/add-group-action.tsx b/apps/dashboard/src/components/conditions-editor/add-group-action.tsx
index 549d6f54a85..589d5977813 100644
--- a/apps/dashboard/src/components/conditions-editor/add-group-action.tsx
+++ b/apps/dashboard/src/components/conditions-editor/add-group-action.tsx
@@ -3,7 +3,14 @@ import { ActionWithRulesAndAddersProps } from 'react-querybuilder';
 import { StackedPlusLine } from '@/components/icons/stacked-plus-line';
 import { Button } from '@/components/primitives/button';
 
-export const AddGroupAction = ({ label, title, level, rules, handleOnClick }: ActionWithRulesAndAddersProps) => {
+export const AddGroupAction = ({
+  label,
+  title,
+  level,
+  rules,
+  handleOnClick,
+  context,
+}: ActionWithRulesAndAddersProps) => {
   if (level === 1 || (rules && rules.length >= 10)) {
     return null;
   }
@@ -14,7 +21,10 @@ export const AddGroupAction = ({ label, title, level, rules, handleOnClick }: Ac
       variant="secondary"
       size="2xs"
       className="bg-transparent"
-      onClick={handleOnClick}
+      onClick={(e) => {
+        handleOnClick(e);
+        context?.saveForm();
+      }}
       leadingIcon={StackedPlusLine}
       title={title}
     >
diff --git a/apps/dashboard/src/components/conditions-editor/combinator-selector.tsx b/apps/dashboard/src/components/conditions-editor/combinator-selector.tsx
index 9298bc8d802..9a86cbf6285 100644
--- a/apps/dashboard/src/components/conditions-editor/combinator-selector.tsx
+++ b/apps/dashboard/src/components/conditions-editor/combinator-selector.tsx
@@ -1,12 +1,19 @@
 import { type CombinatorSelectorProps } from 'react-querybuilder';
 
+import { toSelectOptions } from '@/components/conditions-editor/select-option-utils';
 import { Select, SelectContent, SelectTrigger, SelectValue } from '@/components/primitives/select';
 import { cn } from '@/utils/ui';
-import { toSelectOptions } from '@/components/conditions-editor/select-option-utils';
 
-export const CombinatorSelector = ({ disabled, value, options, handleOnChange }: CombinatorSelectorProps) => {
+export const CombinatorSelector = ({ disabled, value, options, handleOnChange, context }: CombinatorSelectorProps) => {
   return (
-    <Select onValueChange={handleOnChange} disabled={disabled} value={value}>
+    <Select
+      onValueChange={(e) => {
+        handleOnChange(e);
+        context?.saveForm();
+      }}
+      disabled={disabled}
+      value={value}
+    >
       <SelectTrigger size="2xs" className={cn('w-18 hover:bg-bg-weak hover:text-text-strong text-label-xs gap-1')}>
         <SelectValue />
       </SelectTrigger>
diff --git a/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx b/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx
index c79c1c36d07..4466ab53d8e 100644
--- a/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx
+++ b/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx
@@ -55,14 +55,16 @@ function InternalConditionsEditor({
   isAllowedVariable,
   query,
   onQueryChange,
+  saveForm,
 }: {
   fields: Field[];
   variables: LiquidVariable[];
   isAllowedVariable: IsAllowedVariable;
   query: RuleGroupType;
   onQueryChange: (query: RuleGroupType) => void;
+  saveForm: () => void;
 }) {
-  const context = useMemo(() => ({ variables, isAllowedVariable }), [variables, isAllowedVariable]);
+  const context = useMemo(() => ({ variables, isAllowedVariable, saveForm }), [variables, isAllowedVariable, saveForm]);
 
   return (
     <QueryBuilder
@@ -79,16 +81,24 @@ function InternalConditionsEditor({
   );
 }
 
+export type ConditionsEditorContext = {
+  variables: LiquidVariable[];
+  isAllowedVariable: IsAllowedVariable;
+  saveForm: () => void;
+};
+
 export function ConditionsEditor({
   query,
   onQueryChange,
   fields,
+  saveForm,
   variables,
   isAllowedVariable,
 }: {
   query: RuleGroupType;
   onQueryChange: (query: RuleGroupType) => void;
   fields: Field[];
+  saveForm: () => void;
   variables: LiquidVariable[];
   isAllowedVariable: IsAllowedVariable;
 }) {
@@ -100,6 +110,7 @@ export function ConditionsEditor({
         isAllowedVariable={isAllowedVariable}
         query={query}
         onQueryChange={onQueryChange}
+        saveForm={saveForm}
       />
     </ConditionsEditorProvider>
   );
diff --git a/apps/dashboard/src/components/conditions-editor/field-selector.tsx b/apps/dashboard/src/components/conditions-editor/field-selector.tsx
index c2062b2d917..fea2cca0d38 100644
--- a/apps/dashboard/src/components/conditions-editor/field-selector.tsx
+++ b/apps/dashboard/src/components/conditions-editor/field-selector.tsx
@@ -1,12 +1,12 @@
 import React, { useMemo } from 'react';
-import { FieldSelectorProps } from 'react-querybuilder';
 import { useFormContext } from 'react-hook-form';
+import { FieldSelectorProps } from 'react-querybuilder';
 
-import { Code2 } from '@/components/icons/code-2';
 import { VariableSelect } from '@/components/conditions-editor/variable-select';
+import { Code2 } from '@/components/icons/code-2';
 
 export const FieldSelector = React.memo(
-  ({ handleOnChange, options, path, value, disabled }: FieldSelectorProps) => {
+  ({ handleOnChange, options, path, value, disabled, context }: FieldSelectorProps) => {
     const form = useFormContext();
     const queryPath = 'query.rules.' + path.join('.rules.') + '.field';
     const { error } = form.getFieldState(queryPath, form.formState);
@@ -23,7 +23,10 @@ export const FieldSelector = React.memo(
     return (
       <VariableSelect
         leftIcon={<Code2 className="text-feature size-3 min-w-3" />}
-        onChange={handleOnChange}
+        onChange={(e) => {
+          handleOnChange(e);
+          context?.saveForm();
+        }}
         options={optionsArray}
         title="Fields"
         value={value}
diff --git a/apps/dashboard/src/components/conditions-editor/operator-selector.tsx b/apps/dashboard/src/components/conditions-editor/operator-selector.tsx
index 0140efca510..8b08789710d 100644
--- a/apps/dashboard/src/components/conditions-editor/operator-selector.tsx
+++ b/apps/dashboard/src/components/conditions-editor/operator-selector.tsx
@@ -1,14 +1,21 @@
 import React from 'react';
 import { OperatorSelectorProps } from 'react-querybuilder';
 
+import { toSelectOptions } from '@/components/conditions-editor/select-option-utils';
 import { Select, SelectContent, SelectTrigger, SelectValue } from '@/components/primitives/select';
 import { cn } from '@/utils/ui';
-import { toSelectOptions } from '@/components/conditions-editor/select-option-utils';
 
 export const OperatorSelector = React.memo(
-  ({ disabled, value, options, handleOnChange }: OperatorSelectorProps) => {
+  ({ disabled, value, options, handleOnChange, context }: OperatorSelectorProps) => {
     return (
-      <Select onValueChange={handleOnChange} disabled={disabled} value={value}>
+      <Select
+        onValueChange={(e) => {
+          handleOnChange(e);
+          context?.saveForm();
+        }}
+        disabled={disabled}
+        value={value}
+      >
         <SelectTrigger
           size="2xs"
           className={cn('w-18 bg-background hover:bg-bg-weak hover:text-text-strong text-label-xs gap-1')}
diff --git a/apps/dashboard/src/components/conditions-editor/rule-actions.tsx b/apps/dashboard/src/components/conditions-editor/rule-actions.tsx
index 6ce55a211dd..b199e575a75 100644
--- a/apps/dashboard/src/components/conditions-editor/rule-actions.tsx
+++ b/apps/dashboard/src/components/conditions-editor/rule-actions.tsx
@@ -1,7 +1,10 @@
 import React, { useMemo } from 'react';
-import { isRuleGroup, ActionWithRulesProps, getParentPath } from 'react-querybuilder';
 import { RiMore2Fill } from 'react-icons/ri';
+import { ActionWithRulesProps, getParentPath, isRuleGroup } from 'react-querybuilder';
 
+import { Delete } from '@/components/icons/delete';
+import { SquareTwoStack } from '@/components/icons/square-two-stack';
+import { CompactButton } from '@/components/primitives/button-compact';
 import {
   DropdownMenu,
   DropdownMenuContent,
@@ -9,14 +12,11 @@ import {
   DropdownMenuItem,
   DropdownMenuTrigger,
 } from '@/components/primitives/dropdown-menu';
-import { useConditionsEditorContext } from './conditions-editor-context';
-import { CompactButton } from '@/components/primitives/button-compact';
-import { Delete } from '@/components/icons/delete';
-import { SquareTwoStack } from '@/components/icons/square-two-stack';
 import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from '@/components/primitives/tooltip';
+import { useConditionsEditorContext } from './conditions-editor-context';
 
 export const RuleActions = React.memo(
-  ({ path, ruleOrGroup }: ActionWithRulesProps) => {
+  ({ path, ruleOrGroup, context }: ActionWithRulesProps) => {
     const { removeRuleOrGroup, cloneRuleOrGroup, getParentGroup } = useConditionsEditorContext();
     const parentGroup = useMemo(() => getParentGroup(ruleOrGroup.id), [ruleOrGroup, getParentGroup]);
     const isGroup = isRuleGroup(ruleOrGroup);
@@ -40,6 +40,7 @@ export const RuleActions = React.memo(
                 <DropdownMenuItem
                   onClick={() => {
                     cloneRuleOrGroup(ruleOrGroup, getParentPath(path));
+                    context?.saveForm();
                   }}
                   className="text-foreground-600 text-label-xs h-7"
                   disabled={isDuplicateDisabled}
@@ -56,7 +57,13 @@ export const RuleActions = React.memo(
               </TooltipPortal>
             </Tooltip>
 
-            <DropdownMenuItem onClick={() => removeRuleOrGroup(path)} className="text-error-base text-label-xs h-7">
+            <DropdownMenuItem
+              onClick={() => {
+                removeRuleOrGroup(path);
+                context?.saveForm();
+              }}
+              className="text-error-base text-label-xs h-7"
+            >
               <Delete className="[&&]:size-3.5" />
               Delete {isGroup ? `group` : `condition`}
             </DropdownMenuItem>
diff --git a/apps/dashboard/src/components/workflow-editor/nodes.tsx b/apps/dashboard/src/components/workflow-editor/nodes.tsx
index fd853640eea..60656e64154 100644
--- a/apps/dashboard/src/components/workflow-editor/nodes.tsx
+++ b/apps/dashboard/src/components/workflow-editor/nodes.tsx
@@ -1,12 +1,13 @@
-import { ComponentProps } from 'react';
-import { Link, useNavigate, useParams } from 'react-router-dom';
+import { WorkflowOriginEnum } from '@novu/shared';
 import { Node as FlowNode, Handle, NodeProps, Position } from '@xyflow/react';
+import { ComponentProps } from 'react';
 import { RiFilter3Fill, RiPlayCircleLine } from 'react-icons/ri';
 import { RQBJsonLogic } from 'react-querybuilder';
-import { WorkflowOriginEnum } from '@novu/shared';
+import { Link, useNavigate, useParams } from 'react-router-dom';
 
 import { createStep } from '@/components/workflow-editor/step-utils';
 import { useWorkflow } from '@/components/workflow-editor/workflow-provider';
+import { useConditionsCount } from '@/hooks/use-conditions-count';
 import { STEP_TYPE_TO_COLOR } from '@/utils/color';
 import { INLINE_CONFIGURABLE_STEP_TYPES, TEMPLATE_CONFIGURABLE_STEP_TYPES } from '@/utils/constants';
 import { StepTypeEnum } from '@/utils/enums';
@@ -16,7 +17,6 @@ import { cn } from '@/utils/ui';
 import { STEP_TYPE_TO_ICON } from '../icons/utils';
 import { AddStepMenu } from './add-step-menu';
 import { Node, NodeBody, NodeError, NodeHeader, NodeIcon, NodeName } from './base-node';
-import { useConditionsCount } from '@/hooks/use-conditions-count';
 
 export type NodeData = {
   addStepIndex?: number;
@@ -360,8 +360,6 @@ export const AddNode = (_props: NodeProps<NodeType>) => {
             },
             {
               onSuccess: (data) => {
-                console.log('data', data);
-
                 if (TEMPLATE_CONFIGURABLE_STEP_TYPES.includes(stepType)) {
                   navigate(
                     buildRoute(ROUTES.EDIT_STEP_TEMPLATE, {
diff --git a/apps/dashboard/src/components/workflow-editor/step-default-values.ts b/apps/dashboard/src/components/workflow-editor/step-default-values.ts
index 081cfd026a2..8635f3bb655 100644
--- a/apps/dashboard/src/components/workflow-editor/step-default-values.ts
+++ b/apps/dashboard/src/components/workflow-editor/step-default-values.ts
@@ -1,14 +1,22 @@
-import { StepResponseDto } from '@novu/shared';
 import { buildDefaultValues, buildDefaultValuesOfDataSchema } from '@/utils/schema';
+import { StepResponseDto } from '@novu/shared';
 
 // Use the UI Schema to build the default values if it exists else use the data schema (code-first approach) values
 export const getStepDefaultValues = (step: StepResponseDto): Record<string, unknown> => {
   const controlValues = step.controls.values;
-  const hasControlValues = Object.keys(controlValues).length > 0;
+
+  const uiSchemaDefaultValues = buildDefaultValues(step.controls.uiSchema ?? {});
+  const dataSchemaDefaultValues = buildDefaultValuesOfDataSchema(step.controls.dataSchema ?? {});
 
   if (Object.keys(step.controls.uiSchema ?? {}).length !== 0) {
-    return hasControlValues ? controlValues : buildDefaultValues(step.controls.uiSchema ?? {});
+    return {
+      ...uiSchemaDefaultValues,
+      ...controlValues,
+    };
   }
 
-  return hasControlValues ? controlValues : buildDefaultValuesOfDataSchema(step.controls.dataSchema ?? {});
+  return {
+    ...dataSchemaDefaultValues,
+    ...controlValues,
+  };
 };
diff --git a/apps/dashboard/src/components/workflow-editor/steps/conditions/edit-step-conditions-form.tsx b/apps/dashboard/src/components/workflow-editor/steps/conditions/edit-step-conditions-form.tsx
index 824175c18e3..8e8f9ff8427 100644
--- a/apps/dashboard/src/components/workflow-editor/steps/conditions/edit-step-conditions-form.tsx
+++ b/apps/dashboard/src/components/workflow-editor/steps/conditions/edit-step-conditions-form.tsx
@@ -10,6 +10,8 @@ import { ConditionsEditor } from '@/components/conditions-editor/conditions-edit
 import { Form, FormField } from '@/components/primitives/form/form';
 import { updateStepInWorkflow } from '@/components/workflow-editor/step-utils';
 import { useWorkflow } from '@/components/workflow-editor/workflow-provider';
+import { useDataRef } from '@/hooks/use-data-ref';
+import { useFormAutosave } from '@/hooks/use-form-autosave';
 import { useParseVariables } from '@/hooks/use-parse-variables';
 import { useTelemetry } from '@/hooks/use-telemetry';
 import { countConditions, getUniqueFieldNamespaces, getUniqueOperators } from '@/utils/conditions';
@@ -106,46 +108,61 @@ export const EditStepConditionsForm = () => {
       query,
     },
   });
-  const { formState } = form;
 
-  const onSubmit = (values: z.infer<ReturnType<typeof getConditionsSchema>>) => {
-    if (!step || !workflow) return;
+  const { onBlur, saveForm } = useFormAutosave({
+    previousData: {
+      query,
+    },
+    form,
+    shouldClientValidate: true,
+    save: (data) => {
+      if (!step || !workflow) return;
+
+      const skip = formatQuery(data.query, 'jsonlogic');
+      const updateStepData: Partial<StepUpdateDto> = {
+        controlValues: { ...step.controls.values, skip },
+      };
+
+      if (!skip) {
+        updateStepData.controlValues!.skip = null;
+      }
+
+      update(updateStepInWorkflow(workflow, step.stepId, updateStepData), {
+        onSuccess: () => {
+          const uniqueFieldTypes: string[] = getUniqueFieldNamespaces(skip);
+          const uniqueOperators: string[] = getUniqueOperators(skip);
+
+          if (!hasConditions) {
+            track(TelemetryEvent.STEP_CONDITIONS_ADDED, {
+              stepType: step.type,
+              fieldTypes: uniqueFieldTypes,
+              operators: uniqueOperators,
+            });
+          } else {
+            const oldConditionsCount = countConditions(step.controls.values.skip as RQBJsonLogic);
+            const newConditionsCount = countConditions(skip);
+
+            track(TelemetryEvent.STEP_CONDITIONS_UPDATED, {
+              stepType: step.type,
+              fieldTypes: uniqueFieldTypes,
+              operators: uniqueOperators,
+              type: newConditionsCount < oldConditionsCount ? 'deletion' : 'update',
+            });
+          }
+        },
+      });
+      form.reset(data);
+    },
+  });
 
-    const skip = formatQuery(values.query, 'jsonlogic');
-    const updateStepData: Partial<StepUpdateDto> = {
-      controlValues: { ...step.controls.values, skip },
+  // Run saveForm on unmount
+  const saveFormRef = useDataRef(saveForm);
+  useEffect(() => {
+    return () => {
+      // eslint-disable-next-line react-hooks/exhaustive-deps
+      saveFormRef.current();
     };
-
-    if (!skip) {
-      updateStepData.controlValues!.skip = null;
-    }
-
-    update(updateStepInWorkflow(workflow, step.stepId, updateStepData), {
-      onSuccess: () => {
-        const uniqueFieldTypes: string[] = getUniqueFieldNamespaces(skip);
-        const uniqueOperators: string[] = getUniqueOperators(skip);
-
-        if (!hasConditions) {
-          track(TelemetryEvent.STEP_CONDITIONS_ADDED, {
-            stepType: step.type,
-            fieldTypes: uniqueFieldTypes,
-            operators: uniqueOperators,
-          });
-        } else {
-          const oldConditionsCount = countConditions(step.controls.values.skip as RQBJsonLogic);
-          const newConditionsCount = countConditions(skip);
-
-          track(TelemetryEvent.STEP_CONDITIONS_UPDATED, {
-            stepType: step.type,
-            fieldTypes: uniqueFieldTypes,
-            operators: uniqueOperators,
-            type: newConditionsCount < oldConditionsCount ? 'deletion' : 'update',
-          });
-        }
-      },
-    });
-    form.reset(values);
-  };
+  }, [saveFormRef]);
 
   useEffect(() => {
     if (!step) return;
@@ -173,15 +190,19 @@ export const EditStepConditionsForm = () => {
     <>
       <Form {...form}>
         <EditStepConditionsLayout
-          onSubmit={form.handleSubmit(onSubmit)}
           stepName={step?.name}
-          disabled={!formState.isDirty}
+          onBlur={onBlur}
+          onSubmit={(e) => {
+            e.preventDefault();
+            e.stopPropagation();
+          }}
         >
           <FormField
             control={form.control}
             name="query"
             render={({ field }) => (
               <ConditionsEditor
+                saveForm={saveForm}
                 query={field.value}
                 onQueryChange={field.onChange}
                 fields={fields}
diff --git a/apps/dashboard/src/components/workflow-editor/steps/conditions/edit-step-conditions-layout.tsx b/apps/dashboard/src/components/workflow-editor/steps/conditions/edit-step-conditions-layout.tsx
index 4ba29b5d425..c81c2e2ca11 100644
--- a/apps/dashboard/src/components/workflow-editor/steps/conditions/edit-step-conditions-layout.tsx
+++ b/apps/dashboard/src/components/workflow-editor/steps/conditions/edit-step-conditions-layout.tsx
@@ -1,23 +1,21 @@
 import { RiInputField, RiQuestionLine } from 'react-icons/ri';
 import { Link } from 'react-router-dom';
 
-import { Button } from '@/components/primitives/button';
 import { FormRoot } from '@/components/primitives/form/form';
 import { Panel, PanelContent, PanelHeader } from '@/components/primitives/panel';
+import { ComponentProps } from 'react';
 
-export const EditStepConditionsLayout = ({
-  stepName,
-  disabled,
-  onSubmit,
-  children,
-}: {
+type EditStepConditionsLayoutProps = ComponentProps<typeof FormRoot> & {
   stepName?: string;
-  disabled?: boolean;
-  onSubmit?: () => void;
   children: React.ReactNode;
-}) => {
+  disabled?: boolean;
+};
+
+export const EditStepConditionsLayout = (props: EditStepConditionsLayoutProps) => {
+  const { stepName, children, ...rest } = props;
+
   return (
-    <FormRoot className="flex h-full flex-col overflow-hidden" onSubmit={onSubmit}>
+    <FormRoot className="flex h-full flex-col overflow-hidden" {...rest}>
       <div className="flex flex-col gap-3 overflow-y-auto overflow-x-hidden px-3 py-5">
         <Panel className="overflow-initial">
           <PanelHeader>
@@ -34,11 +32,6 @@ export const EditStepConditionsLayout = ({
           <RiQuestionLine className="size-4" /> Learn more about conditional step execution
         </Link>
       </div>
-      <div className="mt-auto flex justify-end border-t border-neutral-200 p-3">
-        <Button type="submit" variant="secondary" disabled={disabled}>
-          Save Conditions
-        </Button>
-      </div>
     </FormRoot>
   );
 };
diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-tabs.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-tabs.tsx
index 50cc98a4179..fc35a1f620d 100644
--- a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-tabs.tsx
+++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-tabs.tsx
@@ -1,11 +1,11 @@
-import { useState } from 'react';
-import { useFormContext } from 'react-hook-form';
+import { StepEditorProps } from '@/components/workflow-editor/steps/configure-step-template-form';
 import { InAppEditor } from '@/components/workflow-editor/steps/in-app/in-app-editor';
 import { InAppEditorPreview } from '@/components/workflow-editor/steps/in-app/in-app-editor-preview';
-import { CustomStepControls } from '../controls/custom-step-controls';
-import { StepEditorProps } from '@/components/workflow-editor/steps/configure-step-template-form';
 import { TemplateTabs } from '@/components/workflow-editor/steps/template-tabs';
 import { WorkflowOriginEnum } from '@/utils/enums';
+import { useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { CustomStepControls } from '../controls/custom-step-controls';
 import { useEditorPreview } from '../use-editor-preview';
 
 export const InAppTabs = (props: StepEditorProps) => {


Reviews:
user: login: SokratisVidros
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:10:21Z
user: login: LetItRock
body: hey, found a few things that we need to improve:
1. When I pick the field value with keyboard the page gets refreshed.

https://github.com/user-attachments/assets/be7523b0-8a37-4571-b78b-32df17046d6d

2. When the field value changes we don't save the form right away.


https://github.com/user-attachments/assets/1be00866-0a45-49b6-b121-fdc0a7701b1d

3. When the group or rule is removed the form is not saved right away.

https://github.com/user-attachments/assets/46172630-347c-4182-9f85-8d000ca9cb47

4. When group or rule is duplicated the page get's refreshed.

https://github.com/user-attachments/assets/ad7ba00b-caa1-4644-96e6-d366f03cf51f


state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T11:05:01Z
user: login: desiprisg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T11:08:34Z
user: login: LetItRock
body: nice, all works well! 🙌 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T12:33:18Z

Review comments:
diff_hunk: @@ -55,19 +55,41 @@ function InternalConditionsEditor({
   isAllowedVariable,
   query,
   onQueryChange,
+  saveForm,
 }: {
   fields: Field[];
   variables: LiquidVariable[];
   isAllowedVariable: IsAllowedVariable;
   query: RuleGroupType;
   onQueryChange: (query: RuleGroupType) => void;
+  saveForm: () => void;
 }) {
   const context = useMemo(() => ({ variables, isAllowedVariable }), [variables, isAllowedVariable]);
 
   return (
     <QueryBuilder
       fields={fields}
       context={context}
+      onAddRule={() => {
+        saveForm();
+        return true;
path: apps/dashboard/src/components/conditions-editor/conditions-editor.tsx
user: login: LetItRock
body: please define one callback and reuse it between the props
created_at: 2025-04-22T10:57:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,14 +1,22 @@
-import { StepResponseDto } from '@novu/shared';
 import { buildDefaultValues, buildDefaultValuesOfDataSchema } from '@/utils/schema';
+import { StepResponseDto } from '@novu/shared';
 
 // Use the UI Schema to build the default values if it exists else use the data schema (code-first approach) values
 export const getStepDefaultValues = (step: StepResponseDto): Record<string, unknown> => {
   const controlValues = step.controls.values;
-  const hasControlValues = Object.keys(controlValues).length > 0;
+
+  const uiSchemaDefaultValues = buildDefaultValues(step.controls.uiSchema ?? {});
+  const dataSchemaDefaultValues = buildDefaultValuesOfDataSchema(step.controls.dataSchema ?? {});
 
   if (Object.keys(step.controls.uiSchema ?? {}).length !== 0) {
-    return hasControlValues ? controlValues : buildDefaultValues(step.controls.uiSchema ?? {});
+    return {
+      ...uiSchemaDefaultValues,
+      ...controlValues,
+    };
path: apps/dashboard/src/components/workflow-editor/step-default-values.ts
user: login: LetItRock
body: why we needed to update this?
created_at: 2025-04-22T11:00:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,14 +1,22 @@
-import { StepResponseDto } from '@novu/shared';
 import { buildDefaultValues, buildDefaultValuesOfDataSchema } from '@/utils/schema';
+import { StepResponseDto } from '@novu/shared';
 
 // Use the UI Schema to build the default values if it exists else use the data schema (code-first approach) values
 export const getStepDefaultValues = (step: StepResponseDto): Record<string, unknown> => {
   const controlValues = step.controls.values;
-  const hasControlValues = Object.keys(controlValues).length > 0;
+
+  const uiSchemaDefaultValues = buildDefaultValues(step.controls.uiSchema ?? {});
+  const dataSchemaDefaultValues = buildDefaultValuesOfDataSchema(step.controls.dataSchema ?? {});
 
   if (Object.keys(step.controls.uiSchema ?? {}).length !== 0) {
-    return hasControlValues ? controlValues : buildDefaultValues(step.controls.uiSchema ?? {});
+    return {
+      ...uiSchemaDefaultValues,
+      ...controlValues,
+    };
path: apps/dashboard/src/components/workflow-editor/step-default-values.ts
user: login: desiprisg
body: There's a bug where if you visit step conditions and make a change before touching the template, your app crashes when visiting the template afterwards.
created_at: 2025-04-22T11:08:34Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-04-15T10:54:43Z
body: <p><a href="https://linear.app/novu/issue/NV-5344/add-autosave-functionality-to-step-conditions">NV-5344 Add autosave functionality to Step Conditions</a></p>
author_association: NONE
user: login: netlify[bot]
created_at: 2025-04-15T10:55:24Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *dashboard-v2-novu-staging* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 34e5fcdd9cccde787048442aa900818edbf5d8bc |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/dashboard-v2-novu-staging/deploys/68078391017f520008e32b62 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-8134.dashboard-v2.novu-staging.co](https://deploy-preview-8134.dashboard-v2.novu-staging.co) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTgxMzQuZGFzaGJvYXJkLXYyLm5vdnUtc3RhZ2luZy5jbyJ9.zcn-cAVt-A6nqAXRuQimMbVgBnvhoJosL6Sd_mUPVZQ)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [dashboard-v2-novu-staging Preview](https://deploy-preview-8134.dashboard-v2.novu-staging.co) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/dashboard-v2-novu-staging/configuration/notifications#deploy-webhooks)._
author_association: NONE