8723 workflow add editor in serverless function code step (#8805)
- create a serverless function when creating a new workflow code step - add code editor in workflow code step - move workflowVersion steps management from frontend to backend - add a custom resolver for workflow-version management - fix optimistic rendering on frontend - fix css - delete serverless function when deleting workflow code step TODO - Don't update serverlessFunction if no code change - Factorize what can be between crud trigger and crud step - Publish serverless version when activating workflow - delete serverless functions when deleting workflow or workflowVersion - fix optimistic rendering for code updates - Unify CRUD types <img width="1279" alt="image" src="https://github.com/user-attachments/assets/3d97ee9f-4b96-4abc-9d36-5c0280058be4">
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
import { WorkflowStepDetail } from '@/workflow/components/WorkflowStepDetail';
|
||||
import { useUpdateWorkflowVersionStep } from '@/workflow/hooks/useUpdateWorkflowVersionStep';
|
||||
import { useUpdateStep } from '@/workflow/hooks/useUpdateStep';
|
||||
import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
@ -19,9 +19,8 @@ export const RightDrawerWorkflowEditStepContent = ({
|
||||
}
|
||||
|
||||
const { updateTrigger } = useUpdateWorkflowVersionTrigger({ workflow });
|
||||
const { updateStep } = useUpdateWorkflowVersionStep({
|
||||
const { updateStep } = useUpdateStep({
|
||||
workflow,
|
||||
stepId: workflowSelectedNode,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { WorkflowDiagramStepNodeBase } from '@/workflow/components/WorkflowDiagramStepNodeBase';
|
||||
import { useDeleteOneStep } from '@/workflow/hooks/useDeleteOneStep';
|
||||
import { useDeleteStep } from '@/workflow/hooks/useDeleteStep';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
|
||||
@ -21,9 +21,8 @@ export const WorkflowDiagramStepNodeEditable = ({
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
assertWorkflowWithCurrentVersionIsDefined(workflowWithCurrentVersion);
|
||||
|
||||
const { deleteOneStep } = useDeleteOneStep({
|
||||
const { deleteStep } = useDeleteStep({
|
||||
workflow: workflowWithCurrentVersion,
|
||||
stepId: id,
|
||||
});
|
||||
|
||||
return (
|
||||
@ -35,7 +34,7 @@ export const WorkflowDiagramStepNodeEditable = ({
|
||||
size="medium"
|
||||
Icon={IconTrash}
|
||||
onClick={() => {
|
||||
return deleteOneStep();
|
||||
deleteStep(id);
|
||||
}}
|
||||
/>
|
||||
) : undefined
|
||||
|
||||
@ -1,256 +0,0 @@
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
|
||||
import { getDefaultFunctionInputFromInputSchema } from '@/workflow/utils/getDefaultFunctionInputFromInputSchema';
|
||||
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/utils/mergeDefaultFunctionInputAndFunctionInput';
|
||||
import { setNestedValue } from '@/workflow/utils/setNestedValue';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Fragment, ReactNode, useState } from 'react';
|
||||
import { HorizontalSeparator, IconCode, isDefined } from 'twenty-ui';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: inline-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 StyledInputContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type WorkflowEditActionFormServerlessFunctionInnerProps = {
|
||||
action: WorkflowCodeAction;
|
||||
actionOptions:
|
||||
| {
|
||||
readonly: true;
|
||||
}
|
||||
| {
|
||||
readonly?: false;
|
||||
onActionUpdate: (action: WorkflowCodeAction) => void;
|
||||
};
|
||||
};
|
||||
|
||||
type ServerlessFunctionInputFormData = {
|
||||
[field: string]: string | ServerlessFunctionInputFormData;
|
||||
};
|
||||
|
||||
export const WorkflowEditActionFormServerlessFunctionInner = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionFormServerlessFunctionInnerProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
||||
|
||||
const getFunctionInput = (serverlessFunctionId: string) => {
|
||||
if (!serverlessFunctionId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const serverlessFunction = serverlessFunctions.find(
|
||||
(f) => f.id === serverlessFunctionId,
|
||||
);
|
||||
const inputSchema = serverlessFunction?.latestVersionInputSchema;
|
||||
const defaultFunctionInput =
|
||||
getDefaultFunctionInputFromInputSchema(inputSchema);
|
||||
|
||||
return defaultFunctionInput;
|
||||
};
|
||||
|
||||
const [selectedFunctionId, setSelectedFunctionId] = useState(
|
||||
action.settings.input.serverlessFunctionId,
|
||||
);
|
||||
|
||||
const [functionInput, setFunctionInput] =
|
||||
useState<ServerlessFunctionInputFormData>(
|
||||
mergeDefaultFunctionInputAndFunctionInput({
|
||||
defaultFunctionInput: getFunctionInput(selectedFunctionId),
|
||||
functionInput: action.settings.input.serverlessFunctionInput,
|
||||
}),
|
||||
);
|
||||
|
||||
const updateFunctionInput = useDebouncedCallback(
|
||||
async (newFunctionInput: object) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
...action.settings.input,
|
||||
serverlessFunctionInput: newFunctionInput,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
1_000,
|
||||
);
|
||||
|
||||
const handleInputChange = (value: any, path: string[]) => {
|
||||
const updatedFunctionInput = setNestedValue(functionInput, path, value);
|
||||
|
||||
setFunctionInput(updatedFunctionInput);
|
||||
|
||||
updateFunctionInput(updatedFunctionInput);
|
||||
};
|
||||
|
||||
const availableFunctions: Array<SelectOption<string>> = [
|
||||
...serverlessFunctions
|
||||
.filter((serverlessFunction) =>
|
||||
isDefined(serverlessFunction.latestVersion),
|
||||
)
|
||||
.map((serverlessFunction) => ({
|
||||
label: serverlessFunction.name,
|
||||
value: serverlessFunction.id,
|
||||
latestVersionInputSchema: serverlessFunction.latestVersionInputSchema,
|
||||
})),
|
||||
];
|
||||
|
||||
const handleFunctionChange = (newServerlessFunctionId: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateFunctionInput.cancel();
|
||||
|
||||
setSelectedFunctionId(newServerlessFunctionId);
|
||||
|
||||
const serverlessFunction = serverlessFunctions.find(
|
||||
(f) => f.id === newServerlessFunctionId,
|
||||
);
|
||||
|
||||
const newFunctionInput = getFunctionInput(newServerlessFunctionId);
|
||||
|
||||
const newProps = {
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
serverlessFunctionId: newServerlessFunctionId,
|
||||
serverlessFunctionVersion:
|
||||
serverlessFunction?.latestVersion || 'latest',
|
||||
serverlessFunctionInput: newFunctionInput,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setFunctionInput(newFunctionInput);
|
||||
|
||||
actionOptions.onActionUpdate(newProps);
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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>
|
||||
<StyledInputContainer>
|
||||
{renderFields(inputValue, currentPath, false)}
|
||||
</StyledInputContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FormTextFieldInput
|
||||
key={pathKey}
|
||||
label={inputKey}
|
||||
placeholder="Enter value"
|
||||
defaultValue={inputValue ? String(inputValue) : ''}
|
||||
readonly={actionOptions.readonly}
|
||||
onPersist={(value) => {
|
||||
handleInputChange(value, currentPath);
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const headerTitle = isDefined(action.name)
|
||||
? action.name
|
||||
: 'Code - Serverless Function';
|
||||
|
||||
return (
|
||||
<WorkflowEditGenericFormBase
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions?.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
HeaderIcon={<IconCode color={theme.color.orange} />}
|
||||
headerTitle={headerTitle}
|
||||
headerType="Code"
|
||||
>
|
||||
<Select
|
||||
dropdownId="select-serverless-function-id"
|
||||
label="Function"
|
||||
fullWidth
|
||||
value={selectedFunctionId}
|
||||
options={availableFunctions}
|
||||
emptyOption={{ label: 'None', value: '' }}
|
||||
disabled={actionOptions.readonly}
|
||||
onChange={handleFunctionChange}
|
||||
/>
|
||||
{renderFields(functionInput)}
|
||||
</WorkflowEditGenericFormBase>
|
||||
);
|
||||
};
|
||||
@ -1,27 +1,36 @@
|
||||
import { TextInput } from '@/ui/field/input/components/TextInput';
|
||||
import styled from '@emotion/styled';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { IconComponent } from 'packages/twenty-ui';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${({ theme }) => theme.spacing(6)};
|
||||
flex-direction: row;
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledHeaderTitle = styled.p`
|
||||
const StyledHeaderInfo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledHeaderTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
font-size: ${({ theme }) => theme.font.size.xl};
|
||||
|
||||
margin: ${({ theme }) => theme.spacing(3)} 0;
|
||||
width: 420px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledHeaderType = styled.p`
|
||||
const StyledHeaderType = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
margin: 0;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledHeaderIconContainer = styled.div`
|
||||
@ -30,8 +39,8 @@ const StyledHeaderIconContainer = styled.div`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
@ -43,39 +52,54 @@ const StyledContentContainer = styled.div`
|
||||
|
||||
export const WorkflowEditGenericFormBase = ({
|
||||
onTitleChange,
|
||||
HeaderIcon,
|
||||
headerTitle,
|
||||
Icon,
|
||||
iconColor,
|
||||
initialTitle,
|
||||
headerType,
|
||||
children,
|
||||
}: {
|
||||
onTitleChange: (newTitle: string) => void;
|
||||
HeaderIcon: React.ReactNode;
|
||||
headerTitle: string;
|
||||
Icon: IconComponent;
|
||||
iconColor: string;
|
||||
initialTitle: string;
|
||||
headerType: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [title, setTitle] = useState(initialTitle);
|
||||
const debouncedOnTitleChange = useDebouncedCallback(onTitleChange, 100);
|
||||
const handleChange = (newTitle: string) => {
|
||||
setTitle(newTitle);
|
||||
debouncedOnTitleChange(newTitle);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledHeader>
|
||||
<StyledHeaderIconContainer>{HeaderIcon}</StyledHeaderIconContainer>
|
||||
|
||||
<StyledHeaderTitle>
|
||||
<TextInput
|
||||
value={headerTitle}
|
||||
copyButton={false}
|
||||
hotkeyScope="workflow-step-title"
|
||||
onEnter={onTitleChange}
|
||||
onEscape={onTitleChange}
|
||||
onChange={debouncedOnTitleChange}
|
||||
shouldTrim={false}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
|
||||
<StyledHeaderType>{headerType}</StyledHeaderType>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -71,8 +71,9 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
HeaderIcon={<IconPlaylistAdd color={theme.font.color.tertiary} />}
|
||||
headerTitle={headerTitle}
|
||||
Icon={IconPlaylistAdd}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
>
|
||||
<Select
|
||||
|
||||
@ -58,8 +58,9 @@ export const WorkflowEditTriggerManualForm = ({
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
HeaderIcon={<IconHandMove color={theme.font.color.tertiary} />}
|
||||
headerTitle={headerTitle}
|
||||
Icon={IconHandMove}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Trigger · Manual"
|
||||
>
|
||||
<Select
|
||||
|
||||
@ -3,10 +3,7 @@ import styled from '@emotion/styled';
|
||||
import { CodeEditor, isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledSourceCodeContainer = styled.div`
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
margin: ${({ theme }) => theme.spacing(4)};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const WorkflowRunOutputVisualizer = ({
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/components/WorkflowEditTriggerDatabaseEventForm';
|
||||
import { WorkflowEditTriggerManualForm } from '@/workflow/components/WorkflowEditTriggerManualForm';
|
||||
import {
|
||||
@ -12,8 +13,16 @@ import { isWorkflowRecordUpdateAction } from '@/workflow/utils/isWorkflowRecordU
|
||||
import { WorkflowEditActionFormRecordCreate } from '@/workflow/workflow-actions/components/WorkflowEditActionFormRecordCreate';
|
||||
import { WorkflowEditActionFormRecordUpdate } from '@/workflow/workflow-actions/components/WorkflowEditActionFormRecordUpdate';
|
||||
import { WorkflowEditActionFormSendEmail } from '@/workflow/workflow-actions/components/WorkflowEditActionFormSendEmail';
|
||||
import { WorkflowEditActionFormServerlessFunction } from '@/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
|
||||
|
||||
const WorkflowEditActionFormServerlessFunction = lazy(() =>
|
||||
import(
|
||||
'@/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction'
|
||||
).then((module) => ({
|
||||
default: module.WorkflowEditActionFormServerlessFunction,
|
||||
})),
|
||||
);
|
||||
|
||||
type WorkflowStepDetailProps =
|
||||
| {
|
||||
@ -80,10 +89,12 @@ export const WorkflowStepDetail = ({
|
||||
switch (stepDefinition.definition.type) {
|
||||
case 'CODE': {
|
||||
return (
|
||||
<WorkflowEditActionFormServerlessFunction
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
<Suspense fallback={<RightDrawerSkeletonLoader />}>
|
||||
<WorkflowEditActionFormServerlessFunction
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
|
||||
@ -33,36 +33,6 @@ export const BlankInitialVersion: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
graphql.query('FindManyWorkflows', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
workflows: {
|
||||
__typename: 'WorkflowConnection',
|
||||
totalCount: 1,
|
||||
pageInfo: {
|
||||
__typename: 'PageInfo',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
|
||||
endCursor:
|
||||
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WorkflowEdge',
|
||||
cursor:
|
||||
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
|
||||
node: {
|
||||
__typename: 'Workflow',
|
||||
id: blankInitialVersionWorkflowId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphql.query('FindOneWorkflow', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
@ -112,44 +82,6 @@ export const BlankInitialVersion: Story = {
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphql.query('FindManyWorkflowVersions', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
workflowVersions: {
|
||||
__typename: 'WorkflowVersionConnection',
|
||||
totalCount: 1,
|
||||
pageInfo: {
|
||||
__typename: 'PageInfo',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
|
||||
endCursor:
|
||||
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WorkflowVersionEdge',
|
||||
cursor:
|
||||
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
|
||||
node: {
|
||||
__typename: 'WorkflowVersion',
|
||||
updatedAt: '2024-09-19T10:13:12.075Z',
|
||||
steps: null,
|
||||
createdAt: '2024-09-19T10:10:04.725Z',
|
||||
status: 'DRAFT',
|
||||
name: 'v1',
|
||||
id: 'f618843a-26be-4a54-a60f-f4ce88a594f0',
|
||||
trigger: null,
|
||||
deletedAt: null,
|
||||
workflowId: blankInitialVersionWorkflowId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
...graphqlMocks.handlers,
|
||||
],
|
||||
},
|
||||
@ -172,53 +104,19 @@ export const ActiveVersion: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
graphql.query('FindManyWorkflows', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
workflows: {
|
||||
__typename: 'WorkflowConnection',
|
||||
totalCount: 1,
|
||||
pageInfo: {
|
||||
__typename: 'PageInfo',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'eyJwb3NpdGlvbiI6LTEsImlkIjoiN2JlM2E4MmMtNDRiNy00MTUwLWEyZTgtNDA4ODcxNDZmNGQ0In0=',
|
||||
endCursor:
|
||||
'eyJwb3NpdGlvbiI6LTEsImlkIjoiN2JlM2E4MmMtNDRiNy00MTUwLWEyZTgtNDA4ODcxNDZmNGQ0In0=',
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WorkflowEdge',
|
||||
cursor:
|
||||
'eyJwb3NpdGlvbiI6LTEsImlkIjoiN2JlM2E4MmMtNDRiNy00MTUwLWEyZTgtNDA4ODcxNDZmNGQ0In0=',
|
||||
node: {
|
||||
__typename: 'Workflow',
|
||||
id: activeVersionWorkflowId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphql.query('FindOneWorkflow', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
workflow: {
|
||||
__typename: 'Workflow',
|
||||
name: 'test qqqq',
|
||||
lastPublishedVersionId: 'b57e577a-ae55-4de2-ba08-fe361dcc1a57',
|
||||
id: activeVersionWorkflowId,
|
||||
deletedAt: null,
|
||||
id: blankInitialVersionWorkflowId,
|
||||
name: '1231 qqerrt',
|
||||
statuses: null,
|
||||
createdAt: '2024-09-20T10:18:59.977Z',
|
||||
updatedAt: '2024-09-20T16:59:37.212Z',
|
||||
position: -1,
|
||||
runs: {
|
||||
__typename: 'WorkflowRunConnection',
|
||||
edges: [],
|
||||
},
|
||||
lastPublishedVersionId: '',
|
||||
deletedAt: null,
|
||||
updatedAt: '2024-09-19T10:10:04.505Z',
|
||||
position: 0,
|
||||
createdAt: '2024-09-19T10:10:04.505Z',
|
||||
favorites: {
|
||||
__typename: 'FavoriteConnection',
|
||||
edges: [],
|
||||
@ -227,6 +125,10 @@ export const ActiveVersion: Story = {
|
||||
__typename: 'WorkflowEventListenerConnection',
|
||||
edges: [],
|
||||
},
|
||||
runs: {
|
||||
__typename: 'WorkflowRunConnection',
|
||||
edges: [],
|
||||
},
|
||||
versions: {
|
||||
__typename: 'WorkflowVersionConnection',
|
||||
edges: [
|
||||
@ -234,165 +136,15 @@ export const ActiveVersion: Story = {
|
||||
__typename: 'WorkflowVersionEdge',
|
||||
node: {
|
||||
__typename: 'WorkflowVersion',
|
||||
updatedAt: '2024-09-20T16:59:37.212Z',
|
||||
status: 'ARCHIVED',
|
||||
deletedAt: null,
|
||||
steps: [
|
||||
{
|
||||
id: '93c41c1d-eff3-4c91-ac61-f56cc1a0df8a',
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
serverlessFunctionId: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
workflowId: activeVersionWorkflowId,
|
||||
trigger: {
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'note.created',
|
||||
},
|
||||
},
|
||||
name: 'v1',
|
||||
id: '394cd0b5-bd48-41d7-a110-a92cafaf171d',
|
||||
createdAt: '2024-09-20T10:19:00.141Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'WorkflowVersionEdge',
|
||||
node: {
|
||||
__typename: 'WorkflowVersion',
|
||||
updatedAt: '2024-09-20T17:01:15.637Z',
|
||||
status: 'DRAFT',
|
||||
deletedAt: null,
|
||||
steps: [
|
||||
{
|
||||
id: '93c41c1d-eff3-4c91-ac61-f56cc1a0df8a',
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
serverlessFunctionId: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4177d57d-35dc-4eb1-a467-07e25cb31da0',
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
serverlessFunctionId: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '0cc392d9-5f28-4d92-90a0-08180f264e68',
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
serverlessFunctionId: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
workflowId: activeVersionWorkflowId,
|
||||
trigger: {
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'note.created',
|
||||
},
|
||||
},
|
||||
name: 'v3',
|
||||
id: '5eae34ef-9d62-4a9e-b827-3eb927481728',
|
||||
createdAt: '2024-09-20T17:01:15.637Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'WorkflowVersionEdge',
|
||||
node: {
|
||||
__typename: 'WorkflowVersion',
|
||||
updatedAt: '2024-09-20T17:00:16.097Z',
|
||||
updatedAt: '2024-09-19T10:13:12.075Z',
|
||||
steps: null,
|
||||
createdAt: '2024-09-19T10:10:04.725Z',
|
||||
status: 'ACTIVE',
|
||||
name: 'v1',
|
||||
id: 'f618843a-26be-4a54-a60f-f4ce88a594f0',
|
||||
trigger: null,
|
||||
deletedAt: null,
|
||||
steps: [
|
||||
{
|
||||
id: '93c41c1d-eff3-4c91-ac61-f56cc1a0df8a',
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
serverlessFunctionId: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4177d57d-35dc-4eb1-a467-07e25cb31da0',
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
serverlessFunctionId: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
workflowId: activeVersionWorkflowId,
|
||||
trigger: {
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'note.created',
|
||||
},
|
||||
},
|
||||
name: 'v2',
|
||||
id: 'b57e577a-ae55-4de2-ba08-fe361dcc1a57',
|
||||
createdAt: '2024-09-20T16:59:35.755Z',
|
||||
workflowId: blankInitialVersionWorkflowId,
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -401,101 +153,6 @@ export const ActiveVersion: Story = {
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphql.query('FindManyWorkflowVersions', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
workflowVersions: {
|
||||
__typename: 'WorkflowVersionConnection',
|
||||
totalCount: 3,
|
||||
pageInfo: {
|
||||
__typename: 'PageInfo',
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTIwVDE3OjAxOjE1LjYzN1oiLCJpZCI6IjVlYWUzNGVmLTlkNjItNGE5ZS1iODI3LTNlYjkyNzQ4MTcyOCJ9',
|
||||
endCursor:
|
||||
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTIwVDE3OjAxOjE1LjYzN1oiLCJpZCI6IjVlYWUzNGVmLTlkNjItNGE5ZS1iODI3LTNlYjkyNzQ4MTcyOCJ9',
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WorkflowVersionEdge',
|
||||
cursor:
|
||||
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTIwVDE3OjAxOjE1LjYzN1oiLCJpZCI6IjVlYWUzNGVmLTlkNjItNGE5ZS1iODI3LTNlYjkyNzQ4MTcyOCJ9',
|
||||
node: {
|
||||
__typename: 'WorkflowVersion',
|
||||
updatedAt: '2024-09-20T17:01:15.637Z',
|
||||
status: 'ACTIVE',
|
||||
deletedAt: null,
|
||||
steps: [
|
||||
{
|
||||
id: '93c41c1d-eff3-4c91-ac61-f56cc1a0df8a',
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
serverlessFunctionId: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4177d57d-35dc-4eb1-a467-07e25cb31da0',
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
serverlessFunctionId: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '0cc392d9-5f28-4d92-90a0-08180f264e68',
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
serverlessFunctionId: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
workflowId: activeVersionWorkflowId,
|
||||
trigger: {
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'note.created',
|
||||
},
|
||||
},
|
||||
name: 'v3',
|
||||
id: '5eae34ef-9d62-4a9e-b827-3eb927481728',
|
||||
createdAt: '2024-09-20T17:01:15.637Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
...graphqlMocks.handlers,
|
||||
],
|
||||
},
|
||||
@ -518,36 +175,6 @@ export const DraftVersionWithPreviousActiveVersion: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
graphql.query('FindManyWorkflows', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
workflows: {
|
||||
__typename: 'WorkflowConnection',
|
||||
totalCount: 1,
|
||||
pageInfo: {
|
||||
__typename: 'PageInfo',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
|
||||
endCursor:
|
||||
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WorkflowEdge',
|
||||
cursor:
|
||||
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
|
||||
node: {
|
||||
__typename: 'Workflow',
|
||||
id: draftVersionWithPreviousActiveVersionWorkflowId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphql.query('FindOneWorkflow', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
@ -614,45 +241,6 @@ export const DraftVersionWithPreviousActiveVersion: Story = {
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphql.query('FindManyWorkflowVersions', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
workflowVersions: {
|
||||
__typename: 'WorkflowVersionConnection',
|
||||
totalCount: 1,
|
||||
pageInfo: {
|
||||
__typename: 'PageInfo',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
|
||||
endCursor:
|
||||
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WorkflowVersionEdge',
|
||||
cursor:
|
||||
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
|
||||
node: {
|
||||
__typename: 'WorkflowVersion',
|
||||
updatedAt: '2024-09-19T10:13:12.075Z',
|
||||
steps: null,
|
||||
createdAt: '2024-09-19T10:10:05.725Z',
|
||||
status: 'DRAFT',
|
||||
name: 'v2',
|
||||
id: 'f618843a-26be-4a54-a60f-f4ce88a594f1',
|
||||
trigger: null,
|
||||
deletedAt: null,
|
||||
workflowId:
|
||||
draftVersionWithPreviousActiveVersionWorkflowId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
...graphqlMocks.handlers,
|
||||
],
|
||||
},
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_WORKFLOW_VERSION_STEP = gql`
|
||||
mutation CreateWorkflowVersionStep($input: CreateWorkflowVersionStepInput!) {
|
||||
createWorkflowVersionStep(input: $input) {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
valid
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_WORKFLOW_VERSION_STEP = gql`
|
||||
mutation DeleteWorkflowVersionStep($input: DeleteWorkflowVersionStepInput!) {
|
||||
deleteWorkflowVersionStep(input: $input) {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
valid
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_WORKFLOW_VERSION_STEP = gql`
|
||||
mutation UpdateWorkflowVersionStep($input: UpdateWorkflowVersionStepInput!) {
|
||||
updateWorkflowVersionStep(input: $input) {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
valid
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -3,10 +3,9 @@ import { renderHook } from '@testing-library/react';
|
||||
import { useCreateStep } from '../useCreateStep';
|
||||
|
||||
const mockOpenRightDrawer = jest.fn();
|
||||
const mockUpdateOneWorkflowVersion = jest.fn();
|
||||
const mockCreateNewWorkflowVersion = jest.fn();
|
||||
const mockComputeStepOutputSchema = jest.fn().mockResolvedValue({
|
||||
data: { computeStepOutputSchema: { type: 'object' } },
|
||||
const mockCreateWorkflowVersionStep = jest.fn().mockResolvedValue({
|
||||
data: { createWorkflowVersionStep: { id: '1' } },
|
||||
});
|
||||
|
||||
jest.mock('recoil', () => ({
|
||||
@ -15,19 +14,15 @@ jest.mock('recoil', () => ({
|
||||
atom: (params: any) => params,
|
||||
}));
|
||||
|
||||
jest.mock('@/workflow/states/workflowCreateStepFromParentStepIdState', () => ({
|
||||
workflowCreateStepFromParentStepIdState: 'mockAtomState',
|
||||
}));
|
||||
|
||||
jest.mock('@/ui/layout/right-drawer/hooks/useRightDrawer', () => ({
|
||||
useRightDrawer: () => ({
|
||||
openRightDrawer: mockOpenRightDrawer,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/object-record/hooks/useUpdateOneRecord', () => ({
|
||||
useUpdateOneRecord: () => ({
|
||||
updateOneRecord: mockUpdateOneWorkflowVersion,
|
||||
jest.mock('@/workflow/hooks/useCreateWorkflowVersionStep', () => ({
|
||||
useCreateWorkflowVersionStep: () => ({
|
||||
createWorkflowVersionStep: mockCreateWorkflowVersionStep,
|
||||
}),
|
||||
}));
|
||||
|
||||
@ -37,24 +32,6 @@ jest.mock('@/workflow/hooks/useCreateNewWorkflowVersion', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/workflow/hooks/useComputeStepOutputSchema', () => ({
|
||||
useComputeStepOutputSchema: () => ({
|
||||
computeStepOutputSchema: mockComputeStepOutputSchema,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/object-metadata/hooks/useFilteredObjectMetadataItems', () => ({
|
||||
useFilteredObjectMetadataItems: () => ({
|
||||
activeObjectMetadataItems: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/workflow/utils/insertStep', () => ({
|
||||
insertStep: jest
|
||||
.fn()
|
||||
.mockImplementation(({ steps, stepToAdd }) => [...steps, stepToAdd]),
|
||||
}));
|
||||
|
||||
describe('useCreateStep', () => {
|
||||
const mockWorkflow = {
|
||||
id: '123',
|
||||
@ -75,7 +52,7 @@ describe('useCreateStep', () => {
|
||||
);
|
||||
await result.current.createStep('CODE');
|
||||
|
||||
expect(mockUpdateOneWorkflowVersion).toHaveBeenCalled();
|
||||
expect(mockCreateWorkflowVersionStep).toHaveBeenCalled();
|
||||
expect(mockOpenRightDrawer).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useDeleteStep } from '@/workflow/hooks/useDeleteStep';
|
||||
|
||||
const mockCloseRightDrawer = jest.fn();
|
||||
const mockCreateNewWorkflowVersion = jest.fn();
|
||||
const mockDeleteWorkflowVersionStep = jest.fn();
|
||||
const updateOneRecordMock = jest.fn();
|
||||
|
||||
jest.mock('@/object-record/hooks/useUpdateOneRecord', () => ({
|
||||
useUpdateOneRecord: () => ({
|
||||
updateOneRecord: updateOneRecordMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('recoil', () => ({
|
||||
useRecoilValue: () => 'parent-step-id',
|
||||
useSetRecoilState: () => jest.fn(),
|
||||
atom: (params: any) => params,
|
||||
}));
|
||||
|
||||
jest.mock('@/ui/layout/right-drawer/hooks/useRightDrawer', () => ({
|
||||
useRightDrawer: () => ({
|
||||
closeRightDrawer: mockCloseRightDrawer,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/workflow/hooks/useDeleteWorkflowVersionStep', () => ({
|
||||
useDeleteWorkflowVersionStep: () => ({
|
||||
deleteWorkflowVersionStep: mockDeleteWorkflowVersionStep,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/workflow/hooks/useCreateNewWorkflowVersion', () => ({
|
||||
useCreateNewWorkflowVersion: () => ({
|
||||
createNewWorkflowVersion: mockCreateNewWorkflowVersion,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useDeleteStep', () => {
|
||||
const mockWorkflow = {
|
||||
id: '123',
|
||||
currentVersion: {
|
||||
id: '456',
|
||||
status: 'DRAFT',
|
||||
steps: [],
|
||||
trigger: { type: 'manual' },
|
||||
},
|
||||
versions: [],
|
||||
};
|
||||
|
||||
it('should delete step in draft version', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useDeleteStep({
|
||||
workflow: mockWorkflow as unknown as WorkflowWithCurrentVersion,
|
||||
}),
|
||||
);
|
||||
await result.current.deleteStep('1');
|
||||
|
||||
expect(mockDeleteWorkflowVersionStep).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,62 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
|
||||
const mockCreateNewWorkflowVersion = jest.fn().mockResolvedValue({
|
||||
id: '457',
|
||||
name: 'toto',
|
||||
createdAt: '2024-07-03T20:03:35.064Z',
|
||||
updatedAt: '2024-07-03T20:03:35.064Z',
|
||||
workflowId: '123',
|
||||
__typename: 'WorkflowVersion',
|
||||
status: 'DRAFT',
|
||||
steps: [],
|
||||
trigger: null,
|
||||
});
|
||||
|
||||
jest.mock('@/workflow/hooks/useCreateNewWorkflowVersion', () => ({
|
||||
useCreateNewWorkflowVersion: () => ({
|
||||
createNewWorkflowVersion: mockCreateNewWorkflowVersion,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useGetUpdatableWorkflowVersion', () => {
|
||||
const mockWorkflow = (status: 'ACTIVE' | 'DRAFT') =>
|
||||
({
|
||||
id: '123',
|
||||
__typename: 'Workflow',
|
||||
statuses: [],
|
||||
lastPublishedVersionId: '1',
|
||||
name: 'toto',
|
||||
versions: [],
|
||||
currentVersion: {
|
||||
id: '456',
|
||||
name: 'toto',
|
||||
createdAt: '2024-07-03T20:03:35.064Z',
|
||||
updatedAt: '2024-07-03T20:03:35.064Z',
|
||||
workflowId: '123',
|
||||
__typename: 'WorkflowVersion',
|
||||
status,
|
||||
steps: [],
|
||||
trigger: null,
|
||||
},
|
||||
}) as WorkflowWithCurrentVersion;
|
||||
|
||||
it('should not create workflow version if draft version exists', async () => {
|
||||
const { result } = renderHook(() => useGetUpdatableWorkflowVersion());
|
||||
const workflowVersion = await result.current.getUpdatableWorkflowVersion(
|
||||
mockWorkflow('DRAFT'),
|
||||
);
|
||||
expect(mockCreateNewWorkflowVersion).not.toHaveBeenCalled();
|
||||
expect(workflowVersion.id === '456');
|
||||
});
|
||||
|
||||
it('should create workflow version if no draft version exists', async () => {
|
||||
const { result } = renderHook(() => useGetUpdatableWorkflowVersion());
|
||||
const workflowVersion = await result.current.getUpdatableWorkflowVersion(
|
||||
mockWorkflow('ACTIVE'),
|
||||
);
|
||||
expect(mockCreateNewWorkflowVersion).toHaveBeenCalled();
|
||||
expect(workflowVersion.id === '457');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,66 @@
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useTriggerNodeSelection } from '@/workflow/hooks/useTriggerNodeSelection';
|
||||
import { RecoilRoot, useRecoilState } from 'recoil';
|
||||
import { useReactFlow } from '@xyflow/react';
|
||||
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState';
|
||||
|
||||
jest.mock('@xyflow/react', () => ({
|
||||
useReactFlow: jest.fn(),
|
||||
}));
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useTriggerNodeSelection', () => {
|
||||
const mockUpdateNode = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
(useReactFlow as jest.Mock).mockReturnValue({
|
||||
updateNode: mockUpdateNode,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should trigger node selection', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const [
|
||||
workflowDiagramTriggerNodeSelection,
|
||||
setWorkflowDiagramTriggerNodeSelection,
|
||||
] = useRecoilState(workflowDiagramTriggerNodeSelectionState);
|
||||
|
||||
useTriggerNodeSelection();
|
||||
|
||||
return {
|
||||
workflowDiagramTriggerNodeSelection,
|
||||
setWorkflowDiagramTriggerNodeSelection,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const mockNodeId = 'test-node-id';
|
||||
|
||||
act(() => {
|
||||
result.current.setWorkflowDiagramTriggerNodeSelection(mockNodeId);
|
||||
});
|
||||
|
||||
expect(mockUpdateNode).toHaveBeenCalledWith(mockNodeId, { selected: true });
|
||||
expect(result.current.workflowDiagramTriggerNodeSelection).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not trigger update if state is not defined', () => {
|
||||
renderHook(() => useTriggerNodeSelection(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Ensure updateNode is not called when state is undefined
|
||||
expect(mockUpdateNode).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,69 @@
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useUpdateStep } from '@/workflow/hooks/useUpdateStep';
|
||||
|
||||
const mockCreateNewWorkflowVersion = jest.fn();
|
||||
const mockUpdateWorkflowVersionStep = jest.fn();
|
||||
|
||||
jest.mock('recoil', () => ({
|
||||
useRecoilValue: () => 'parent-step-id',
|
||||
useSetRecoilState: () => jest.fn(),
|
||||
atom: (params: any) => params,
|
||||
}));
|
||||
|
||||
jest.mock('@/workflow/hooks/useUpdateWorkflowVersionStep', () => ({
|
||||
useUpdateWorkflowVersionStep: () => ({
|
||||
updateWorkflowVersionStep: mockUpdateWorkflowVersionStep,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/workflow/hooks/useCreateNewWorkflowVersion', () => ({
|
||||
useCreateNewWorkflowVersion: () => ({
|
||||
createNewWorkflowVersion: mockCreateNewWorkflowVersion,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useUpdateStep', () => {
|
||||
const mockWorkflow = {
|
||||
id: '123',
|
||||
currentVersion: {
|
||||
id: '456',
|
||||
status: 'DRAFT',
|
||||
steps: [],
|
||||
trigger: { type: 'manual' },
|
||||
},
|
||||
versions: [],
|
||||
};
|
||||
|
||||
it('should update step in draft version', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useUpdateStep({
|
||||
workflow: mockWorkflow as unknown as WorkflowWithCurrentVersion,
|
||||
}),
|
||||
);
|
||||
await result.current.updateStep({
|
||||
id: '1',
|
||||
name: 'name',
|
||||
valid: true,
|
||||
type: 'CODE',
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'id',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: true,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockUpdateWorkflowVersionStep).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -67,5 +67,5 @@ export const useAllActiveWorkflowVersions = ({
|
||||
};
|
||||
}
|
||||
|
||||
return { records };
|
||||
return { records: records.filter((record) => isDefined(record.workflow)) };
|
||||
};
|
||||
|
||||
@ -8,13 +8,13 @@ export const useCreateNewWorkflowVersion = () => {
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const createNewWorkflowVersion = (
|
||||
const createNewWorkflowVersion = async (
|
||||
workflowVersionData: Pick<
|
||||
WorkflowVersion,
|
||||
'workflowId' | 'name' | 'status' | 'trigger' | 'steps'
|
||||
>,
|
||||
) => {
|
||||
return createOneWorkflowVersion(workflowVersionData);
|
||||
return await createOneWorkflowVersion(workflowVersionData);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,23 +1,16 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema';
|
||||
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
|
||||
import { workflowCreateStepFromParentStepIdState } from '@/workflow/states/workflowCreateStepFromParentStepIdState';
|
||||
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import {
|
||||
WorkflowStep,
|
||||
WorkflowStepType,
|
||||
WorkflowVersion,
|
||||
WorkflowWithCurrentVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { getStepDefaultDefinition } from '@/workflow/utils/getStepDefaultDefinition';
|
||||
import { insertStep } from '@/workflow/utils/insertStep';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { useCreateWorkflowVersionStep } from '@/workflow/hooks/useCreateWorkflowVersionStep';
|
||||
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
||||
|
||||
export const useCreateStep = ({
|
||||
workflow,
|
||||
@ -25,6 +18,7 @@ export const useCreateStep = ({
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const { openRightDrawer } = useRightDrawer();
|
||||
const { createWorkflowVersionStep } = useCreateWorkflowVersionStep();
|
||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||
|
||||
const workflowCreateStepFromParentStepId = useRecoilValue(
|
||||
@ -35,82 +29,27 @@ export const useCreateStep = ({
|
||||
workflowDiagramTriggerNodeSelectionState,
|
||||
);
|
||||
|
||||
const { updateOneRecord: updateOneWorkflowVersion } =
|
||||
useUpdateOneRecord<WorkflowVersion>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||
|
||||
const { computeStepOutputSchema } = useComputeStepOutputSchema();
|
||||
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
|
||||
const insertNodeAndSave = async ({
|
||||
parentNodeId,
|
||||
nodeToAdd,
|
||||
}: {
|
||||
parentNodeId: string;
|
||||
nodeToAdd: WorkflowStep;
|
||||
}) => {
|
||||
const currentVersion = workflow.currentVersion;
|
||||
if (!isDefined(currentVersion)) {
|
||||
throw new Error("Can't add a node when there is no current version.");
|
||||
}
|
||||
|
||||
const updatedSteps = insertStep({
|
||||
steps: currentVersion.steps ?? [],
|
||||
parentStepId: parentNodeId,
|
||||
stepToAdd: nodeToAdd,
|
||||
});
|
||||
|
||||
if (workflow.currentVersion.status === 'DRAFT') {
|
||||
await updateOneWorkflowVersion({
|
||||
idToUpdate: currentVersion.id,
|
||||
updateOneRecordInput: {
|
||||
steps: updatedSteps,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
name: `v${workflow.versions.length + 1}`,
|
||||
status: 'DRAFT',
|
||||
trigger: workflow.currentVersion.trigger,
|
||||
steps: updatedSteps,
|
||||
});
|
||||
};
|
||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||
|
||||
const createStep = async (newStepType: WorkflowStepType) => {
|
||||
if (!isDefined(workflowCreateStepFromParentStepId)) {
|
||||
throw new Error('Select a step to create a new step from first.');
|
||||
}
|
||||
|
||||
const newStep = getStepDefaultDefinition({
|
||||
type: newStepType,
|
||||
activeObjectMetadataItems,
|
||||
});
|
||||
const workflowVersion = await getUpdatableWorkflowVersion(workflow);
|
||||
|
||||
const outputSchema = (
|
||||
await computeStepOutputSchema({
|
||||
step: newStep,
|
||||
const createdStep = (
|
||||
await createWorkflowVersionStep({
|
||||
workflowVersionId: workflowVersion.id,
|
||||
stepType: newStepType,
|
||||
})
|
||||
)?.data?.computeStepOutputSchema;
|
||||
)?.data?.createWorkflowVersionStep;
|
||||
|
||||
newStep.settings = {
|
||||
...newStep.settings,
|
||||
outputSchema: outputSchema || {},
|
||||
};
|
||||
if (!createdStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
await insertNodeAndSave({
|
||||
parentNodeId: workflowCreateStepFromParentStepId,
|
||||
nodeToAdd: newStep,
|
||||
});
|
||||
|
||||
setWorkflowSelectedNode(newStep.id);
|
||||
setWorkflowSelectedNode(createdStep.id);
|
||||
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
|
||||
|
||||
/**
|
||||
@ -120,7 +59,7 @@ export const useCreateStep = ({
|
||||
*
|
||||
* Selecting the node will cause a right drawer to open in order to edit the step.
|
||||
*/
|
||||
setWorkflowDiagramTriggerNodeSelection(newStep.id);
|
||||
setWorkflowDiagramTriggerNodeSelection(createdStep.id);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
import { useApolloClient, useMutation } from '@apollo/client';
|
||||
import {
|
||||
CreateWorkflowVersionStepMutation,
|
||||
CreateWorkflowVersionStepMutationVariables,
|
||||
CreateWorkflowVersionStepInput,
|
||||
} from '~/generated/graphql';
|
||||
import { CREATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/createWorkflowVersionStep';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
|
||||
export const useCreateWorkflowVersionStep = () => {
|
||||
const apolloClient = useApolloClient();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
const [mutate] = useMutation<
|
||||
CreateWorkflowVersionStepMutation,
|
||||
CreateWorkflowVersionStepMutationVariables
|
||||
>(CREATE_WORKFLOW_VERSION_STEP, {
|
||||
client: apolloClient,
|
||||
});
|
||||
const createWorkflowVersionStep = async (
|
||||
input: CreateWorkflowVersionStepInput,
|
||||
) => {
|
||||
const result = await mutate({
|
||||
variables: { input },
|
||||
});
|
||||
const createdStep = result?.data?.createWorkflowVersionStep;
|
||||
if (!isDefined(createdStep)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedRecord = getRecordFromCache<WorkflowVersion>(
|
||||
input.workflowVersionId,
|
||||
);
|
||||
if (!cachedRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCachedRecord = {
|
||||
...cachedRecord,
|
||||
steps: [...(cachedRecord.steps || []), createdStep],
|
||||
};
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: newCachedRecord,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
return { createWorkflowVersionStep };
|
||||
};
|
||||
@ -1,81 +0,0 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
|
||||
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
|
||||
import {
|
||||
WorkflowVersion,
|
||||
WorkflowWithCurrentVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { removeStep } from '@/workflow/utils/removeStep';
|
||||
|
||||
export const useDeleteOneStep = ({
|
||||
stepId,
|
||||
workflow,
|
||||
}: {
|
||||
stepId: string;
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const { closeRightDrawer } = useRightDrawer();
|
||||
|
||||
const { updateOneRecord: updateOneWorkflowVersion } =
|
||||
useUpdateOneRecord<WorkflowVersion>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||
|
||||
const deleteOneStep = async () => {
|
||||
closeRightDrawer();
|
||||
|
||||
if (workflow.currentVersion.status !== 'DRAFT') {
|
||||
const newVersionName = `v${workflow.versions.length + 1}`;
|
||||
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
name: newVersionName,
|
||||
status: 'DRAFT',
|
||||
trigger: null,
|
||||
steps: workflow.currentVersion.steps,
|
||||
});
|
||||
} else {
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
name: newVersionName,
|
||||
status: 'DRAFT',
|
||||
trigger: workflow.currentVersion.trigger,
|
||||
steps: removeStep({
|
||||
steps: workflow.currentVersion.steps ?? [],
|
||||
stepId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
await updateOneWorkflowVersion({
|
||||
idToUpdate: workflow.currentVersion.id,
|
||||
updateOneRecordInput: {
|
||||
trigger: null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await updateOneWorkflowVersion({
|
||||
idToUpdate: workflow.currentVersion.id,
|
||||
updateOneRecordInput: {
|
||||
steps: removeStep({
|
||||
steps: workflow.currentVersion.steps ?? [],
|
||||
stepId,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
deleteOneStep,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
import {
|
||||
WorkflowWithCurrentVersion,
|
||||
WorkflowVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { useDeleteWorkflowVersionStep } from '@/workflow/hooks/useDeleteWorkflowVersionStep';
|
||||
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
|
||||
export const useDeleteStep = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const { deleteWorkflowVersionStep } = useDeleteWorkflowVersionStep();
|
||||
const { updateOneRecord: updateOneWorkflowVersion } =
|
||||
useUpdateOneRecord<WorkflowVersion>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||
const { closeRightDrawer } = useRightDrawer();
|
||||
|
||||
const deleteStep = async (stepId: string) => {
|
||||
closeRightDrawer();
|
||||
const workflowVersion = await getUpdatableWorkflowVersion(workflow);
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
await updateOneWorkflowVersion({
|
||||
idToUpdate: workflow.currentVersion.id,
|
||||
updateOneRecordInput: {
|
||||
trigger: null,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
await deleteWorkflowVersionStep({
|
||||
workflowVersionId: workflowVersion.id,
|
||||
stepId,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
deleteStep,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,64 @@
|
||||
import { useApolloClient, useMutation } from '@apollo/client';
|
||||
import {
|
||||
DeleteWorkflowVersionStepMutation,
|
||||
DeleteWorkflowVersionStepMutationVariables,
|
||||
DeleteWorkflowVersionStepInput,
|
||||
WorkflowAction,
|
||||
} from '~/generated/graphql';
|
||||
import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
|
||||
export const useDeleteWorkflowVersionStep = () => {
|
||||
const apolloClient = useApolloClient();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
const [mutate] = useMutation<
|
||||
DeleteWorkflowVersionStepMutation,
|
||||
DeleteWorkflowVersionStepMutationVariables
|
||||
>(DELETE_WORKFLOW_VERSION_STEP, {
|
||||
client: apolloClient,
|
||||
});
|
||||
const deleteWorkflowVersionStep = async (
|
||||
input: DeleteWorkflowVersionStepInput,
|
||||
) => {
|
||||
const result = await mutate({ variables: { input } });
|
||||
const deletedStep = result?.data?.deleteWorkflowVersionStep;
|
||||
if (!isDefined(deletedStep)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedRecord = getRecordFromCache<WorkflowVersion>(
|
||||
input.workflowVersionId,
|
||||
);
|
||||
if (!cachedRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCachedRecord = {
|
||||
...cachedRecord,
|
||||
steps: (cachedRecord.steps || []).filter(
|
||||
(step: WorkflowAction) => step.id !== deletedStep.id,
|
||||
),
|
||||
};
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: newCachedRecord,
|
||||
});
|
||||
};
|
||||
|
||||
return { deleteWorkflowVersionStep };
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
|
||||
|
||||
export const useGetUpdatableWorkflowVersion = () => {
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||
const getUpdatableWorkflowVersion = async (
|
||||
workflow: WorkflowWithCurrentVersion,
|
||||
) => {
|
||||
if (workflow.currentVersion.status === 'DRAFT') {
|
||||
return workflow.currentVersion;
|
||||
}
|
||||
|
||||
return await createNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
name: `v${workflow.versions.length + 1}`,
|
||||
status: 'DRAFT',
|
||||
trigger: workflow.currentVersion.trigger,
|
||||
steps: workflow.currentVersion.steps,
|
||||
});
|
||||
};
|
||||
|
||||
return { getUpdatableWorkflowVersion };
|
||||
};
|
||||
@ -5,18 +5,16 @@ import {
|
||||
} from '@/workflow/types/WorkflowDiagram';
|
||||
import { useReactFlow } from '@xyflow/react';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const useTriggerNodeSelection = () => {
|
||||
const reactflow = useReactFlow<WorkflowDiagramNode, WorkflowDiagramEdge>();
|
||||
|
||||
const workflowDiagramTriggerNodeSelection = useRecoilValue(
|
||||
workflowDiagramTriggerNodeSelectionState,
|
||||
);
|
||||
const setWorkflowDiagramTriggerNodeSelection = useSetRecoilState(
|
||||
workflowDiagramTriggerNodeSelectionState,
|
||||
);
|
||||
const [
|
||||
workflowDiagramTriggerNodeSelection,
|
||||
setWorkflowDiagramTriggerNodeSelection,
|
||||
] = useRecoilState(workflowDiagramTriggerNodeSelectionState);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(workflowDiagramTriggerNodeSelection)) {
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
import {
|
||||
WorkflowStep,
|
||||
WorkflowWithCurrentVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
||||
import { useUpdateWorkflowVersionStep } from '@/workflow/hooks/useUpdateWorkflowVersionStep';
|
||||
|
||||
export const useUpdateStep = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||
const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
|
||||
|
||||
const updateStep = async <T extends WorkflowStep>(updatedStep: T) => {
|
||||
if (!isDefined(workflow.currentVersion)) {
|
||||
throw new Error('Can not update an undefined workflow version.');
|
||||
}
|
||||
|
||||
const workflowVersion = await getUpdatableWorkflowVersion(workflow);
|
||||
await updateWorkflowVersionStep({
|
||||
workflowVersionId: workflowVersion.id,
|
||||
step: updatedStep,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
updateStep,
|
||||
};
|
||||
};
|
||||
@ -1,73 +1,69 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
|
||||
import { useApolloClient, useMutation } from '@apollo/client';
|
||||
import {
|
||||
WorkflowStep,
|
||||
WorkflowVersion,
|
||||
WorkflowWithCurrentVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { replaceStep } from '@/workflow/utils/replaceStep';
|
||||
UpdateWorkflowVersionStepInput,
|
||||
UpdateWorkflowVersionStepMutation,
|
||||
UpdateWorkflowVersionStepMutationVariables,
|
||||
WorkflowAction,
|
||||
} from '~/generated/graphql';
|
||||
import { UPDATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/updateWorkflowVersionStep';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
|
||||
export const useUpdateWorkflowVersionStep = ({
|
||||
workflow,
|
||||
stepId,
|
||||
}: {
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
stepId: string;
|
||||
}) => {
|
||||
const { updateOneRecord: updateOneWorkflowVersion } =
|
||||
useUpdateOneRecord<WorkflowVersion>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||
const { computeStepOutputSchema } = useComputeStepOutputSchema();
|
||||
|
||||
const updateStep = async <T extends WorkflowStep>(updatedStep: T) => {
|
||||
if (!isDefined(workflow.currentVersion)) {
|
||||
throw new Error('Can not update an undefined workflow version.');
|
||||
}
|
||||
|
||||
const outputSchema = (
|
||||
await computeStepOutputSchema({
|
||||
step: updatedStep,
|
||||
})
|
||||
)?.data?.computeStepOutputSchema;
|
||||
|
||||
updatedStep.settings = {
|
||||
...updatedStep.settings,
|
||||
outputSchema: outputSchema || {},
|
||||
};
|
||||
|
||||
const updatedSteps = replaceStep({
|
||||
steps: workflow.currentVersion.steps ?? [],
|
||||
stepId,
|
||||
stepToReplace: updatedStep,
|
||||
});
|
||||
|
||||
if (workflow.currentVersion.status === 'DRAFT') {
|
||||
await updateOneWorkflowVersion({
|
||||
idToUpdate: workflow.currentVersion.id,
|
||||
updateOneRecordInput: {
|
||||
steps: updatedSteps,
|
||||
},
|
||||
});
|
||||
export const useUpdateWorkflowVersionStep = () => {
|
||||
const apolloClient = useApolloClient();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
const [mutate] = useMutation<
|
||||
UpdateWorkflowVersionStepMutation,
|
||||
UpdateWorkflowVersionStepMutationVariables
|
||||
>(UPDATE_WORKFLOW_VERSION_STEP, {
|
||||
client: apolloClient,
|
||||
});
|
||||
|
||||
const updateWorkflowVersionStep = async (
|
||||
input: UpdateWorkflowVersionStepInput,
|
||||
) => {
|
||||
const result = await mutate({ variables: { input } });
|
||||
const updatedStep = result?.data?.updateWorkflowVersionStep;
|
||||
if (!isDefined(updatedStep)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
name: `v${workflow.versions.length + 1}`,
|
||||
status: 'DRAFT',
|
||||
trigger: workflow.currentVersion.trigger,
|
||||
steps: updatedSteps,
|
||||
const cachedRecord = getRecordFromCache<WorkflowVersion>(
|
||||
input.workflowVersionId,
|
||||
);
|
||||
if (!cachedRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCachedRecord = {
|
||||
...cachedRecord,
|
||||
steps: (cachedRecord.steps || []).map((step: WorkflowAction) => {
|
||||
if (step.id === updatedStep.id) {
|
||||
return updatedStep;
|
||||
}
|
||||
return step;
|
||||
}),
|
||||
};
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: newCachedRecord,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
updateStep,
|
||||
};
|
||||
return { updateWorkflowVersionStep };
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
|
||||
import {
|
||||
WorkflowTrigger,
|
||||
WorkflowVersion,
|
||||
@ -8,6 +7,7 @@ import {
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema';
|
||||
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
||||
|
||||
export const useUpdateWorkflowVersionTrigger = ({
|
||||
workflow,
|
||||
@ -19,7 +19,7 @@ export const useUpdateWorkflowVersionTrigger = ({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||
|
||||
const { computeStepOutputSchema } = useComputeStepOutputSchema();
|
||||
|
||||
@ -28,34 +28,24 @@ export const useUpdateWorkflowVersionTrigger = ({
|
||||
throw new Error('Can not update an undefined workflow version.');
|
||||
}
|
||||
|
||||
if (workflow.currentVersion.status === 'DRAFT') {
|
||||
const outputSchema = (
|
||||
await computeStepOutputSchema({
|
||||
step: updatedTrigger,
|
||||
})
|
||||
)?.data?.computeStepOutputSchema;
|
||||
const workflowVersion = await getUpdatableWorkflowVersion(workflow);
|
||||
|
||||
updatedTrigger.settings = {
|
||||
...updatedTrigger.settings,
|
||||
outputSchema: outputSchema || {},
|
||||
};
|
||||
const outputSchema = (
|
||||
await computeStepOutputSchema({
|
||||
step: updatedTrigger,
|
||||
})
|
||||
)?.data?.computeStepOutputSchema;
|
||||
|
||||
await updateOneWorkflowVersion({
|
||||
idToUpdate: workflow.currentVersion.id,
|
||||
updateOneRecordInput: {
|
||||
trigger: updatedTrigger,
|
||||
},
|
||||
});
|
||||
updatedTrigger.settings = {
|
||||
...updatedTrigger.settings,
|
||||
outputSchema: outputSchema || {},
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
name: `v${workflow.versions.length + 1}`,
|
||||
status: 'DRAFT',
|
||||
trigger: updatedTrigger,
|
||||
steps: workflow.currentVersion.steps,
|
||||
await updateOneWorkflowVersion({
|
||||
idToUpdate: workflowVersion.id,
|
||||
updateOneRecordInput: {
|
||||
trigger: updatedTrigger,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import {
|
||||
Workflow,
|
||||
WorkflowVersion,
|
||||
WorkflowWithCurrentVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { useMemo } from 'react';
|
||||
@ -19,40 +17,25 @@ export const useWorkflowWithCurrentVersion = (
|
||||
id: true,
|
||||
name: true,
|
||||
statuses: true,
|
||||
versions: {
|
||||
totalCount: true,
|
||||
},
|
||||
versions: true,
|
||||
},
|
||||
skip: !isDefined(workflowId),
|
||||
});
|
||||
|
||||
const {
|
||||
records: mostRecentWorkflowVersions,
|
||||
loading: loadingMostRecentWorkflowVersions,
|
||||
} = useFindManyRecords<WorkflowVersion>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
filter: {
|
||||
workflowId: {
|
||||
eq: workflowId,
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
createdAt: 'DescNullsLast',
|
||||
},
|
||||
],
|
||||
skip: !isDefined(workflowId),
|
||||
});
|
||||
|
||||
const workflowWithCurrentVersion = useMemo(() => {
|
||||
if (!isDefined(workflow) || loadingMostRecentWorkflowVersions) {
|
||||
return useMemo(() => {
|
||||
if (!isDefined(workflow)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const draftVersion = mostRecentWorkflowVersions.find(
|
||||
const draftVersion = workflow.versions.find(
|
||||
(workflowVersion) => workflowVersion.status === 'DRAFT',
|
||||
);
|
||||
const latestVersion = mostRecentWorkflowVersions[0];
|
||||
|
||||
const workflowVersions = [...workflow.versions];
|
||||
|
||||
workflowVersions.sort((a, b) => (a.createdAt > b.createdAt ? -1 : 1));
|
||||
|
||||
const latestVersion = workflowVersions[0];
|
||||
|
||||
const currentVersion = draftVersion ?? latestVersion;
|
||||
|
||||
@ -64,7 +47,5 @@ export const useWorkflowWithCurrentVersion = (
|
||||
...workflow,
|
||||
currentVersion,
|
||||
};
|
||||
}, [loadingMostRecentWorkflowVersions, mostRecentWorkflowVersions, workflow]);
|
||||
|
||||
return workflowWithCurrentVersion;
|
||||
}, [workflow]);
|
||||
};
|
||||
|
||||
@ -9,7 +9,7 @@ export type InputSchemaPropertyType =
|
||||
| 'unknown'
|
||||
| FieldMetadataType;
|
||||
|
||||
type InputSchemaProperty = {
|
||||
export type InputSchemaProperty = {
|
||||
type: InputSchemaPropertyType;
|
||||
enum?: string[];
|
||||
items?: InputSchemaProperty;
|
||||
|
||||
@ -1,142 +0,0 @@
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { getStepDefaultDefinition } from '../getStepDefaultDefinition';
|
||||
|
||||
it('returns a valid definition for CODE actions', () => {
|
||||
expect(
|
||||
getStepDefaultDefinition({
|
||||
type: 'CODE',
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
id: expect.any(String),
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: '',
|
||||
serverlessFunctionVersion: '',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a valid definition for SEND_EMAIL actions', () => {
|
||||
expect(
|
||||
getStepDefaultDefinition({
|
||||
type: 'SEND_EMAIL',
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
id: expect.any(String),
|
||||
name: 'Send Email',
|
||||
type: 'SEND_EMAIL',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
connectedAccountId: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
body: '',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a valid definition for RECORD_CRUD.CREATE actions', () => {
|
||||
expect(
|
||||
getStepDefaultDefinition({
|
||||
type: 'RECORD_CRUD.CREATE',
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
id: expect.any(String),
|
||||
name: 'Create Record',
|
||||
type: 'RECORD_CRUD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
type: 'CREATE',
|
||||
objectName: generatedMockObjectMetadataItems[0].nameSingular,
|
||||
objectRecord: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a valid definition for RECORD_CRUD.UPDATE actions', () => {
|
||||
expect(
|
||||
getStepDefaultDefinition({
|
||||
type: 'RECORD_CRUD.UPDATE',
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
id: expect.any(String),
|
||||
name: 'Update Record',
|
||||
type: 'RECORD_CRUD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
type: 'UPDATE',
|
||||
objectName: generatedMockObjectMetadataItems[0].nameSingular,
|
||||
objectRecord: {},
|
||||
objectRecordId: '',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("throws for RECORD_CRUD.DELETE actions as it's not implemented yet", () => {
|
||||
expect(() => {
|
||||
getStepDefaultDefinition({
|
||||
type: 'RECORD_CRUD.DELETE',
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
});
|
||||
}).toThrow('Not implemented yet');
|
||||
});
|
||||
|
||||
it('throws when providing an unknown type', () => {
|
||||
expect(() => {
|
||||
getStepDefaultDefinition({
|
||||
type: 'unknown' as any,
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
});
|
||||
}).toThrow('Unknown type: unknown');
|
||||
});
|
||||
@ -0,0 +1,129 @@
|
||||
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;
|
||||
};
|
||||
@ -1,100 +0,0 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { WorkflowStep, WorkflowStepType } from '@/workflow/types/Workflow';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const BASE_DEFAULT_STEP_SETTINGS = {
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getStepDefaultDefinition = ({
|
||||
type,
|
||||
activeObjectMetadataItems,
|
||||
}: {
|
||||
type: WorkflowStepType;
|
||||
activeObjectMetadataItems: ObjectMetadataItem[];
|
||||
}): WorkflowStep => {
|
||||
const newStepId = v4();
|
||||
|
||||
switch (type) {
|
||||
case 'CODE': {
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: '',
|
||||
serverlessFunctionVersion: '',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
...BASE_DEFAULT_STEP_SETTINGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Send Email',
|
||||
type: 'SEND_EMAIL',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
connectedAccountId: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
body: '',
|
||||
},
|
||||
...BASE_DEFAULT_STEP_SETTINGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'RECORD_CRUD.CREATE': {
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Create Record',
|
||||
type: 'RECORD_CRUD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
type: 'CREATE',
|
||||
objectName: activeObjectMetadataItems[0].nameSingular,
|
||||
objectRecord: {},
|
||||
},
|
||||
...BASE_DEFAULT_STEP_SETTINGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'RECORD_CRUD.UPDATE':
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Update Record',
|
||||
type: 'RECORD_CRUD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
type: 'UPDATE',
|
||||
objectName: activeObjectMetadataItems[0].nameSingular,
|
||||
objectRecordId: '',
|
||||
objectRecord: {},
|
||||
},
|
||||
...BASE_DEFAULT_STEP_SETTINGS,
|
||||
},
|
||||
};
|
||||
case 'RECORD_CRUD.DELETE': {
|
||||
throw new Error('Not implemented yet');
|
||||
}
|
||||
default: {
|
||||
return assertUnreachable(type, `Unknown type: ${type}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -81,6 +81,7 @@ export const WorkflowEditActionFormRecordCreate = ({
|
||||
const selectedObjectMetadataItem = activeObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === selectedObjectMetadataItemNameSingular,
|
||||
);
|
||||
|
||||
if (!isDefined(selectedObjectMetadataItem)) {
|
||||
throw new Error('Should have found the metadata item');
|
||||
}
|
||||
@ -135,13 +136,9 @@ export const WorkflowEditActionFormRecordCreate = ({
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
HeaderIcon={
|
||||
<IconAddressBook
|
||||
color={theme.font.color.tertiary}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
}
|
||||
headerTitle={headerTitle}
|
||||
Icon={IconAddressBook}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Action"
|
||||
>
|
||||
<Select
|
||||
|
||||
@ -135,13 +135,9 @@ export const WorkflowEditActionFormRecordUpdate = ({
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
HeaderIcon={
|
||||
<IconAddressBook
|
||||
color={theme.font.color.tertiary}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
}
|
||||
headerTitle={headerTitle}
|
||||
Icon={IconAddressBook}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Action"
|
||||
>
|
||||
<Select
|
||||
|
||||
@ -182,8 +182,9 @@ export const WorkflowEditActionFormSendEmail = ({
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
HeaderIcon={<IconMail color={theme.color.blue} />}
|
||||
headerTitle={headerTitle}
|
||||
Icon={IconMail}
|
||||
iconColor={theme.color.blue}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Email"
|
||||
>
|
||||
<Controller
|
||||
|
||||
@ -1,6 +1,59 @@
|
||||
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||
import { WorkflowEditActionFormServerlessFunctionInner } from '@/workflow/components/WorkflowEditActionFormServerlessFunctionInner';
|
||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
|
||||
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/utils/mergeDefaultFunctionInputAndFunctionInput';
|
||||
import { setNestedValue } from '@/workflow/utils/setNestedValue';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Fragment, ReactNode, useState } from 'react';
|
||||
import {
|
||||
CodeEditor,
|
||||
HorizontalSeparator,
|
||||
IconCode,
|
||||
isDefined,
|
||||
} from 'twenty-ui';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
||||
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
||||
import { AutoTypings } from 'monaco-editor-auto-typings';
|
||||
import { editor } from 'monaco-editor';
|
||||
import { Monaco } from '@monaco-editor/react';
|
||||
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||
import { getFunctionInputSchema } from '@/workflow/utils/getFunctionInputSchema';
|
||||
import { getDefaultFunctionInputFromInputSchema } from '@/workflow/utils/getDefaultFunctionInputFromInputSchema';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: inline-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 StyledInputContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type WorkflowEditActionFormServerlessFunctionProps = {
|
||||
action: WorkflowCodeAction;
|
||||
@ -14,21 +67,227 @@ type WorkflowEditActionFormServerlessFunctionProps = {
|
||||
};
|
||||
};
|
||||
|
||||
type ServerlessFunctionInputFormData = {
|
||||
[field: string]: string | ServerlessFunctionInputFormData;
|
||||
};
|
||||
|
||||
const INDEX_FILE_PATH = 'src/index.ts';
|
||||
|
||||
export const WorkflowEditActionFormServerlessFunction = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionFormServerlessFunctionProps) => {
|
||||
const { loading: isLoadingServerlessFunctions } =
|
||||
useGetManyServerlessFunctions();
|
||||
const theme = useTheme();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction();
|
||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
|
||||
const workflowId = useRecoilValue(workflowIdState);
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||
const { availablePackages } = useGetAvailablePackages({
|
||||
id: serverlessFunctionId,
|
||||
});
|
||||
|
||||
if (isLoadingServerlessFunctions) {
|
||||
return null;
|
||||
}
|
||||
const { formValues, setFormValues, loading } =
|
||||
useServerlessFunctionUpdateFormState(serverlessFunctionId);
|
||||
|
||||
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 handleSave = usePreventOverlapCallback(save, 1000);
|
||||
|
||||
const onCodeChange = async (value: string) => {
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
code: { ...prevState.code, [INDEX_FILE_PATH]: value },
|
||||
}));
|
||||
await handleSave();
|
||||
await handleUpdateFunctionInputSchema();
|
||||
};
|
||||
|
||||
const updateFunctionInputSchema = async () => {
|
||||
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);
|
||||
};
|
||||
|
||||
const handleUpdateFunctionInputSchema = useDebouncedCallback(
|
||||
updateFunctionInputSchema,
|
||||
100,
|
||||
);
|
||||
|
||||
const [functionInput, setFunctionInput] =
|
||||
useState<ServerlessFunctionInputFormData>(
|
||||
action.settings.input.serverlessFunctionInput,
|
||||
);
|
||||
|
||||
const updateFunctionInput = useDebouncedCallback(
|
||||
async (newFunctionInput: object) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
...action.settings.input,
|
||||
serverlessFunctionInput: newFunctionInput,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
1_000,
|
||||
);
|
||||
|
||||
const handleInputChange = async (value: any, path: string[]) => {
|
||||
const updatedFunctionInput = setNestedValue(functionInput, path, value);
|
||||
|
||||
setFunctionInput(updatedFunctionInput);
|
||||
|
||||
await updateFunctionInput(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;
|
||||
};
|
||||
|
||||
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>
|
||||
<StyledInputContainer>
|
||||
{renderFields(inputValue, currentPath, false)}
|
||||
</StyledInputContainer>
|
||||
</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 headerTitle = isDefined(action.name)
|
||||
? action.name
|
||||
: 'Code - Serverless Function';
|
||||
|
||||
const handleEditorDidMount = async (
|
||||
editor: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => {
|
||||
await AutoTypings.create(editor, {
|
||||
monaco,
|
||||
preloadPackages: true,
|
||||
onlySpecifiedPackages: true,
|
||||
versions: availablePackages,
|
||||
debounceDuration: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const onActionUpdate = (actionUpdate: Partial<WorkflowCodeAction>) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions?.onActionUpdate({
|
||||
...action,
|
||||
...actionUpdate,
|
||||
});
|
||||
};
|
||||
|
||||
const checkWorkflowUpdatable = async () => {
|
||||
if (actionOptions.readonly === true || !isDefined(workflow)) {
|
||||
return;
|
||||
}
|
||||
await getUpdatableWorkflowVersion(workflow);
|
||||
};
|
||||
|
||||
return (
|
||||
<WorkflowEditActionFormServerlessFunctionInner
|
||||
action={action}
|
||||
actionOptions={actionOptions}
|
||||
/>
|
||||
!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}
|
||||
/>
|
||||
{renderFields(functionInput)}
|
||||
</WorkflowEditGenericFormBase>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user