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 ( - { + handleOnChange(e); + context?.saveForm(); + }} + disabled={disabled} + value={value} + > 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 ( 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} /> ); 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 ( } - 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 ( - { + handleOnChange(e); + context?.saveForm(); + }} + disabled={disabled} + value={value} + > { + ({ 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( { 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( - removeRuleOrGroup(path)} className="text-error-base text-label-xs h-7"> + { + removeRuleOrGroup(path); + context?.saveForm(); + }} + className="text-error-base text-label-xs h-7" + > Delete {isGroup ? `group` : `condition`} 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) => { }, { 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 => { 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>) => { - 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 = { + 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 = { - 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 = () => { <>
{ + e.preventDefault(); + e.stopPropagation(); + }} > ( & { stepName?: string; - disabled?: boolean; - onSubmit?: () => void; children: React.ReactNode; -}) => { + disabled?: boolean; +}; + +export const EditStepConditionsLayout = (props: EditStepConditionsLayoutProps) => { + const { stepName, children, ...rest } = props; + return ( - +
@@ -34,11 +32,6 @@ export const EditStepConditionsLayout = ({ Learn more about conditional step execution
-
- -
); }; 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) => {