Skip to content

Commit

Permalink
Scaffold empty workflow (#6926)
Browse files Browse the repository at this point in the history
- Create a workflow version when the user visits an empty workflow.
- If the trigger is not defined yet and the user selects either the
standard object type or the event type first, we automatically select
the first option of the other value. Indeed, every state update is
automatically saved on the backend and we need both standard object and
event types to save the event name.
- Introduces a change in the backend. I removed the assertions that
throw when a workflow version is not complete, that is, when it doesn't
have a defined trigger, which is the case when scaffolding a new
workflow with a first empty workflow version.
- We should keep validating the workflow versions, at least when we
publish them. That should be done in a second step.
  • Loading branch information
Devessier authored Sep 12, 2024
1 parent 3c41687 commit 3548751
Show file tree
Hide file tree
Showing 26 changed files with 548 additions and 269 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ const getStepDefinitionOrThrow = ({

if (stepId === TRIGGER_STEP_ID) {
if (!isDefined(currentVersion.trigger)) {
throw new Error('Expected to find the definition of the trigger');
return {
type: 'trigger',
definition: undefined,
} as const;
}

return {
Expand All @@ -33,7 +36,9 @@ const getStepDefinitionOrThrow = ({
}

if (!isDefined(currentVersion.steps)) {
throw new Error('Expected to find an array of steps');
throw new Error(
'Malformed workflow version: missing steps information; be sure to create at least one step before trying to edit one',
);
}

const selectedNodePosition = findStepPositionOrThrow({
Expand Down Expand Up @@ -74,15 +79,15 @@ export const RightDrawerWorkflowEditStepContent = ({
return (
<WorkflowEditTriggerForm
trigger={stepDefinition.definition}
onUpdateTrigger={updateTrigger}
onTriggerUpdate={updateTrigger}
/>
);
}

return (
<WorkflowEditActionForm
action={stepDefinition.definition}
onUpdateAction={updateStep}
onActionUpdate={updateStep}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { WorkflowDiagramCanvas } from '@/workflow/components/WorkflowDiagramCanvas';
import { WorkflowShowPageEffect } from '@/workflow/components/WorkflowShowPageEffect';
import { WorkflowEffect } from '@/workflow/components/WorkflowEffect';
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
import styled from '@emotion/styled';
import '@xyflow/react/dist/style.css';
Expand Down Expand Up @@ -36,7 +36,7 @@ export const Workflow = ({

return (
<>
<WorkflowShowPageEffect workflowId={workflowId} />
<WorkflowEffect workflowId={workflowId} />

<StyledFlowContainer>
{workflowDiagram === undefined ? null : (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
import styled from '@emotion/styled';
import { Handle, Position } from '@xyflow/react';
import React from 'react';
import { capitalize } from '~/utils/string/capitalize';

type Variant = 'placeholder';

const StyledStepNodeContainer = styled.div`
display: flex;
flex-direction: column;
padding-bottom: 12px;
padding-top: 6px;
`;

const StyledStepNodeType = styled.div`
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.border.radius.sm}
${({ theme }) => theme.border.radius.sm} 0 0;
color: ${({ theme }) => theme.color.gray50};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
position: absolute;
top: 0;
transform: translateY(-100%);
.selectable.selected &,
.selectable:focus &,
.selectable:focus-visible & {
background-color: ${({ theme }) => theme.color.blue};
color: ${({ theme }) => theme.font.color.inverted};
}
`;

const StyledStepNodeInnerContainer = styled.div<{ variant?: Variant }>`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-style: ${({ variant }) =>
variant === 'placeholder' ? 'dashed' : null};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
position: relative;
box-shadow: ${({ variant, theme }) =>
variant === 'placeholder' ? 'none' : theme.boxShadow.superHeavy};
.selectable.selected &,
.selectable:focus &,
.selectable:focus-visible & {
background-color: ${({ theme }) => theme.color.blue10};
border-color: ${({ theme }) => theme.color.blue};
}
`;

const StyledStepNodeLabel = styled.div<{ variant?: Variant }>`
align-items: center;
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
column-gap: ${({ theme }) => theme.spacing(2)};
color: ${({ variant, theme }) =>
variant === 'placeholder' ? theme.font.color.extraLight : null};
`;

const StyledSourceHandle = styled(Handle)`
background-color: ${({ theme }) => theme.color.gray50};
`;

export const StyledTargetHandle = styled(Handle)`
visibility: hidden;
`;

export const WorkflowDiagramBaseStepNode = ({
nodeType,
label,
variant,
Icon,
}: {
nodeType: WorkflowDiagramStepNodeData['nodeType'];
label: string;
variant?: Variant;
Icon?: React.ReactNode;
}) => {
return (
<StyledStepNodeContainer>
{nodeType !== 'trigger' ? (
<StyledTargetHandle type="target" position={Position.Top} />
) : null}

<StyledStepNodeInnerContainer variant={variant}>
<StyledStepNodeType>{capitalize(nodeType)}</StyledStepNodeType>

<StyledStepNodeLabel variant={variant}>
{Icon}

{label}
</StyledStepNodeLabel>
</StyledStepNodeInnerContainer>

<StyledSourceHandle type="source" position={Position.Bottom} />
</StyledStepNodeContainer>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { WorkflowDiagramCanvasEffect } from '@/workflow/components/WorkflowDiagramCanvasEffect';
import { WorkflowDiagramCreateStepNode } from '@/workflow/components/WorkflowDiagramCreateStepNode';
import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagramEmptyTrigger';
import { WorkflowDiagramStepNode } from '@/workflow/components/WorkflowDiagramStepNode';
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
import {
Expand Down Expand Up @@ -72,6 +73,7 @@ export const WorkflowDiagramCanvas = ({
nodeTypes={{
default: WorkflowDiagramStepNode,
'create-step': WorkflowDiagramCreateStepNode,
'empty-trigger': WorkflowDiagramEmptyTrigger,
}}
fitView
nodes={nodes.map((node) => ({ ...node, draggable: false }))}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { WorkflowDiagramBaseStepNode } from '@/workflow/components/WorkflowDiagramBaseStepNode';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconPlaylistAdd } from 'twenty-ui';

const StyledStepNodeLabelIconContainer = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.transparent.light};
border-radius: ${({ theme }) => theme.spacing(1)};
display: flex;
justify-content: center;
padding: ${({ theme }) => theme.spacing(1)};
`;

export const WorkflowDiagramEmptyTrigger = () => {
const theme = useTheme();

return (
<WorkflowDiagramBaseStepNode
label="Add a Trigger"
nodeType="trigger"
variant="placeholder"
Icon={
<StyledStepNodeLabelIconContainer>
<IconPlaylistAdd size={16} color={theme.font.color.tertiary} />
</StyledStepNodeLabelIconContainer>
}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,87 +1,54 @@
import { WorkflowDiagramBaseStepNode } from '@/workflow/components/WorkflowDiagramBaseStepNode';
import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Handle, Position } from '@xyflow/react';
import { IconCode, IconPlaylistAdd } from 'twenty-ui';

const StyledStepNodeContainer = styled.div`
const StyledStepNodeLabelIconContainer = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.transparent.light};
border-radius: ${({ theme }) => theme.spacing(1)};
display: flex;
flex-direction: column;
padding-bottom: 12px;
padding-top: 6px;
`;

const StyledStepNodeType = styled.div`
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.border.radius.sm}
${({ theme }) => theme.border.radius.sm} 0 0;
color: ${({ theme }) => theme.color.gray50};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
position: absolute;
top: 0;
transform: translateY(-100%);
.selectable.selected &,
.selectable:focus &,
.selectable:focus-visible & {
background-color: ${({ theme }) => theme.color.blue};
color: ${({ theme }) => theme.font.color.inverted};
}
`;

const StyledStepNodeInnerContainer = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
position: relative;
box-shadow: ${({ theme }) => theme.boxShadow.superHeavy};
.selectable.selected &,
.selectable:focus &,
.selectable:focus-visible & {
background-color: ${({ theme }) => theme.color.blue10};
border-color: ${({ theme }) => theme.color.blue};
}
`;

const StyledStepNodeLabel = styled.div`
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;

const StyledSourceHandle = styled(Handle)`
background-color: ${({ theme }) => theme.color.gray50};
`;

export const StyledTargetHandle = styled(Handle)`
visibility: hidden;
justify-content: center;
padding: ${({ theme }) => theme.spacing(1)};
`;

export const WorkflowDiagramStepNode = ({
data,
}: {
data: WorkflowDiagramStepNodeData;
}) => {
return (
<StyledStepNodeContainer>
{data.nodeType !== 'trigger' ? (
<StyledTargetHandle type="target" position={Position.Top} />
) : null}

<StyledStepNodeInnerContainer>
<StyledStepNodeType>{data.nodeType}</StyledStepNodeType>
const theme = useTheme();

const renderStepIcon = () => {
switch (data.nodeType) {
case 'trigger': {
return (
<StyledStepNodeLabelIconContainer>
<IconPlaylistAdd
size={theme.icon.size.sm}
color={theme.font.color.tertiary}
/>
</StyledStepNodeLabelIconContainer>
);
}
case 'action': {
return (
<StyledStepNodeLabelIconContainer>
<IconCode size={theme.icon.size.sm} color={theme.color.orange} />
</StyledStepNodeLabelIconContainer>
);
}
}

return null;
};

<StyledStepNodeLabel>{data.label}</StyledStepNodeLabel>
</StyledStepNodeInnerContainer>

<StyledSourceHandle type="source" position={Position.Bottom} />
</StyledStepNodeContainer>
return (
<WorkflowDiagramBaseStepNode
nodeType={data.nodeType}
label={data.label}
Icon={renderStepIcon()}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ const StyledTriggerSettings = styled.div`

export const WorkflowEditActionForm = ({
action,
onUpdateAction,
onActionUpdate,
}: {
action: WorkflowAction;
onUpdateAction: (trigger: WorkflowAction) => void;
onActionUpdate: (trigger: WorkflowAction) => void;
}) => {
const theme = useTheme();

Expand Down Expand Up @@ -88,7 +88,7 @@ export const WorkflowEditActionForm = ({
value={action.settings.serverlessFunctionId}
options={availableFunctions}
onChange={(updatedFunction) => {
onUpdateAction({
onActionUpdate({
...action,
settings: {
...action.settings,
Expand Down
Loading

0 comments on commit 3548751

Please sign in to comment.