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:
martmull
2024-12-03 09:41:13 +01:00
committed by GitHub
parent 9d7632cb4f
commit d0ff1ffd5f
75 changed files with 2192 additions and 1527 deletions

View File

@ -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 (

View File

@ -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

View File

@ -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>
);
};

View File

@ -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>
</>
);

View File

@ -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

View File

@ -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

View File

@ -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 = ({

View File

@ -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': {

View File

@ -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,
],
},