8726 workflow add a test button in workflow code step (#9016)
- add test button to workflow code step - add test tab to workflow code step https://github.com/user-attachments/assets/e180a827-7321-49a2-8026-88490c557da2  
This commit is contained in:
@ -1,11 +1,12 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||
import { OBJECT_EVENT_TRIGGERS } from '@/workflow/constants/ObjectEventTriggers';
|
||||
import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
|
||||
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { IconPlaylistAdd, isDefined } from 'twenty-ui';
|
||||
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||
|
||||
type WorkflowEditTriggerDatabaseEventFormProps = {
|
||||
trigger: WorkflowDatabaseEventTrigger;
|
||||
@ -60,88 +61,91 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
|
||||
: '-';
|
||||
|
||||
return (
|
||||
<WorkflowEditGenericFormBase
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconPlaylistAdd}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
>
|
||||
<Select
|
||||
dropdownId="workflow-edit-trigger-record-type"
|
||||
label="Record Type"
|
||||
fullWidth
|
||||
disabled={triggerOptions.readonly}
|
||||
value={triggerEvent?.objectType}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedRecordType) => {
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate(
|
||||
isDefined(trigger) && isDefined(triggerEvent)
|
||||
? {
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
eventName: `${updatedRecordType}.${triggerEvent.event}`,
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: headerTitle,
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`,
|
||||
outputSchema: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconPlaylistAdd}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
/>
|
||||
<Select
|
||||
dropdownId="workflow-edit-trigger-event-type"
|
||||
label="Event type"
|
||||
fullWidth
|
||||
value={triggerEvent?.event}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={OBJECT_EVENT_TRIGGERS}
|
||||
disabled={triggerOptions.readonly}
|
||||
onChange={(updatedEvent) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-trigger-record-type"
|
||||
label="Record Type"
|
||||
fullWidth
|
||||
disabled={triggerOptions.readonly}
|
||||
value={triggerEvent?.objectType}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedRecordType) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate(
|
||||
isDefined(trigger) && isDefined(triggerEvent)
|
||||
? {
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
eventName: `${triggerEvent.objectType}.${updatedEvent}`,
|
||||
triggerOptions.onTriggerUpdate(
|
||||
isDefined(trigger) && isDefined(triggerEvent)
|
||||
? {
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
eventName: `${updatedRecordType}.${triggerEvent.event}`,
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: headerTitle,
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`,
|
||||
outputSchema: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: headerTitle,
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: `${availableMetadata[0].value}.${updatedEvent}`,
|
||||
outputSchema: {},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
dropdownId="workflow-edit-trigger-event-type"
|
||||
label="Event type"
|
||||
fullWidth
|
||||
value={triggerEvent?.event}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={OBJECT_EVENT_TRIGGERS}
|
||||
disabled={triggerOptions.readonly}
|
||||
onChange={(updatedEvent) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate(
|
||||
isDefined(trigger) && isDefined(triggerEvent)
|
||||
? {
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
eventName: `${triggerEvent.objectType}.${updatedEvent}`,
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: headerTitle,
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: `${availableMetadata?.[0].value}.${updatedEvent}`,
|
||||
outputSchema: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WorkflowEditGenericFormBase>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||
import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/constants/ManualTriggerAvailabilityOptions';
|
||||
import {
|
||||
WorkflowManualTrigger,
|
||||
@ -9,6 +9,7 @@ import {
|
||||
import { getManualTriggerDefaultSettings } from '@/workflow/utils/getManualTriggerDefaultSettings';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { IconHandMove, isDefined, useIcons } from 'twenty-ui';
|
||||
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||
|
||||
type WorkflowEditTriggerManualFormProps = {
|
||||
trigger: WorkflowManualTrigger;
|
||||
@ -47,67 +48,70 @@ export const WorkflowEditTriggerManualForm = ({
|
||||
const headerTitle = isDefined(trigger.name) ? trigger.name : 'Manual Trigger';
|
||||
|
||||
return (
|
||||
<WorkflowEditGenericFormBase
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconHandMove}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Trigger · Manual"
|
||||
>
|
||||
<Select
|
||||
dropdownId="workflow-edit-manual-trigger-availability"
|
||||
label="Available"
|
||||
fullWidth
|
||||
disabled={triggerOptions.readonly}
|
||||
value={manualTriggerAvailability}
|
||||
options={MANUAL_TRIGGER_AVAILABILITY_OPTIONS}
|
||||
onChange={(updatedTriggerType) => {
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: getManualTriggerDefaultSettings({
|
||||
availability: updatedTriggerType,
|
||||
activeObjectMetadataItems,
|
||||
}),
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconHandMove}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Trigger · Manual"
|
||||
/>
|
||||
|
||||
{manualTriggerAvailability === 'WHEN_RECORD_SELECTED' ? (
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-manual-trigger-object"
|
||||
label="Object"
|
||||
dropdownId="workflow-edit-manual-trigger-availability"
|
||||
label="Available"
|
||||
fullWidth
|
||||
value={trigger.settings.objectType}
|
||||
options={availableMetadata}
|
||||
disabled={triggerOptions.readonly}
|
||||
onChange={(updatedObject) => {
|
||||
value={manualTriggerAvailability}
|
||||
options={MANUAL_TRIGGER_AVAILABILITY_OPTIONS}
|
||||
onChange={(updatedTriggerType) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: {
|
||||
objectType: updatedObject,
|
||||
outputSchema: {},
|
||||
},
|
||||
settings: getManualTriggerDefaultSettings({
|
||||
availability: updatedTriggerType,
|
||||
activeObjectMetadataItems,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</WorkflowEditGenericFormBase>
|
||||
|
||||
{manualTriggerAvailability === 'WHEN_RECORD_SELECTED' ? (
|
||||
<Select
|
||||
dropdownId="workflow-edit-manual-trigger-object"
|
||||
label="Object"
|
||||
fullWidth
|
||||
value={trigger.settings.objectType}
|
||||
options={availableMetadata}
|
||||
disabled={triggerOptions.readonly}
|
||||
onChange={(updatedObject) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: {
|
||||
objectType: updatedObject,
|
||||
outputSchema: {},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</WorkflowStepBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,8 +6,8 @@ import {
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer';
|
||||
import { StyledFormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputRowContainer';
|
||||
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
||||
import { SingleRecordSelect } from '@/object-record/relation-picker/components/SingleRecordSelect';
|
||||
import { useRecordPicker } from '@/object-record/relation-picker/hooks/useRecordPicker';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
@ -137,9 +137,9 @@ export const WorkflowSingleRecordPicker = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledFormFieldInputContainer>
|
||||
<FormFieldInputContainer>
|
||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||
<StyledFormFieldInputRowContainer>
|
||||
<FormFieldInputRowContainer>
|
||||
<StyledFormSelectContainer>
|
||||
<WorkflowSingleRecordFieldChip
|
||||
draftValue={draftValue}
|
||||
@ -193,7 +193,7 @@ export const WorkflowSingleRecordPicker = ({
|
||||
objectNameSingularToSelect={objectNameSingular}
|
||||
/>
|
||||
</StyledSearchVariablesDropdownContainer>
|
||||
</StyledFormFieldInputRowContainer>
|
||||
</StyledFormFieldInputContainer>
|
||||
</FormFieldInputRowContainer>
|
||||
</FormFieldInputContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledWorkflowStepBody = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
padding: ${({ theme }) => theme.spacing(6)};
|
||||
row-gap: ${({ theme }) => theme.spacing(6)};
|
||||
flex: 1 1 auto;
|
||||
`;
|
||||
|
||||
export { StyledWorkflowStepBody as WorkflowStepBody };
|
||||
@ -10,8 +10,8 @@ import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrTh
|
||||
import { WorkflowEditActionFormCreateRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormCreateRecord';
|
||||
import { WorkflowEditActionFormDeleteRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormDeleteRecord';
|
||||
import { WorkflowEditActionFormSendEmail } from '@/workflow/workflow-actions/components/WorkflowEditActionFormSendEmail';
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { WorkflowEditActionFormUpdateRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormUpdateRecord';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
|
||||
|
||||
|
||||
@ -43,27 +43,18 @@ const StyledHeaderIconContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(6)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const WorkflowEditGenericFormBase = ({
|
||||
export const WorkflowStepHeader = ({
|
||||
onTitleChange,
|
||||
Icon,
|
||||
iconColor,
|
||||
initialTitle,
|
||||
headerType,
|
||||
children,
|
||||
}: {
|
||||
onTitleChange: (newTitle: string) => void;
|
||||
Icon: IconComponent;
|
||||
iconColor: string;
|
||||
initialTitle: string;
|
||||
headerType: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [title, setTitle] = useState(initialTitle);
|
||||
@ -74,33 +65,30 @@ export const WorkflowEditGenericFormBase = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledHeader>
|
||||
<StyledHeaderIconContainer>
|
||||
{
|
||||
<Icon
|
||||
color={iconColor}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
size={theme.icon.size.lg}
|
||||
/>
|
||||
}
|
||||
</StyledHeaderIconContainer>
|
||||
<StyledHeaderInfo>
|
||||
<StyledHeaderTitle>
|
||||
<TextInput
|
||||
value={title}
|
||||
copyButton={false}
|
||||
hotkeyScope="workflow-step-title"
|
||||
onEnter={onTitleChange}
|
||||
onEscape={onTitleChange}
|
||||
onChange={handleChange}
|
||||
shouldTrim={false}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
<StyledHeaderType>{headerType}</StyledHeaderType>
|
||||
</StyledHeaderInfo>
|
||||
</StyledHeader>
|
||||
<StyledContentContainer>{children}</StyledContentContainer>
|
||||
</>
|
||||
<StyledHeader>
|
||||
<StyledHeaderIconContainer>
|
||||
{
|
||||
<Icon
|
||||
color={iconColor}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
size={theme.icon.size.lg}
|
||||
/>
|
||||
}
|
||||
</StyledHeaderIconContainer>
|
||||
<StyledHeaderInfo>
|
||||
<StyledHeaderTitle>
|
||||
<TextInput
|
||||
value={title}
|
||||
copyButton={false}
|
||||
hotkeyScope="workflow-step-title"
|
||||
onEnter={onTitleChange}
|
||||
onEscape={onTitleChange}
|
||||
onChange={handleChange}
|
||||
shouldTrim={false}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
<StyledHeaderType>{headerType}</StyledHeaderType>
|
||||
</StyledHeaderInfo>
|
||||
</StyledHeader>
|
||||
);
|
||||
};
|
||||
@ -14,10 +14,7 @@ export const useUpdateStep = ({
|
||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||
const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
|
||||
|
||||
const updateStep = async <T extends WorkflowStep>(
|
||||
updatedStep: T,
|
||||
shouldUpdateStepOutput = true,
|
||||
) => {
|
||||
const updateStep = async <T extends WorkflowStep>(updatedStep: T) => {
|
||||
if (!isDefined(workflow.currentVersion)) {
|
||||
throw new Error('Can not update an undefined workflow version.');
|
||||
}
|
||||
@ -26,7 +23,6 @@ export const useUpdateStep = ({
|
||||
await updateWorkflowVersionStep({
|
||||
workflowVersionId: workflowVersion.id,
|
||||
step: updatedStep,
|
||||
shouldUpdateStepOutput,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||
|
||||
export type ServerlessFunctionTestData = {
|
||||
input: { [field: string]: any };
|
||||
output: {
|
||||
data?: string;
|
||||
duration?: number;
|
||||
status?: ServerlessFunctionExecutionStatus;
|
||||
error?: string;
|
||||
};
|
||||
language: 'plaintext' | 'json';
|
||||
height: number;
|
||||
};
|
||||
|
||||
export const DEFAULT_OUTPUT_VALUE =
|
||||
'Enter an input above then press "run Function"';
|
||||
|
||||
export const serverlessFunctionTestDataFamilyState = createFamilyState<
|
||||
ServerlessFunctionTestData,
|
||||
string
|
||||
>({
|
||||
key: 'serverlessFunctionTestDataFamilyState',
|
||||
defaultValue: {
|
||||
language: 'plaintext',
|
||||
height: 64,
|
||||
input: {},
|
||||
output: { data: DEFAULT_OUTPUT_VALUE },
|
||||
},
|
||||
});
|
||||
@ -13,9 +13,11 @@ export type InputSchemaProperty = {
|
||||
type: InputSchemaPropertyType;
|
||||
enum?: string[];
|
||||
items?: InputSchemaProperty;
|
||||
properties?: InputSchema;
|
||||
properties?: Properties;
|
||||
};
|
||||
|
||||
export type InputSchema = {
|
||||
type Properties = {
|
||||
[name: string]: InputSchemaProperty;
|
||||
};
|
||||
|
||||
export type InputSchema = InputSchemaProperty[];
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
import { getDefaultFunctionInputFromInputSchema } from '@/workflow/utils/getDefaultFunctionInputFromInputSchema';
|
||||
import { InputSchema } from '@/workflow/types/InputSchema';
|
||||
|
||||
describe('getDefaultFunctionInputFromInputSchema', () => {
|
||||
it('should init function input properly', () => {
|
||||
const inputSchema = {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as InputSchema;
|
||||
const expectedResult = {
|
||||
params: {
|
||||
a: null,
|
||||
b: null,
|
||||
},
|
||||
};
|
||||
expect(getDefaultFunctionInputFromInputSchema(inputSchema)).toEqual(
|
||||
expectedResult,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,22 +0,0 @@
|
||||
import { InputSchema } from '@/workflow/types/InputSchema';
|
||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const getDefaultFunctionInputFromInputSchema = (
|
||||
inputSchema: InputSchema | undefined,
|
||||
): FunctionInput => {
|
||||
return isDefined(inputSchema)
|
||||
? Object.entries(inputSchema).reduce((acc, [key, value]) => {
|
||||
if (['string', 'number', 'boolean'].includes(value.type)) {
|
||||
acc[key] = null;
|
||||
} else if (value.type === 'object') {
|
||||
acc[key] = isDefined(value.properties)
|
||||
? getDefaultFunctionInputFromInputSchema(value.properties)
|
||||
: {};
|
||||
} else if (value.type === 'array' && isDefined(value.items)) {
|
||||
acc[key] = [];
|
||||
}
|
||||
return acc;
|
||||
}, {} as FunctionInput)
|
||||
: {};
|
||||
};
|
||||
@ -1,129 +0,0 @@
|
||||
import {
|
||||
ArrayTypeNode,
|
||||
ArrowFunction,
|
||||
createSourceFile,
|
||||
FunctionDeclaration,
|
||||
LiteralTypeNode,
|
||||
PropertySignature,
|
||||
ScriptTarget,
|
||||
StringLiteral,
|
||||
SyntaxKind,
|
||||
TypeNode,
|
||||
UnionTypeNode,
|
||||
VariableStatement,
|
||||
} from 'typescript';
|
||||
import { InputSchema, InputSchemaProperty } from '@/workflow/types/InputSchema';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const getTypeString = (typeNode: TypeNode): InputSchemaProperty => {
|
||||
switch (typeNode.kind) {
|
||||
case SyntaxKind.NumberKeyword:
|
||||
return { type: 'number' };
|
||||
case SyntaxKind.StringKeyword:
|
||||
return { type: 'string' };
|
||||
case SyntaxKind.BooleanKeyword:
|
||||
return { type: 'boolean' };
|
||||
case SyntaxKind.ArrayType:
|
||||
return {
|
||||
type: 'array',
|
||||
items: getTypeString((typeNode as ArrayTypeNode).elementType),
|
||||
};
|
||||
case SyntaxKind.ObjectKeyword:
|
||||
return { type: 'object' };
|
||||
case SyntaxKind.TypeLiteral: {
|
||||
const properties: InputSchema = {};
|
||||
|
||||
(typeNode as any).members.forEach((member: PropertySignature) => {
|
||||
if (isDefined(member.name) && isDefined(member.type)) {
|
||||
const memberName = (member.name as any).text;
|
||||
|
||||
properties[memberName] = getTypeString(member.type);
|
||||
}
|
||||
});
|
||||
|
||||
return { type: 'object', properties };
|
||||
}
|
||||
case SyntaxKind.UnionType: {
|
||||
const unionNode = typeNode as UnionTypeNode;
|
||||
const enumValues: string[] = [];
|
||||
|
||||
let isEnum = true;
|
||||
|
||||
unionNode.types.forEach((subType) => {
|
||||
if (subType.kind === SyntaxKind.LiteralType) {
|
||||
const literal = (subType as LiteralTypeNode).literal;
|
||||
|
||||
if (literal.kind === SyntaxKind.StringLiteral) {
|
||||
enumValues.push((literal as StringLiteral).text);
|
||||
} else {
|
||||
isEnum = false;
|
||||
}
|
||||
} else {
|
||||
isEnum = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (isEnum) {
|
||||
return { type: 'string', enum: enumValues };
|
||||
}
|
||||
|
||||
return { type: 'unknown' };
|
||||
}
|
||||
default:
|
||||
return { type: 'unknown' };
|
||||
}
|
||||
};
|
||||
|
||||
export const getFunctionInputSchema = (fileContent: string): InputSchema => {
|
||||
const sourceFile = createSourceFile(
|
||||
'temp.ts',
|
||||
fileContent,
|
||||
ScriptTarget.ESNext,
|
||||
true,
|
||||
);
|
||||
|
||||
const schema: InputSchema = {};
|
||||
|
||||
sourceFile.forEachChild((node) => {
|
||||
if (node.kind === SyntaxKind.FunctionDeclaration) {
|
||||
const funcNode = node as FunctionDeclaration;
|
||||
const params = funcNode.parameters;
|
||||
|
||||
params.forEach((param) => {
|
||||
const paramName = param.name.getText();
|
||||
const typeNode = param.type;
|
||||
|
||||
if (isDefined(typeNode)) {
|
||||
schema[paramName] = getTypeString(typeNode);
|
||||
} else {
|
||||
schema[paramName] = { type: 'unknown' };
|
||||
}
|
||||
});
|
||||
} else if (node.kind === SyntaxKind.VariableStatement) {
|
||||
const varStatement = node as VariableStatement;
|
||||
|
||||
varStatement.declarationList.declarations.forEach((declaration) => {
|
||||
if (
|
||||
isDefined(declaration.initializer) &&
|
||||
declaration.initializer.kind === SyntaxKind.ArrowFunction
|
||||
) {
|
||||
const arrowFunction = declaration.initializer as ArrowFunction;
|
||||
const params = arrowFunction.parameters;
|
||||
|
||||
params.forEach((param: any) => {
|
||||
const paramName = param.name.text;
|
||||
const typeNode = param.type;
|
||||
|
||||
if (isDefined(typeNode)) {
|
||||
schema[paramName] = getTypeString(typeNode);
|
||||
} else {
|
||||
schema[paramName] = { type: 'unknown' };
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return schema;
|
||||
};
|
||||
@ -3,7 +3,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
||||
import { WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
@ -17,6 +17,7 @@ import {
|
||||
import { JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||
|
||||
type WorkflowEditActionFormCreateRecordProps = {
|
||||
action: WorkflowCreateRecordAction;
|
||||
@ -136,58 +137,61 @@ export const WorkflowEditActionFormCreateRecord = ({
|
||||
const headerTitle = isDefined(action.name) ? action.name : `Create Record`;
|
||||
|
||||
return (
|
||||
<WorkflowEditGenericFormBase
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconAddressBook}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Action"
|
||||
>
|
||||
<Select
|
||||
dropdownId="workflow-edit-action-record-create-object-name"
|
||||
label="Object"
|
||||
fullWidth
|
||||
disabled={isFormDisabled}
|
||||
value={formData.objectName}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedObjectName) => {
|
||||
const newFormData: CreateRecordFormData = {
|
||||
objectName: updatedObjectName,
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconAddressBook}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Action"
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-action-record-create-object-name"
|
||||
label="Object"
|
||||
fullWidth
|
||||
disabled={isFormDisabled}
|
||||
value={formData.objectName}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedObjectName) => {
|
||||
const newFormData: CreateRecordFormData = {
|
||||
objectName: updatedObjectName,
|
||||
};
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
setFormData(newFormData);
|
||||
|
||||
{inlineFieldDefinitions.map((field) => {
|
||||
const currentValue = formData[field.metadata.fieldName] as JsonValue;
|
||||
saveAction(newFormData);
|
||||
}}
|
||||
/>
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
key={field.metadata.fieldName}
|
||||
defaultValue={currentValue}
|
||||
field={field}
|
||||
onPersist={(value) => {
|
||||
handleFieldChange(field.metadata.fieldName, value);
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</WorkflowEditGenericFormBase>
|
||||
<HorizontalSeparator noMargin />
|
||||
|
||||
{inlineFieldDefinitions.map((field) => {
|
||||
const currentValue = formData[field.metadata.fieldName] as JsonValue;
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
key={field.metadata.fieldName}
|
||||
defaultValue={currentValue}
|
||||
field={field}
|
||||
onPersist={(value) => {
|
||||
handleFieldChange(field.metadata.fieldName, value);
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</WorkflowStepBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||
import { WorkflowSingleRecordPicker } from '@/workflow/components/WorkflowSingleRecordPicker';
|
||||
import { WorkflowDeleteRecordAction } from '@/workflow/types/Workflow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
@ -14,6 +14,7 @@ import {
|
||||
|
||||
import { JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||
|
||||
type WorkflowEditActionFormDeleteRecordProps = {
|
||||
action: WorkflowDeleteRecordAction;
|
||||
@ -118,52 +119,55 @@ export const WorkflowEditActionFormDeleteRecord = ({
|
||||
const headerTitle = isDefined(action.name) ? action.name : `Delete Record`;
|
||||
|
||||
return (
|
||||
<WorkflowEditGenericFormBase
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconAddressBook}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Action"
|
||||
>
|
||||
<Select
|
||||
dropdownId="workflow-edit-action-record-delete-object-name"
|
||||
label="Object"
|
||||
fullWidth
|
||||
disabled={isFormDisabled}
|
||||
value={formData.objectName}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(objectName) => {
|
||||
const newFormData: DeleteRecordFormData = {
|
||||
objectName,
|
||||
objectRecordId: '',
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconAddressBook}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Action"
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-action-record-delete-object-name"
|
||||
label="Object"
|
||||
fullWidth
|
||||
disabled={isFormDisabled}
|
||||
value={formData.objectName}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(objectName) => {
|
||||
const newFormData: DeleteRecordFormData = {
|
||||
objectName,
|
||||
objectRecordId: '',
|
||||
};
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
setFormData(newFormData);
|
||||
|
||||
<WorkflowSingleRecordPicker
|
||||
label="Record"
|
||||
onChange={(objectRecordId) =>
|
||||
handleFieldChange('objectRecordId', objectRecordId)
|
||||
}
|
||||
objectNameSingular={formData.objectName}
|
||||
defaultValue={formData.objectRecordId}
|
||||
/>
|
||||
</WorkflowEditGenericFormBase>
|
||||
saveAction(newFormData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
|
||||
<WorkflowSingleRecordPicker
|
||||
label="Record"
|
||||
onChange={(objectRecordId) =>
|
||||
handleFieldChange('objectRecordId', objectRecordId)
|
||||
}
|
||||
objectNameSingular={formData.objectName}
|
||||
defaultValue={formData.objectRecordId}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,7 +5,7 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { WorkflowSendEmailAction } from '@/workflow/types/Workflow';
|
||||
@ -15,6 +15,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconMail, IconPlus, isDefined } from 'twenty-ui';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||
|
||||
type WorkflowEditActionFormSendEmailProps = {
|
||||
action: WorkflowSendEmailAction;
|
||||
@ -171,99 +172,104 @@ export const WorkflowEditActionFormSendEmail = ({
|
||||
|
||||
return (
|
||||
!loading && (
|
||||
<WorkflowEditGenericFormBase
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconMail}
|
||||
iconColor={theme.color.blue}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Email"
|
||||
>
|
||||
<Controller
|
||||
name="connectedAccountId"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
dropdownId="select-connected-account-id"
|
||||
label="Account"
|
||||
fullWidth
|
||||
emptyOption={emptyOption}
|
||||
value={field.value}
|
||||
options={connectedAccountOptions}
|
||||
callToActionButton={{
|
||||
onClick: () =>
|
||||
triggerApisOAuth('google', { redirectLocation: redirectUrl }),
|
||||
Icon: IconPlus,
|
||||
text: 'Add account',
|
||||
}}
|
||||
onChange={(connectedAccountId) => {
|
||||
field.onChange(connectedAccountId);
|
||||
handleSave(true);
|
||||
}}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
)}
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconMail}
|
||||
iconColor={theme.color.blue}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Email"
|
||||
/>
|
||||
<Controller
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormTextFieldInput
|
||||
label="Email"
|
||||
placeholder="Enter receiver email"
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={field.value}
|
||||
onPersist={(value) => {
|
||||
field.onChange(value);
|
||||
handleSave();
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="subject"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormTextFieldInput
|
||||
label="Subject"
|
||||
placeholder="Enter email subject"
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={field.value}
|
||||
onPersist={(value) => {
|
||||
field.onChange(value);
|
||||
handleSave();
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="body"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormTextFieldInput
|
||||
label="Body"
|
||||
placeholder="Enter email body"
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={field.value}
|
||||
onPersist={(value) => {
|
||||
field.onChange(value);
|
||||
handleSave();
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</WorkflowEditGenericFormBase>
|
||||
<WorkflowStepBody>
|
||||
<Controller
|
||||
name="connectedAccountId"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
dropdownId="select-connected-account-id"
|
||||
label="Account"
|
||||
fullWidth
|
||||
emptyOption={emptyOption}
|
||||
value={field.value}
|
||||
options={connectedAccountOptions}
|
||||
callToActionButton={{
|
||||
onClick: () =>
|
||||
triggerApisOAuth('google', {
|
||||
redirectLocation: redirectUrl,
|
||||
}),
|
||||
Icon: IconPlus,
|
||||
text: 'Add account',
|
||||
}}
|
||||
onChange={(connectedAccountId) => {
|
||||
field.onChange(connectedAccountId);
|
||||
handleSave(true);
|
||||
}}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormTextFieldInput
|
||||
label="Email"
|
||||
placeholder="Enter receiver email"
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={field.value}
|
||||
onPersist={(value) => {
|
||||
field.onChange(value);
|
||||
handleSave();
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="subject"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormTextFieldInput
|
||||
label="Subject"
|
||||
placeholder="Enter email subject"
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={field.value}
|
||||
onPersist={(value) => {
|
||||
field.onChange(value);
|
||||
handleSave();
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="body"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormTextFieldInput
|
||||
label="Body"
|
||||
placeholder="Enter email body"
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={field.value}
|
||||
onPersist={(value) => {
|
||||
field.onChange(value);
|
||||
handleSave();
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,48 +1,53 @@
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||
import { StyledFormCompositeFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer';
|
||||
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
||||
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
|
||||
import { getDefaultFunctionInputFromInputSchema } from '@/workflow/utils/getDefaultFunctionInputFromInputSchema';
|
||||
import { getFunctionInputSchema } from '@/workflow/utils/getFunctionInputSchema';
|
||||
import { setNestedValue } from '@/workflow/utils/setNestedValue';
|
||||
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-actions/utils/mergeDefaultFunctionInputAndFunctionInput';
|
||||
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Monaco } from '@monaco-editor/react';
|
||||
import { editor } from 'monaco-editor';
|
||||
import { AutoTypings } from 'monaco-editor-auto-typings';
|
||||
import { Fragment, ReactNode, useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
CodeEditor,
|
||||
HorizontalSeparator,
|
||||
IconCode,
|
||||
isDefined,
|
||||
} from 'twenty-ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { CodeEditor, IconCode, isDefined, IconPlayerPlay } from 'twenty-ui';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback';
|
||||
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
||||
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||
import { ServerlessFunctionExecutionResult } from '@/serverless-functions/components/ServerlessFunctionExecutionResult';
|
||||
import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
|
||||
import { useTestServerlessFunction } from '@/serverless-functions/hooks/useTestServerlessFunction';
|
||||
import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctionOutputSchema';
|
||||
import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode';
|
||||
import { mergeDefaultFunctionInputAndFunctionInput } from '@/serverless-functions/utils/mergeDefaultFunctionInputAndFunctionInput';
|
||||
import { WorkflowEditActionFormServerlessFunctionFields } from '@/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunctionFields';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledCodeEditorContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
const StyledTabList = styled(TabList)`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type WorkflowEditActionFormServerlessFunctionProps = {
|
||||
@ -53,10 +58,7 @@ type WorkflowEditActionFormServerlessFunctionProps = {
|
||||
}
|
||||
| {
|
||||
readonly?: false;
|
||||
onActionUpdate: (
|
||||
action: WorkflowCodeAction,
|
||||
shouldUpdateStepOutput?: boolean,
|
||||
) => void;
|
||||
onActionUpdate: (action: WorkflowCodeAction) => void;
|
||||
};
|
||||
};
|
||||
|
||||
@ -64,14 +66,14 @@ type ServerlessFunctionInputFormData = {
|
||||
[field: string]: string | ServerlessFunctionInputFormData;
|
||||
};
|
||||
|
||||
const INDEX_FILE_PATH = 'src/index.ts';
|
||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-code-step';
|
||||
|
||||
export const WorkflowEditActionFormServerlessFunction = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionFormServerlessFunctionProps) => {
|
||||
const theme = useTheme();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { activeTabId, setActiveTabId } = useTabList(TAB_LIST_COMPONENT_ID);
|
||||
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction();
|
||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
|
||||
@ -81,6 +83,9 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
||||
id: serverlessFunctionId,
|
||||
});
|
||||
|
||||
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
|
||||
useRecoilState(serverlessFunctionTestDataFamilyState(serverlessFunctionId));
|
||||
|
||||
const [functionInput, setFunctionInput] =
|
||||
useState<ServerlessFunctionInputFormData>(
|
||||
action.settings.input.serverlessFunctionInput,
|
||||
@ -89,83 +94,80 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
||||
const { formValues, setFormValues, loading } =
|
||||
useServerlessFunctionUpdateFormState(serverlessFunctionId);
|
||||
|
||||
const headerTitle = action.name || 'Code - Serverless Function';
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
await updateOneServerlessFunction({
|
||||
id: serverlessFunctionId,
|
||||
name: formValues.name,
|
||||
description: formValues.description,
|
||||
code: formValues.code,
|
||||
});
|
||||
} catch (err) {
|
||||
enqueueSnackBar(
|
||||
(err as Error)?.message || 'An error occurred while updating function',
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
const updateOutputSchemaFromTestResult = async (testResult: object) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
const newOutputSchema = getFunctionOutputSchema(testResult);
|
||||
updateAction({
|
||||
...action,
|
||||
settings: { ...action.settings, outputSchema: newOutputSchema },
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = usePreventOverlapCallback(save, 1000);
|
||||
const { testServerlessFunction } = useTestServerlessFunction(
|
||||
serverlessFunctionId,
|
||||
updateOutputSchemaFromTestResult,
|
||||
);
|
||||
|
||||
const onCodeChange = async (value: string) => {
|
||||
const handleSave = useDebouncedCallback(async () => {
|
||||
await updateOneServerlessFunction({
|
||||
id: serverlessFunctionId,
|
||||
name: formValues.name,
|
||||
description: formValues.description,
|
||||
code: formValues.code,
|
||||
});
|
||||
}, 1_000);
|
||||
|
||||
const onCodeChange = async (newCode: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
code: { ...prevState.code, [INDEX_FILE_PATH]: value },
|
||||
code: { ...prevState.code, [INDEX_FILE_PATH]: newCode },
|
||||
}));
|
||||
await handleSave();
|
||||
await handleUpdateFunctionInputSchema();
|
||||
};
|
||||
|
||||
const updateFunctionInputSchema = async () => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
const sourceCode = formValues.code?.[INDEX_FILE_PATH];
|
||||
if (!isDefined(sourceCode)) {
|
||||
return;
|
||||
}
|
||||
const functionInputSchema = getFunctionInputSchema(sourceCode);
|
||||
const newMergedInputSchema = mergeDefaultFunctionInputAndFunctionInput({
|
||||
defaultFunctionInput:
|
||||
getDefaultFunctionInputFromInputSchema(functionInputSchema),
|
||||
functionInput: action.settings.input.serverlessFunctionInput,
|
||||
});
|
||||
|
||||
setFunctionInput(newMergedInputSchema);
|
||||
await updateFunctionInput(newMergedInputSchema);
|
||||
await handleUpdateFunctionInputSchema(newCode);
|
||||
};
|
||||
|
||||
const handleUpdateFunctionInputSchema = useDebouncedCallback(
|
||||
updateFunctionInputSchema,
|
||||
100,
|
||||
);
|
||||
|
||||
const updateFunctionInput = useDebouncedCallback(
|
||||
async (newFunctionInput: object, shouldUpdateStepOutput = true) => {
|
||||
async (sourceCode: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate(
|
||||
{
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
...action.settings.input,
|
||||
serverlessFunctionInput: newFunctionInput,
|
||||
},
|
||||
if (!isDefined(sourceCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newFunctionInput = getFunctionInputFromSourceCode(sourceCode);
|
||||
const newMergedInput = mergeDefaultFunctionInputAndFunctionInput({
|
||||
newInput: newFunctionInput,
|
||||
oldInput: action.settings.input.serverlessFunctionInput,
|
||||
});
|
||||
const newMergedTestInput = mergeDefaultFunctionInputAndFunctionInput({
|
||||
newInput: newFunctionInput,
|
||||
oldInput: serverlessFunctionTestData.input,
|
||||
});
|
||||
|
||||
setFunctionInput(newMergedInput);
|
||||
setServerlessFunctionTestData((prev) => ({
|
||||
...prev,
|
||||
input: newMergedTestInput,
|
||||
}));
|
||||
|
||||
updateAction({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
outputSchema: {},
|
||||
input: {
|
||||
...action.settings.input,
|
||||
serverlessFunctionInput: newMergedInput,
|
||||
},
|
||||
},
|
||||
shouldUpdateStepOutput,
|
||||
);
|
||||
});
|
||||
},
|
||||
1_000,
|
||||
);
|
||||
@ -175,63 +177,33 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
||||
|
||||
setFunctionInput(updatedFunctionInput);
|
||||
|
||||
await updateFunctionInput(updatedFunctionInput, false);
|
||||
updateAction({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
...action.settings.input,
|
||||
serverlessFunctionInput: updatedFunctionInput,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderFields = (
|
||||
functionInput: FunctionInput,
|
||||
path: string[] = [],
|
||||
isRoot = true,
|
||||
): ReactNode[] => {
|
||||
const displaySeparator = (functionInput: FunctionInput) => {
|
||||
const keys = Object.keys(functionInput);
|
||||
if (keys.length > 1) {
|
||||
return true;
|
||||
}
|
||||
if (keys.length === 1) {
|
||||
const subKeys = Object.keys(functionInput[keys[0]]);
|
||||
return subKeys.length > 0;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const handleTestInputChange = async (value: any, path: string[]) => {
|
||||
const updatedTestFunctionInput = setNestedValue(
|
||||
serverlessFunctionTestData.input,
|
||||
path,
|
||||
value,
|
||||
);
|
||||
setServerlessFunctionTestData((prev) => ({
|
||||
...prev,
|
||||
input: updatedTestFunctionInput,
|
||||
}));
|
||||
};
|
||||
|
||||
return Object.entries(functionInput).map(([inputKey, inputValue]) => {
|
||||
const currentPath = [...path, inputKey];
|
||||
const pathKey = currentPath.join('.');
|
||||
|
||||
if (inputValue !== null && typeof inputValue === 'object') {
|
||||
if (isRoot) {
|
||||
return (
|
||||
<Fragment key={pathKey}>
|
||||
{displaySeparator(functionInput) && (
|
||||
<HorizontalSeparator noMargin />
|
||||
)}
|
||||
{renderFields(inputValue, currentPath, false)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StyledContainer key={pathKey}>
|
||||
<StyledLabel>{inputKey}</StyledLabel>
|
||||
<StyledFormCompositeFieldInputContainer>
|
||||
{renderFields(inputValue, currentPath, false)}
|
||||
</StyledFormCompositeFieldInputContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FormTextFieldInput
|
||||
key={pathKey}
|
||||
label={inputKey}
|
||||
placeholder="Enter value"
|
||||
defaultValue={inputValue ? `${inputValue}` : ''}
|
||||
readonly={actionOptions.readonly}
|
||||
onPersist={(value) => handleInputChange(value, currentPath)}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
const handleRunFunction = async () => {
|
||||
await testServerlessFunction();
|
||||
setActiveTabId('test');
|
||||
};
|
||||
|
||||
const handleEditorDidMount = async (
|
||||
@ -247,58 +219,103 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
||||
});
|
||||
};
|
||||
|
||||
const onActionUpdate = (actionUpdate: Partial<WorkflowCodeAction>) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
const updateAction = useDebouncedCallback(
|
||||
(actionUpdate: Partial<WorkflowCodeAction>) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions?.onActionUpdate(
|
||||
{
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
...actionUpdate,
|
||||
},
|
||||
false,
|
||||
);
|
||||
};
|
||||
});
|
||||
},
|
||||
500,
|
||||
);
|
||||
|
||||
const checkWorkflowUpdatable = async () => {
|
||||
const handleCodeChange = async (value: string) => {
|
||||
if (actionOptions.readonly === true || !isDefined(workflow)) {
|
||||
return;
|
||||
}
|
||||
await getUpdatableWorkflowVersion(workflow);
|
||||
await onCodeChange(value);
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'code', title: 'Code', Icon: IconCode },
|
||||
{ id: 'test', title: 'Test', Icon: IconPlayerPlay },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
setFunctionInput(action.settings.input.serverlessFunctionInput);
|
||||
}, [action]);
|
||||
|
||||
return (
|
||||
!loading && (
|
||||
<WorkflowEditGenericFormBase
|
||||
onTitleChange={(newName: string) => {
|
||||
onActionUpdate({ name: newName });
|
||||
}}
|
||||
Icon={IconCode}
|
||||
iconColor={theme.color.orange}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Code"
|
||||
>
|
||||
<CodeEditor
|
||||
height={340}
|
||||
value={formValues.code?.[INDEX_FILE_PATH]}
|
||||
language={'typescript'}
|
||||
onChange={async (value) => {
|
||||
await checkWorkflowUpdatable();
|
||||
await onCodeChange(value);
|
||||
}}
|
||||
onMount={handleEditorDidMount}
|
||||
options={{
|
||||
readOnly: actionOptions.readonly,
|
||||
domReadOnly: actionOptions.readonly,
|
||||
}}
|
||||
<StyledContainer>
|
||||
<StyledTabList
|
||||
tabListInstanceId={TAB_LIST_COMPONENT_ID}
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
/>
|
||||
{renderFields(functionInput)}
|
||||
</WorkflowEditGenericFormBase>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
updateAction({ name: newName });
|
||||
}}
|
||||
Icon={IconCode}
|
||||
iconColor={theme.color.orange}
|
||||
initialTitle={action.name || 'Code - Serverless Function'}
|
||||
headerType="Code"
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
{activeTabId === 'code' && (
|
||||
<>
|
||||
<WorkflowEditActionFormServerlessFunctionFields
|
||||
functionInput={functionInput}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
onInputChange={handleInputChange}
|
||||
readonly={actionOptions.readonly}
|
||||
/>
|
||||
<StyledCodeEditorContainer>
|
||||
<InputLabel>Code</InputLabel>
|
||||
<CodeEditor
|
||||
height={343}
|
||||
value={formValues.code?.[INDEX_FILE_PATH]}
|
||||
language={'typescript'}
|
||||
onChange={handleCodeChange}
|
||||
onMount={handleEditorDidMount}
|
||||
options={{
|
||||
readOnly: actionOptions.readonly,
|
||||
domReadOnly: actionOptions.readonly,
|
||||
}}
|
||||
/>
|
||||
</StyledCodeEditorContainer>
|
||||
</>
|
||||
)}
|
||||
{activeTabId === 'test' && (
|
||||
<>
|
||||
<WorkflowEditActionFormServerlessFunctionFields
|
||||
functionInput={serverlessFunctionTestData.input}
|
||||
onInputChange={handleTestInputChange}
|
||||
readonly={actionOptions.readonly}
|
||||
/>
|
||||
<StyledCodeEditorContainer>
|
||||
<InputLabel>Result</InputLabel>
|
||||
<ServerlessFunctionExecutionResult
|
||||
serverlessFunctionTestData={serverlessFunctionTestData}
|
||||
/>
|
||||
</StyledCodeEditorContainer>
|
||||
</>
|
||||
)}
|
||||
</WorkflowStepBody>
|
||||
{activeTabId === 'test' && (
|
||||
<RightDrawerFooter
|
||||
actions={[
|
||||
<CmdEnterActionButton title="Test" onClick={handleRunFunction} />,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||
import styled from '@emotion/styled';
|
||||
import { ReactNode } from 'react';
|
||||
import { FormNestedFieldInputContainer } from '@/object-record/record-field/form-types/components/FormNestedFieldInputContainer';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const WorkflowEditActionFormServerlessFunctionFields = ({
|
||||
functionInput,
|
||||
path = [],
|
||||
VariablePicker,
|
||||
onInputChange,
|
||||
readonly = false,
|
||||
}: {
|
||||
functionInput: FunctionInput;
|
||||
path?: string[];
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
onInputChange: (value: any, path: string[]) => void;
|
||||
readonly?: boolean;
|
||||
}) => {
|
||||
const renderFields = ({
|
||||
functionInput,
|
||||
path = [],
|
||||
VariablePicker,
|
||||
onInputChange,
|
||||
readonly = false,
|
||||
}: {
|
||||
functionInput: FunctionInput;
|
||||
path?: string[];
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
onInputChange: (value: any, path: string[]) => void;
|
||||
readonly?: boolean;
|
||||
}): ReactNode[] => {
|
||||
return Object.entries(functionInput).map(([inputKey, inputValue]) => {
|
||||
const currentPath = [...path, inputKey];
|
||||
const pathKey = currentPath.join('.');
|
||||
if (inputValue !== null && isObject(inputValue)) {
|
||||
return (
|
||||
<StyledContainer key={pathKey}>
|
||||
<InputLabel>{inputKey}</InputLabel>
|
||||
<FormNestedFieldInputContainer>
|
||||
{renderFields({
|
||||
functionInput: inputValue,
|
||||
path: currentPath,
|
||||
VariablePicker,
|
||||
onInputChange,
|
||||
})}
|
||||
</FormNestedFieldInputContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FormTextFieldInput
|
||||
key={pathKey}
|
||||
label={inputKey}
|
||||
placeholder="Enter value"
|
||||
defaultValue={inputValue ? `${inputValue}` : ''}
|
||||
readonly={readonly}
|
||||
onPersist={(value) => onInputChange(value, currentPath)}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderFields({
|
||||
functionInput,
|
||||
path,
|
||||
VariablePicker,
|
||||
onInputChange,
|
||||
readonly,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||
import { WorkflowSingleRecordPicker } from '@/workflow/components/WorkflowSingleRecordPicker';
|
||||
import { WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
@ -14,6 +14,7 @@ import {
|
||||
|
||||
import { JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||
|
||||
type WorkflowEditActionFormUpdateRecordProps = {
|
||||
action: WorkflowUpdateRecordAction;
|
||||
@ -123,52 +124,55 @@ export const WorkflowEditActionFormUpdateRecord = ({
|
||||
const headerTitle = isDefined(action.name) ? action.name : `Update Record`;
|
||||
|
||||
return (
|
||||
<WorkflowEditGenericFormBase
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconAddressBook}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Action"
|
||||
>
|
||||
<Select
|
||||
dropdownId="workflow-edit-action-record-update-object-name"
|
||||
label="Object"
|
||||
fullWidth
|
||||
disabled={isFormDisabled}
|
||||
value={formData.objectName}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedObjectName) => {
|
||||
const newFormData: UpdateRecordFormData = {
|
||||
objectName: updatedObjectName,
|
||||
objectRecordId: '',
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconAddressBook}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Action"
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-action-record-update-object-name"
|
||||
label="Object"
|
||||
fullWidth
|
||||
disabled={isFormDisabled}
|
||||
value={formData.objectName}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedObjectName) => {
|
||||
const newFormData: UpdateRecordFormData = {
|
||||
objectName: updatedObjectName,
|
||||
objectRecordId: '',
|
||||
};
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
setFormData(newFormData);
|
||||
|
||||
<WorkflowSingleRecordPicker
|
||||
label="Record"
|
||||
onChange={(objectRecordId) =>
|
||||
handleFieldChange('objectRecordId', objectRecordId)
|
||||
}
|
||||
objectNameSingular={formData.objectName}
|
||||
defaultValue={formData.objectRecordId}
|
||||
/>
|
||||
</WorkflowEditGenericFormBase>
|
||||
saveAction(newFormData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
|
||||
<WorkflowSingleRecordPicker
|
||||
label="Record"
|
||||
onChange={(objectRecordId) =>
|
||||
handleFieldChange('objectRecordId', objectRecordId)
|
||||
}
|
||||
objectNameSingular={formData.objectName}
|
||||
defaultValue={formData.objectRecordId}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import { mergeDefaultFunctionInputAndFunctionInput } from '../mergeDefaultFunctionInputAndFunctionInput';
|
||||
|
||||
describe('mergeDefaultFunctionInputAndFunctionInput', () => {
|
||||
it('should merge properly', () => {
|
||||
const defaultFunctionInput = {
|
||||
params: { a: null, b: null, c: { cc: null } },
|
||||
};
|
||||
const functionInput = {
|
||||
params: { a: 'a', c: 'c' },
|
||||
};
|
||||
const expectedResult = {
|
||||
params: { a: 'a', b: null, c: { cc: null } },
|
||||
};
|
||||
expect(
|
||||
mergeDefaultFunctionInputAndFunctionInput({
|
||||
defaultFunctionInput,
|
||||
functionInput,
|
||||
}),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
@ -1,32 +0,0 @@
|
||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||
|
||||
export const mergeDefaultFunctionInputAndFunctionInput = ({
|
||||
defaultFunctionInput,
|
||||
functionInput,
|
||||
}: {
|
||||
defaultFunctionInput: FunctionInput;
|
||||
functionInput: FunctionInput;
|
||||
}): FunctionInput => {
|
||||
const result: FunctionInput = {};
|
||||
|
||||
for (const key of Object.keys(defaultFunctionInput)) {
|
||||
if (!(key in functionInput)) {
|
||||
result[key] = defaultFunctionInput[key];
|
||||
} else {
|
||||
if (
|
||||
defaultFunctionInput[key] !== null &&
|
||||
typeof defaultFunctionInput[key] === 'object'
|
||||
) {
|
||||
result[key] = mergeDefaultFunctionInputAndFunctionInput({
|
||||
defaultFunctionInput: defaultFunctionInput[key],
|
||||
functionInput:
|
||||
typeof functionInput[key] === 'object' ? functionInput[key] : {},
|
||||
});
|
||||
} else {
|
||||
result[key] = functionInput[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
Reference in New Issue
Block a user