diff --git a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonArrayNode.tsx b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonArrayNode.tsx
index 0e1fde435..9ef496bc3 100644
--- a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonArrayNode.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonArrayNode.tsx
@@ -7,10 +7,14 @@ export const JsonArrayNode = ({
label,
value,
depth,
+ keyPath,
+ shouldHighlightNode,
}: {
label?: string;
value: JsonArray;
depth: number;
+ keyPath: string;
+ shouldHighlightNode?: (keyPath: string) => boolean;
}) => {
const { t } = useLingui();
@@ -26,6 +30,8 @@ export const JsonArrayNode = ({
Icon={IconBrackets}
depth={depth}
emptyElementsText={t`Empty Array`}
+ keyPath={keyPath}
+ shouldHighlightNode={shouldHighlightNode}
/>
);
};
diff --git a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonNestedNode.tsx b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonNestedNode.tsx
index 4e9204181..2ffda032b 100644
--- a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonNestedNode.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonNestedNode.tsx
@@ -3,6 +3,7 @@ import { JsonList } from '@/workflow/components/json-visualizer/components/inter
import { JsonNodeLabel } from '@/workflow/components/json-visualizer/components/internal/JsonNodeLabel';
import { JsonNode } from '@/workflow/components/json-visualizer/components/JsonNode';
import styled from '@emotion/styled';
+import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { isDefined } from 'twenty-shared';
import { IconComponent } from 'twenty-ui';
@@ -35,6 +36,8 @@ export const JsonNestedNode = ({
renderElementsCount,
emptyElementsText,
depth,
+ keyPath,
+ shouldHighlightNode,
}: {
label?: string;
Icon: IconComponent;
@@ -42,6 +45,8 @@ export const JsonNestedNode = ({
renderElementsCount?: (count: number) => string;
emptyElementsText: string;
depth: number;
+ keyPath: string;
+ shouldHighlightNode?: (keyPath: string) => boolean;
}) => {
const hideRoot = !isDefined(label);
@@ -52,9 +57,22 @@ export const JsonNestedNode = ({
{elements.length === 0 ? (
{emptyElementsText}
) : (
- elements.map(({ id, label, value }) => (
-
- ))
+ elements.map(({ id, label, value }) => {
+ const nextKeyPath = isNonEmptyString(keyPath)
+ ? `${keyPath}.${id}`
+ : String(id);
+
+ return (
+
+ );
+ })
)}
);
diff --git a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonNode.tsx b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonNode.tsx
index 643382ba2..e326bf4fc 100644
--- a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonNode.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonNode.tsx
@@ -15,17 +15,24 @@ export const JsonNode = ({
label,
value,
depth,
+ keyPath,
+ shouldHighlightNode,
}: {
label?: string;
value: JsonValue;
depth: number;
+ keyPath: string;
+ shouldHighlightNode?: (keyPath: string) => boolean;
}) => {
+ const isHighlighted = shouldHighlightNode?.(keyPath) ?? false;
+
if (isNull(value)) {
return (
);
}
@@ -36,6 +43,7 @@ export const JsonNode = ({
label={label}
valueAsString={value}
Icon={IconTypography}
+ isHighlighted={isHighlighted}
/>
);
}
@@ -46,6 +54,7 @@ export const JsonNode = ({
label={label}
valueAsString={String(value)}
Icon={IconNumber9}
+ isHighlighted={isHighlighted}
/>
);
}
@@ -56,13 +65,30 @@ export const JsonNode = ({
label={label}
valueAsString={String(value)}
Icon={IconCheckbox}
+ isHighlighted={isHighlighted}
/>
);
}
if (isArray(value)) {
- return ;
+ return (
+
+ );
}
- return ;
+ return (
+
+ );
};
diff --git a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonObjectNode.tsx b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonObjectNode.tsx
index 5d8535e9f..57bea7d37 100644
--- a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonObjectNode.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonObjectNode.tsx
@@ -7,10 +7,14 @@ export const JsonObjectNode = ({
label,
value,
depth,
+ keyPath,
+ shouldHighlightNode,
}: {
label?: string;
value: JsonObject;
depth: number;
+ keyPath: string;
+ shouldHighlightNode?: (keyPath: string) => boolean;
}) => {
const { t } = useLingui();
@@ -26,6 +30,8 @@ export const JsonObjectNode = ({
Icon={IconCube}
depth={depth}
emptyElementsText={t`Empty Object`}
+ keyPath={keyPath}
+ shouldHighlightNode={shouldHighlightNode}
/>
);
};
diff --git a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonTree.tsx b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonTree.tsx
index 0bc84c6d2..43c474a1f 100644
--- a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonTree.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonTree.tsx
@@ -2,10 +2,21 @@ import { JsonList } from '@/workflow/components/json-visualizer/components/inter
import { JsonNode } from '@/workflow/components/json-visualizer/components/JsonNode';
import { JsonValue } from 'type-fest';
-export const JsonTree = ({ value }: { value: JsonValue }) => {
+export const JsonTree = ({
+ value,
+ shouldHighlightNode,
+}: {
+ value: JsonValue;
+ shouldHighlightNode?: (keyPath: string) => boolean;
+}) => {
return (
-
+
);
};
diff --git a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonValueNode.tsx b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonValueNode.tsx
index 09a79f1f0..10d5cb927 100644
--- a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonValueNode.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/JsonValueNode.tsx
@@ -10,6 +10,7 @@ const StyledListItem = styled(JsonListItem)`
type JsonValueNodeProps = {
valueAsString: string;
+ isHighlighted: boolean;
} & (
| {
label: string;
@@ -24,9 +25,18 @@ type JsonValueNodeProps = {
export const JsonValueNode = (props: JsonValueNodeProps) => {
return (
- {props.label && }
+ {props.label && (
+
+ )}
-
+
);
};
diff --git a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/internal/JsonNodeLabel.tsx b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/internal/JsonNodeLabel.tsx
index a725ee115..7e3a8a5d3 100644
--- a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/internal/JsonNodeLabel.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/internal/JsonNodeLabel.tsx
@@ -2,10 +2,12 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from 'twenty-ui';
-const StyledLabelContainer = styled.span`
+const StyledLabelContainer = styled.span<{ isHighlighted?: boolean }>`
align-items: center;
background-color: ${({ theme }) => theme.background.transparent.lighter};
border-color: ${({ theme }) => theme.border.color.medium};
+ color: ${({ theme, isHighlighted }) =>
+ isHighlighted ? theme.color.blue : theme.font.color.primary};
border-radius: ${({ theme }) => theme.border.radius.sm};
border-style: solid;
border-width: 1px;
@@ -23,15 +25,20 @@ const StyledLabelContainer = styled.span`
export const JsonNodeLabel = ({
label,
Icon,
+ isHighlighted,
}: {
label: string;
Icon: IconComponent;
+ isHighlighted?: boolean;
}) => {
const theme = useTheme();
return (
-
-
+
+
{label}
diff --git a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/internal/JsonNodeValue.tsx b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/internal/JsonNodeValue.tsx
index bcf8d8b55..56a33e899 100644
--- a/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/internal/JsonNodeValue.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/json-visualizer/components/internal/JsonNodeValue.tsx
@@ -1,9 +1,16 @@
import styled from '@emotion/styled';
-const StyledText = styled.span`
- color: ${({ theme }) => theme.font.color.tertiary};
+const StyledText = styled.span<{ isHighlighted?: boolean }>`
+ color: ${({ theme, isHighlighted }) =>
+ isHighlighted ? theme.adaptiveColors.blue4 : theme.font.color.tertiary};
`;
-export const JsonNodeValue = ({ valueAsString }: { valueAsString: string }) => {
- return {valueAsString};
+export const JsonNodeValue = ({
+ valueAsString,
+ isHighlighted,
+}: {
+ valueAsString: string;
+ isHighlighted?: boolean;
+}) => {
+ return {valueAsString};
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx
index cb25aff93..f1bee4ca5 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx
@@ -2,6 +2,7 @@ import { JsonNestedNode } from '@/workflow/components/json-visualizer/components
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
+import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared';
import { IconBrackets } from 'twenty-ui';
@@ -16,17 +17,25 @@ const StyledContainer = styled.div`
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
const workflowRunId = useWorkflowRunIdOrThrow();
const workflowRun = useWorkflowRun({ workflowRunId });
+ const step = workflowRun?.output?.flow.steps.find(
+ (step) => step.id === stepId,
+ );
if (
!(
isDefined(workflowRun) &&
isDefined(workflowRun.context) &&
- isDefined(workflowRun.output?.flow)
+ isDefined(workflowRun.output?.flow) &&
+ isDefined(step)
)
) {
return null;
}
+ const variablesUsedInStep = getWorkflowVariablesUsedInStep({
+ step,
+ });
+
const stepContext = getWorkflowRunStepContext({
context: workflowRun.context,
flow: workflowRun.output.flow,
@@ -48,6 +57,8 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
Icon={IconBrackets}
emptyElementsText=""
depth={0}
+ keyPath=""
+ shouldHighlightNode={(keyPath) => variablesUsedInStep.has(keyPath)}
/>
);
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getWorkflowVariablesUsedInStep.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getWorkflowVariablesUsedInStep.test.ts
new file mode 100644
index 000000000..b6a3140bf
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getWorkflowVariablesUsedInStep.test.ts
@@ -0,0 +1,173 @@
+import { WorkflowStep } from '@/workflow/types/Workflow';
+import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
+
+describe('getWorkflowVariablesUsedInStep', () => {
+ it('returns the variables used in a one-level object', () => {
+ const step: WorkflowStep = {
+ id: '42e8b60e-dd44-417a-875f-823d63f16819',
+ name: 'Code - Serverless Function',
+ type: 'CODE',
+ valid: false,
+ settings: {
+ input: {
+ serverlessFunctionId: '5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2d',
+ serverlessFunctionInput: {
+ a: '{{5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.a.b.c.d}}',
+ },
+ serverlessFunctionVersion: 'draft',
+ },
+ outputSchema: {},
+ errorHandlingOptions: {
+ retryOnFailure: {
+ value: false,
+ },
+ continueOnFailure: {
+ value: false,
+ },
+ },
+ },
+ };
+
+ expect(
+ getWorkflowVariablesUsedInStep({
+ step,
+ }),
+ ).toMatchInlineSnapshot(`
+Set {
+ "5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.a.b.c.d",
+}
+`);
+ });
+
+ it('returns the variables used in a computed field', () => {
+ const step: WorkflowStep = {
+ id: '42e8b60e-dd44-417a-875f-823d63f16819',
+ name: 'Create Record',
+ type: 'CREATE_RECORD',
+ valid: false,
+ settings: {
+ input: {
+ objectName: 'company',
+ objectRecord: {
+ name: 'Test',
+ address: {
+ addressLat: null,
+ addressLng: null,
+ addressCity: '{{trigger.address.addressCity}}',
+ addressState: '{{trigger.address.addressState}}',
+ addressCountry: '{{trigger.address.addressCountry}}',
+ addressStreet1: '{{trigger.address.addressStreet1}}',
+ addressStreet2: '{{trigger.address.addressStreet2}}',
+ addressPostcode: '{{trigger.address.addressPostcode}}',
+ },
+ domainName: {
+ primaryLinkUrl: '',
+ primaryLinkLabel: '',
+ },
+ },
+ },
+ outputSchema: {},
+ errorHandlingOptions: {
+ retryOnFailure: {
+ value: false,
+ },
+ continueOnFailure: {
+ value: false,
+ },
+ },
+ },
+ };
+
+ expect(
+ getWorkflowVariablesUsedInStep({
+ step,
+ }),
+ ).toMatchInlineSnapshot(`
+Set {
+ "trigger.address.addressCity",
+ "trigger.address.addressState",
+ "trigger.address.addressCountry",
+ "trigger.address.addressStreet1",
+ "trigger.address.addressStreet2",
+ "trigger.address.addressPostcode",
+}
+`);
+ });
+
+ it('returns all the variables used in a single field', () => {
+ const step: WorkflowStep = {
+ id: '42e8b60e-dd44-417a-875f-823d63f16819',
+ name: 'Code - Serverless Function',
+ type: 'CODE',
+ valid: false,
+ settings: {
+ input: {
+ serverlessFunctionId: '5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2d',
+ serverlessFunctionInput: {
+ a: '{{5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.a}} {{5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.b}} {{5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.c}} {{5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.d}}',
+ },
+ serverlessFunctionVersion: 'draft',
+ },
+ outputSchema: {},
+ errorHandlingOptions: {
+ retryOnFailure: {
+ value: false,
+ },
+ continueOnFailure: {
+ value: false,
+ },
+ },
+ },
+ };
+
+ expect(
+ getWorkflowVariablesUsedInStep({
+ step,
+ }),
+ ).toMatchInlineSnapshot(`
+Set {
+ "5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.a",
+ "5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.b",
+ "5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.c",
+ "5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.d",
+}
+`);
+ });
+
+ it('returns the variables used many times only once', () => {
+ const step: WorkflowStep = {
+ id: '42e8b60e-dd44-417a-875f-823d63f16819',
+ name: 'Code - Serverless Function',
+ type: 'CODE',
+ valid: false,
+ settings: {
+ input: {
+ serverlessFunctionId: '5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2d',
+ serverlessFunctionInput: {
+ a: '{{5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.a}} {{5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.a}} {{5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.a}} {{5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.a}}',
+ },
+ serverlessFunctionVersion: 'draft',
+ },
+ outputSchema: {},
+ errorHandlingOptions: {
+ retryOnFailure: {
+ value: false,
+ },
+ continueOnFailure: {
+ value: false,
+ },
+ },
+ },
+ };
+
+ expect(
+ getWorkflowVariablesUsedInStep({
+ step,
+ }),
+ ).toMatchInlineSnapshot(`
+Set {
+ "5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c.a",
+}
+`);
+ });
+});
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep.ts
new file mode 100644
index 000000000..039cc38b7
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep.ts
@@ -0,0 +1,36 @@
+import { WorkflowStep } from '@/workflow/types/Workflow';
+import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
+import { isObject, isString } from '@sniptt/guards';
+import { JsonValue } from 'type-fest';
+
+function* resolveVariables(value: JsonValue): Generator {
+ if (isString(value)) {
+ for (const [, variablePath] of value.matchAll(
+ CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX,
+ )) {
+ yield variablePath;
+ }
+
+ return;
+ }
+
+ if (isObject(value)) {
+ for (const nestedValue of Object.values(value)) {
+ yield* resolveVariables(nestedValue);
+ }
+ }
+}
+
+export const getWorkflowVariablesUsedInStep = ({
+ step,
+}: {
+ step: WorkflowStep;
+}) => {
+ const variablesUsedInStep = new Set();
+
+ for (const variable of resolveVariables(step.settings.input)) {
+ variablesUsedInStep.add(variable);
+ }
+
+ return variablesUsedInStep;
+};