Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visualize Workflows #6697

Merged
merged 26 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3908549
feat: scaffold the workflow editor with xyflow
Devessier Aug 12, 2024
9d33288
feat: automatically position nodes and add + buttons to unlinked nodes
Devessier Aug 13, 2024
ef3b409
feat: fetch workflow last version from the backend and display it as …
Devessier Aug 14, 2024
c85f43e
feat: scaffold workflow right drawer
Devessier Aug 14, 2024
e546d19
feat: put current workflow state in recoil
Devessier Aug 20, 2024
140a84d
lint: rename interface to type and replace variables with one letter
Devessier Aug 20, 2024
1913265
refactor: separate the current workflow states
Devessier Aug 20, 2024
510741e
feat: enable the workflows show page through a feature flag
Devessier Aug 20, 2024
041a40f
refactor: rename flow to workflow diagram to improve clarity
Devessier Aug 20, 2024
8fc7b10
refactor: delete unused route
Devessier Aug 20, 2024
1a1a3d8
fix: correctly type the current workflow error
Devessier Aug 20, 2024
1926923
feat: add missing entry for workflows
Devessier Aug 20, 2024
cfc3af1
fix: redirect to workflows objects view
Devessier Aug 20, 2024
aa69097
refactor: move some workflow types to the dedicated types directory
Devessier Aug 20, 2024
554b54b
refactor: rename states, move types and simplify reactflow state mana…
Devessier Aug 22, 2024
20bcee7
refactor: use uuid v4 and isDefined
Devessier Aug 22, 2024
bdf0677
refactor: create separate type for diagram node and edge and extract …
Devessier Aug 22, 2024
7685838
refactor: drop useless useMemo and compute directly in useEffect
Devessier Aug 22, 2024
9119dce
refactor: put most of the code in the workflows module instead of the…
Devessier Aug 22, 2024
b9544af
feat: comply to the new workflow definition format
Devessier Aug 22, 2024
bec87c5
lint: fix issue
Devessier Aug 23, 2024
fc23fe9
refactor: extract function to its own file in the utils directory
Devessier Aug 23, 2024
e97e2fc
refactor: flatten the components directory for workflows
Devessier Aug 23, 2024
a5354c8
test: test the function that generates the workflow diagram
Devessier Aug 23, 2024
b1b24c1
test: write tests for complex workflows business functions
Devessier Aug 23, 2024
02b3939
lint: fix linting issues
Devessier Aug 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/twenty-front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"workerDirectory": "public"
},
"dependencies": {
"@xyflow/react": "^12.0.4",
"transliteration": "^2.3.5"
}
}
11 changes: 11 additions & 0 deletions packages/twenty-front/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import { SettingsBilling } from '~/pages/settings/SettingsBilling';
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
import { WorkflowShowPage } from '~/pages/workflows/WorkflowShowPage';
import { getPageTitleFromPath } from '~/utils/title-utils';

const ProvidersThatNeedRouterContext = () => {
Expand Down Expand Up @@ -135,6 +136,7 @@ const createRouter = (
isBillingEnabled?: boolean,
isCRMMigrationEnabled?: boolean,
isServerlessFunctionSettingsEnabled?: boolean,
isWorkflowEnabled?: boolean,
) =>
createBrowserRouter(
createRoutesFromElements(
Expand Down Expand Up @@ -163,6 +165,13 @@ const createRouter = (
<Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} />
<Route path={AppPath.RecordShowPage} element={<RecordShowPage />} />

{isWorkflowEnabled === true ? (
<Route
path={AppPath.WorkflowShowPage}
element={<WorkflowShowPage />}
/>
) : null}

<Route
path={AppPath.SettingsCatchAll}
element={
Expand Down Expand Up @@ -326,6 +335,7 @@ export const App = () => {
const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled(
'IS_FUNCTION_SETTINGS_ENABLED',
);
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');

const isBillingPageEnabled =
billing?.isBillingEnabled && !isFreeAccessEnabled;
Expand All @@ -336,6 +346,7 @@ export const App = () => {
isBillingPageEnabled,
isCRMMigrationEnabled,
isServerlessFunctionSettingsEnabled,
isWorkflowEnabled,
)}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ export enum CoreObjectNameSingular {
Webhook = 'webhook',
WorkspaceMember = 'workspaceMember',
MessageThreadSubscriber = 'messageThreadSubscriber',
Workflow = 'workflow',
}
2 changes: 2 additions & 0 deletions packages/twenty-front/src/modules/types/AppPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export enum AppPath {
Developers = `developers`,
DevelopersCatchAll = `/${Developers}/*`,

WorkflowShowPage = `/workflow/:workflowId`,

// Impersonate
Impersonate = '/impersonate/:userId',

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/Compo
import { isDefined } from 'twenty-ui';
import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages';
import { RightDrawerWorkflow } from '@/workflow/components/RightDrawerWorkflow';

const StyledRightDrawerPage = styled.div`
display: flex;
Expand All @@ -35,6 +36,7 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = {
[RightDrawerPages.ViewCalendarEvent]: <RightDrawerCalendarEvent />,
[RightDrawerPages.ViewRecord]: <RightDrawerRecord />,
[RightDrawerPages.Copilot]: <RightDrawerAIChat />,
[RightDrawerPages.Workflow]: <RightDrawerWorkflow />,
};

export const RightDrawerRouter = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const RIGHT_DRAWER_PAGE_ICONS = {
[RightDrawerPages.ViewCalendarEvent]: 'IconCalendarEvent',
[RightDrawerPages.ViewRecord]: 'Icon123',
[RightDrawerPages.Copilot]: 'IconSparkles',
[RightDrawerPages.Workflow]: 'IconSparkles',
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const RIGHT_DRAWER_PAGE_TITLES = {
[RightDrawerPages.ViewCalendarEvent]: 'Calendar Event',
[RightDrawerPages.ViewRecord]: 'Record Editor',
[RightDrawerPages.Copilot]: 'Copilot',
[RightDrawerPages.Workflow]: 'Workflow',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introcuce RightDrawerPages.Action which a generic component for any object
and WorkflowNodeEdit / WorflowConditionEdit...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will fix this issue in my next PR as this PR doesn't implement much things about the Right Drawer.

Things that remain to do:

  • Don't hesitate to create many drawers. Create a drawer per view.

};
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export enum RightDrawerPages {
ViewCalendarEvent = 'view-calendar-event',
ViewRecord = 'view-record',
Copilot = 'copilot',
Workflow = 'workflow',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import styled from '@emotion/styled';

const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
justify-content: flex-start;
overflow-y: auto;
position: relative;
`;

const StyledChatArea = styled.div`
flex: 1;
display: flex;
flex-direction: column;
overflow-y: scroll;
padding: ${({ theme }) => theme.spacing(6)};
padding-bottom: 0px;
`;

const StyledNewMessageArea = styled.div`
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(6)};
padding-top: 0px;
`;

export const RightDrawerWorkflow = () => {
const handleCreateCodeBlock = () => {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: handleCreateCodeBlock is empty. Implement the logic for creating a code block.


return (
<StyledContainer>
<StyledChatArea>{/* TODO */}</StyledChatArea>
<StyledNewMessageArea>
<button onClick={handleCreateCodeBlock}>Create code block</button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using a more specific button component from your UI library instead of a plain HTML button.

</StyledNewMessageArea>
</StyledContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { WorkflowShowPageDiagramCreateStepNode } from '@/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx';
import { WorkflowShowPageDiagramStepNode } from '@/workflow/components/WorkflowShowPageDiagramStepNode';
import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState';
import {
WorkflowDiagram,
WorkflowDiagramEdge,
WorkflowDiagramNode,
} from '@/workflow/types/WorkflowDiagram';
import { getOrganizedDiagram } from '@/workflow/utils/getOrganizedDiagram';
import {
applyEdgeChanges,
applyNodeChanges,
Background,
EdgeChange,
NodeChange,
ReactFlow,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { useMemo } from 'react';
import { useSetRecoilState } from 'recoil';
import { GRAY_SCALE, isDefined } from 'twenty-ui';

export const WorkflowShowPageDiagram = ({
diagram,
}: {
diagram: WorkflowDiagram;
}) => {
const { nodes, edges } = useMemo(
() => getOrganizedDiagram(diagram),
[diagram],
);

const setShowPageWorkflowDiagram = useSetRecoilState(
showPageWorkflowDiagramState,
);

const handleNodesChange = (
nodeChanges: Array<NodeChange<WorkflowDiagramNode>>,
) => {
setShowPageWorkflowDiagram((diagram) => {
if (isDefined(diagram) === false) {
throw new Error(
'It must be impossible for the nodes to be updated if the diagram is not defined yet. Be sure the diagram is rendered only when defined.',
);
}

return {
...diagram,
nodes: applyNodeChanges(nodeChanges, diagram.nodes),
};
});
};

const handleEdgesChange = (
edgeChanges: Array<EdgeChange<WorkflowDiagramEdge>>,
) => {
setShowPageWorkflowDiagram((diagram) => {
if (isDefined(diagram) === false) {
throw new Error(
'It must be impossible for the edges to be updated if the diagram is not defined yet. Be sure the diagram is rendered only when defined.',
);
}

return {
...diagram,
edges: applyEdgeChanges(edgeChanges, diagram.edges),
};
});
};

return (
<ReactFlow
nodeTypes={{
default: WorkflowShowPageDiagramStepNode,
'create-step': WorkflowShowPageDiagramCreateStepNode,
}}
fitView
nodes={nodes.map((node) => ({ ...node, draggable: false }))}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgesChange}
>
<Background color={GRAY_SCALE.gray25} size={2} />
</ReactFlow>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { IconButton } from '@/ui/input/button/components/IconButton';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import styled from '@emotion/styled';
import { Handle, Position } from '@xyflow/react';
import { IconPlus } from 'twenty-ui';

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

export const WorkflowShowPageDiagramCreateStepNode = () => {
const { openRightDrawer } = useRightDrawer();

const handleCreateStepNodeButtonClick = () => {
openRightDrawer(RightDrawerPages.Workflow);
};

return (
<div>
<StyledTargetHandle type="target" position={Position.Top} />

<IconButton Icon={IconPlus} onClick={handleCreateStepNodeButtonClick} />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
import styled from '@emotion/styled';
import { Handle, Position } from '@xyflow/react';

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`
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;
`;

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

<StyledStepNodeInnerContainer>
<StyledStepNodeType>{data.nodeType}</StyledStepNodeType>

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

<StyledSourceHandle type="source" position={Position.Bottom} />
</StyledStepNodeContainer>
);
};
Loading
Loading