From d43cc186cf8bd46c6a9a0d1facc5a3ae101e7d22 Mon Sep 17 00:00:00 2001 From: desiprisg Date: Fri, 8 Nov 2024 14:06:08 +0200 Subject: [PATCH] feat(dashboard): Autosuggestions for step variables --- .../steps/in-app/in-app-body.tsx | 13 +++- .../steps/in-app/in-app-subject.tsx | 8 ++- .../steps/in-app/in-app-tabs.tsx | 1 + .../parseStepVariablesToLiquidVariables.ts | 61 +++++++++++++++++++ 4 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 apps/dashboard/src/utils/parseStepVariablesToLiquidVariables.ts 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 b43e4e801da..9eff060ef0f 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 0daf67acd71..11cb2aae5c4 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 ade24ef0d9b..1eaaf7c6462 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 @@ -97,6 +97,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 00000000000..a52b0cc67cc --- /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; +}