Create a right drawer for viewing steps in workflow runs (#10366)

- Improve the type-safety of the objects mapping the id of a right
drawer or side panel view to a React component
- Improve the types of the `useTabList` hook to type the available tab
identifiers strictly
- Create a specialized `WorkflowRunDiagramCanvas` component to render a
`WorkflowRunDiagramCanvasEffect` component that opens
`RightDrawerPages.WorkflowRunStepView` when a step is selected
- Create a new side panel view specifically for workflow run step
details
- Create tab list in the new side panel; all the tabs are `Node`,
`Input` and `Output`
- Create a hook `useWorkflowSelectedNodeOrThrow` not to duplicate
throwing mechanisms

Closes https://github.com/twentyhq/core-team-issues/issues/432

## Demo


https://github.com/user-attachments/assets/8d5df7dc-0b99-49a2-9a54-d3eaee80a8e6
This commit is contained in:
Baptiste Devessier
2025-02-26 16:48:24 +01:00
committed by GitHub
parent 694553608b
commit f74e4bedc4
28 changed files with 418 additions and 148 deletions

View File

@ -9,8 +9,8 @@ import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isR
import { RightDrawerContainer } from '@/ui/layout/right-drawer/components/RightDrawerContainer';
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage';
import { RightDrawerWorkflowEditStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStep';
import { RightDrawerWorkflowRunViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowRunViewStep';
import { RightDrawerWorkflowViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowViewStep';
import { RightDrawerWorkflowSelectAction } from '@/workflow/workflow-steps/workflow-actions/components/RightDrawerWorkflowSelectAction';
import { RightDrawerWorkflowSelectTriggerType } from '@/workflow/workflow-trigger/components/RightDrawerWorkflowSelectTriggerType';
@ -28,7 +28,7 @@ const StyledRightDrawerBody = styled.div`
position: relative;
`;
const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = {
const RIGHT_DRAWER_PAGES_CONFIG = {
[RightDrawerPages.ViewEmailThread]: <RightDrawerEmailThread />,
[RightDrawerPages.ViewCalendarEvent]: <RightDrawerCalendarEvent />,
[RightDrawerPages.ViewRecord]: <RightDrawerRecord />,
@ -41,7 +41,8 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = {
),
[RightDrawerPages.WorkflowStepEdit]: <RightDrawerWorkflowEditStep />,
[RightDrawerPages.WorkflowStepView]: <RightDrawerWorkflowViewStep />,
};
[RightDrawerPages.WorkflowRunStepView]: <RightDrawerWorkflowRunViewStep />,
} satisfies Record<RightDrawerPages, JSX.Element>;
export const RightDrawerRouter = () => {
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);

View File

@ -9,4 +9,5 @@ export const RIGHT_DRAWER_PAGE_ICONS = {
[RightDrawerPages.WorkflowStepSelectAction]: 'IconSparkles',
[RightDrawerPages.WorkflowStepEdit]: 'IconSparkles',
[RightDrawerPages.WorkflowStepView]: 'IconSparkles',
};
[RightDrawerPages.WorkflowRunStepView]: 'IconSparkles',
} satisfies Record<RightDrawerPages, string>;

View File

@ -9,4 +9,5 @@ export const RIGHT_DRAWER_PAGE_TITLES = {
[RightDrawerPages.WorkflowStepSelectAction]: 'Workflow',
[RightDrawerPages.WorkflowStepEdit]: 'Workflow',
[RightDrawerPages.WorkflowStepView]: 'Workflow',
};
[RightDrawerPages.WorkflowRunStepView]: 'Workflow',
} satisfies Record<RightDrawerPages, string>;

View File

@ -1,5 +0,0 @@
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
export type ComponentByRightDrawerPage = {
[componentName in RightDrawerPages]?: JSX.Element;
};

View File

@ -7,4 +7,5 @@ export enum RightDrawerPages {
WorkflowStepSelectAction = 'workflow-step-select-action',
WorkflowStepView = 'workflow-step-view',
WorkflowStepEdit = 'workflow-step-edit',
WorkflowRunStepView = 'workflow-run-step-view',
}

View File

@ -4,24 +4,25 @@ import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPage
export const mapRightDrawerPageToCommandMenuPage = (
rightDrawerPage: RightDrawerPages,
) => {
switch (rightDrawerPage) {
case RightDrawerPages.ViewRecord:
return CommandMenuPages.ViewRecord;
case RightDrawerPages.ViewEmailThread:
return CommandMenuPages.ViewEmailThread;
case RightDrawerPages.ViewCalendarEvent:
return CommandMenuPages.ViewCalendarEvent;
case RightDrawerPages.Copilot:
return CommandMenuPages.Copilot;
case RightDrawerPages.WorkflowStepSelectTriggerType:
return CommandMenuPages.WorkflowStepSelectTriggerType;
case RightDrawerPages.WorkflowStepSelectAction:
return CommandMenuPages.WorkflowStepSelectAction;
case RightDrawerPages.WorkflowStepView:
return CommandMenuPages.WorkflowStepView;
case RightDrawerPages.WorkflowStepEdit:
return CommandMenuPages.WorkflowStepEdit;
default:
return CommandMenuPages.Root;
}
const rightDrawerPagesToCommandMenuPages: Record<
RightDrawerPages,
CommandMenuPages
> = {
[RightDrawerPages.ViewRecord]: CommandMenuPages.ViewRecord,
[RightDrawerPages.ViewEmailThread]: CommandMenuPages.ViewEmailThread,
[RightDrawerPages.ViewCalendarEvent]: CommandMenuPages.ViewCalendarEvent,
[RightDrawerPages.Copilot]: CommandMenuPages.Copilot,
[RightDrawerPages.WorkflowStepSelectTriggerType]:
CommandMenuPages.WorkflowStepSelectTriggerType,
[RightDrawerPages.WorkflowStepSelectAction]:
CommandMenuPages.WorkflowStepSelectAction,
[RightDrawerPages.WorkflowStepView]: CommandMenuPages.WorkflowStepView,
[RightDrawerPages.WorkflowRunStepView]:
CommandMenuPages.WorkflowRunStepView,
[RightDrawerPages.WorkflowStepEdit]: CommandMenuPages.WorkflowStepEdit,
};
return (
rightDrawerPagesToCommandMenuPages[rightDrawerPage] ?? CommandMenuPages.Root
);
};

View File

@ -9,6 +9,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
import { ShowPageSubContainerTabListContainer } from '@/ui/layout/show-page/components/ShowPageSubContainerTabListContainer';
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
@ -26,14 +27,8 @@ const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>`
`;
const StyledTabListContainer = styled.div<{ shouldDisplay: boolean }>`
align-items: center;
padding-left: ${({ theme }) => theme.spacing(2)};
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
box-sizing: border-box;
display: ${({ shouldDisplay }) => (shouldDisplay ? 'flex' : 'none')};
gap: ${({ theme }) => theme.spacing(2)};
height: 40px;
`;
`.withComponent(ShowPageSubContainerTabListContainer);
const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
flex: 1;

View File

@ -0,0 +1,13 @@
import styled from '@emotion/styled';
const StyledTabListContainer = styled.div`
align-items: center;
padding-left: ${({ theme }) => theme.spacing(2)};
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
box-sizing: border-box;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
height: 40px;
`;
export { StyledTabListContainer as ShowPageSubContainerTabListContainer };

View File

@ -9,10 +9,10 @@ import { useEffect } from 'react';
import { IconComponent } from 'twenty-ui';
import { Tab } from './Tab';
export type SingleTabProps = {
export type SingleTabProps<T extends string = string> = {
title: string;
Icon?: IconComponent;
id: string;
id: T;
hide?: boolean;
disabled?: boolean;
pill?: string | React.ReactElement;

View File

@ -1,13 +1,15 @@
import { useRecoilState } from 'recoil';
import { RecoilState, useRecoilState } from 'recoil';
import { useTabListStates } from '@/ui/layout/tab/hooks/internal/useTabListStates';
export const useTabList = (tabListId?: string) => {
export const useTabList = <T extends string>(tabListId?: string) => {
const { activeTabIdState } = useTabListStates({
tabListScopeId: tabListId,
});
const [activeTabId, setActiveTabId] = useRecoilState(activeTabIdState);
const [activeTabId, setActiveTabId] = useRecoilState(
activeTabIdState as RecoilState<T | null>,
);
return {
activeTabId,