Highlight consumed variables in workflow runs (#10788)
- Create a function to resolve the variables used in the configuration of a step - Let the JSON tree components take a prop (`getNodeHighlighting`) to determine whether an element must be highlighted - Compute each element's keyPath recursively; the keyPath is passed as an argument to the `getNodeHighlighting` function ## Demo https://github.com/user-attachments/assets/8586f43d-53d1-41ba-ab48-08bb8c74e145 Closes https://github.com/twentyhq/core-team-issues/issues/435
This commit is contained in:
committed by
GitHub
parent
7e291f3cff
commit
ecf282ad99
@ -7,10 +7,14 @@ export const JsonArrayNode = ({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
depth,
|
depth,
|
||||||
|
keyPath,
|
||||||
|
shouldHighlightNode,
|
||||||
}: {
|
}: {
|
||||||
label?: string;
|
label?: string;
|
||||||
value: JsonArray;
|
value: JsonArray;
|
||||||
depth: number;
|
depth: number;
|
||||||
|
keyPath: string;
|
||||||
|
shouldHighlightNode?: (keyPath: string) => boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
@ -26,6 +30,8 @@ export const JsonArrayNode = ({
|
|||||||
Icon={IconBrackets}
|
Icon={IconBrackets}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
emptyElementsText={t`Empty Array`}
|
emptyElementsText={t`Empty Array`}
|
||||||
|
keyPath={keyPath}
|
||||||
|
shouldHighlightNode={shouldHighlightNode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { JsonList } from '@/workflow/components/json-visualizer/components/inter
|
|||||||
import { JsonNodeLabel } from '@/workflow/components/json-visualizer/components/internal/JsonNodeLabel';
|
import { JsonNodeLabel } from '@/workflow/components/json-visualizer/components/internal/JsonNodeLabel';
|
||||||
import { JsonNode } from '@/workflow/components/json-visualizer/components/JsonNode';
|
import { JsonNode } from '@/workflow/components/json-visualizer/components/JsonNode';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
@ -35,6 +36,8 @@ export const JsonNestedNode = ({
|
|||||||
renderElementsCount,
|
renderElementsCount,
|
||||||
emptyElementsText,
|
emptyElementsText,
|
||||||
depth,
|
depth,
|
||||||
|
keyPath,
|
||||||
|
shouldHighlightNode,
|
||||||
}: {
|
}: {
|
||||||
label?: string;
|
label?: string;
|
||||||
Icon: IconComponent;
|
Icon: IconComponent;
|
||||||
@ -42,6 +45,8 @@ export const JsonNestedNode = ({
|
|||||||
renderElementsCount?: (count: number) => string;
|
renderElementsCount?: (count: number) => string;
|
||||||
emptyElementsText: string;
|
emptyElementsText: string;
|
||||||
depth: number;
|
depth: number;
|
||||||
|
keyPath: string;
|
||||||
|
shouldHighlightNode?: (keyPath: string) => boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const hideRoot = !isDefined(label);
|
const hideRoot = !isDefined(label);
|
||||||
|
|
||||||
@ -52,9 +57,22 @@ export const JsonNestedNode = ({
|
|||||||
{elements.length === 0 ? (
|
{elements.length === 0 ? (
|
||||||
<StyledEmptyState>{emptyElementsText}</StyledEmptyState>
|
<StyledEmptyState>{emptyElementsText}</StyledEmptyState>
|
||||||
) : (
|
) : (
|
||||||
elements.map(({ id, label, value }) => (
|
elements.map(({ id, label, value }) => {
|
||||||
<JsonNode key={id} label={label} value={value} depth={depth + 1} />
|
const nextKeyPath = isNonEmptyString(keyPath)
|
||||||
))
|
? `${keyPath}.${id}`
|
||||||
|
: String(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JsonNode
|
||||||
|
key={id}
|
||||||
|
label={label}
|
||||||
|
value={value}
|
||||||
|
depth={depth + 1}
|
||||||
|
keyPath={nextKeyPath}
|
||||||
|
shouldHighlightNode={shouldHighlightNode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</JsonList>
|
</JsonList>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,17 +15,24 @@ export const JsonNode = ({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
depth,
|
depth,
|
||||||
|
keyPath,
|
||||||
|
shouldHighlightNode,
|
||||||
}: {
|
}: {
|
||||||
label?: string;
|
label?: string;
|
||||||
value: JsonValue;
|
value: JsonValue;
|
||||||
depth: number;
|
depth: number;
|
||||||
|
keyPath: string;
|
||||||
|
shouldHighlightNode?: (keyPath: string) => boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const isHighlighted = shouldHighlightNode?.(keyPath) ?? false;
|
||||||
|
|
||||||
if (isNull(value)) {
|
if (isNull(value)) {
|
||||||
return (
|
return (
|
||||||
<JsonValueNode
|
<JsonValueNode
|
||||||
label={label}
|
label={label}
|
||||||
valueAsString="[null]"
|
valueAsString="[null]"
|
||||||
Icon={IconCircleOff}
|
Icon={IconCircleOff}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -36,6 +43,7 @@ export const JsonNode = ({
|
|||||||
label={label}
|
label={label}
|
||||||
valueAsString={value}
|
valueAsString={value}
|
||||||
Icon={IconTypography}
|
Icon={IconTypography}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -46,6 +54,7 @@ export const JsonNode = ({
|
|||||||
label={label}
|
label={label}
|
||||||
valueAsString={String(value)}
|
valueAsString={String(value)}
|
||||||
Icon={IconNumber9}
|
Icon={IconNumber9}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -56,13 +65,30 @@ export const JsonNode = ({
|
|||||||
label={label}
|
label={label}
|
||||||
valueAsString={String(value)}
|
valueAsString={String(value)}
|
||||||
Icon={IconCheckbox}
|
Icon={IconCheckbox}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isArray(value)) {
|
if (isArray(value)) {
|
||||||
return <JsonArrayNode label={label} value={value} depth={depth} />;
|
return (
|
||||||
|
<JsonArrayNode
|
||||||
|
label={label}
|
||||||
|
value={value}
|
||||||
|
depth={depth}
|
||||||
|
keyPath={keyPath}
|
||||||
|
shouldHighlightNode={shouldHighlightNode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <JsonObjectNode label={label} value={value} depth={depth} />;
|
return (
|
||||||
|
<JsonObjectNode
|
||||||
|
label={label}
|
||||||
|
value={value}
|
||||||
|
depth={depth}
|
||||||
|
keyPath={keyPath}
|
||||||
|
shouldHighlightNode={shouldHighlightNode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,10 +7,14 @@ export const JsonObjectNode = ({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
depth,
|
depth,
|
||||||
|
keyPath,
|
||||||
|
shouldHighlightNode,
|
||||||
}: {
|
}: {
|
||||||
label?: string;
|
label?: string;
|
||||||
value: JsonObject;
|
value: JsonObject;
|
||||||
depth: number;
|
depth: number;
|
||||||
|
keyPath: string;
|
||||||
|
shouldHighlightNode?: (keyPath: string) => boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
@ -26,6 +30,8 @@ export const JsonObjectNode = ({
|
|||||||
Icon={IconCube}
|
Icon={IconCube}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
emptyElementsText={t`Empty Object`}
|
emptyElementsText={t`Empty Object`}
|
||||||
|
keyPath={keyPath}
|
||||||
|
shouldHighlightNode={shouldHighlightNode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,10 +2,21 @@ import { JsonList } from '@/workflow/components/json-visualizer/components/inter
|
|||||||
import { JsonNode } from '@/workflow/components/json-visualizer/components/JsonNode';
|
import { JsonNode } from '@/workflow/components/json-visualizer/components/JsonNode';
|
||||||
import { JsonValue } from 'type-fest';
|
import { JsonValue } from 'type-fest';
|
||||||
|
|
||||||
export const JsonTree = ({ value }: { value: JsonValue }) => {
|
export const JsonTree = ({
|
||||||
|
value,
|
||||||
|
shouldHighlightNode,
|
||||||
|
}: {
|
||||||
|
value: JsonValue;
|
||||||
|
shouldHighlightNode?: (keyPath: string) => boolean;
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<JsonList depth={0}>
|
<JsonList depth={0}>
|
||||||
<JsonNode value={value} depth={0} />
|
<JsonNode
|
||||||
|
value={value}
|
||||||
|
depth={0}
|
||||||
|
keyPath=""
|
||||||
|
shouldHighlightNode={shouldHighlightNode}
|
||||||
|
/>
|
||||||
</JsonList>
|
</JsonList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,6 +10,7 @@ const StyledListItem = styled(JsonListItem)`
|
|||||||
|
|
||||||
type JsonValueNodeProps = {
|
type JsonValueNodeProps = {
|
||||||
valueAsString: string;
|
valueAsString: string;
|
||||||
|
isHighlighted: boolean;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
label: string;
|
label: string;
|
||||||
@ -24,9 +25,18 @@ type JsonValueNodeProps = {
|
|||||||
export const JsonValueNode = (props: JsonValueNodeProps) => {
|
export const JsonValueNode = (props: JsonValueNodeProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledListItem>
|
<StyledListItem>
|
||||||
{props.label && <JsonNodeLabel label={props.label} Icon={props.Icon} />}
|
{props.label && (
|
||||||
|
<JsonNodeLabel
|
||||||
|
label={props.label}
|
||||||
|
Icon={props.Icon}
|
||||||
|
isHighlighted={props.isHighlighted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<JsonNodeValue valueAsString={props.valueAsString} />
|
<JsonNodeValue
|
||||||
|
valueAsString={props.valueAsString}
|
||||||
|
isHighlighted={props.isHighlighted}
|
||||||
|
/>
|
||||||
</StyledListItem>
|
</StyledListItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,10 +2,12 @@ import { useTheme } from '@emotion/react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledLabelContainer = styled.span`
|
const StyledLabelContainer = styled.span<{ isHighlighted?: boolean }>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
border-color: ${({ theme }) => theme.border.color.medium};
|
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-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
@ -23,15 +25,20 @@ const StyledLabelContainer = styled.span`
|
|||||||
export const JsonNodeLabel = ({
|
export const JsonNodeLabel = ({
|
||||||
label,
|
label,
|
||||||
Icon,
|
Icon,
|
||||||
|
isHighlighted,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
Icon: IconComponent;
|
Icon: IconComponent;
|
||||||
|
isHighlighted?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledLabelContainer>
|
<StyledLabelContainer isHighlighted={isHighlighted}>
|
||||||
<Icon size={theme.icon.size.md} color={theme.font.color.tertiary} />
|
<Icon
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
color={isHighlighted ? theme.color.blue : theme.font.color.tertiary}
|
||||||
|
/>
|
||||||
|
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</StyledLabelContainer>
|
</StyledLabelContainer>
|
||||||
|
|||||||
@ -1,9 +1,16 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const StyledText = styled.span`
|
const StyledText = styled.span<{ isHighlighted?: boolean }>`
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme, isHighlighted }) =>
|
||||||
|
isHighlighted ? theme.adaptiveColors.blue4 : theme.font.color.tertiary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const JsonNodeValue = ({ valueAsString }: { valueAsString: string }) => {
|
export const JsonNodeValue = ({
|
||||||
return <StyledText>{valueAsString}</StyledText>;
|
valueAsString,
|
||||||
|
isHighlighted,
|
||||||
|
}: {
|
||||||
|
valueAsString: string;
|
||||||
|
isHighlighted?: boolean;
|
||||||
|
}) => {
|
||||||
|
return <StyledText isHighlighted={isHighlighted}>{valueAsString}</StyledText>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { JsonNestedNode } from '@/workflow/components/json-visualizer/components
|
|||||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||||
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
|
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
|
||||||
|
import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { IconBrackets } from 'twenty-ui';
|
import { IconBrackets } from 'twenty-ui';
|
||||||
@ -16,17 +17,25 @@ const StyledContainer = styled.div`
|
|||||||
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||||
|
const step = workflowRun?.output?.flow.steps.find(
|
||||||
|
(step) => step.id === stepId,
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
isDefined(workflowRun) &&
|
isDefined(workflowRun) &&
|
||||||
isDefined(workflowRun.context) &&
|
isDefined(workflowRun.context) &&
|
||||||
isDefined(workflowRun.output?.flow)
|
isDefined(workflowRun.output?.flow) &&
|
||||||
|
isDefined(step)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const variablesUsedInStep = getWorkflowVariablesUsedInStep({
|
||||||
|
step,
|
||||||
|
});
|
||||||
|
|
||||||
const stepContext = getWorkflowRunStepContext({
|
const stepContext = getWorkflowRunStepContext({
|
||||||
context: workflowRun.context,
|
context: workflowRun.context,
|
||||||
flow: workflowRun.output.flow,
|
flow: workflowRun.output.flow,
|
||||||
@ -48,6 +57,8 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
|||||||
Icon={IconBrackets}
|
Icon={IconBrackets}
|
||||||
emptyElementsText=""
|
emptyElementsText=""
|
||||||
depth={0}
|
depth={0}
|
||||||
|
keyPath=""
|
||||||
|
shouldHighlightNode={(keyPath) => variablesUsedInStep.has(keyPath)}
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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<string> {
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
for (const variable of resolveVariables(step.settings.input)) {
|
||||||
|
variablesUsedInStep.add(variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variablesUsedInStep;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user