Add variable path (#10720)
<img width="537" alt="Capture d’écran 2025-03-07 à 09 44 21" src="https://github.com/user-attachments/assets/52c4d292-01af-4389-aa66-551be2358dd7" /> - search through step output schema the variable - build the variable path - returns the variable label - display both
This commit is contained in:
@ -0,0 +1 @@
|
||||
export const CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX = /{{([^{}]+)}}/g;
|
||||
@ -33,9 +33,9 @@ export const useAvailableVariablesInWorkflowStep = ({
|
||||
const availableStepsOutputSchema: StepOutputSchema[] =
|
||||
getStepsOutputSchema(previousStepIds).filter(isDefined);
|
||||
|
||||
const triggersOutputSchema: StepOutputSchema[] = getStepsOutputSchema([
|
||||
TRIGGER_STEP_ID,
|
||||
]).filter(isDefined);
|
||||
const triggersOutputSchema: StepOutputSchema[] = isDefined(flow.trigger)
|
||||
? getStepsOutputSchema([TRIGGER_STEP_ID]).filter(isDefined)
|
||||
: [];
|
||||
|
||||
const availableVariablesInWorkflowStep = [
|
||||
...availableStepsOutputSchema,
|
||||
|
||||
@ -1,13 +1,36 @@
|
||||
import { extractVariableLabel } from '../extractVariableLabel';
|
||||
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||
|
||||
it('returns the last part of a properly formatted variable', () => {
|
||||
const rawVariable = '{{a.b.c}}';
|
||||
describe('extractRawVariableNamePart', () => {
|
||||
it('returns the last part of a properly formatted variable', () => {
|
||||
const rawVariable = '{{a.b.c}}';
|
||||
|
||||
expect(extractVariableLabel(rawVariable)).toBe('c');
|
||||
});
|
||||
|
||||
it('stops on unclosed variables', () => {
|
||||
const rawVariable = '{{ test {{a.b.c}}';
|
||||
|
||||
expect(extractVariableLabel(rawVariable)).toBe('c');
|
||||
expect(
|
||||
extractRawVariableNamePart({
|
||||
rawVariableName: rawVariable,
|
||||
part: 'selectedField',
|
||||
}),
|
||||
).toBe('c');
|
||||
});
|
||||
|
||||
it('returns the first part of a properly formatted variable', () => {
|
||||
const rawVariable = '{{a.b.c}}';
|
||||
|
||||
expect(
|
||||
extractRawVariableNamePart({
|
||||
rawVariableName: rawVariable,
|
||||
part: 'stepId',
|
||||
}),
|
||||
).toBe('a');
|
||||
});
|
||||
|
||||
it('stops on unclosed variables', () => {
|
||||
const rawVariable = '{{ test {{a.b.c}}';
|
||||
|
||||
expect(
|
||||
extractRawVariableNamePart({
|
||||
rawVariableName: rawVariable,
|
||||
part: 'selectedField',
|
||||
}),
|
||||
).toBe('c');
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,195 @@
|
||||
import { StepOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
|
||||
import { searchVariableThroughOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughOutputSchema';
|
||||
|
||||
const mockStep = {
|
||||
id: 'step-1',
|
||||
name: 'Step 1',
|
||||
outputSchema: {
|
||||
company: {
|
||||
isLeaf: false,
|
||||
icon: 'company',
|
||||
label: 'Company',
|
||||
value: {
|
||||
object: {
|
||||
nameSingular: 'company',
|
||||
fieldIdName: 'id',
|
||||
label: 'Company',
|
||||
value: 'John',
|
||||
isLeaf: true,
|
||||
},
|
||||
fields: {
|
||||
name: { label: 'Name', value: 'Twenty', isLeaf: true },
|
||||
address: { label: 'Address', value: '123 Main St', isLeaf: true },
|
||||
},
|
||||
_outputSchemaType: 'RECORD',
|
||||
},
|
||||
},
|
||||
person: {
|
||||
isLeaf: false,
|
||||
icon: 'person',
|
||||
label: 'Person',
|
||||
value: {
|
||||
object: {
|
||||
nameSingular: 'person',
|
||||
fieldIdName: 'id',
|
||||
label: 'Person',
|
||||
value: 'Jane',
|
||||
isLeaf: true,
|
||||
},
|
||||
fields: {
|
||||
firstName: { label: 'First Name', value: 'Jane', isLeaf: true },
|
||||
lastName: { label: 'Last Name', value: 'Doe', isLeaf: true },
|
||||
email: { label: 'Email', value: 'jane@example.com', isLeaf: true },
|
||||
},
|
||||
_outputSchemaType: 'RECORD',
|
||||
},
|
||||
},
|
||||
simpleData: {
|
||||
isLeaf: true,
|
||||
label: 'Simple Data',
|
||||
value: 'Simple value',
|
||||
},
|
||||
nestedData: {
|
||||
isLeaf: false,
|
||||
label: 'Nested Data',
|
||||
value: {
|
||||
field1: { label: 'Field 1', value: 'Value 1', isLeaf: true },
|
||||
field2: { label: 'Field 2', value: 'Value 2', isLeaf: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies StepOutputSchema;
|
||||
|
||||
describe('searchVariableThroughOutputSchema', () => {
|
||||
it('should find a company field variable', () => {
|
||||
const result = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: mockStep,
|
||||
rawVariableName: '{{step-1.company.name}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Name',
|
||||
variablePathLabel: 'Step 1 > Company > Name',
|
||||
});
|
||||
});
|
||||
|
||||
it('should find a person field variable', () => {
|
||||
const result = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: mockStep,
|
||||
rawVariableName: '{{step-1.person.email}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Email',
|
||||
variablePathLabel: 'Step 1 > Person > Email',
|
||||
});
|
||||
});
|
||||
|
||||
it('should find a company object variable', () => {
|
||||
const result = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: mockStep,
|
||||
rawVariableName: '{{step-1.company.id}}',
|
||||
isFullRecord: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Company',
|
||||
variablePathLabel: 'Step 1 > Company > Company',
|
||||
});
|
||||
});
|
||||
|
||||
it('should find a person object variable', () => {
|
||||
const result = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: mockStep,
|
||||
rawVariableName: '{{step-1.person.id}}',
|
||||
isFullRecord: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Person',
|
||||
variablePathLabel: 'Step 1 > Person > Person',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle simple data fields', () => {
|
||||
const result = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: mockStep,
|
||||
rawVariableName: '{{step-1.simpleData}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Simple Data',
|
||||
variablePathLabel: 'Step 1 > Simple Data',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nested data fields', () => {
|
||||
const result = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: mockStep,
|
||||
rawVariableName: '{{step-1.nestedData.field1}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Field 1',
|
||||
variablePathLabel: 'Step 1 > Nested Data > Field 1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid variable names', () => {
|
||||
const result = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: mockStep,
|
||||
rawVariableName: '{{invalid}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: 'Step 1 > undefined',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle non-existent paths', () => {
|
||||
const result = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: mockStep,
|
||||
rawVariableName: '{{step-1.nonExistent.field}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: 'Step 1 > undefined',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle the case where the path has dots in field names', () => {
|
||||
const mockStepWithDotInField = {
|
||||
id: 'step-1',
|
||||
name: 'Step 1',
|
||||
outputSchema: {
|
||||
'complex.field': {
|
||||
isLeaf: false,
|
||||
label: 'Complex Field',
|
||||
value: {
|
||||
field1: { label: 'Field 1', value: 'Value 1', isLeaf: true },
|
||||
field2: { label: 'Field 2', value: 'Value 2', isLeaf: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies StepOutputSchema;
|
||||
|
||||
const result = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: mockStepWithDotInField,
|
||||
rawVariableName: '{{step-1.complex.field.field1}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Field 1',
|
||||
variablePathLabel: 'Step 1 > Complex Field > Field 1',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const extractRawVariableNamePart = ({
|
||||
rawVariableName,
|
||||
part,
|
||||
}: {
|
||||
rawVariableName: string;
|
||||
part: 'stepId' | 'selectedField';
|
||||
}) => {
|
||||
const variableWithoutBrackets = rawVariableName.replace(
|
||||
CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX,
|
||||
(_, variableName) => {
|
||||
return variableName;
|
||||
},
|
||||
);
|
||||
|
||||
const parts = variableWithoutBrackets.split('.');
|
||||
|
||||
const extractedPart =
|
||||
part === 'stepId'
|
||||
? parts.at(0)
|
||||
: part === 'selectedField'
|
||||
? parts.at(-1)
|
||||
: null;
|
||||
|
||||
if (!isDefined(extractedPart)) {
|
||||
throw new Error('Expected to find at least one splitted chunk.');
|
||||
}
|
||||
|
||||
return extractedPart;
|
||||
};
|
||||
@ -1,21 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
const CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX = /{{([^{}]+)}}/g;
|
||||
|
||||
export const extractVariableLabel = (rawVariableName: string) => {
|
||||
const variableWithoutBrackets = rawVariableName.replace(
|
||||
CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX,
|
||||
(_, variableName) => {
|
||||
return variableName;
|
||||
},
|
||||
);
|
||||
|
||||
const parts = variableWithoutBrackets.split('.');
|
||||
const displayText = parts.at(-1);
|
||||
|
||||
if (!isDefined(displayText)) {
|
||||
throw new Error('Expected to find at least one splitted chunk.');
|
||||
}
|
||||
|
||||
return displayText;
|
||||
};
|
||||
@ -0,0 +1,128 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import {
|
||||
OutputSchema,
|
||||
StepOutputSchema,
|
||||
} from '@/workflow/workflow-variables/types/StepOutputSchema';
|
||||
import { isBaseOutputSchema } from '@/workflow/workflow-variables/utils/isBaseOutputSchema';
|
||||
import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
const getDisplayedSubStepObjectLabel = (outputSchema: OutputSchema) => {
|
||||
if (!isRecordOutputSchema(outputSchema)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return outputSchema.object.label;
|
||||
};
|
||||
|
||||
const getDisplayedSubStepFieldLabel = (
|
||||
key: string,
|
||||
outputSchema: OutputSchema,
|
||||
) => {
|
||||
if (isBaseOutputSchema(outputSchema)) {
|
||||
return outputSchema[key]?.label;
|
||||
}
|
||||
|
||||
if (isRecordOutputSchema(outputSchema)) {
|
||||
return outputSchema.fields[key]?.label;
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
const searchCurrentStepOutputSchema = ({
|
||||
stepOutputSchema,
|
||||
path,
|
||||
isFullRecord,
|
||||
selectedField,
|
||||
}: {
|
||||
stepOutputSchema: StepOutputSchema;
|
||||
path: string[];
|
||||
isFullRecord: boolean;
|
||||
selectedField: string;
|
||||
}) => {
|
||||
let currentSubStep = stepOutputSchema.outputSchema;
|
||||
let nextKeyIndex = 0;
|
||||
let nextKey = path[nextKeyIndex];
|
||||
let variablePathLabel = stepOutputSchema.name;
|
||||
|
||||
while (nextKeyIndex < path.length) {
|
||||
if (isRecordOutputSchema(currentSubStep)) {
|
||||
const currentField = currentSubStep.fields[nextKey];
|
||||
currentSubStep = currentField?.value;
|
||||
nextKey = path[nextKeyIndex + 1];
|
||||
variablePathLabel = `${variablePathLabel} > ${currentField?.label}`;
|
||||
} else if (isBaseOutputSchema(currentSubStep)) {
|
||||
if (isDefined(currentSubStep[nextKey])) {
|
||||
const currentField = currentSubStep[nextKey];
|
||||
currentSubStep = currentField?.value;
|
||||
nextKey = path[nextKeyIndex + 1];
|
||||
variablePathLabel = `${variablePathLabel} > ${currentField?.label}`;
|
||||
} else {
|
||||
// If the key is not found in the step, we handle the case where the path has been wrongly split
|
||||
// For example, if there is a dot in the field name
|
||||
if (nextKeyIndex + 1 < path.length) {
|
||||
nextKey = `${nextKey}.${path[nextKeyIndex + 1]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
nextKeyIndex++;
|
||||
}
|
||||
|
||||
if (!isDefined(currentSubStep)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
variableLabel: isFullRecord
|
||||
? getDisplayedSubStepObjectLabel(currentSubStep)
|
||||
: getDisplayedSubStepFieldLabel(selectedField, currentSubStep),
|
||||
variablePathLabel,
|
||||
};
|
||||
};
|
||||
|
||||
export const searchVariableThroughOutputSchema = ({
|
||||
stepOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord = false,
|
||||
}: {
|
||||
stepOutputSchema: StepOutputSchema;
|
||||
rawVariableName: string;
|
||||
isFullRecord?: boolean;
|
||||
}) => {
|
||||
const variableWithoutBrackets = rawVariableName.replace(
|
||||
CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX,
|
||||
(_, variableName) => {
|
||||
return variableName;
|
||||
},
|
||||
);
|
||||
|
||||
const parts = variableWithoutBrackets.split('.');
|
||||
|
||||
const stepId = parts.at(0);
|
||||
const selectedField = parts.at(-1);
|
||||
// path is the remaining parts of the variable name
|
||||
const path = parts.slice(1, -1);
|
||||
|
||||
if (!isDefined(stepId) || !isDefined(selectedField)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const { variableLabel, variablePathLabel } = searchCurrentStepOutputSchema({
|
||||
stepOutputSchema,
|
||||
path,
|
||||
isFullRecord,
|
||||
selectedField,
|
||||
});
|
||||
|
||||
return {
|
||||
variableLabel,
|
||||
variablePathLabel: `${variablePathLabel} > ${variableLabel}`,
|
||||
};
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { WorkflowTextEditorVariableChip } from '@/workflow/workflow-variables/components/WorkflowTextEditorVariableChip';
|
||||
import { extractVariableLabel } from '@/workflow/workflow-variables/utils/extractVariableLabel';
|
||||
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||
import { Node } from '@tiptap/core';
|
||||
import { mergeAttributes, ReactNodeViewRenderer } from '@tiptap/react';
|
||||
|
||||
@ -38,7 +38,10 @@ export const VariableTag = Node.create({
|
||||
'data-type': 'variableTag',
|
||||
class: 'variable-tag',
|
||||
}),
|
||||
extractVariableLabel(variable),
|
||||
extractRawVariableNamePart({
|
||||
rawVariableName: variable,
|
||||
part: 'selectedField',
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user