From 72fd4f677408fd5e49462204a2bec30ddb1a2273 Mon Sep 17 00:00:00 2001 From: Biswajeet Das Date: Wed, 2 Apr 2025 22:53:02 +0530 Subject: [PATCH 1/7] feat(dashboard): enhance liquid autocomplete validation for dynamic paths --- .../src/utils/liquid-autocomplete.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/dashboard/src/utils/liquid-autocomplete.ts b/apps/dashboard/src/utils/liquid-autocomplete.ts index 6fddcd9e0e9..182b89a8eab 100644 --- a/apps/dashboard/src/utils/liquid-autocomplete.ts +++ b/apps/dashboard/src/utils/liquid-autocomplete.ts @@ -15,7 +15,16 @@ const ROOT_PREFIXES = { steps: 'steps.', } as const; -const VALID_DYNAMIC_PATHS = ['subscriber.data.', 'payload.', /^steps\.[^.]+\.events\[\d+\]\.payload\./] as const; +const VALID_DYNAMIC_PATH_SUGGESTIONS = [ + 'subscriber.data.', + 'payload.', + /^steps\.[^.]+\.events\[\d+\]\.payload\./, +] as const; +const INVALID_DYNAMIC_PATH_VALUES = [ + 'subscriber.data', + 'payload', + /steps\.[^.]+\.events\[\d+\]\.payload(?!\.)/, +] as const; /** * Liquid variable autocomplete for the following patterns: @@ -140,11 +149,17 @@ function getFilterCompletions(afterPipe: string, isEnhancedDigestEnabled: boolea } function isValidDynamicPath(searchText: string): boolean { - return VALID_DYNAMIC_PATHS.some((path) => + return VALID_DYNAMIC_PATH_SUGGESTIONS.some((path) => typeof path === 'string' ? searchText.startsWith(path) : path.test(searchText) ); } +function isInvalidDynamicPathValues(searchText: string): boolean { + return INVALID_DYNAMIC_PATH_VALUES.some((path) => + typeof path === 'string' ? searchText === path : path.test(searchText) + ); +} + function validateSubscriberField(searchText: string, matches: LiquidVariable[]): LiquidVariable[] { const parts = searchText.split('.'); @@ -230,11 +245,13 @@ export function createAutocompleteSource(variables: LiquidVariable[], isEnhanced const beforeCursor = content.slice(0, from); const afterCursor = content.slice(to); + const isInvalidValue = isInvalidDynamicPathValues(selectedValue); + // Ensure proper {{ }} wrapping const needsOpening = !beforeCursor.endsWith('{{'); const needsClosing = !afterCursor.startsWith('}}'); - const wrappedValue = `${needsOpening ? '{{' : ''}${selectedValue}${needsClosing ? '}}' : ''}`; + const wrappedValue = `${needsOpening ? '{{' : ''}${selectedValue}${isInvalidValue ? '.' : ''}${needsClosing && !isInvalidValue ? '}}' : ''}`; // Calculate the final cursor position // Add 2 if we need to account for closing brackets From d60964a6134823ff7e0bd9cedc51ef0d41fc9744 Mon Sep 17 00:00:00 2001 From: Biswajeet Das Date: Mon, 7 Apr 2025 17:02:04 +0530 Subject: [PATCH 2/7] feat(dashboard): implement dynamic path suggestions in autocomplete and enhance variable filtering --- .../control-input/control-input.tsx | 2 +- .../workflow-editor/steps/email/maily.tsx | 49 +++++++++++++++++++ apps/dashboard/src/utils/constants.ts | 3 ++ .../src/utils/liquid-autocomplete.ts | 29 +++++++++-- .../dashboard/src/utils/parseStepVariables.ts | 17 +++++++ 5 files changed, 95 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/src/components/primitives/control-input/control-input.tsx b/apps/dashboard/src/components/primitives/control-input/control-input.tsx index a3c92d94107..33f66e13830 100644 --- a/apps/dashboard/src/components/primitives/control-input/control-input.tsx +++ b/apps/dashboard/src/components/primitives/control-input/control-input.tsx @@ -91,7 +91,7 @@ export function ControlInput({ onSelect: handleVariableSelect, isAllowedVariable, }), - [handleVariableSelect] + [handleVariableSelect, isAllowedVariable] ); const extensions = useMemo(() => { diff --git a/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx b/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx index a4ca321285f..f9a9842d683 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx @@ -12,6 +12,7 @@ import { createEditorBlocks, createExtensions, DEFAULT_EDITOR_CONFIG, MAILY_EMAI import { calculateVariables, VariableFrom } from './variables/variables'; import { RepeatMenuDescription } from './views/repeat-menu-description'; import { useFeatureFlag } from '@/hooks/use-feature-flag'; +import { DYNAMIC_PATH_ROOTS, DYNAMIC_STEP_NAME_ROOT_REGEX } from '@/utils/constants'; type MailyProps = HTMLAttributes & { value: string; @@ -115,3 +116,51 @@ export const Maily = ({ value, onChange, className, ...rest }: MailyProps) => { ); }; + +const dedupAndSortVariables = ( + variables: { name: string; required: boolean }[], + query: string +): { name: string; required: boolean }[] => { + // Filter variables that match the query + let filteredVariables = variables.filter((variable) => variable.name.toLowerCase().includes(query.toLowerCase())); + + if (filteredVariables.length === 0 && variables.length > 0) { + /** + * When typing a variable that is not present in the list, + * we need to check if the variable is a dynamic path. + * If it is, we need to add the dynamic path values to the list. + * For example, if the user types "hello" and there are dynamic paths like "steps.x.events[n].payload", + * we need to add "steps.x.events[n].payload.hello", "payload.hello" , "subscriber.data.hello" to the list. + * This is done to allow the user to create dynamic paths. + */ + const dynamicStepNames = variables + .map((entry) => entry.name.match(DYNAMIC_STEP_NAME_ROOT_REGEX)) + .filter((match): match is RegExpMatchArray => match !== null) + .map((match) => `${match[0]}.`); + + filteredVariables = [...DYNAMIC_PATH_ROOTS, ...dynamicStepNames].map((value) => { + return { + name: value + query.trim(), + required: false, + }; + }); + } + + // Deduplicate based on name property + const uniqueVariables = Array.from(new Map(filteredVariables.map((item) => [item.name, item])).values()); + + // Sort variables: exact matches first, then starts with query, then alphabetically + return uniqueVariables.sort((a, b) => { + const aExactMatch = a.name.toLowerCase() === query.toLowerCase(); + const bExactMatch = b.name.toLowerCase() === query.toLowerCase(); + const aStartsWithQuery = a.name.toLowerCase().startsWith(query.toLowerCase()); + const bStartsWithQuery = b.name.toLowerCase().startsWith(query.toLowerCase()); + + if (aExactMatch && !bExactMatch) return -1; + if (!aExactMatch && bExactMatch) return 1; + if (aStartsWithQuery && !bStartsWithQuery) return -1; + if (!aStartsWithQuery && bStartsWithQuery) return 1; + + return a.name.localeCompare(b.name); + }); +}; diff --git a/apps/dashboard/src/utils/constants.ts b/apps/dashboard/src/utils/constants.ts index 6839b5243de..4b612b26b98 100644 --- a/apps/dashboard/src/utils/constants.ts +++ b/apps/dashboard/src/utils/constants.ts @@ -36,3 +36,6 @@ export const DEFAULT_CONTROL_DIGEST_AMOUNT = 30; export const DEFAULT_CONTROL_DIGEST_UNIT = TimeUnitEnum.SECONDS; export const DEFAULT_CONTROL_DIGEST_CRON = ''; export const DEFAULT_CONTROL_DIGEST_DIGEST_KEY = ''; + +export const DYNAMIC_PATH_ROOTS = ['subscriber.data.', 'payload.'] as const; +export const DYNAMIC_STEP_NAME_ROOT_REGEX = /^steps\.(.+?)\.events\[\d+\]\.payload$/; diff --git a/apps/dashboard/src/utils/liquid-autocomplete.ts b/apps/dashboard/src/utils/liquid-autocomplete.ts index 182b89a8eab..a0126a81f76 100644 --- a/apps/dashboard/src/utils/liquid-autocomplete.ts +++ b/apps/dashboard/src/utils/liquid-autocomplete.ts @@ -2,6 +2,7 @@ import { getFilters } from '@/components/variable/constants'; import { LiquidVariable } from '@/utils/parseStepVariables'; import { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; import { EditorView } from '@uiw/react-codemirror'; +import { DYNAMIC_PATH_ROOTS, DYNAMIC_STEP_NAME_ROOT_REGEX } from './constants'; interface CompletionOption { label: string; @@ -20,10 +21,14 @@ const VALID_DYNAMIC_PATH_SUGGESTIONS = [ 'payload.', /^steps\.[^.]+\.events\[\d+\]\.payload\./, ] as const; + const INVALID_DYNAMIC_PATH_VALUES = [ 'subscriber.data', + 'subscriber.data.', 'payload', - /steps\.[^.]+\.events\[\d+\]\.payload(?!\.)/, + 'payload.', + /^steps\.[^.]+\.events\[\d+\]\.payload$/, // Invalidates "steps.x.events[n].payload" + /^steps\.[^.]+\.events\[\d+\]\.payload\.$/, // Invalidates "steps.x.events[n].payload." ] as const; /** @@ -220,7 +225,23 @@ function getMatchingVariables(searchText: string, variables: LiquidVariable[]): } // Default case: show any variables containing the search text - return variables.filter((v) => v.label.toLowerCase().includes(searchLower)); + let result = variables.filter((v) => v.label.toLowerCase().includes(searchLower)); + + if (result.length === 0) { + const dynamicStepNames = variables + .map((entry) => entry.label.match(DYNAMIC_STEP_NAME_ROOT_REGEX)) + .filter((match): match is RegExpMatchArray => match !== null) + .map((match) => `${match[0]}.`); + + result = [...DYNAMIC_PATH_ROOTS, ...dynamicStepNames].map((value) => { + return { + label: value + searchLower.trim(), + type: 'variable', + }; + }); + } + + return result; } export function createAutocompleteSource(variables: LiquidVariable[], isEnhancedDigestEnabled: boolean) { @@ -251,11 +272,11 @@ export function createAutocompleteSource(variables: LiquidVariable[], isEnhanced const needsOpening = !beforeCursor.endsWith('{{'); const needsClosing = !afterCursor.startsWith('}}'); - const wrappedValue = `${needsOpening ? '{{' : ''}${selectedValue}${isInvalidValue ? '.' : ''}${needsClosing && !isInvalidValue ? '}}' : ''}`; + const wrappedValue = `${needsOpening ? '{{' : ''}${selectedValue}${isInvalidValue && !selectedValue.endsWith('.') ? '.' : ''}${needsClosing && !isInvalidValue ? '}}' : ''}`; // Calculate the final cursor position // Add 2 if we need to account for closing brackets - const finalCursorPos = from + wrappedValue.length + (needsClosing ? 0 : 2); + const finalCursorPos = from + wrappedValue.length + (needsClosing || isInvalidValue ? 0 : 2); view.dispatch({ changes: { from, to, insert: wrappedValue }, diff --git a/apps/dashboard/src/utils/parseStepVariables.ts b/apps/dashboard/src/utils/parseStepVariables.ts index cb7c78a00d3..907d8b19cb1 100644 --- a/apps/dashboard/src/utils/parseStepVariables.ts +++ b/apps/dashboard/src/utils/parseStepVariables.ts @@ -16,6 +16,15 @@ export interface ParsedVariables { isAllowedVariable: IsAllowedVariable; } +const INVALID_DYNAMIC_PATH_VALUES = [ + 'subscriber.data', + 'subscriber.data.', + 'payload', + 'payload.', + /^steps\.[^.]+\.events\[\d+\]\.payload$/, // Invalidates "steps.x.events[n].payload" + /^steps\.[^.]+\.events\[\d+\]\.payload\.$/, // Invalidates "steps.x.events[n].payload." +] as const; + /** * Parse JSON Schema and extract variables for Liquid autocompletion. * @param schema - The JSON Schema to parse. @@ -112,6 +121,14 @@ export function parseStepVariables(schema: JSONSchemaDefinition, isEnhancedDiges function isAllowedVariable(path: string): boolean { if (typeof schema === 'boolean') return false; + if ( + INVALID_DYNAMIC_PATH_VALUES.some((invalid) => + typeof invalid === 'string' ? path === invalid : invalid.test(path) + ) + ) { + return false; + } + if (result.primitives.some((primitive) => primitive.label === path)) { return true; } From 158fa0fec28ec769804476afb976b855aa46c2a3 Mon Sep 17 00:00:00 2001 From: Biswajeet Das Date: Mon, 7 Apr 2025 17:23:04 +0530 Subject: [PATCH 3/7] feat(dashboard): add invalid dynamic path values for enhanced autocomplete validation --- apps/dashboard/src/utils/constants.ts | 9 +++++++++ apps/dashboard/src/utils/liquid-autocomplete.ts | 11 +---------- apps/dashboard/src/utils/parseStepVariables.ts | 10 +--------- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/apps/dashboard/src/utils/constants.ts b/apps/dashboard/src/utils/constants.ts index 4b612b26b98..d48bb6679f4 100644 --- a/apps/dashboard/src/utils/constants.ts +++ b/apps/dashboard/src/utils/constants.ts @@ -39,3 +39,12 @@ export const DEFAULT_CONTROL_DIGEST_DIGEST_KEY = ''; export const DYNAMIC_PATH_ROOTS = ['subscriber.data.', 'payload.'] as const; export const DYNAMIC_STEP_NAME_ROOT_REGEX = /^steps\.(.+?)\.events\[\d+\]\.payload$/; + +export const INVALID_DYNAMIC_PATH_VALUES = [ + 'subscriber.data', + 'subscriber.data.', + 'payload', + 'payload.', + /^steps\.[^.]+\.events\[\d+\]\.payload$/, // Invalidates "steps.x.events[n].payload" + /^steps\.[^.]+\.events\[\d+\]\.payload\.$/, // Invalidates "steps.x.events[n].payload." +] as const; diff --git a/apps/dashboard/src/utils/liquid-autocomplete.ts b/apps/dashboard/src/utils/liquid-autocomplete.ts index a0126a81f76..7b8f939a034 100644 --- a/apps/dashboard/src/utils/liquid-autocomplete.ts +++ b/apps/dashboard/src/utils/liquid-autocomplete.ts @@ -2,7 +2,7 @@ import { getFilters } from '@/components/variable/constants'; import { LiquidVariable } from '@/utils/parseStepVariables'; import { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; import { EditorView } from '@uiw/react-codemirror'; -import { DYNAMIC_PATH_ROOTS, DYNAMIC_STEP_NAME_ROOT_REGEX } from './constants'; +import { DYNAMIC_PATH_ROOTS, DYNAMIC_STEP_NAME_ROOT_REGEX, INVALID_DYNAMIC_PATH_VALUES } from './constants'; interface CompletionOption { label: string; @@ -22,15 +22,6 @@ const VALID_DYNAMIC_PATH_SUGGESTIONS = [ /^steps\.[^.]+\.events\[\d+\]\.payload\./, ] as const; -const INVALID_DYNAMIC_PATH_VALUES = [ - 'subscriber.data', - 'subscriber.data.', - 'payload', - 'payload.', - /^steps\.[^.]+\.events\[\d+\]\.payload$/, // Invalidates "steps.x.events[n].payload" - /^steps\.[^.]+\.events\[\d+\]\.payload\.$/, // Invalidates "steps.x.events[n].payload." -] as const; - /** * Liquid variable autocomplete for the following patterns: * diff --git a/apps/dashboard/src/utils/parseStepVariables.ts b/apps/dashboard/src/utils/parseStepVariables.ts index 907d8b19cb1..8183a124583 100644 --- a/apps/dashboard/src/utils/parseStepVariables.ts +++ b/apps/dashboard/src/utils/parseStepVariables.ts @@ -1,4 +1,5 @@ import type { JSONSchemaDefinition } from '@novu/shared'; +import { INVALID_DYNAMIC_PATH_VALUES } from './constants'; export interface LiquidVariable { type: 'variable'; @@ -16,15 +17,6 @@ export interface ParsedVariables { isAllowedVariable: IsAllowedVariable; } -const INVALID_DYNAMIC_PATH_VALUES = [ - 'subscriber.data', - 'subscriber.data.', - 'payload', - 'payload.', - /^steps\.[^.]+\.events\[\d+\]\.payload$/, // Invalidates "steps.x.events[n].payload" - /^steps\.[^.]+\.events\[\d+\]\.payload\.$/, // Invalidates "steps.x.events[n].payload." -] as const; - /** * Parse JSON Schema and extract variables for Liquid autocompletion. * @param schema - The JSON Schema to parse. From 9ddeefac76dcefcc04b2d044a900341342397419 Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Mon, 7 Apr 2025 17:59:10 +0300 Subject: [PATCH 4/7] fix(dashboard): Fix merge conflicts --- .../workflow-editor/steps/email/maily.tsx | 50 ------------------- .../steps/email/variables/variables.ts | 28 ++++++++++- .../src/utils/liquid-autocomplete.ts | 6 +-- 3 files changed, 28 insertions(+), 56 deletions(-) diff --git a/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx b/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx index f9a9842d683..7446ef02601 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx @@ -1,5 +1,4 @@ import { Editor } from '@maily-to/core'; - import type { Editor as TiptapEditor } from '@tiptap/core'; import { HTMLAttributes, useCallback, useMemo, useState } from 'react'; import { FeatureFlagsKeysEnum } from '@novu/shared'; @@ -12,7 +11,6 @@ import { createEditorBlocks, createExtensions, DEFAULT_EDITOR_CONFIG, MAILY_EMAI import { calculateVariables, VariableFrom } from './variables/variables'; import { RepeatMenuDescription } from './views/repeat-menu-description'; import { useFeatureFlag } from '@/hooks/use-feature-flag'; -import { DYNAMIC_PATH_ROOTS, DYNAMIC_STEP_NAME_ROOT_REGEX } from '@/utils/constants'; type MailyProps = HTMLAttributes & { value: string; @@ -116,51 +114,3 @@ export const Maily = ({ value, onChange, className, ...rest }: MailyProps) => { ); }; - -const dedupAndSortVariables = ( - variables: { name: string; required: boolean }[], - query: string -): { name: string; required: boolean }[] => { - // Filter variables that match the query - let filteredVariables = variables.filter((variable) => variable.name.toLowerCase().includes(query.toLowerCase())); - - if (filteredVariables.length === 0 && variables.length > 0) { - /** - * When typing a variable that is not present in the list, - * we need to check if the variable is a dynamic path. - * If it is, we need to add the dynamic path values to the list. - * For example, if the user types "hello" and there are dynamic paths like "steps.x.events[n].payload", - * we need to add "steps.x.events[n].payload.hello", "payload.hello" , "subscriber.data.hello" to the list. - * This is done to allow the user to create dynamic paths. - */ - const dynamicStepNames = variables - .map((entry) => entry.name.match(DYNAMIC_STEP_NAME_ROOT_REGEX)) - .filter((match): match is RegExpMatchArray => match !== null) - .map((match) => `${match[0]}.`); - - filteredVariables = [...DYNAMIC_PATH_ROOTS, ...dynamicStepNames].map((value) => { - return { - name: value + query.trim(), - required: false, - }; - }); - } - - // Deduplicate based on name property - const uniqueVariables = Array.from(new Map(filteredVariables.map((item) => [item.name, item])).values()); - - // Sort variables: exact matches first, then starts with query, then alphabetically - return uniqueVariables.sort((a, b) => { - const aExactMatch = a.name.toLowerCase() === query.toLowerCase(); - const bExactMatch = b.name.toLowerCase() === query.toLowerCase(); - const aStartsWithQuery = a.name.toLowerCase().startsWith(query.toLowerCase()); - const bStartsWithQuery = b.name.toLowerCase().startsWith(query.toLowerCase()); - - if (aExactMatch && !bExactMatch) return -1; - if (!aExactMatch && bExactMatch) return 1; - if (aStartsWithQuery && !bStartsWithQuery) return -1; - if (!aStartsWithQuery && bStartsWithQuery) return 1; - - return a.name.localeCompare(b.name); - }); -}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/email/variables/variables.ts b/apps/dashboard/src/components/workflow-editor/steps/email/variables/variables.ts index fcdcf36b98b..36ed3a1bf16 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/email/variables/variables.ts +++ b/apps/dashboard/src/components/workflow-editor/steps/email/variables/variables.ts @@ -1,4 +1,5 @@ import { Variable, Variables } from '@maily-to/core/extensions'; +import { DYNAMIC_PATH_ROOTS, DYNAMIC_STEP_NAME_ROOT_REGEX } from '@/utils/constants'; import type { Editor as TiptapEditor } from '@tiptap/core'; export enum VariableFrom { @@ -95,10 +96,35 @@ const getRepeatBlockEachVariable = (editor: TiptapEditor): Array => { }; const dedupAndSortVariables = (variables: Array, query: string): Array => { - const filteredVariables = variables.filter((variable) => variable.name.toLowerCase().includes(query.toLowerCase())); + // Filter variables that match the query + let filteredVariables = variables.filter((variable) => variable.name.toLowerCase().includes(query.toLowerCase())); + + if (filteredVariables.length === 0 && variables.length > 0) { + /** + * When typing a variable that is not present in the list, + * we need to check if the variable is a dynamic path. + * If it is, we need to add the dynamic path values to the list. + * For example, if the user types "hello" and there are dynamic paths like "steps.x.events[n].payload", + * we need to add "steps.x.events[n].payload.hello", "payload.hello" , "subscriber.data.hello" to the list. + * This is done to allow the user to create dynamic paths. + */ + const dynamicStepNames = variables + .map((entry) => entry.name.match(DYNAMIC_STEP_NAME_ROOT_REGEX)) + .filter((match): match is RegExpMatchArray => match !== null) + .map((match) => `${match[0]}.`); + + filteredVariables = [...DYNAMIC_PATH_ROOTS, ...dynamicStepNames].map((value) => { + return { + name: value + query.trim(), + required: false, + }; + }); + } + // Deduplicate based on name property const uniqueVariables = Array.from(new Map(filteredVariables.map((item) => [item.name, item])).values()); + // Sort variables: exact matches first, then starts with query, then alphabetically return uniqueVariables.sort((a, b) => { const aExactMatch = a.name.toLowerCase() === query.toLowerCase(); const bExactMatch = b.name.toLowerCase() === query.toLowerCase(); diff --git a/apps/dashboard/src/utils/liquid-autocomplete.ts b/apps/dashboard/src/utils/liquid-autocomplete.ts index 7b8f939a034..7f4314adb8f 100644 --- a/apps/dashboard/src/utils/liquid-autocomplete.ts +++ b/apps/dashboard/src/utils/liquid-autocomplete.ts @@ -16,11 +16,7 @@ const ROOT_PREFIXES = { steps: 'steps.', } as const; -const VALID_DYNAMIC_PATH_SUGGESTIONS = [ - 'subscriber.data.', - 'payload.', - /^steps\.[^.]+\.events\[\d+\]\.payload\./, -] as const; +const VALID_DYNAMIC_PATH_SUGGESTIONS = ['subscriber.data.', /^steps\.[^.]+\.events\[\d+\]\.payload\./] as const; /** * Liquid variable autocomplete for the following patterns: From 7379a1f7c23c2aae320f9748fe3131b31d100f0b Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Mon, 7 Apr 2025 17:59:40 +0300 Subject: [PATCH 5/7] fix(dashboard): Exclude naked payload from suggested variables The new UX prepends payload to all unknown {{foo}} variables as the user types them. --- apps/dashboard/src/utils/parseStepVariables.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/src/utils/parseStepVariables.ts b/apps/dashboard/src/utils/parseStepVariables.ts index 8183a124583..6b914a3e662 100644 --- a/apps/dashboard/src/utils/parseStepVariables.ts +++ b/apps/dashboard/src/utils/parseStepVariables.ts @@ -35,8 +35,9 @@ export function parseStepVariables(schema: JSONSchemaDefinition, isEnhancedDiges if (typeof obj === 'boolean') return; if (obj.type === 'object') { - // Handle object with additionalProperties - if (obj.additionalProperties === true) { + // Exclude naked payload from suggested variables to try the new UX of appending payload to all unknown step variables. + // TODO: Move the exclusion to the API side after the two deployments to avoid breaking the contract between the Dashboard and the API. + if (obj.additionalProperties === true && !path.includes('payload')) { result.namespaces.push({ type: 'variable', label: path, From 36dcc94756775cdf375ca4be1cdd2f10d456bb07 Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Wed, 9 Apr 2025 12:01:32 +0300 Subject: [PATCH 6/7] WIP --- apps/dashboard/package.json | 1 + .../control-input/control-input.tsx | 15 +- .../variable-plugin/plugin-view.ts | 2 +- .../src/components/primitives/form/form.tsx | 4 +- .../src/components/variable/constants.ts | 6 +- .../variable}/parseStepVariables.ts | 85 +++-------- .../utils/codemirror-autocomplete.ts} | 141 +++++------------- .../workflow-editor/steps/email/maily.tsx | 1 + .../steps/email/variables/variables.ts | 59 +++++--- .../src/hooks/use-parse-variables.ts | 2 +- apps/dashboard/src/utils/constants.ts | 12 -- pnpm-lock.yaml | 8 + 12 files changed, 127 insertions(+), 209 deletions(-) rename apps/dashboard/src/{utils => components/variable}/parseStepVariables.ts (57%) rename apps/dashboard/src/{utils/liquid-autocomplete.ts => components/variable/utils/codemirror-autocomplete.ts} (60%) diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 16099e5af25..62bd54feaea 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -78,6 +78,7 @@ "launchdarkly-react-client-sdk": "^3.3.2", "liquidjs": "^10.20.0", "lodash.debounce": "^4.0.8", + "lodash.isempty": "^4.4.0", "lodash.isequal": "^4.5.0", "lodash.merge": "^4.6.2", "lucide-react": "^0.439.0", diff --git a/apps/dashboard/src/components/primitives/control-input/control-input.tsx b/apps/dashboard/src/components/primitives/control-input/control-input.tsx index 33f66e13830..131019bca4b 100644 --- a/apps/dashboard/src/components/primitives/control-input/control-input.tsx +++ b/apps/dashboard/src/components/primitives/control-input/control-input.tsx @@ -6,8 +6,8 @@ import { useCallback, useMemo, useRef } from 'react'; import { Editor } from '@/components/primitives/editor'; import { EditVariablePopover } from '@/components/variable/edit-variable-popover'; -import { createAutocompleteSource } from '@/utils/liquid-autocomplete'; -import { IsAllowedVariable, LiquidVariable } from '@/utils/parseStepVariables'; +import { createAutocompleteSource } from '@/components/variable/utils/codemirror-autocomplete'; +import { IsAllowedVariable, LiquidVariable } from '@/components/variable/parseStepVariables'; import { useVariables } from './hooks/use-variables'; import { createVariableExtension } from './variable-plugin'; import { variablePillTheme } from './variable-plugin/variable-theme'; @@ -79,6 +79,17 @@ export function ControlInput({ closeOnBlur: true, defaultKeymap: true, activateOnTyping: true, + addToOptions: [ + { + render: (completion) => { + console.log('completion', completion); + const div = document.createElement('div'); + div.textContent = 'Hello'; + return div; + }, + position: 0, + }, + ], }), [completionSource] ); diff --git a/apps/dashboard/src/components/primitives/control-input/variable-plugin/plugin-view.ts b/apps/dashboard/src/components/primitives/control-input/variable-plugin/plugin-view.ts index 60ea9137001..e46e4a7a779 100644 --- a/apps/dashboard/src/components/primitives/control-input/variable-plugin/plugin-view.ts +++ b/apps/dashboard/src/components/primitives/control-input/variable-plugin/plugin-view.ts @@ -1,4 +1,4 @@ -import { IsAllowedVariable } from '@/utils/parseStepVariables'; +import { IsAllowedVariable } from '@/components/variable/parseStepVariables'; import { Decoration, DecorationSet, EditorView, Range } from '@uiw/react-codemirror'; import { MutableRefObject } from 'react'; import { VARIABLE_REGEX_STRING } from './'; diff --git a/apps/dashboard/src/components/primitives/form/form.tsx b/apps/dashboard/src/components/primitives/form/form.tsx index a4af2fc82e0..70d981ec585 100644 --- a/apps/dashboard/src/components/primitives/form/form.tsx +++ b/apps/dashboard/src/components/primitives/form/form.tsx @@ -129,9 +129,9 @@ FormMessagePure.displayName = 'FormMessagePure'; const FormMessage = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes & { suppressError?: boolean } ->(({ children, suppressError, ...rest }, ref) => { +>(({ children, ...rest }, ref) => { const { error, formMessageId } = useFormField(); - const content = !suppressError && error ? String(error.message) : children; + const content = error ? String(error.message) : children; const icon = error ? RiErrorWarningFill : RiInformationLine; const isFirstMount = useRef(true); diff --git a/apps/dashboard/src/components/variable/constants.ts b/apps/dashboard/src/components/variable/constants.ts index 51d8829e3e4..3d3b0d51c1d 100644 --- a/apps/dashboard/src/components/variable/constants.ts +++ b/apps/dashboard/src/components/variable/constants.ts @@ -1,7 +1,9 @@ import { Filters } from './types'; -const FILTERS: Filters[] = [ - // Text Transformations +export const FIXED_NAMESPACES_WITH_UNKNOWN_KEYS = ['subscriber.data', 'payload']; +export const DYNAMIC_NAMESPACES_WITH_UNKNOWN_KEYS_REGEX = /^steps\.(.+?)\.events\[\d+\]/; + +export const FILTERS: Filters[] = [ { label: 'Uppercase', value: 'upcase', diff --git a/apps/dashboard/src/utils/parseStepVariables.ts b/apps/dashboard/src/components/variable/parseStepVariables.ts similarity index 57% rename from apps/dashboard/src/utils/parseStepVariables.ts rename to apps/dashboard/src/components/variable/parseStepVariables.ts index 6b914a3e662..87a16f7b193 100644 --- a/apps/dashboard/src/utils/parseStepVariables.ts +++ b/apps/dashboard/src/components/variable/parseStepVariables.ts @@ -1,5 +1,4 @@ import type { JSONSchemaDefinition } from '@novu/shared'; -import { INVALID_DYNAMIC_PATH_VALUES } from './constants'; export interface LiquidVariable { type: 'variable'; @@ -24,6 +23,9 @@ export interface ParsedVariables { */ export function parseStepVariables(schema: JSONSchemaDefinition, isEnhancedDigestEnabled: boolean): ParsedVariables { const result: ParsedVariables = { + /** + * deprecated: use variables instead + */ primitives: [], arrays: [], variables: [], @@ -35,18 +37,14 @@ export function parseStepVariables(schema: JSONSchemaDefinition, isEnhancedDiges if (typeof obj === 'boolean') return; if (obj.type === 'object') { - // Exclude naked payload from suggested variables to try the new UX of appending payload to all unknown step variables. - // TODO: Move the exclusion to the API side after the two deployments to avoid breaking the contract between the Dashboard and the API. - if (obj.additionalProperties === true && !path.includes('payload')) { + if (obj.additionalProperties === true) { result.namespaces.push({ type: 'variable', label: path, }); } - if (!obj.properties) return; - - for (const [key, value] of Object.entries(obj.properties)) { + for (const [key, value] of Object.entries(obj.properties || {})) { const fullPath = path ? `${path}.${key}` : key; if (typeof value === 'object') { @@ -93,76 +91,39 @@ export function parseStepVariables(schema: JSONSchemaDefinition, isEnhancedDiges extractProperties(schema); - function parseVariablePath(path: string): string[] | null { - const parts = path - .split(/\.|\[(\d+)\]/) - .filter(Boolean) - .map((part): string | null => { - const num = parseInt(part); - - if (!isNaN(num)) { - if (num < 0) return null; - return num.toString(); - } - - return part; - }); - - return parts.includes(null) ? null : (parts as string[]); - } - - function isAllowedVariable(path: string): boolean { - if (typeof schema === 'boolean') return false; + function isAllowedVariable(value: string): boolean { + // Validate is variable is valid against the parsed array of variables coming from the server + const isValidFromServerResponse = !!result.variables.find((v) => v.label === value); - if ( - INVALID_DYNAMIC_PATH_VALUES.some((invalid) => - typeof invalid === 'string' ? path === invalid : invalid.test(path) - ) - ) { - return false; - } - - if (result.primitives.some((primitive) => primitive.label === path)) { + if (isValidFromServerResponse) { return true; } - const parts = parseVariablePath(path); - if (!parts) return false; - - let currentObj: JSONSchemaDefinition = schema; + // Handle array variables and validate them against the parsed array variables. + // For example: steps.digest.events[1].payload.name is validated against steps.digest.events that is returned by the server + const isValidFromArrayNameSpace = result.arrays.some((v) => value.startsWith(v.label)); - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - - if (typeof currentObj === 'boolean' || !('type' in currentObj)) return false; - - if (currentObj.type === 'array') { - const items = Array.isArray(currentObj.items) ? currentObj.items[0] : currentObj.items; - currentObj = items as JSONSchemaDefinition; - continue; - } - - if (currentObj.type !== 'object') return false; - - if (currentObj.additionalProperties === true) { - return true; - } + if (isValidFromArrayNameSpace) { + return true; + } - if (!currentObj.properties || !(part in currentObj.properties)) { - return false; - } + // Handle variable for payload and subscriber.data namespace such as payload.name or subscriber.data.name + const isValidFromNamespaceWithUnknownKeys = result.namespaces.some( + (v) => value.startsWith(v.label) && value !== v.label + ); - currentObj = currentObj.properties[part]; + if (isValidFromNamespaceWithUnknownKeys) { + return true; } - return true; + return false; } return { ...result, variables: isEnhancedDigestEnabled ? [...result.primitives, ...result.arrays, ...result.namespaces] - : [...result.primitives, ...result.namespaces], + : [...result.primitives], isAllowedVariable, }; } diff --git a/apps/dashboard/src/utils/liquid-autocomplete.ts b/apps/dashboard/src/components/variable/utils/codemirror-autocomplete.ts similarity index 60% rename from apps/dashboard/src/utils/liquid-autocomplete.ts rename to apps/dashboard/src/components/variable/utils/codemirror-autocomplete.ts index 7f4314adb8f..40b5915f9c9 100644 --- a/apps/dashboard/src/utils/liquid-autocomplete.ts +++ b/apps/dashboard/src/components/variable/utils/codemirror-autocomplete.ts @@ -1,8 +1,9 @@ import { getFilters } from '@/components/variable/constants'; -import { LiquidVariable } from '@/utils/parseStepVariables'; +import type { LiquidVariable } from '@/components/variable/parseStepVariables'; + import { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; import { EditorView } from '@uiw/react-codemirror'; -import { DYNAMIC_PATH_ROOTS, DYNAMIC_STEP_NAME_ROOT_REGEX, INVALID_DYNAMIC_PATH_VALUES } from './constants'; +import { FIXED_NAMESPACES_WITH_UNKNOWN_KEYS, DYNAMIC_NAMESPACES_WITH_UNKNOWN_KEYS_REGEX } from '../constants'; interface CompletionOption { label: string; @@ -10,21 +11,12 @@ interface CompletionOption { boost?: number; } -const ROOT_PREFIXES = { - subscriber: 'subscriber.', - payload: 'payload.', - steps: 'steps.', -} as const; - -const VALID_DYNAMIC_PATH_SUGGESTIONS = ['subscriber.data.', /^steps\.[^.]+\.events\[\d+\]\.payload\./] as const; - /** * Liquid variable autocomplete for the following patterns: * * 1. Payload Variables: * Valid: - * - payload. - * - payload.user + * - payload.userId * - payload.anyNewField (allows any new field) * - payload.deeply.nested.field * Invalid: @@ -100,17 +92,16 @@ export const completions = }; } - const matchingVariables = getMatchingVariables(searchText, variables); + const matchingVariables = getVariableCompletions(searchText, variables); + + const options = matchingVariables.map((v) => createCompletionOption(v.label, 'variable')); // If we have matches or we're in a valid context, show them if (matchingVariables.length > 0 || isInsideLiquidBlock(beforeCursor)) { return { from: lastOpenBrace + 2, to: pos, - options: - matchingVariables.length > 0 - ? matchingVariables.map((v) => createCompletionOption(v.label, 'variable')) - : variables.map((v) => createCompletionOption(v.label, 'variable')), + options, }; } @@ -118,9 +109,7 @@ export const completions = }; function isInsideLiquidBlock(beforeCursor: string): boolean { - const lastOpenBrace = beforeCursor.lastIndexOf('{{'); - - return lastOpenBrace !== -1; + return beforeCursor.lastIndexOf('{{') !== -1; } function getContentAfterPipe(content: string): string | null { @@ -140,95 +129,43 @@ function getFilterCompletions(afterPipe: string, isEnhancedDigestEnabled: boolea .map((f) => createCompletionOption(f.value, 'function')); } -function isValidDynamicPath(searchText: string): boolean { - return VALID_DYNAMIC_PATH_SUGGESTIONS.some((path) => - typeof path === 'string' ? searchText.startsWith(path) : path.test(searchText) - ); -} - -function isInvalidDynamicPathValues(searchText: string): boolean { - return INVALID_DYNAMIC_PATH_VALUES.some((path) => - typeof path === 'string' ? searchText === path : path.test(searchText) - ); -} - -function validateSubscriberField(searchText: string, matches: LiquidVariable[]): LiquidVariable[] { - const parts = searchText.split('.'); - - if (parts.length === 2 && parts[0] === 'subscriber') { - if (!matches.some((v) => v.label === searchText)) { - return []; - } - } - - return matches; -} - -function validateStepId(searchText: string, variables: LiquidVariable[]): boolean { - if (!searchText.startsWith('steps.')) return true; - - const stepMatch = searchText.match(/^steps\.([^.]+)/); - if (!stepMatch) return true; - - const stepId = stepMatch[1]; - return variables.some((v) => v.label.startsWith(`steps.${stepId}.`)); -} - -function getMatchingVariables(searchText: string, variables: LiquidVariable[]): LiquidVariable[] { +function getVariableCompletions(searchText: string, variables: LiquidVariable[]): LiquidVariable[] { if (!searchText) return variables; const searchLower = searchText.toLowerCase(); - // Handle root prefixes and their partials - for (const [root, prefix] of Object.entries(ROOT_PREFIXES)) { - if (searchLower.startsWith(root) || root.startsWith(searchLower)) { - let matches = variables.filter((v) => v.label.startsWith(prefix)); + const dynamicNamespacesWithUnknownKeys = variables.reduce((acc, entry) => { + const match = entry.label.match(DYNAMIC_NAMESPACES_WITH_UNKNOWN_KEYS_REGEX); - // Special handling for subscriber fields - if (prefix === 'subscriber.') { - matches = validateSubscriberField(searchText, matches); - } - - // Allow new paths for dynamic paths - if (isValidDynamicPath(searchText)) { - if (!matches.some((v) => v.label === searchText)) { - matches.push({ label: searchText, type: 'variable' } as LiquidVariable); - } - } - - return matches; + if (match) { + acc.push(`${match[0]}.payload`); } - } - - // Handle dot endings - if (searchText.endsWith('.')) { - const prefix = searchText.slice(0, -1); - return variables.filter((v) => v.label.startsWith(prefix)); - } - - // Validate step ID exists - if (!validateStepId(searchText, variables)) { - return []; - } - // Default case: show any variables containing the search text - let result = variables.filter((v) => v.label.toLowerCase().includes(searchLower)); + return acc; + }, []); + + const dynamicVariables = [...FIXED_NAMESPACES_WITH_UNKNOWN_KEYS, ...dynamicNamespacesWithUnknownKeys].reduce< + LiquidVariable[] + >((acc, namespace) => { + if (searchText.startsWith(namespace) && searchText !== namespace) { + // Ensure that if the user types payload.foo the first suggestion is payload.foo + acc.push({ label: searchText, type: 'variable' }); + } else if (!searchText.startsWith(namespace)) { + // For all other values, suggest payload.whatever, subscriber.data.whatever + acc.push({ + label: `${namespace}.${searchLower.trim()}`, + type: 'variable', + }); + } - if (result.length === 0) { - const dynamicStepNames = variables - .map((entry) => entry.label.match(DYNAMIC_STEP_NAME_ROOT_REGEX)) - .filter((match): match is RegExpMatchArray => match !== null) - .map((match) => `${match[0]}.`); + return acc; + }, []); - result = [...DYNAMIC_PATH_ROOTS, ...dynamicStepNames].map((value) => { - return { - label: value + searchLower.trim(), - type: 'variable', - }; - }); - } + const uniqueVariables = Array.from( + new Map([...variables, ...dynamicVariables].map((item) => [item.label, item])).values() + ); - return result; + return uniqueVariables.filter((v) => v.label.toLowerCase().includes(searchLower)); } export function createAutocompleteSource(variables: LiquidVariable[], isEnhancedDigestEnabled: boolean) { @@ -253,17 +190,15 @@ export function createAutocompleteSource(variables: LiquidVariable[], isEnhanced const beforeCursor = content.slice(0, from); const afterCursor = content.slice(to); - const isInvalidValue = isInvalidDynamicPathValues(selectedValue); - // Ensure proper {{ }} wrapping const needsOpening = !beforeCursor.endsWith('{{'); const needsClosing = !afterCursor.startsWith('}}'); - const wrappedValue = `${needsOpening ? '{{' : ''}${selectedValue}${isInvalidValue && !selectedValue.endsWith('.') ? '.' : ''}${needsClosing && !isInvalidValue ? '}}' : ''}`; + const wrappedValue = `${needsOpening ? '{{' : ''}${selectedValue}${needsClosing ? '}}' : ''}`; // Calculate the final cursor position // Add 2 if we need to account for closing brackets - const finalCursorPos = from + wrappedValue.length + (needsClosing || isInvalidValue ? 0 : 2); + const finalCursorPos = from + wrappedValue.length + (needsClosing ? 0 : 2); view.dispatch({ changes: { from, to, insert: wrappedValue }, diff --git a/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx b/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx index 7446ef02601..5c0c32775f4 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx @@ -22,6 +22,7 @@ export const Maily = ({ value, onChange, className, ...rest }: MailyProps) => { const { step } = useWorkflow(); const isEnhancedDigestEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_ENHANCED_DIGEST_ENABLED); const parsedVariables = useParseVariables(step?.variables); + const primitives = useMemo( () => parsedVariables.primitives.map((v) => ({ name: v.label, required: false })), [parsedVariables.primitives] diff --git a/apps/dashboard/src/components/workflow-editor/steps/email/variables/variables.ts b/apps/dashboard/src/components/workflow-editor/steps/email/variables/variables.ts index 36ed3a1bf16..5f97462de1f 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/email/variables/variables.ts +++ b/apps/dashboard/src/components/workflow-editor/steps/email/variables/variables.ts @@ -1,6 +1,9 @@ import { Variable, Variables } from '@maily-to/core/extensions'; -import { DYNAMIC_PATH_ROOTS, DYNAMIC_STEP_NAME_ROOT_REGEX } from '@/utils/constants'; import type { Editor as TiptapEditor } from '@tiptap/core'; +import { + FIXED_NAMESPACES_WITH_UNKNOWN_KEYS, + DYNAMIC_NAMESPACES_WITH_UNKNOWN_KEYS_REGEX, +} from '@/components/variable/constants'; export enum VariableFrom { Content = 'content-variable', @@ -97,32 +100,40 @@ const getRepeatBlockEachVariable = (editor: TiptapEditor): Array => { const dedupAndSortVariables = (variables: Array, query: string): Array => { // Filter variables that match the query - let filteredVariables = variables.filter((variable) => variable.name.toLowerCase().includes(query.toLowerCase())); - - if (filteredVariables.length === 0 && variables.length > 0) { - /** - * When typing a variable that is not present in the list, - * we need to check if the variable is a dynamic path. - * If it is, we need to add the dynamic path values to the list. - * For example, if the user types "hello" and there are dynamic paths like "steps.x.events[n].payload", - * we need to add "steps.x.events[n].payload.hello", "payload.hello" , "subscriber.data.hello" to the list. - * This is done to allow the user to create dynamic paths. - */ - const dynamicStepNames = variables - .map((entry) => entry.name.match(DYNAMIC_STEP_NAME_ROOT_REGEX)) - .filter((match): match is RegExpMatchArray => match !== null) - .map((match) => `${match[0]}.`); - - filteredVariables = [...DYNAMIC_PATH_ROOTS, ...dynamicStepNames].map((value) => { - return { - name: value + query.trim(), - required: false, - }; - }); + const filteredVariables = variables.filter((variable) => variable.name.toLowerCase().includes(query.toLowerCase())); + + /** + * When typing a variable that is not present in the list, + * we need to check if the variable is a dynamic path. + * If it is, we need to add the dynamic path values to the list. + * For example, if the user types "hello" and there are dynamic paths like "steps.x.events[n].payload", + * we need to add "steps.x.events[n].payload.hello", "payload.hello" , "subscriber.data.hello" to the list. + * This is done to allow the user to create dynamic paths. + */ + const dynamicStepNames = variables + .map((entry) => entry.name.match(DYNAMIC_NAMESPACES_WITH_UNKNOWN_KEYS_REGEX)) + .filter((match): match is RegExpMatchArray => match !== null) + .map((match) => `${match[0]}.`); + + const dynamicNamespaces = []; + + if (query) { + dynamicNamespaces.push(...FIXED_NAMESPACES_WITH_UNKNOWN_KEYS); } + dynamicNamespaces.push(...dynamicStepNames); + + const dynamicVariables = [...dynamicNamespaces, ...dynamicStepNames].map((value) => { + return { + name: value + query.trim(), + required: false, + }; + }); + // Deduplicate based on name property - const uniqueVariables = Array.from(new Map(filteredVariables.map((item) => [item.name, item])).values()); + const uniqueVariables = Array.from( + new Map([...filteredVariables, ...dynamicVariables].map((item) => [item.name, item])).values() + ); // Sort variables: exact matches first, then starts with query, then alphabetically return uniqueVariables.sort((a, b) => { diff --git a/apps/dashboard/src/hooks/use-parse-variables.ts b/apps/dashboard/src/hooks/use-parse-variables.ts index 71979dd65e3..393a45993a4 100644 --- a/apps/dashboard/src/hooks/use-parse-variables.ts +++ b/apps/dashboard/src/hooks/use-parse-variables.ts @@ -1,4 +1,4 @@ -import { parseStepVariables } from '@/utils/parseStepVariables'; +import { parseStepVariables } from '@/components/variable/parseStepVariables'; import type { JSONSchemaDefinition } from '@novu/shared'; import { useMemo } from 'react'; import { useFeatureFlag } from './use-feature-flag'; diff --git a/apps/dashboard/src/utils/constants.ts b/apps/dashboard/src/utils/constants.ts index d48bb6679f4..6839b5243de 100644 --- a/apps/dashboard/src/utils/constants.ts +++ b/apps/dashboard/src/utils/constants.ts @@ -36,15 +36,3 @@ export const DEFAULT_CONTROL_DIGEST_AMOUNT = 30; export const DEFAULT_CONTROL_DIGEST_UNIT = TimeUnitEnum.SECONDS; export const DEFAULT_CONTROL_DIGEST_CRON = ''; export const DEFAULT_CONTROL_DIGEST_DIGEST_KEY = ''; - -export const DYNAMIC_PATH_ROOTS = ['subscriber.data.', 'payload.'] as const; -export const DYNAMIC_STEP_NAME_ROOT_REGEX = /^steps\.(.+?)\.events\[\d+\]\.payload$/; - -export const INVALID_DYNAMIC_PATH_VALUES = [ - 'subscriber.data', - 'subscriber.data.', - 'payload', - 'payload.', - /^steps\.[^.]+\.events\[\d+\]\.payload$/, // Invalidates "steps.x.events[n].payload" - /^steps\.[^.]+\.events\[\d+\]\.payload\.$/, // Invalidates "steps.x.events[n].payload." -] as const; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b328c873664..e7a6a73742a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -857,6 +857,9 @@ importers: lodash.debounce: specifier: ^4.0.8 version: 4.0.8 + lodash.isempty: + specifier: ^4.4.0 + version: 4.4.0 lodash.isequal: specifier: ^4.5.0 version: 4.5.0 @@ -25820,6 +25823,9 @@ packages: lodash.isboolean@3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + lodash.isempty@4.4.0: + resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==} + lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. @@ -70086,6 +70092,8 @@ snapshots: lodash.isboolean@3.0.3: {} + lodash.isempty@4.4.0: {} + lodash.isequal@4.5.0: {} lodash.isinteger@4.0.4: {} From ecee771f28bcfc3c792bb1b306cb6b5b61d0501a Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Wed, 9 Apr 2025 13:17:01 +0300 Subject: [PATCH 7/7] fixup! WIP --- .../conditions-editor/conditions-editor.tsx | 2 +- .../src/components/conditions-editor/value-editor.tsx | 2 +- .../primitives/control-input/control-input.tsx | 11 ----------- .../primitives/control-input/variable-plugin/types.ts | 2 +- .../src/components/variable/edit-variable-popover.tsx | 2 +- .../src/components/variable/parseStepVariables.ts | 4 +--- .../steps/email/views/variable-view.tsx | 2 +- .../src/components/workflow-editor/url-input.tsx | 2 +- 8 files changed, 7 insertions(+), 20 deletions(-) diff --git a/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx b/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx index c79c1c36d07..b046ff140c3 100644 --- a/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx +++ b/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx @@ -10,7 +10,7 @@ import { FieldSelector } from '@/components/conditions-editor/field-selector'; import { OperatorSelector } from '@/components/conditions-editor/operator-selector'; import { RuleActions } from '@/components/conditions-editor/rule-actions'; import { ValueEditor } from '@/components/conditions-editor/value-editor'; -import { IsAllowedVariable, LiquidVariable } from '@/utils/parseStepVariables'; +import { IsAllowedVariable, LiquidVariable } from '@/components/variable/parseStepVariables'; const ruleActionsClassName = `[&>[data-actions="true"]]:opacity-0 [&:hover>[data-actions="true"]]:opacity-100 [&>[data-actions="true"]:has(~[data-radix-popper-content-wrapper])]:opacity-100`; const groupActionsClassName = `[&_.ruleGroup-header>[data-actions="true"]]:opacity-0 [&_.ruleGroup-header:hover>[data-actions="true"]]:opacity-100 [&_.ruleGroup-header>[data-actions="true"]:has(~[data-radix-popper-content-wrapper])]:opacity-100`; diff --git a/apps/dashboard/src/components/conditions-editor/value-editor.tsx b/apps/dashboard/src/components/conditions-editor/value-editor.tsx index 26bffe9cb6a..d3fd0e06d64 100644 --- a/apps/dashboard/src/components/conditions-editor/value-editor.tsx +++ b/apps/dashboard/src/components/conditions-editor/value-editor.tsx @@ -2,7 +2,7 @@ import { useFormContext } from 'react-hook-form'; import { useValueEditor, ValueEditorProps } from 'react-querybuilder'; import { InputRoot } from '@/components/primitives/input'; -import { IsAllowedVariable, LiquidVariable } from '@/utils/parseStepVariables'; +import { IsAllowedVariable, LiquidVariable } from '@/components/variable/parseStepVariables'; import { ControlInput } from '../primitives/control-input/control-input'; export const ValueEditor = (props: ValueEditorProps) => { diff --git a/apps/dashboard/src/components/primitives/control-input/control-input.tsx b/apps/dashboard/src/components/primitives/control-input/control-input.tsx index 131019bca4b..43075a415ae 100644 --- a/apps/dashboard/src/components/primitives/control-input/control-input.tsx +++ b/apps/dashboard/src/components/primitives/control-input/control-input.tsx @@ -79,17 +79,6 @@ export function ControlInput({ closeOnBlur: true, defaultKeymap: true, activateOnTyping: true, - addToOptions: [ - { - render: (completion) => { - console.log('completion', completion); - const div = document.createElement('div'); - div.textContent = 'Hello'; - return div; - }, - position: 0, - }, - ], }), [completionSource] ); diff --git a/apps/dashboard/src/components/primitives/control-input/variable-plugin/types.ts b/apps/dashboard/src/components/primitives/control-input/variable-plugin/types.ts index 06d3b427516..dbd9b3211a8 100644 --- a/apps/dashboard/src/components/primitives/control-input/variable-plugin/types.ts +++ b/apps/dashboard/src/components/primitives/control-input/variable-plugin/types.ts @@ -1,4 +1,4 @@ -import { IsAllowedVariable } from '@/utils/parseStepVariables'; +import { IsAllowedVariable } from '@/components/variable/parseStepVariables'; import { EditorView } from '@uiw/react-codemirror'; import { MutableRefObject } from 'react'; diff --git a/apps/dashboard/src/components/variable/edit-variable-popover.tsx b/apps/dashboard/src/components/variable/edit-variable-popover.tsx index 110cb7f62d2..b5871818e20 100644 --- a/apps/dashboard/src/components/variable/edit-variable-popover.tsx +++ b/apps/dashboard/src/components/variable/edit-variable-popover.tsx @@ -1,6 +1,6 @@ import { Popover, PopoverTrigger } from '@/components/primitives/popover'; import { EditVariablePopoverContent } from '@/components/variable/edit-variable-popover-content'; -import { IsAllowedVariable } from '@/utils/parseStepVariables'; +import { IsAllowedVariable } from '@/components/variable/parseStepVariables'; import { ReactNode } from 'react'; type EditVariablePopoverProps = { diff --git a/apps/dashboard/src/components/variable/parseStepVariables.ts b/apps/dashboard/src/components/variable/parseStepVariables.ts index 87a16f7b193..afab7df150c 100644 --- a/apps/dashboard/src/components/variable/parseStepVariables.ts +++ b/apps/dashboard/src/components/variable/parseStepVariables.ts @@ -121,9 +121,7 @@ export function parseStepVariables(schema: JSONSchemaDefinition, isEnhancedDiges return { ...result, - variables: isEnhancedDigestEnabled - ? [...result.primitives, ...result.arrays, ...result.namespaces] - : [...result.primitives], + variables: [...result.primitives, ...result.arrays], isAllowedVariable, }; } diff --git a/apps/dashboard/src/components/workflow-editor/steps/email/views/variable-view.tsx b/apps/dashboard/src/components/workflow-editor/steps/email/views/variable-view.tsx index 17ee4b22f28..f3b68442ab1 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/email/views/variable-view.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/email/views/variable-view.tsx @@ -6,7 +6,7 @@ import { VARIABLE_REGEX_STRING } from '@/components/primitives/control-input/var import { parseVariable } from '@/components/primitives/control-input/variable-plugin/utils'; import { EditVariablePopover } from '@/components/variable/edit-variable-popover'; import { VariablePill } from '@/components/variable/variable-pill'; -import { IsAllowedVariable } from '@/utils/parseStepVariables'; +import { IsAllowedVariable } from '@/components/variable/parseStepVariables'; type InternalVariableViewProps = NodeViewProps & { isAllowedVariable: IsAllowedVariable; diff --git a/apps/dashboard/src/components/workflow-editor/url-input.tsx b/apps/dashboard/src/components/workflow-editor/url-input.tsx index 1fd6f9a40d7..553e1213de4 100644 --- a/apps/dashboard/src/components/workflow-editor/url-input.tsx +++ b/apps/dashboard/src/components/workflow-editor/url-input.tsx @@ -5,7 +5,7 @@ import { FormControl, FormField, FormItem, FormMessage } from '@/components/prim import { InputProps, InputRoot } from '@/components/primitives/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/primitives/select'; import { useSaveForm } from '@/components/workflow-editor/steps/save-form-context'; -import { IsAllowedVariable, LiquidVariable } from '@/utils/parseStepVariables'; +import { IsAllowedVariable, LiquidVariable } from '@/components/variable/parseStepVariables'; type URLInputProps = Omit & { options: string[];