584 Refactor Tabs (#11008)
Closes https://github.com/twentyhq/core-team-issues/issues/584 This PR: - Migrates the component state `activeTabIdComponentState` from the deprecated V1 version to V2. - Allows the active tab state to be preserved during navigation inside the side panel and reset when the side panel is closed. - Allows the active tab state to be preserved when we open a record in full page from the side panel https://github.com/user-attachments/assets/f2329d7a-ea15-4bd8-81dc-e98ce11edbd0 https://github.com/user-attachments/assets/474bffd5-29e0-40ba-97f4-fa5e9be34dc2
This commit is contained in:
-23
@@ -1,23 +0,0 @@
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { WorkflowStepContextProvider } from '@/workflow/states/context/WorkflowStepContext';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { RightDrawerWorkflowEditStepContent } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const RightDrawerWorkflowEditStep = () => {
|
||||
const workflowId = useRecoilValue(workflowIdState);
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
if (!isDefined(workflow)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowStepContextProvider
|
||||
value={{ workflowVersionId: workflow.currentVersion.id }}
|
||||
>
|
||||
<RightDrawerWorkflowEditStepContent workflow={workflow} />
|
||||
</WorkflowStepContextProvider>
|
||||
);
|
||||
};
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
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';
|
||||
|
||||
export const RightDrawerWorkflowEditStepContent = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const flow = useFlowOrThrow();
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
|
||||
const { updateTrigger } = useUpdateWorkflowVersionTrigger({ workflow });
|
||||
const { updateStep } = useUpdateStep({
|
||||
workflow,
|
||||
});
|
||||
|
||||
return (
|
||||
<WorkflowStepDetail
|
||||
stepId={workflowSelectedNode}
|
||||
trigger={flow.trigger}
|
||||
steps={flow.steps}
|
||||
onActionUpdate={updateStep}
|
||||
onTriggerUpdate={updateTrigger}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { WorkflowStepContextProvider } from '@/workflow/states/context/WorkflowStepContext';
|
||||
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
||||
import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail';
|
||||
import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail';
|
||||
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 { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui';
|
||||
|
||||
const StyledTabList = styled(TabList)`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type TabId = 'node' | 'input' | 'output';
|
||||
|
||||
export const RightDrawerWorkflowRunViewStep = () => {
|
||||
const flow = useFlowOrThrow();
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||
|
||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||
|
||||
const { activeTabId } = useTabList<TabId>(
|
||||
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
|
||||
const stepExecutionStatus = isDefined(workflowRun)
|
||||
? getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: workflowRun.output,
|
||||
stepId: workflowSelectedNode,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const areInputAndOutputTabsDisabled =
|
||||
workflowSelectedNode === TRIGGER_STEP_ID ||
|
||||
stepExecutionStatus === 'running' ||
|
||||
stepExecutionStatus === 'not-executed';
|
||||
|
||||
const tabs: SingleTabProps<TabId>[] = [
|
||||
{ id: 'node', title: 'Node', Icon: IconStepInto },
|
||||
{
|
||||
id: 'input',
|
||||
title: 'Input',
|
||||
Icon: IconLogin2,
|
||||
disabled: areInputAndOutputTabsDisabled,
|
||||
},
|
||||
{
|
||||
id: 'output',
|
||||
title: 'Output',
|
||||
Icon: IconLogout,
|
||||
disabled: areInputAndOutputTabsDisabled,
|
||||
},
|
||||
];
|
||||
|
||||
if (!isDefined(workflowRun)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowStepContextProvider
|
||||
value={{ workflowVersionId: flow.workflowVersionId }}
|
||||
>
|
||||
<StyledTabList
|
||||
tabListInstanceId={WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID}
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
/>
|
||||
|
||||
{activeTabId === 'node' ? (
|
||||
<WorkflowStepDetail
|
||||
readonly
|
||||
stepId={workflowSelectedNode}
|
||||
trigger={flow.trigger}
|
||||
steps={flow.steps}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeTabId === 'input' ? (
|
||||
<WorkflowRunStepInputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeTabId === 'output' ? (
|
||||
<WorkflowRunStepOutputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
</WorkflowStepContextProvider>
|
||||
);
|
||||
};
|
||||
-213
@@ -1,213 +0,0 @@
|
||||
import { flowState } from '@/workflow/states/flowState';
|
||||
import { workflowRunIdState } from '@/workflow/states/workflowRunIdState';
|
||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import styled from '@emotion/styled';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { HttpResponse, graphql } from 'msw';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { oneFailedWorkflowRunQueryResult } from '~/testing/mock-data/workflow-run';
|
||||
import { RightDrawerWorkflowRunViewStep } from '../RightDrawerWorkflowRunViewStep';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 500px;
|
||||
`;
|
||||
|
||||
const meta: Meta<typeof RightDrawerWorkflowRunViewStep> = {
|
||||
title: 'Modules/Workflow/RightDrawerWorkflowRunViewStep',
|
||||
component: RightDrawerWorkflowRunViewStep,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<StyledWrapper>
|
||||
<Story />
|
||||
</StyledWrapper>
|
||||
),
|
||||
I18nFrontDecorator,
|
||||
ComponentDecorator,
|
||||
(Story) => {
|
||||
const setFlow = useSetRecoilState(flowState);
|
||||
const setWorkflowSelectedNode = useSetRecoilState(
|
||||
workflowSelectedNodeState,
|
||||
);
|
||||
const setWorkflowRunId = useSetRecoilState(workflowRunIdState);
|
||||
|
||||
setFlow({
|
||||
workflowVersionId:
|
||||
oneFailedWorkflowRunQueryResult.workflowRun.workflowVersionId,
|
||||
trigger:
|
||||
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.trigger,
|
||||
steps: oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps,
|
||||
});
|
||||
setWorkflowSelectedNode(
|
||||
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps[0].id,
|
||||
);
|
||||
setWorkflowRunId(oneFailedWorkflowRunQueryResult.workflowRun.id);
|
||||
|
||||
return <Story />;
|
||||
},
|
||||
RouterDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
WorkspaceDecorator,
|
||||
WorkflowStepDecorator,
|
||||
],
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
graphql.query('FindOneWorkflowRun', () => {
|
||||
const workflowRunContext =
|
||||
oneFailedWorkflowRunQueryResult.workflowRun.context;
|
||||
|
||||
// Rendering the whole objectMetadata information in the JSON viewer is too long for storybook
|
||||
// so we remove it for the story
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
...oneFailedWorkflowRunQueryResult,
|
||||
workflowRun: {
|
||||
...oneFailedWorkflowRunQueryResult.workflowRun,
|
||||
context: {
|
||||
...workflowRunContext,
|
||||
trigger: {
|
||||
...workflowRunContext.trigger,
|
||||
objectMetadata: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
...graphqlMocks.handlers,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RightDrawerWorkflowRunViewStep>;
|
||||
|
||||
export const NodeTab: Story = {};
|
||||
|
||||
export const InputTab: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.click(await canvas.findByRole('button', { name: 'Input' }));
|
||||
|
||||
expect(await canvas.findByText('Trigger')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const InputTabDisabledForTrigger: Story = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const setWorkflowSelectedNode = useSetRecoilState(
|
||||
workflowSelectedNodeState,
|
||||
);
|
||||
|
||||
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
||||
|
||||
return <Story />;
|
||||
},
|
||||
],
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const inputTab = await canvas.findByRole('button', { name: 'Input' });
|
||||
|
||||
expect(inputTab).toBeDisabled();
|
||||
},
|
||||
};
|
||||
|
||||
export const InputTabNotExecutedStep: Story = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const setWorkflowSelectedNode = useSetRecoilState(
|
||||
workflowSelectedNodeState,
|
||||
);
|
||||
|
||||
setWorkflowSelectedNode(
|
||||
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps.at(-1)!
|
||||
.id,
|
||||
);
|
||||
|
||||
return <Story />;
|
||||
},
|
||||
],
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const inputTab = await canvas.findByRole('button', { name: 'Input' });
|
||||
|
||||
expect(inputTab).toBeDisabled();
|
||||
},
|
||||
};
|
||||
|
||||
export const OutputTab: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.click(
|
||||
await canvas.findByRole('button', { name: 'Output' }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.queryByText('Create Record')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(await canvas.findByText('result')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const OutputTabDisabledForTrigger: Story = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const setWorkflowSelectedNode = useSetRecoilState(
|
||||
workflowSelectedNodeState,
|
||||
);
|
||||
|
||||
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
||||
|
||||
return <Story />;
|
||||
},
|
||||
],
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const outputTab = await canvas.findByRole('button', { name: 'Output' });
|
||||
|
||||
expect(outputTab).toBeDisabled();
|
||||
},
|
||||
};
|
||||
|
||||
export const OutputTabNotExecutedStep: Story = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const setWorkflowSelectedNode = useSetRecoilState(
|
||||
workflowSelectedNodeState,
|
||||
);
|
||||
|
||||
setWorkflowSelectedNode(
|
||||
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps.at(-1)!
|
||||
.id,
|
||||
);
|
||||
|
||||
return <Story />;
|
||||
},
|
||||
],
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const outputTab = await canvas.findByRole('button', { name: 'Output' });
|
||||
|
||||
expect(outputTab).toBeDisabled();
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user