diff --git a/.cspell.json b/.cspell.json index f7bbaa85f125..a0d2d8f211f6 100644 --- a/.cspell.json +++ b/.cspell.json @@ -292,7 +292,7 @@ "lastindex", "Lato", "Lentczner", - "lezer" + "lezer", "libarary", "libauth", "libspf", diff --git a/apps/dashboard/src/components/workflow-editor/configure-workflow.tsx b/apps/dashboard/src/components/workflow-editor/configure-workflow.tsx index e5512d382437..268b55ef18d3 100644 --- a/apps/dashboard/src/components/workflow-editor/configure-workflow.tsx +++ b/apps/dashboard/src/components/workflow-editor/configure-workflow.tsx @@ -2,14 +2,14 @@ import { useFormContext } from 'react-hook-form'; import { motion } from 'framer-motion'; import { RouteFill } from '../icons'; import { Input, InputField } from '../primitives/input'; -import { RiArrowRightSLine, RiSettingsLine } from 'react-icons/ri'; +// import { RiArrowRightSLine, RiSettingsLine } from 'react-icons/ri'; import * as z from 'zod'; import { Separator } from '../primitives/separator'; import { TagInput } from '../primitives/tag-input'; import { Textarea } from '../primitives/textarea'; import { workflowSchema } from './schema'; import { useTagsQuery } from '@/hooks/use-tags-query'; -import { Button } from '../primitives/button'; +// import { Button } from '../primitives/button'; import { CopyButton } from '../primitives/copy-button'; import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '../primitives/form/form'; import { Switch } from '../primitives/switch'; @@ -128,13 +128,13 @@ export function ConfigureWorkflow() { /> - + {/* - + */} ); } diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-body.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-body.tsx index b43e4e801da9..9eff060ef0f4 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-body.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-body.tsx @@ -1,11 +1,14 @@ -import { useFormContext } from 'react-hook-form'; import { liquid } from '@codemirror/lang-liquid'; import { EditorView } from '@uiw/react-codemirror'; +import { useFormContext } from 'react-hook-form'; +import { Editor } from '@/components/primitives/editor'; import { FormControl, FormField, FormItem, FormMessage } from '@/components/primitives/form/form'; import { InputField } from '@/components/primitives/input'; -import { Editor } from '@/components/primitives/editor'; +import { useFetchStep } from '@/hooks/use-fetch-step'; +import { parseStepVariablesToLiquidVariables } from '@/utils/parseStepVariablesToLiquidVariables'; import { capitalize } from '@/utils/string'; +import { useParams } from 'react-router-dom'; const bodyKey = 'body'; @@ -15,6 +18,10 @@ export const InAppBody = () => { formState: { errors }, } = useFormContext(); + const { workflowSlug = '', stepSlug = '' } = useParams<{ workflowSlug: string; stepSlug: string }>(); + + const { step } = useFetchStep({ workflowSlug, stepSlug }); + return ( { id={field.name} extensions={[ liquid({ - variables: [{ type: 'variable', label: 'asdf' }], + variables: step ? parseStepVariablesToLiquidVariables(step.variables) : [], }), EditorView.lineWrapping, ]} diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-subject.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-subject.tsx index 0daf67acd719..11cb2aae5c4f 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-subject.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-subject.tsx @@ -6,6 +6,9 @@ import { FormControl, FormField, FormItem, FormMessage } from '@/components/prim import { InputField } from '@/components/primitives/input'; import { Editor } from '@/components/primitives/editor'; import { capitalize } from '@/utils/string'; +import { useParams } from 'react-router-dom'; +import { useFetchStep } from '@/hooks/use-fetch-step'; +import { parseStepVariablesToLiquidVariables } from '@/utils/parseStepVariablesToLiquidVariables'; const subjectKey = 'subject'; @@ -14,6 +17,9 @@ export const InAppSubject = () => { control, formState: { errors }, } = useFormContext(); + const { workflowSlug = '', stepSlug = '' } = useParams<{ workflowSlug: string; stepSlug: string }>(); + + const { step } = useFetchStep({ workflowSlug, stepSlug }); return ( { id={field.name} extensions={[ liquid({ - variables: [{ type: 'variable', label: 'asdf' }], + variables: step ? parseStepVariablesToLiquidVariables(step.variables) : [], }), EditorView.lineWrapping, ]} 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 ed7176b7d885..052c7c322a85 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 @@ -98,6 +98,7 @@ export const InAppTabs = ({ workflow, step }: { workflow: WorkflowResponseDto; s }); setEditorValue(JSON.stringify(res.previewPayloadExample, null, 2)); }; + const formValues = useWatch(form); useDebouncedEffect( () => { diff --git a/apps/dashboard/src/utils/parseStepVariablesToLiquidVariables.ts b/apps/dashboard/src/utils/parseStepVariablesToLiquidVariables.ts new file mode 100644 index 000000000000..a52b0cc67cc0 --- /dev/null +++ b/apps/dashboard/src/utils/parseStepVariablesToLiquidVariables.ts @@ -0,0 +1,61 @@ +import { StepDataDto } from '@novu/shared'; + +interface LiquidVariable { + type: 'variable'; + label: string; + detail: string; +} + +type JSONSchema = StepDataDto['variables']; + +/** + * Parse JSON Schema and extract variables for Liquid autocompletion. + * @param schema - The JSON Schema to parse. + * @returns An array of variable objects suitable for the Liquid language. + */ +export function parseStepVariablesToLiquidVariables(schema: JSONSchema): LiquidVariable[] { + const variables: LiquidVariable[] = []; + + function extractProperties(obj: JSONSchema, path = ''): void { + if (typeof obj === 'boolean') return; // Handle boolean schema + + if (obj.type === 'object' && obj.properties) { + for (const [key, value] of Object.entries(obj.properties)) { + const fullPath = path ? `${path}.${key}` : key; + + // Add each property as a variable for autocompletion + variables.push({ + type: 'variable', + label: `${fullPath}`, + detail: typeof value !== 'boolean' ? value.description || 'JSON Schema variable' : 'JSON Schema variable', + }); + + // Recursively process nested objects + if (typeof value === 'object' && (value.type === 'object' || value.type === 'array')) { + extractProperties(value, fullPath); + } + } + } else if (obj.type === 'array' && obj.items) { + // For arrays, add a placeholder for array indexing + const items = Array.isArray(obj.items) ? obj.items[0] : obj.items; + extractProperties(items, `${path}[0]`); + } + + // Handle combinators (allOf, anyOf, oneOf) + ['allOf', 'anyOf', 'oneOf'].forEach((combiner) => { + if (Array.isArray(obj[combiner as keyof typeof obj])) { + for (const subSchema of obj[combiner as keyof typeof obj] as JSONSchema[]) { + extractProperties(subSchema, path); + } + } + }); + + // Handle conditional schemas (if/then/else) + if (obj.if) extractProperties(obj.if, path); + if (obj.then) extractProperties(obj.then, path); + if (obj.else) extractProperties(obj.else, path); + } + + extractProperties(schema); + return variables; +}