Skip to content

Commit

Permalink
Create workflow version show page (twentyhq#7466)
Browse files Browse the repository at this point in the history
In this PR:

- Refactored components to clarify their behavior. For example, I
renamed the `Workflow` component to `WorkflowVisualizer`. This moved
forward the issue #7010.
- Create two variants of several workflow-related components: one
version for editing and another for viewing. For instance, there is
`WorkflowDiagramCanvasEditable.tsx` and
`WorkflowDiagramCanvasReadonly.tsx`
- Implement the show page for workflow versions. On this page, we
display a readonly workflow visualizer. Users can click on nodes and it
will expand the right drawer.
- I added buttons in the header of the RecordShowPage for workflow
versions: users can activate, deactivate or use the currently viewed
version as the next draft.

**There are many cache desynchronisation and I'll fix them really
soon.**

## Demo

(Turn sound on)


https://github.com/user-attachments/assets/97fafa48-8902-4dab-8b39-f40848bf041e
  • Loading branch information
Devessier authored and harshit078 committed Oct 14, 2024
1 parent 8512b22 commit 800e508
Show file tree
Hide file tree
Showing 39 changed files with 856 additions and 310 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDraw
import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage';
import { RightDrawerWorkflowEditStep } from '@/workflow/components/RightDrawerWorkflowEditStep';
import { RightDrawerWorkflowSelectAction } from '@/workflow/components/RightDrawerWorkflowSelectAction';
import { RightDrawerWorkflowViewStep } from '@/workflow/components/RightDrawerWorkflowViewStep';
import { isDefined } from 'twenty-ui';
import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages';
Expand Down Expand Up @@ -41,6 +42,7 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = {
<RightDrawerWorkflowSelectAction />
),
[RightDrawerPages.WorkflowStepEdit]: <RightDrawerWorkflowEditStep />,
[RightDrawerPages.WorkflowStepView]: <RightDrawerWorkflowViewStep />,
};

export const RightDrawerRouter = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const RIGHT_DRAWER_PAGE_ICONS = {
[RightDrawerPages.Copilot]: 'IconSparkles',
[RightDrawerPages.WorkflowStepEdit]: 'IconSparkles',
[RightDrawerPages.WorkflowStepSelectAction]: 'IconSparkles',
[RightDrawerPages.WorkflowStepView]: 'IconSparkles',
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const RIGHT_DRAWER_PAGE_TITLES = {
[RightDrawerPages.Copilot]: 'Copilot',
[RightDrawerPages.WorkflowStepEdit]: 'Workflow',
[RightDrawerPages.WorkflowStepSelectAction]: 'Workflow',
[RightDrawerPages.WorkflowStepView]: 'Workflow',
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export enum RightDrawerPages {
ViewRecord = 'view-record',
Copilot = 'copilot',
WorkflowStepSelectAction = 'workflow-step-select-action',
WorkflowStepView = 'workflow-step-view',
WorkflowStepEdit = 'workflow-step-edit',
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/Show
import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { Workflow } from '@/workflow/components/Workflow';
import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer';
import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect';
import { WorkflowVisualizer } from '@/workflow/components/WorkflowVisualizer';
import { WorkflowVisualizerEffect } from '@/workflow/components/WorkflowVisualizerEffect';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
import { useState } from 'react';
Expand Down Expand Up @@ -130,6 +133,10 @@ export const ShowPageRightContainer = ({
isWorkflowEnabled &&
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Workflow;
const isWorkflowVersion =
isWorkflowEnabled &&
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.WorkflowVersion;

const shouldDisplayCalendarTab = isCompanyOrPerson;
const shouldDisplayEmailsTab = emails && isCompanyOrPerson;
Expand Down Expand Up @@ -162,7 +169,7 @@ export const ShowPageRightContainer = ({
id: 'timeline',
title: 'Timeline',
Icon: IconTimelineEvent,
hide: !timeline || isInRightDrawer || isWorkflow,
hide: !timeline || isInRightDrawer || isWorkflow || isWorkflowVersion,
},
{
id: 'tasks',
Expand All @@ -174,7 +181,8 @@ export const ShowPageRightContainer = ({
CoreObjectNameSingular.Note ||
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Task ||
isWorkflow,
isWorkflow ||
isWorkflowVersion,
},
{
id: 'notes',
Expand All @@ -186,13 +194,14 @@ export const ShowPageRightContainer = ({
CoreObjectNameSingular.Note ||
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Task ||
isWorkflow,
isWorkflow ||
isWorkflowVersion,
},
{
id: 'files',
title: 'Files',
Icon: IconPaperclip,
hide: !notes || isWorkflow,
hide: !notes || isWorkflow || isWorkflowVersion,
},
{
id: 'emails',
Expand All @@ -212,6 +221,12 @@ export const ShowPageRightContainer = ({
Icon: IconSettings,
hide: !isWorkflow,
},
{
id: 'workflowVersion',
title: 'Workflow Version',
Icon: IconSettings,
hide: !isWorkflowVersion,
},
];
const renderActiveTabContent = () => {
switch (activeTabId) {
Expand Down Expand Up @@ -251,7 +266,25 @@ export const ShowPageRightContainer = ({
case 'calendar':
return <Calendar targetableObject={targetableObject} />;
case 'workflow':
return <Workflow targetableObject={targetableObject} />;
return (
<>
<WorkflowVisualizerEffect workflowId={targetableObject.id} />

<WorkflowVisualizer targetableObject={targetableObject} />
</>
);
case 'workflowVersion':
return (
<>
<WorkflowVersionVisualizerEffect
workflowVersionId={targetableObject.id}
/>

<WorkflowVersionVisualizer
workflowVersionId={targetableObject.id}
/>
</>
);
default:
return <></>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { assertWorkflowWithCurrentVersionIsDefined } from '../utils/assertWorkfl
export const RecordShowPageWorkflowHeader = ({
workflowId,
}: {
workflowId: string | undefined;
workflowId: string;
}) => {
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { Button } from '@/ui/input/button/components/Button';
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion';
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
import { Workflow, WorkflowVersion } from '@/workflow/types/Workflow';
import { IconPencil, IconPlayerStop, IconPower, isDefined } from 'twenty-ui';

export const RecordShowPageWorkflowVersionHeader = ({
workflowVersionId,
}: {
workflowVersionId: string;
}) => {
const workflowVersion = useWorkflowVersion(workflowVersionId);

const workflowVersionRelatedWorkflowQuery = useFindOneRecord<
Pick<Workflow, '__typename' | 'id' | 'lastPublishedVersionId'>
>({
objectNameSingular: CoreObjectNameSingular.Workflow,
objectRecordId: workflowVersion?.workflowId,
recordGqlFields: {
id: true,
lastPublishedVersionId: true,
},
skip: !isDefined(workflowVersion),
});

// TODO: In the future, use the workflow.status property to determine if there is a draft version
const {
records: draftWorkflowVersions,
loading: loadingDraftWorkflowVersions,
} = useFindManyRecords<WorkflowVersion>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
filter: {
workflowId: {
eq: workflowVersion?.workflow.id,
},
status: {
eq: 'DRAFT',
},
},
skip: !isDefined(workflowVersion),
limit: 1,
});

const showUseAsDraftButton =
!loadingDraftWorkflowVersions &&
isDefined(workflowVersion) &&
!workflowVersionRelatedWorkflowQuery.loading &&
isDefined(workflowVersionRelatedWorkflowQuery.record) &&
workflowVersion.status !== 'DRAFT' &&
workflowVersion.id !==
workflowVersionRelatedWorkflowQuery.record.lastPublishedVersionId;

const hasAlreadyDraftVersion =
!loadingDraftWorkflowVersions && draftWorkflowVersions.length > 0;

const isWaitingForWorkflowVersion = !isDefined(workflowVersion);

const { activateWorkflowVersion } = useActivateWorkflowVersion();
const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion();
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();

const { updateOneRecord: updateOneWorkflowVersion } =
useUpdateOneRecord<WorkflowVersion>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});

return (
<>
{showUseAsDraftButton ? (
<Button
title={`Use as Draft${hasAlreadyDraftVersion ? ' (override)' : ''}`}
variant="secondary"
Icon={IconPencil}
disabled={isWaitingForWorkflowVersion}
onClick={async () => {
if (hasAlreadyDraftVersion) {
await updateOneWorkflowVersion({
idToUpdate: draftWorkflowVersions[0].id,
updateOneRecordInput: {
trigger: workflowVersion.trigger,
steps: workflowVersion.steps,
},
});
} else {
await createNewWorkflowVersion({
workflowId: workflowVersion.workflow.id,
name: `v${workflowVersion.workflow.versions.length + 1}`,
status: 'DRAFT',
trigger: workflowVersion.trigger,
steps: workflowVersion.steps,
});
}
}}
/>
) : null}

{workflowVersion?.status === 'DRAFT' ||
workflowVersion?.status === 'DEACTIVATED' ? (
<Button
title="Activate"
variant="secondary"
Icon={IconPower}
disabled={isWaitingForWorkflowVersion}
onClick={() => {
return activateWorkflowVersion(workflowVersion.id);
}}
/>
) : workflowVersion?.status === 'ACTIVE' ? (
<Button
title="Deactivate"
variant="secondary"
Icon={IconPlayerStop}
disabled={isWaitingForWorkflowVersion}
onClick={() => {
return deactivateWorkflowVersion(workflowVersion.id);
}}
/>
) : null}
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,62 +1,11 @@
import { WorkflowEditActionFormSendEmail } from '@/workflow/components/WorkflowEditActionFormSendEmail';
import { WorkflowEditActionFormServerlessFunction } from '@/workflow/components/WorkflowEditActionFormServerlessFunction';
import { WorkflowEditTriggerForm } from '@/workflow/components/WorkflowEditTriggerForm';
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
import { WorkflowStepDetail } from '@/workflow/components/WorkflowStepDetail';
import { useUpdateWorkflowVersionStep } from '@/workflow/hooks/useUpdateWorkflowVersionStep';
import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger';
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { findStepPosition } from '@/workflow/utils/findStepPosition';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';

const getStepDefinitionOrThrow = ({
stepId,
workflow,
}: {
stepId: string;
workflow: WorkflowWithCurrentVersion;
}) => {
const currentVersion = workflow.currentVersion;
if (!isDefined(currentVersion)) {
throw new Error('Expected to find a current version');
}

if (stepId === TRIGGER_STEP_ID) {
if (!isDefined(currentVersion.trigger)) {
return {
type: 'trigger',
definition: undefined,
} as const;
}

return {
type: 'trigger',
definition: currentVersion.trigger,
} as const;
}

if (!isDefined(currentVersion.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 = findStepPosition({
steps: currentVersion.steps,
stepId: stepId,
});
if (!isDefined(selectedNodePosition)) {
return undefined;
}

return {
type: 'action',
definition: selectedNodePosition.steps[selectedNodePosition.index],
} as const;
};

export const RightDrawerWorkflowEditStepContent = ({
workflow,
}: {
Expand All @@ -75,47 +24,12 @@ export const RightDrawerWorkflowEditStepContent = ({
stepId: workflowSelectedNode,
});

const stepDefinition = getStepDefinitionOrThrow({
stepId: workflowSelectedNode,
workflow,
});
if (!isDefined(stepDefinition)) {
return null;
}

switch (stepDefinition.type) {
case 'trigger': {
return (
<WorkflowEditTriggerForm
trigger={stepDefinition.definition}
onTriggerUpdate={updateTrigger}
/>
);
}
case 'action': {
switch (stepDefinition.definition.type) {
case 'CODE': {
return (
<WorkflowEditActionFormServerlessFunction
action={stepDefinition.definition}
onActionUpdate={updateStep}
/>
);
}
case 'SEND_EMAIL': {
return (
<WorkflowEditActionFormSendEmail
action={stepDefinition.definition}
onActionUpdate={updateStep}
/>
);
}
}
}
}

return assertUnreachable(
stepDefinition,
`Unsupported step: ${JSON.stringify(stepDefinition)}`,
return (
<WorkflowStepDetail
stepId={workflowSelectedNode}
workflowVersion={workflow.currentVersion}
onActionUpdate={updateStep}
onTriggerUpdate={updateTrigger}
/>
);
};
Loading

0 comments on commit 800e508

Please sign in to comment.