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:
committed by
GitHub
parent
694553608b
commit
f74e4bedc4
+2
-10
@@ -1,11 +1,9 @@
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
||||
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
|
||||
import { useUpdateStep } from '@/workflow/workflow-steps/hooks/useUpdateStep';
|
||||
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const RightDrawerWorkflowEditStepContent = ({
|
||||
workflow,
|
||||
@@ -13,13 +11,7 @@ export const RightDrawerWorkflowEditStepContent = ({
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const flow = useFlowOrThrow();
|
||||
|
||||
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
||||
if (!isDefined(workflowSelectedNode)) {
|
||||
throw new Error(
|
||||
'Expected a node to be selected. Selecting a node is mandatory to edit it.',
|
||||
);
|
||||
}
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
|
||||
const { updateTrigger } = useUpdateWorkflowVersionTrigger({ workflow });
|
||||
const { updateStep } = useUpdateStep({
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
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 { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
||||
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
|
||||
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui';
|
||||
|
||||
const StyledTabListContainer = styled(ShowPageSubContainerTabListContainer)`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
`;
|
||||
|
||||
type TabId = 'node' | 'input' | 'output';
|
||||
|
||||
export const RightDrawerWorkflowRunViewStep = () => {
|
||||
const flow = useFlowOrThrow();
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
|
||||
const { activeTabId } = useTabList<TabId>(
|
||||
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
|
||||
const tabs: SingleTabProps<TabId>[] = [
|
||||
{ id: 'node', title: 'Node', Icon: IconStepInto },
|
||||
{ id: 'input', title: 'Input', Icon: IconLogin2 },
|
||||
{ id: 'output', title: 'Output', Icon: IconLogout },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTabListContainer>
|
||||
<TabList
|
||||
tabListInstanceId={WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID}
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
/>
|
||||
</StyledTabListContainer>
|
||||
|
||||
{activeTabId === 'node' ? (
|
||||
<WorkflowStepDetail
|
||||
readonly
|
||||
stepId={workflowSelectedNode}
|
||||
trigger={flow.trigger}
|
||||
steps={flow.steps}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+2
-10
@@ -1,18 +1,10 @@
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
||||
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const RightDrawerWorkflowViewStep = () => {
|
||||
const flow = useFlowOrThrow();
|
||||
|
||||
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
||||
if (!isDefined(workflowSelectedNode)) {
|
||||
throw new Error(
|
||||
'Expected a node to be selected. Selecting a node is mandatory to view its details.',
|
||||
);
|
||||
}
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
|
||||
return (
|
||||
<WorkflowStepDetail
|
||||
|
||||
+3
-2
@@ -1,13 +1,14 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledWorkflowStepBody = styled.div`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
row-gap: ${({ theme }) => theme.spacing(6)};
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export { StyledWorkflowStepBody as WorkflowStepBody };
|
||||
|
||||
+24
-7
@@ -21,6 +21,14 @@ const WorkflowEditActionFormServerlessFunction = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const WorkflowReadonlyActionFormServerlessFunction = lazy(() =>
|
||||
import(
|
||||
'@/workflow/workflow-steps/workflow-actions/components/WorkflowReadonlyActionFormServerlessFunction'
|
||||
).then((module) => ({
|
||||
default: module.WorkflowReadonlyActionFormServerlessFunction,
|
||||
})),
|
||||
);
|
||||
|
||||
type WorkflowStepDetailProps = {
|
||||
stepId: string;
|
||||
trigger: WorkflowTrigger | null;
|
||||
@@ -50,6 +58,7 @@ export const WorkflowStepDetail = ({
|
||||
trigger,
|
||||
steps,
|
||||
});
|
||||
|
||||
if (!isDefined(stepDefinition) || !isDefined(stepDefinition.definition)) {
|
||||
return null;
|
||||
}
|
||||
@@ -60,6 +69,7 @@ export const WorkflowStepDetail = ({
|
||||
case 'DATABASE_EVENT': {
|
||||
return (
|
||||
<WorkflowEditTriggerDatabaseEventForm
|
||||
key={stepId}
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={props}
|
||||
/>
|
||||
@@ -68,6 +78,7 @@ export const WorkflowStepDetail = ({
|
||||
case 'MANUAL': {
|
||||
return (
|
||||
<WorkflowEditTriggerManualForm
|
||||
key={stepId}
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={props}
|
||||
/>
|
||||
@@ -76,6 +87,7 @@ export const WorkflowStepDetail = ({
|
||||
case 'CRON': {
|
||||
return (
|
||||
<WorkflowEditTriggerCronForm
|
||||
key={stepId}
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={props}
|
||||
/>
|
||||
@@ -93,11 +105,18 @@ export const WorkflowStepDetail = ({
|
||||
case 'CODE': {
|
||||
return (
|
||||
<Suspense fallback={<RightDrawerSkeletonLoader />}>
|
||||
<WorkflowEditActionFormServerlessFunction
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
{props.readonly ? (
|
||||
<WorkflowReadonlyActionFormServerlessFunction
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
/>
|
||||
) : (
|
||||
<WorkflowEditActionFormServerlessFunction
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -150,8 +169,6 @@ export const WorkflowStepDetail = ({
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-10
@@ -43,24 +43,38 @@ const StyledHeaderIconContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type WorkflowStepHeaderProps = {
|
||||
Icon: IconComponent;
|
||||
iconColor: string;
|
||||
initialTitle: string;
|
||||
headerType: string;
|
||||
} & (
|
||||
| {
|
||||
disabled: true;
|
||||
onTitleChange?: never;
|
||||
}
|
||||
| {
|
||||
disabled?: boolean;
|
||||
onTitleChange: (newTitle: string) => void;
|
||||
}
|
||||
);
|
||||
|
||||
export const WorkflowStepHeader = ({
|
||||
onTitleChange,
|
||||
Icon,
|
||||
iconColor,
|
||||
initialTitle,
|
||||
headerType,
|
||||
disabled,
|
||||
}: {
|
||||
onTitleChange: (newTitle: string) => void;
|
||||
Icon: IconComponent;
|
||||
iconColor: string;
|
||||
initialTitle: string;
|
||||
headerType: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
onTitleChange,
|
||||
}: WorkflowStepHeaderProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [title, setTitle] = useState(initialTitle);
|
||||
const debouncedOnTitleChange = useDebouncedCallback(onTitleChange, 100);
|
||||
|
||||
const debouncedOnTitleChange = useDebouncedCallback((newTitle: string) => {
|
||||
onTitleChange?.(newTitle);
|
||||
}, 100);
|
||||
|
||||
const handleChange = (newTitle: string) => {
|
||||
setTitle(newTitle);
|
||||
debouncedOnTitleChange(newTitle);
|
||||
|
||||
Reference in New Issue
Block a user