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

Create new steps in workflow editor #6764

Merged
merged 32 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
28bc974
feat: start implementing the flow to create new nodes
Devessier Aug 21, 2024
52b8681
feat: create more realistic options in workflow actions list
Devessier Aug 22, 2024
6f1beca
feat: create a function to add a step in a workflow version
Devessier Aug 26, 2024
58275a3
feat: update workflow version with an additional node
Devessier Aug 27, 2024
e4b93ac
refactor: make the function to add steps focused on steps and not wor…
Devessier Aug 27, 2024
1c60193
refactor: use structuredClone to make a deep copy instead of the old …
Devessier Aug 27, 2024
7b9e960
feat: get the last version by sorting all of them by creation datetime
Devessier Aug 27, 2024
ad0aa21
feat: open the right drawer for the selected node and select node aft…
Devessier Aug 27, 2024
136bd54
feat: centralize what happens when a node is selected
Devessier Aug 27, 2024
52da10b
refactor: fetch the workflow in a separate right drawer component to …
Devessier Aug 28, 2024
5ade683
refactor: rename the name of the recoil state that stores the id of t…
Devessier Aug 28, 2024
23b98fd
refactor: delete unused type
Devessier Aug 28, 2024
57fda7e
docs: document new critical parts of the code
Devessier Aug 28, 2024
f38f3d4
test: polyfill the structuredClone function in front-end tests
Devessier Aug 28, 2024
2764d45
refactor: use logError instead of console.error
Devessier Aug 28, 2024
1fbd7a4
docs: fix misleading comment about shallow vs deep copy
Devessier Aug 28, 2024
95f505c
refactor: rename enum keys about workflow right drawers
Devessier Aug 29, 2024
42c61a1
refactor: delete useless container component
Devessier Aug 29, 2024
d17470b
refactor: simplify condition
Devessier Aug 29, 2024
77f2221
refactor: let the error throw instead of catching it
Devessier Aug 29, 2024
337b5b6
refactor: delete unwanted comment
Devessier Aug 29, 2024
caf95a8
refactor: fix typo in component file name
Devessier Aug 29, 2024
42ca561
refactor: indicate that the function can throw
Devessier Aug 29, 2024
e280b0f
refactor: use a fragment instead of a div container
Devessier Aug 29, 2024
c97cd9b
refactor: use less explicit conditions
Devessier Aug 29, 2024
ab696d5
refactor: prefix the name of the state with "workflow" to emphasize i…
Devessier Aug 29, 2024
0a46f9b
refactor: use isDefined function instead of an explicit check with un…
Devessier Aug 29, 2024
194986b
fix: solve typo in path
Devessier Aug 29, 2024
4cc20b1
refactor: delete comments
Devessier Aug 30, 2024
42fa8f5
refactor: remove comments
Devessier Aug 30, 2024
c3ad01b
test: fix failing tests
Devessier Aug 30, 2024
bb6e62f
fix: add missing keys for enum
Devessier Aug 30, 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
11 changes: 11 additions & 0 deletions packages/twenty-front/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,14 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

/**
* The structuredClone global function is not available in jsdom, it needs to be mocked for now.
*
* The most naive way to mock structuredClone is to use JSON.stringify and JSON.parse. This works
* for arguments with simple types like primitives, arrays and objects, but doesn't work with functions,
* Map, Set, etc.
*/
global.structuredClone = (val) => {
Copy link
Member

Choose a reason for hiding this comment

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

ok, this introduce a new way of cloning object, this should be the new project convention.
Let's share about it in next daily

return JSON.parse(JSON.stringify(val));
};
Comment on lines +14 to +16
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: This mock implementation of structuredClone will not work correctly for complex types like functions, Map, or Set. Consider using a more robust solution for accurate testing.

Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ export enum CoreObjectNameSingular {
MessageThreadSubscriber = 'messageThreadSubscriber',
Workflow = 'workflow',
MessageChannelMessageAssociation = 'messageChannelMessageAssociation',
WorkflowVersion = 'workflowVersion',
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isR

import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage';
import { RightDrawerWorkflowEditStep } from '@/workflow/components/RightDrawerWorkflowEditStep';
import { RightDrawerWorkflowSelectAction } from '@/workflow/components/RightDrawerWorkflowSelectAction';
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 @@ -36,7 +37,10 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = {
[RightDrawerPages.ViewCalendarEvent]: <RightDrawerCalendarEvent />,
[RightDrawerPages.ViewRecord]: <RightDrawerRecord />,
[RightDrawerPages.Copilot]: <RightDrawerAIChat />,
[RightDrawerPages.Workflow]: <RightDrawerWorkflow />,
[RightDrawerPages.WorkflowStepSelectAction]: (
<RightDrawerWorkflowSelectAction />
),
[RightDrawerPages.WorkflowStepEdit]: <RightDrawerWorkflowEditStep />,
};

export const RightDrawerRouter = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export enum RightDrawerPages {
ViewCalendarEvent = 'view-calendar-event',
ViewRecord = 'view-record',
Copilot = 'copilot',
Workflow = 'workflow',
WorkflowStepSelectAction = 'workflow-step-select-action',
WorkflowStepEdit = 'workflow-step-edit',
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { showPageWorkflowSelectedNodeState } from '@/workflow/states/showPageWorkflowSelectedNodeState';
import { useRecoilValue } from 'recoil';

export const RightDrawerWorkflowEditStep = () => {
const showPageWorkflowSelectedNode = useRecoilValue(
showPageWorkflowSelectedNodeState,
);

return <p>{showPageWorkflowSelectedNode}</p>;
Copy link
Contributor

@thomtrp thomtrp Aug 30, 2024

Choose a reason for hiding this comment

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

do you need <p> ? I would prefer <> and some design if needed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This component is a placeholder for now. It was used to prove that selecting a node worked. It's styled in my next PR and the <p> tag will disappear.

I understood that <p> tags should be prohibited.

};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { RightDrawerWorkflowSelectActionContent } from '@/workflow/components/RightDrawerWorkflowSelectActionContent';
import { showPageWorkflowIdState } from '@/workflow/states/showPageWorkflowIdState';
import { Workflow } from '@/workflow/types/Workflow';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';

export const RightDrawerWorkflowSelectAction = () => {
const showPageWorkflowId = useRecoilValue(showPageWorkflowIdState);

const { record: workflow } = useFindOneRecord<Workflow>({
objectNameSingular: CoreObjectNameSingular.Workflow,
objectRecordId: showPageWorkflowId,
recordGqlFields: {
id: true,
name: true,
versions: true,
publishedVersionId: true,
},
});

if (!isDefined(workflow)) {
return null;
}

return <RightDrawerWorkflowSelectActionContent workflow={workflow} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { TabList } from '@/ui/layout/tab/components/TabList';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useRightDrawerWorkflowSelectAction } from '@/workflow/hooks/useRightDrawerWorkflowSelectAction';
import { Workflow } from '@/workflow/types/Workflow';
import styled from '@emotion/styled';

// FIXME: copy-pasted
Copy link
Contributor

Choose a reason for hiding this comment

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

so this style could not be put in commun with others?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This component styles the tab I used to show in the SelectAction drawer. As we stated, we don't want a tab, so this duplicated component will be removed in my next PR.

const StyledTabListContainer = styled.div`
align-items: center;
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
box-sizing: border-box;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
height: 40px;
`;
Comment on lines +7 to +15
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Remove FIXME comment and consider extracting this styled component to a shared file if it's used in multiple places


const StyledActionListContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;

padding-block: ${({ theme }) => theme.spacing(1)};
padding-inline: ${({ theme }) => theme.spacing(2)};
`;

export const TAB_LIST_COMPONENT_ID =
'workflow-select-action-page-right-tab-list';

export const RightDrawerWorkflowSelectActionContent = ({
workflow,
}: {
workflow: Workflow;
}) => {
const tabListId = `${TAB_LIST_COMPONENT_ID}`;

const { tabs, options, handleActionClick } =
useRightDrawerWorkflowSelectAction({ tabListId, workflow });

return (
<>
<StyledTabListContainer>
<TabList loading={false} tabListId={tabListId} tabs={tabs} />
</StyledTabListContainer>

<StyledActionListContainer>
{options.map((option) => (
<MenuItem
key={option.id}
LeftIcon={option.icon}
text={option.name}
onClick={() => {
handleActionClick(option.id);
}}
/>
))}
</StyledActionListContainer>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { WorkflowShowPageDiagramCreateStepNode } from '@/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx';
import { WorkflowShowPageDiagramCreateStepNode } from '@/workflow/components/WorkflowShowPageDiagramCreateStepNode';
import { WorkflowShowPageDiagramEffect } from '@/workflow/components/WorkflowShowPageDiagramEffect';
import { WorkflowShowPageDiagramStepNode } from '@/workflow/components/WorkflowShowPageDiagramStepNode';
import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState';
import {
Expand Down Expand Up @@ -80,6 +81,8 @@ export const WorkflowShowPageDiagram = ({
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgesChange}
>
<WorkflowShowPageDiagramEffect />

<Background color={GRAY_SCALE.gray25} size={2} />
</ReactFlow>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
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';
Expand All @@ -10,17 +8,11 @@ export const StyledTargetHandle = styled(Handle)`
`;

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>
<IconButton Icon={IconPlus} />
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useStartNodeCreation } from '@/workflow/hooks/useStartNodeCreation';
import { showPageWorkflowDiagramTriggerNodeSelectionState } from '@/workflow/states/showPageWorkflowDiagramTriggerNodeSelectionState';
import { showPageWorkflowSelectedNodeState } from '@/workflow/states/showPageWorkflowSelectedNodeState';
import {
WorkflowDiagramEdge,
WorkflowDiagramNode,
} from '@/workflow/types/WorkflowDiagram';
import {
OnSelectionChangeParams,
useOnSelectionChange,
useReactFlow,
} from '@xyflow/react';
import { useCallback, useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-ui';

export const WorkflowShowPageDiagramEffect = () => {
const reactflow = useReactFlow<WorkflowDiagramNode, WorkflowDiagramEdge>();

const { startNodeCreation } = useStartNodeCreation();

const { openRightDrawer, closeRightDrawer } = useRightDrawer();
const setShowPageWorkflowSelectedNode = useSetRecoilState(
showPageWorkflowSelectedNodeState,
);

const showPageWorkflowDiagramTriggerNodeSelection = useRecoilValue(
showPageWorkflowDiagramTriggerNodeSelectionState,
);

/**
* The callback executed when the selection state of nodes or edges changes.
* It's called when a node or an edge is selected or unselected.
*
* Relying on this callback is safer than listing to click events as nodes and edges
* can be selected in many ways, either via mouse click, tab key or even programatically.
*
* The callback is currently used to open right drawers for step creation or step editing.
*/
const handleSelectionChange = useCallback(
({ nodes }: OnSelectionChangeParams) => {
const selectedNode = nodes[0] as WorkflowDiagramNode;
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 handling the case where nodes array is empty to avoid potential runtime errors

const isClosingStep = isDefined(selectedNode) === false;

if (isClosingStep) {
closeRightDrawer();

return;
}

const isCreateStepNode = selectedNode.type === 'create-step';
if (isCreateStepNode) {
if (selectedNode.data.nodeType !== 'create-step') {
throw new Error('Expected selected node to be a create step node.');
}
Comment on lines +46 to +48
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: This check seems redundant. If selectedNode.type === 'create-step', then selectedNode.data.nodeType should always be 'create-step'. Consider removing this check or clarifying why it's necessary


startNodeCreation(selectedNode.data.parentNodeId);

return;
}

setShowPageWorkflowSelectedNode(selectedNode.id);
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
},
[
closeRightDrawer,
openRightDrawer,
setShowPageWorkflowSelectedNode,
startNodeCreation,
],
);

useOnSelectionChange({
onChange: handleSelectionChange,
});

/**
* We can't access the reactflow instance everywhere, only in children components of the <Reactflow /> component,
* so we use a useEffect and a Recoil state to trigger actions on the diagram, like programatically selecting a node.
*/
useEffect(() => {
if (!isDefined(showPageWorkflowDiagramTriggerNodeSelection)) {
return;
}

reactflow.updateNode(showPageWorkflowDiagramTriggerNodeSelection, {
selected: true,
});
}, [reactflow, showPageWorkflowDiagramTriggerNodeSelection]);

return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState';
import { showPageWorkflowErrorState } from '@/workflow/states/showPageWorkflowErrorState';
import { showPageWorkflowIdState } from '@/workflow/states/showPageWorkflowIdState';
import { showPageWorkflowLoadingState } from '@/workflow/states/showPageWorkflowLoadingState';
import { Workflow } from '@/workflow/types/Workflow';
import { addCreateStepNodes } from '@/workflow/utils/addCreateStepNodes';
Expand Down Expand Up @@ -32,6 +33,7 @@ export const WorkflowShowPageEffect = ({
},
});

const setShowPageWorkflowId = useSetRecoilState(showPageWorkflowIdState);
const setCurrentWorkflowData = useSetRecoilState(
showPageWorkflowDiagramState,
);
Expand All @@ -40,6 +42,10 @@ export const WorkflowShowPageEffect = ({
);
const setCurrentWorkflowError = useSetRecoilState(showPageWorkflowErrorState);

useEffect(() => {
setShowPageWorkflowId(workflowId);
}, [setShowPageWorkflowId, workflowId]);

useEffect(() => {
const flowLastVersion = getWorkflowLastDiagramVersion(workflow);
const flowWithCreateStepNodes = addCreateStepNodes(flowLastVersion);
Expand Down
Loading
Loading