Search variables - finish the search by step type (#14046)
- Add a specific search and type for find records step. Will be mainly useful when we start fetching list of records - Same for forms
This commit is contained in:
+41
-4
@@ -1,9 +1,16 @@
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { useWorkflowVersionIdOrThrow } from '@/workflow/hooks/useWorkflowVersionIdOrThrow';
|
||||
import { stepsOutputSchemaFamilySelector } from '@/workflow/states/selectors/stepsOutputSchemaFamilySelector';
|
||||
import type { BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2';
|
||||
import type { CodeOutputSchema } from '@/workflow/workflow-variables/types/CodeOutputSchema';
|
||||
import type { FindRecordsOutputSchema } from '@/workflow/workflow-variables/types/FindRecordsOutputSchema';
|
||||
import type { FormOutputSchema } from '@/workflow/workflow-variables/types/FormOutputSchema';
|
||||
import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
import { getOutputSchemaType } from '@/workflow/workflow-variables/utils/getOutputSchemaType';
|
||||
import { searchVariableThroughOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughOutputSchema';
|
||||
import { searchVariableThroughBaseOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema';
|
||||
import { searchVariableThroughCodeOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughCodeOutputSchema';
|
||||
import { searchVariableThroughFindRecordsOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughFindRecordsOutputSchema';
|
||||
import { searchVariableThroughFormOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughFormOutputSchema';
|
||||
import { searchVariableThroughRecordEventOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordEventOutputSchema';
|
||||
import { searchVariableThroughRecordOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordOutputSchema';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
@@ -67,9 +74,39 @@ export const useSearchVariable = ({
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: remove old search once all schema types are handled
|
||||
return searchVariableThroughOutputSchema({
|
||||
stepOutputSchema,
|
||||
if (outputSchemaType === 'FIND_RECORDS') {
|
||||
return searchVariableThroughFindRecordsOutputSchema({
|
||||
stepName: stepOutputSchema.name,
|
||||
searchRecordOutputSchema:
|
||||
stepOutputSchema.outputSchema as unknown as FindRecordsOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord,
|
||||
});
|
||||
}
|
||||
|
||||
if (outputSchemaType === 'FORM') {
|
||||
return searchVariableThroughFormOutputSchema({
|
||||
stepName: stepOutputSchema.name,
|
||||
formOutputSchema:
|
||||
stepOutputSchema.outputSchema as unknown as FormOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord,
|
||||
});
|
||||
}
|
||||
|
||||
if (outputSchemaType === 'CODE') {
|
||||
return searchVariableThroughCodeOutputSchema({
|
||||
stepName: stepOutputSchema.name,
|
||||
codeOutputSchema:
|
||||
stepOutputSchema.outputSchema as unknown as CodeOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord,
|
||||
});
|
||||
}
|
||||
|
||||
return searchVariableThroughBaseOutputSchema({
|
||||
stepName: stepOutputSchema.name,
|
||||
baseOutputSchema: stepOutputSchema.outputSchema as BaseOutputSchemaV2,
|
||||
rawVariableName,
|
||||
isFullRecord,
|
||||
});
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
type BaseLeaf = {
|
||||
isLeaf: true;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type LeafString = BaseLeaf & {
|
||||
type: 'string';
|
||||
value: string;
|
||||
};
|
||||
|
||||
type LeafNumber = BaseLeaf & {
|
||||
type: 'number';
|
||||
value: number;
|
||||
};
|
||||
|
||||
type LeafBoolean = BaseLeaf & {
|
||||
type: 'boolean';
|
||||
value: boolean;
|
||||
};
|
||||
|
||||
type LeafArray = BaseLeaf & {
|
||||
type: 'array';
|
||||
value: unknown[];
|
||||
};
|
||||
|
||||
type LeafUnknown = BaseLeaf & {
|
||||
type: 'unknown';
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
type Leaf = LeafString | LeafNumber | LeafBoolean | LeafArray | LeafUnknown;
|
||||
|
||||
type Node = {
|
||||
isLeaf: false;
|
||||
type: 'object' | 'unknown';
|
||||
label: string;
|
||||
value: BaseOutputSchemaV2;
|
||||
};
|
||||
|
||||
export type BaseOutputSchemaV2 = Record<string, Leaf | Node>;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2';
|
||||
|
||||
type Link = {
|
||||
isLeaf: true;
|
||||
tab?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export type LinkOutputSchema = {
|
||||
link: Link;
|
||||
_outputSchemaType: 'LINK';
|
||||
};
|
||||
|
||||
export type CodeOutputSchema = LinkOutputSchema | BaseOutputSchemaV2;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type RecordOutputSchemaV2 } from './RecordOutputSchemaV2';
|
||||
|
||||
type RecordNode = {
|
||||
isLeaf: false;
|
||||
label: string;
|
||||
value: RecordOutputSchemaV2;
|
||||
};
|
||||
|
||||
export type FindRecordsOutputSchema = {
|
||||
first: RecordNode;
|
||||
last: RecordNode;
|
||||
totalCount: number;
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
type FormFieldLeaf = {
|
||||
isLeaf: true;
|
||||
type: FieldMetadataType;
|
||||
label: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
type FormFieldNode = {
|
||||
isLeaf: false;
|
||||
label: string;
|
||||
value: RecordOutputSchemaV2;
|
||||
};
|
||||
|
||||
export type FormOutputSchema = Record<string, FormFieldLeaf | FormFieldNode>;
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
import type { BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2';
|
||||
import { searchVariableThroughBaseOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema';
|
||||
|
||||
describe('searchVariableThroughBaseOutputSchema', () => {
|
||||
const mockBaseSchema: BaseOutputSchemaV2 = {
|
||||
message: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'Message',
|
||||
value: 'Hello World',
|
||||
},
|
||||
count: {
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
label: 'Count',
|
||||
value: 42,
|
||||
},
|
||||
isSuccess: {
|
||||
isLeaf: true,
|
||||
type: 'boolean',
|
||||
label: 'Success Status',
|
||||
value: true,
|
||||
},
|
||||
items: {
|
||||
isLeaf: true,
|
||||
type: 'array',
|
||||
label: 'Items List',
|
||||
value: ['item1', 'item2', 'item3'],
|
||||
},
|
||||
user: {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: 'User Information',
|
||||
value: {
|
||||
name: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'Full Name',
|
||||
value: 'John Doe',
|
||||
},
|
||||
age: {
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
label: 'Age',
|
||||
value: 30,
|
||||
},
|
||||
profile: {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: 'Profile',
|
||||
value: {
|
||||
email: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'Email Address',
|
||||
value: 'john@example.com',
|
||||
},
|
||||
isActive: {
|
||||
isLeaf: true,
|
||||
type: 'boolean',
|
||||
label: 'Is Active',
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: 'Configuration',
|
||||
value: {
|
||||
timeout: {
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
label: 'Timeout (ms)',
|
||||
value: 5000,
|
||||
},
|
||||
retries: {
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
label: 'Retry Count',
|
||||
value: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should handle simple string field access correctly', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.message}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Message',
|
||||
variablePathLabel: 'HTTP Request > Message',
|
||||
variableType: 'string',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle simple number field access correctly', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.count}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Count',
|
||||
variablePathLabel: 'HTTP Request > Count',
|
||||
variableType: 'number',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle simple boolean field access correctly', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.isSuccess}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Success Status',
|
||||
variablePathLabel: 'HTTP Request > Success Status',
|
||||
variableType: 'boolean',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle array field access correctly', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.items}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Items List',
|
||||
variablePathLabel: 'HTTP Request > Items List',
|
||||
variableType: 'array',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nested object field access correctly', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.user.name}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Full Name',
|
||||
variablePathLabel: 'HTTP Request > User Information > Full Name',
|
||||
variableType: 'string',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle deeply nested object field access correctly', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.user.profile.email}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Email Address',
|
||||
variablePathLabel:
|
||||
'HTTP Request > User Information > Profile > Email Address',
|
||||
variableType: 'string',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle deeply nested boolean field access correctly', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.user.profile.isActive}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Is Active',
|
||||
variablePathLabel:
|
||||
'HTTP Request > User Information > Profile > Is Active',
|
||||
variableType: 'boolean',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle config object field access correctly', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.config.timeout}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Timeout (ms)',
|
||||
variablePathLabel: 'Code Action > Configuration > Timeout (ms)',
|
||||
variableType: 'number',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for invalid field name', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.invalidField}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for invalid nested field name', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.user.invalidField}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for deeply invalid nested field name', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.user.profile.invalidField}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when trying to access nested field on a leaf field', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.message.nestedField}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when baseOutputSchema is undefined', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: undefined as any,
|
||||
rawVariableName: '{{step1.message}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when stepId or fieldName is undefined', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle variables without curly braces', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'HTTP Request',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: 'step1.message',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Message',
|
||||
variablePathLabel: 'HTTP Request > Message',
|
||||
variableType: 'string',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle object field access without target field (should return object info)', () => {
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
baseOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.user}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'User Information',
|
||||
variablePathLabel: 'Code Action > User Information',
|
||||
variableType: 'object',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle unknown type field correctly', () => {
|
||||
const schemaWithUnknown: BaseOutputSchemaV2 = {
|
||||
unknownField: {
|
||||
isLeaf: true,
|
||||
type: 'unknown',
|
||||
label: 'Unknown Data',
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
|
||||
const result = searchVariableThroughBaseOutputSchema({
|
||||
stepName: 'AI Agent',
|
||||
baseOutputSchema: schemaWithUnknown,
|
||||
rawVariableName: '{{step1.unknownField}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Unknown Data',
|
||||
variablePathLabel: 'AI Agent > Unknown Data',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
import type { BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2';
|
||||
import type { CodeOutputSchema } from '@/workflow/workflow-variables/types/CodeOutputSchema';
|
||||
import { searchVariableThroughCodeOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughCodeOutputSchema';
|
||||
|
||||
describe('searchVariableThroughCodeOutputSchema', () => {
|
||||
describe('LinkOutputSchema tests', () => {
|
||||
const mockLinkSchema: CodeOutputSchema = {
|
||||
link: {
|
||||
isLeaf: true,
|
||||
tab: 'main',
|
||||
label: 'External Link',
|
||||
},
|
||||
_outputSchemaType: 'LINK',
|
||||
};
|
||||
|
||||
it('should return undefined', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Step',
|
||||
codeOutputSchema: mockLinkSchema,
|
||||
rawVariableName: '{{step1.link}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('BaseOutputSchemaV2 tests', () => {
|
||||
const mockBaseSchema: BaseOutputSchemaV2 = {
|
||||
message: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'Response Message',
|
||||
value: 'Success',
|
||||
},
|
||||
data: {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: 'Response Data',
|
||||
value: {
|
||||
userId: {
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
label: 'User ID',
|
||||
value: 123,
|
||||
},
|
||||
profile: {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: 'User Profile',
|
||||
value: {
|
||||
name: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'Full Name',
|
||||
value: 'John Doe',
|
||||
},
|
||||
email: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'Email Address',
|
||||
value: 'john@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
count: {
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
label: 'Item Count',
|
||||
value: 42,
|
||||
},
|
||||
isEnabled: {
|
||||
isLeaf: true,
|
||||
type: 'boolean',
|
||||
label: 'Is Enabled',
|
||||
value: true,
|
||||
},
|
||||
};
|
||||
|
||||
it('should handle simple string field access correctly', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.message}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Response Message',
|
||||
variablePathLabel: 'Code Action > Response Message',
|
||||
variableType: 'string',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle simple number field access correctly', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.count}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Item Count',
|
||||
variablePathLabel: 'Code Action > Item Count',
|
||||
variableType: 'number',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle simple boolean field access correctly', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.isEnabled}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Is Enabled',
|
||||
variablePathLabel: 'Code Action > Is Enabled',
|
||||
variableType: 'boolean',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nested object field access correctly', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.data.userId}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'User ID',
|
||||
variablePathLabel: 'Code Action > Response Data > User ID',
|
||||
variableType: 'number',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle deeply nested object field access correctly', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.data.profile.name}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Full Name',
|
||||
variablePathLabel:
|
||||
'Code Action > Response Data > User Profile > Full Name',
|
||||
variableType: 'string',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle deeply nested email field access correctly', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.data.profile.email}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Email Address',
|
||||
variablePathLabel:
|
||||
'Code Action > Response Data > User Profile > Email Address',
|
||||
variableType: 'string',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle object field access (returns object info)', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.data}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Response Data',
|
||||
variablePathLabel: 'Code Action > Response Data',
|
||||
variableType: 'object',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for invalid field name', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.invalidField}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for invalid nested field name', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{step1.data.invalidField}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
const mockBaseSchema: BaseOutputSchemaV2 = {
|
||||
simpleField: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'Simple Field',
|
||||
value: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
it('should return undefined when codeOutputSchema is undefined', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: undefined as any,
|
||||
rawVariableName: '{{step1.message}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when stepId or fieldName is undefined', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: mockBaseSchema,
|
||||
rawVariableName: '{{}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle variables without curly braces', () => {
|
||||
const result = searchVariableThroughCodeOutputSchema({
|
||||
stepName: 'Code Action',
|
||||
codeOutputSchema: mockBaseSchema,
|
||||
rawVariableName: 'step1.simpleField',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Simple Field',
|
||||
variablePathLabel: 'Code Action > Simple Field',
|
||||
variableType: 'string',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { type FindRecordsOutputSchema } from '@/workflow/workflow-variables/types/FindRecordsOutputSchema';
|
||||
import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
import { searchVariableThroughFindRecordsOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughFindRecordsOutputSchema';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
describe('searchVariableThroughFindRecordsOutputSchema', () => {
|
||||
const mockRecordSchema: RecordOutputSchemaV2 = {
|
||||
object: {
|
||||
objectMetadataId: 'company-metadata-id',
|
||||
label: 'Company',
|
||||
},
|
||||
fields: {
|
||||
name: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Company Name',
|
||||
value: 'Acme Corp',
|
||||
fieldMetadataId: 'company-name-metadata-id',
|
||||
isCompositeSubField: false,
|
||||
},
|
||||
revenue: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Revenue',
|
||||
value: 1000000,
|
||||
fieldMetadataId: 'company-revenue-metadata-id',
|
||||
isCompositeSubField: false,
|
||||
},
|
||||
},
|
||||
_outputSchemaType: 'RECORD',
|
||||
};
|
||||
|
||||
const mockSearchRecordSchema: FindRecordsOutputSchema = {
|
||||
first: {
|
||||
isLeaf: false,
|
||||
label: 'First',
|
||||
value: mockRecordSchema,
|
||||
},
|
||||
last: {
|
||||
isLeaf: false,
|
||||
label: 'Last',
|
||||
value: mockRecordSchema,
|
||||
},
|
||||
totalCount: 42,
|
||||
};
|
||||
|
||||
it('should handle totalCount variable correctly', () => {
|
||||
const result = searchVariableThroughFindRecordsOutputSchema({
|
||||
stepName: 'Find Companies',
|
||||
searchRecordOutputSchema: mockSearchRecordSchema,
|
||||
rawVariableName: '{{step1.totalCount}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Total Count',
|
||||
variablePathLabel: 'Find Companies > Total Count',
|
||||
variableType: FieldMetadataType.NUMBER,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle first record field access correctly', () => {
|
||||
const result = searchVariableThroughFindRecordsOutputSchema({
|
||||
stepName: 'Find Companies',
|
||||
searchRecordOutputSchema: mockSearchRecordSchema,
|
||||
rawVariableName: '{{step1.first.name}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Company Name',
|
||||
variablePathLabel: 'Find Companies > First > Company Name',
|
||||
variableType: FieldMetadataType.TEXT,
|
||||
fieldMetadataId: 'company-name-metadata-id',
|
||||
compositeFieldSubFieldName: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle last record field access correctly', () => {
|
||||
const result = searchVariableThroughFindRecordsOutputSchema({
|
||||
stepName: 'Find Companies',
|
||||
searchRecordOutputSchema: mockSearchRecordSchema,
|
||||
rawVariableName: '{{step1.last.revenue}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Revenue',
|
||||
variablePathLabel: 'Find Companies > Last > Revenue',
|
||||
variableType: FieldMetadataType.NUMBER,
|
||||
fieldMetadataId: 'company-revenue-metadata-id',
|
||||
compositeFieldSubFieldName: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for invalid field name', () => {
|
||||
const result = searchVariableThroughFindRecordsOutputSchema({
|
||||
stepName: 'Find Companies',
|
||||
searchRecordOutputSchema: mockSearchRecordSchema,
|
||||
rawVariableName: '{{step1.first.invalidField}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for invalid search result key', () => {
|
||||
const result = searchVariableThroughFindRecordsOutputSchema({
|
||||
stepName: 'Find Companies',
|
||||
searchRecordOutputSchema: mockSearchRecordSchema,
|
||||
rawVariableName: '{{step1.invalid.name}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when searchRecordOutputSchema is undefined', () => {
|
||||
const result = searchVariableThroughFindRecordsOutputSchema({
|
||||
stepName: 'Find Companies',
|
||||
searchRecordOutputSchema: undefined as any,
|
||||
rawVariableName: '{{step1.first.name}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when stepId or searchResultKey is undefined', () => {
|
||||
const result = searchVariableThroughFindRecordsOutputSchema({
|
||||
stepName: 'Find Companies',
|
||||
searchRecordOutputSchema: mockSearchRecordSchema,
|
||||
rawVariableName: '{{}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
import { type FormOutputSchema } from '@/workflow/workflow-variables/types/FormOutputSchema';
|
||||
import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
import { searchVariableThroughFormOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughFormOutputSchema';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
describe('searchVariableThroughFormOutputSchema', () => {
|
||||
const mockRecordSchema: RecordOutputSchemaV2 = {
|
||||
object: {
|
||||
objectMetadataId: 'person-metadata-id',
|
||||
label: 'Person',
|
||||
},
|
||||
fields: {
|
||||
firstName: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'First Name',
|
||||
value: 'John',
|
||||
fieldMetadataId: 'person-firstName-metadata-id',
|
||||
isCompositeSubField: false,
|
||||
},
|
||||
lastName: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Last Name',
|
||||
value: 'Doe',
|
||||
fieldMetadataId: 'person-lastName-metadata-id',
|
||||
isCompositeSubField: false,
|
||||
},
|
||||
email: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.EMAILS,
|
||||
label: 'Email',
|
||||
value: 'john.doe@example.com',
|
||||
fieldMetadataId: 'person-email-metadata-id',
|
||||
isCompositeSubField: false,
|
||||
},
|
||||
},
|
||||
_outputSchemaType: 'RECORD',
|
||||
};
|
||||
|
||||
const mockFormSchema: FormOutputSchema = {
|
||||
// Simple text field
|
||||
companyName: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Company Name',
|
||||
value: 'Acme Corp',
|
||||
},
|
||||
// Number field
|
||||
revenue: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Annual Revenue',
|
||||
value: 1000000,
|
||||
},
|
||||
// Email field
|
||||
contactEmail: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.EMAILS,
|
||||
label: 'Contact Email',
|
||||
value: 'contact@acme.com',
|
||||
},
|
||||
// Record field (nested record)
|
||||
contactPerson: {
|
||||
isLeaf: false,
|
||||
label: 'Contact Person',
|
||||
value: mockRecordSchema,
|
||||
},
|
||||
};
|
||||
|
||||
it('should handle simple text field access correctly', () => {
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: mockFormSchema,
|
||||
rawVariableName: '{{step1.companyName}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Company Name',
|
||||
variablePathLabel: 'Company Form > Company Name',
|
||||
variableType: FieldMetadataType.TEXT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle number field access correctly', () => {
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: mockFormSchema,
|
||||
rawVariableName: '{{step1.revenue}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Annual Revenue',
|
||||
variablePathLabel: 'Company Form > Annual Revenue',
|
||||
variableType: FieldMetadataType.NUMBER,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle email field access correctly', () => {
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: mockFormSchema,
|
||||
rawVariableName: '{{step1.contactEmail}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Contact Email',
|
||||
variablePathLabel: 'Company Form > Contact Email',
|
||||
variableType: FieldMetadataType.EMAILS,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nested record field access correctly', () => {
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: mockFormSchema,
|
||||
rawVariableName: '{{step1.contactPerson.firstName}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'First Name',
|
||||
variablePathLabel: 'Company Form > Contact Person > First Name',
|
||||
variableType: FieldMetadataType.TEXT,
|
||||
fieldMetadataId: 'person-firstName-metadata-id',
|
||||
compositeFieldSubFieldName: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nested record email field access correctly', () => {
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: mockFormSchema,
|
||||
rawVariableName: '{{step1.contactPerson.email}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Email',
|
||||
variablePathLabel: 'Company Form > Contact Person > Email',
|
||||
variableType: FieldMetadataType.EMAILS,
|
||||
fieldMetadataId: 'person-email-metadata-id',
|
||||
compositeFieldSubFieldName: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for invalid field name', () => {
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: mockFormSchema,
|
||||
rawVariableName: '{{step1.invalidField}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for invalid nested field name', () => {
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: mockFormSchema,
|
||||
rawVariableName: '{{step1.contactPerson.invalidField}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when trying to access record field without specifying property', () => {
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: mockFormSchema,
|
||||
rawVariableName: '{{step1.contactPerson}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when formOutputSchema is undefined', () => {
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: undefined as any,
|
||||
rawVariableName: '{{step1.companyName}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when stepId or fieldName is undefined', () => {
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: mockFormSchema,
|
||||
rawVariableName: '{{}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle variables without curly braces', () => {
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: mockFormSchema,
|
||||
rawVariableName: 'step1.companyName',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Company Name',
|
||||
variablePathLabel: 'Company Form > Company Name',
|
||||
variableType: FieldMetadataType.TEXT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle complex nested path correctly', () => {
|
||||
// Test with a deeper path in case we have complex nested records
|
||||
const result = searchVariableThroughFormOutputSchema({
|
||||
stepName: 'Company Form',
|
||||
formOutputSchema: mockFormSchema,
|
||||
rawVariableName: '{{step1.contactPerson.lastName}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Last Name',
|
||||
variablePathLabel: 'Company Form > Contact Person > Last Name',
|
||||
variableType: FieldMetadataType.TEXT,
|
||||
fieldMetadataId: 'person-lastName-metadata-id',
|
||||
compositeFieldSubFieldName: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
+7
-1
@@ -5,7 +5,7 @@ import {
|
||||
|
||||
export const getOutputSchemaType = (
|
||||
stepType: WorkflowActionType | WorkflowTriggerType,
|
||||
): 'RECORD' | 'DATABASE_EVENT' | 'BASE' => {
|
||||
): 'RECORD' | 'DATABASE_EVENT' | 'FIND_RECORDS' | 'FORM' | 'CODE' | 'BASE' => {
|
||||
switch (stepType) {
|
||||
case 'CREATE_RECORD':
|
||||
case 'UPDATE_RECORD':
|
||||
@@ -14,6 +14,12 @@ export const getOutputSchemaType = (
|
||||
return 'RECORD';
|
||||
case 'DATABASE_EVENT':
|
||||
return 'DATABASE_EVENT';
|
||||
case 'FIND_RECORDS':
|
||||
return 'FIND_RECORDS';
|
||||
case 'FORM':
|
||||
return 'FORM';
|
||||
case 'CODE':
|
||||
return 'CODE';
|
||||
default:
|
||||
return 'BASE';
|
||||
}
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import type { BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const parseVariableName = (rawVariableName: string) => {
|
||||
const variableWithoutBrackets = rawVariableName.replace(
|
||||
CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX,
|
||||
(_, variableName) => variableName,
|
||||
);
|
||||
|
||||
const parts = variableWithoutBrackets.split('.');
|
||||
const stepId = parts.at(0);
|
||||
|
||||
return {
|
||||
stepId,
|
||||
targetFieldName: parts.at(-1),
|
||||
pathSegments: parts.slice(1, -1),
|
||||
};
|
||||
};
|
||||
|
||||
const navigateToTargetField = (
|
||||
startingSchema: BaseOutputSchemaV2,
|
||||
pathSegments: string[],
|
||||
): { schema: BaseOutputSchemaV2; pathLabels: string[] } | null => {
|
||||
let currentSchema: BaseOutputSchemaV2 = startingSchema;
|
||||
const pathLabels: string[] = [];
|
||||
|
||||
for (const pathSegment of pathSegments) {
|
||||
const field = currentSchema[pathSegment];
|
||||
|
||||
if (!isDefined(field) || field.isLeaf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
pathLabels.push(field.label);
|
||||
currentSchema = field.value;
|
||||
}
|
||||
|
||||
return { schema: currentSchema, pathLabels };
|
||||
};
|
||||
|
||||
const buildVariableResult = (
|
||||
stepName: string,
|
||||
pathLabels: string[],
|
||||
targetSchema: BaseOutputSchemaV2,
|
||||
targetFieldName: string,
|
||||
): VariableSearchResult => {
|
||||
const targetField = targetSchema[targetFieldName];
|
||||
|
||||
if (!isDefined(targetField)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Build the full path: stepName > field1 > field2 > targetField
|
||||
const fullPathSegments = [stepName, ...pathLabels, targetField.label];
|
||||
const variablePathLabel = fullPathSegments.join(' > ');
|
||||
|
||||
return {
|
||||
variableLabel: targetField.label,
|
||||
variablePathLabel,
|
||||
variableType: targetField.type,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches for a variable within a base output schema and returns its metadata
|
||||
*
|
||||
* @param stepName - Display name of the workflow step
|
||||
* @param baseOutputSchema - The base schema to search within
|
||||
* @param rawVariableName - Variable name like "{{step1.fieldName}}" or "step1.object.nested.value"
|
||||
* @param isFullRecord - Whether to return info for the entire record vs specific field (not used for base schema)
|
||||
* @returns Variable metadata including labels, types, and field information
|
||||
*/
|
||||
export const searchVariableThroughBaseOutputSchema = ({
|
||||
stepName,
|
||||
baseOutputSchema,
|
||||
rawVariableName,
|
||||
}: {
|
||||
stepName: string;
|
||||
baseOutputSchema: BaseOutputSchemaV2;
|
||||
rawVariableName: string;
|
||||
isFullRecord?: boolean;
|
||||
}): VariableSearchResult => {
|
||||
if (!isDefined(baseOutputSchema)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const { stepId, pathSegments, targetFieldName } =
|
||||
parseVariableName(rawVariableName);
|
||||
|
||||
if (!isDefined(stepId) || !isDefined(targetFieldName)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const navigationResult = navigateToTargetField(
|
||||
baseOutputSchema,
|
||||
pathSegments,
|
||||
);
|
||||
|
||||
if (!navigationResult) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return buildVariableResult(
|
||||
stepName,
|
||||
navigationResult.pathLabels,
|
||||
navigationResult.schema,
|
||||
targetFieldName,
|
||||
);
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import type { CodeOutputSchema } from '@/workflow/workflow-variables/types/CodeOutputSchema';
|
||||
import { searchVariableThroughBaseOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const isLinkOutputSchema = (
|
||||
codeOutputSchema: CodeOutputSchema,
|
||||
): codeOutputSchema is { link: any; _outputSchemaType: 'LINK' } => {
|
||||
return (
|
||||
isDefined(codeOutputSchema) && codeOutputSchema._outputSchemaType === 'LINK'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches for a variable within a code output schema and returns its metadata
|
||||
*
|
||||
* @param stepName - Display name of the workflow step
|
||||
* @param codeOutputSchema - The code schema to search within
|
||||
* @param rawVariableName - Variable name like "{{step1.fieldName}}" or "step1.link"
|
||||
* @param isFullRecord - Whether to return info for the entire record vs specific field
|
||||
* @returns Variable metadata including labels, types, and field information
|
||||
*/
|
||||
export const searchVariableThroughCodeOutputSchema = ({
|
||||
stepName,
|
||||
codeOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord = false,
|
||||
}: {
|
||||
stepName: string;
|
||||
codeOutputSchema: CodeOutputSchema;
|
||||
rawVariableName: string;
|
||||
isFullRecord?: boolean;
|
||||
}): VariableSearchResult => {
|
||||
if (!isDefined(codeOutputSchema) || isLinkOutputSchema(codeOutputSchema)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return searchVariableThroughBaseOutputSchema({
|
||||
stepName,
|
||||
baseOutputSchema: codeOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord,
|
||||
});
|
||||
};
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import type { FindRecordsOutputSchema } from '@/workflow/workflow-variables/types/FindRecordsOutputSchema';
|
||||
import { searchRecordOutputSchema as searchRecordOutputSchemaUtil } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordOutputSchema';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type SearchResultKey = 'first' | 'last' | 'totalCount';
|
||||
|
||||
/**
|
||||
* Parses a variable name to extract its components for SearchRecord outputs
|
||||
* Example: "{{step1.first.user.name}}" -> { stepId: "step1", searchResultKey: "first", pathSegments: ["user"], fieldName: "name" }
|
||||
* Example: "{{step1.totalCount}}" -> { stepId: "step1", searchResultKey: "totalCount", pathSegments: [], fieldName: undefined }
|
||||
*/
|
||||
const parseVariableName = (rawVariableName: string) => {
|
||||
const variableWithoutBrackets = rawVariableName.replace(
|
||||
CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX,
|
||||
(_, variableName) => variableName,
|
||||
);
|
||||
|
||||
const parts = variableWithoutBrackets.split('.');
|
||||
const stepId = parts.at(0);
|
||||
const searchResultKey = parts.at(1) as SearchResultKey;
|
||||
const remainingParts = parts.slice(2);
|
||||
|
||||
return {
|
||||
stepId,
|
||||
searchResultKey,
|
||||
fieldName: remainingParts.at(-1),
|
||||
pathSegments: remainingParts.slice(0, -1),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches for a variable within a search record output schema and returns its metadata
|
||||
*
|
||||
* @param stepName - Display name of the workflow step
|
||||
* @param searchRecordOutputSchema - The search record schema to search within
|
||||
* @param rawVariableName - Variable name like "{{step1.first.user.name}}" or "step1.totalCount"
|
||||
* @param isFullRecord - Whether to return info for the entire record vs specific field
|
||||
* @returns Variable metadata including labels, types, and field information
|
||||
*/
|
||||
export const searchVariableThroughFindRecordsOutputSchema = ({
|
||||
stepName,
|
||||
searchRecordOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord = false,
|
||||
}: {
|
||||
stepName: string;
|
||||
searchRecordOutputSchema: FindRecordsOutputSchema;
|
||||
rawVariableName: string;
|
||||
isFullRecord?: boolean;
|
||||
}): VariableSearchResult => {
|
||||
if (!isDefined(searchRecordOutputSchema)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const { stepId, searchResultKey, fieldName, pathSegments } =
|
||||
parseVariableName(rawVariableName);
|
||||
|
||||
if (!isDefined(stepId) || !isDefined(searchResultKey)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (searchResultKey === 'totalCount') {
|
||||
return {
|
||||
variableLabel: 'Total Count',
|
||||
variablePathLabel: `${stepName} > Total Count`,
|
||||
variableType: FieldMetadataType.NUMBER,
|
||||
};
|
||||
}
|
||||
|
||||
if (searchResultKey === 'first' || searchResultKey === 'last') {
|
||||
const recordSchema = searchRecordOutputSchema[searchResultKey]?.value;
|
||||
|
||||
if (!isDefined(recordSchema) || !isDefined(fieldName)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return searchRecordOutputSchemaUtil({
|
||||
stepName: `${stepName} > ${searchResultKey === 'first' ? 'First' : 'Last'}`,
|
||||
recordOutputSchema: recordSchema,
|
||||
selectedField: fieldName,
|
||||
path: pathSegments,
|
||||
isFullRecord,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
};
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import type { FormOutputSchema } from '@/workflow/workflow-variables/types/FormOutputSchema';
|
||||
import { searchRecordOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordOutputSchema';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
/**
|
||||
* Parses a variable name to extract its components for Form outputs
|
||||
* Example: "{{step1.fieldName}}" -> { stepId: "step1", fieldName: "fieldName", pathSegments: [] }
|
||||
* Example: "{{step1.recordField.user.name}}" -> { stepId: "step1", fieldName: "recordField", pathSegments: ["user"], recordFieldName: "name" }
|
||||
*/
|
||||
const parseVariableName = (rawVariableName: string) => {
|
||||
const variableWithoutBrackets = rawVariableName.replace(
|
||||
CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX,
|
||||
(_, variableName) => variableName,
|
||||
);
|
||||
|
||||
const parts = variableWithoutBrackets.split('.');
|
||||
const stepId = parts.at(0);
|
||||
const fieldName = parts.at(1);
|
||||
const remainingParts = parts.slice(2);
|
||||
|
||||
return {
|
||||
stepId,
|
||||
fieldName,
|
||||
pathSegments: remainingParts.slice(0, -1),
|
||||
recordFieldName: remainingParts.at(-1),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches for a variable within a form output schema and returns its metadata
|
||||
*
|
||||
* @param stepName - Display name of the workflow step
|
||||
* @param formOutputSchema - The form schema to search within
|
||||
* @param rawVariableName - Variable name like "{{step1.fieldName}}" or "step1.recordField.user.name"
|
||||
* @param isFullRecord - Whether to return info for the entire record vs specific field
|
||||
* @returns Variable metadata including labels, types, and field information
|
||||
*/
|
||||
export const searchVariableThroughFormOutputSchema = ({
|
||||
stepName,
|
||||
formOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord = false,
|
||||
}: {
|
||||
stepName: string;
|
||||
formOutputSchema: FormOutputSchema;
|
||||
rawVariableName: string;
|
||||
isFullRecord?: boolean;
|
||||
}): VariableSearchResult => {
|
||||
if (!isDefined(formOutputSchema)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const { stepId, fieldName, pathSegments, recordFieldName } =
|
||||
parseVariableName(rawVariableName);
|
||||
|
||||
if (!isDefined(stepId) || !isDefined(fieldName)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const formField = formOutputSchema[fieldName];
|
||||
|
||||
if (!isDefined(formField)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (formField.isLeaf) {
|
||||
return {
|
||||
variableLabel: formField.label,
|
||||
variablePathLabel: `${stepName} > ${formField.label}`,
|
||||
variableType: formField.type,
|
||||
};
|
||||
}
|
||||
|
||||
if (!formField.isLeaf && isDefined(recordFieldName)) {
|
||||
return searchRecordOutputSchema({
|
||||
stepName: `${stepName} > ${formField.label}`,
|
||||
recordOutputSchema: formField.value,
|
||||
selectedField: recordFieldName,
|
||||
path: pathSegments,
|
||||
isFullRecord,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user