Display workflow step header in workflow run input and output tabs (#11102)

- Wrap the content of Workflow View, Workflow Edit, and Workflow Run
side panels with a container making them take all the available height
- Remove the `StyledContainer` of code steps as it's redundant with the
global container
- Add the WorkflowStepHeader to the input and output tabs
- Make the JSON visualizer take all the available height in input and
output tabs
- Reuse the WorkflowStepBody component in the input and output tabs as
it applies proper background color

## Demo

![CleanShot 2025-03-21 at 18 30
26@2x](https://github.com/user-attachments/assets/c3fa512b-c371-4d0b-9bf6-a5f84d333dda)

Fixes
https://discord.com/channels/1130383047699738754/1351906809417568376

---------

Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
This commit is contained in:
Baptiste Devessier
2025-03-24 14:06:26 +01:00
committed by GitHub
parent 1c5f3ef5fa
commit e6dec51ca6
23 changed files with 294 additions and 125 deletions
@@ -1,27 +1,29 @@
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { getWorkflowPreviousStepId } from '@/workflow/workflow-steps/utils/getWorkflowPreviousStep';
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
import styled from '@emotion/styled';
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import {
IconBrackets,
JsonNestedNode,
JsonTreeContextProvider,
ShouldExpandNodeInitiallyProps,
useIcons,
} from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
display: grid;
overflow-x: auto;
padding-block: ${({ theme }) => theme.spacing(4)};
padding-inline: ${({ theme }) => theme.spacing(3)};
`;
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
const { t } = useLingui();
const { t, i18n } = useLingui();
const { getIcon } = useIcons();
const theme = useTheme();
const workflowRunId = useWorkflowRunIdOrThrow();
const workflowRun = useWorkflowRun({ workflowRunId });
@@ -49,6 +51,23 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
return null;
}
const stepDefinition = getStepDefinitionOrThrow({
stepId,
trigger: workflowRun.output.flow.trigger,
steps: workflowRun.output.flow.steps,
});
if (stepDefinition?.type !== 'action') {
throw new Error('The input tab must be rendered with an action step.');
}
const headerTitle = stepDefinition.definition.name;
const headerIcon = getActionIcon(stepDefinition.definition.type);
const headerIconColor = getActionIconColorOrThrow({
theme,
actionType: stepDefinition.definition.type,
});
const headerType = getActionHeaderTypeOrThrow(stepDefinition.definition.type);
const variablesUsedInStep = getWorkflowVariablesUsedInStep({
step,
});
@@ -69,30 +88,40 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
keyPath.startsWith(previousStepId) && depth < 2;
return (
<StyledContainer>
<JsonTreeContextProvider
value={{
emptyArrayLabel: t`Empty Array`,
emptyObjectLabel: t`Empty Object`,
emptyStringLabel: t`[empty string]`,
arrowButtonCollapsedLabel: t`Expand`,
arrowButtonExpandedLabel: t`Collapse`,
shouldHighlightNode: (keyPath) => variablesUsedInStep.has(keyPath),
shouldExpandNodeInitially: isFirstNodeDepthOfPreviousStep,
}}
>
<JsonNestedNode
elements={stepContext.map(({ id, name, context }) => ({
id,
label: name,
value: context,
}))}
Icon={IconBrackets}
depth={0}
keyPath=""
emptyElementsText=""
/>
</JsonTreeContextProvider>
</StyledContainer>
<>
<WorkflowStepHeader
disabled
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={i18n._(headerType)}
/>
<WorkflowRunStepJsonContainer>
<JsonTreeContextProvider
value={{
emptyArrayLabel: t`Empty Array`,
emptyObjectLabel: t`Empty Object`,
emptyStringLabel: t`[empty string]`,
arrowButtonCollapsedLabel: t`Expand`,
arrowButtonExpandedLabel: t`Collapse`,
shouldHighlightNode: (keyPath) => variablesUsedInStep.has(keyPath),
shouldExpandNodeInitially: isFirstNodeDepthOfPreviousStep,
}}
>
<JsonNestedNode
elements={stepContext.map(({ id, name, context }) => ({
id,
label: name,
value: context,
}))}
Icon={IconBrackets}
depth={0}
keyPath=""
emptyElementsText=""
/>
</JsonTreeContextProvider>
</WorkflowRunStepJsonContainer>
</>
);
};
@@ -0,0 +1,11 @@
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import styled from '@emotion/styled';
const StyledWorkflowRunStepJsonContainer = styled(WorkflowStepBody)`
grid-template-rows: max-content;
gap: 0;
display: grid;
overflow: auto;
`;
export { StyledWorkflowRunStepJsonContainer as WorkflowRunStepJsonContainer };
@@ -1,40 +1,68 @@
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
import styled from '@emotion/styled';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { isTwoFirstDepths, JsonTree } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
display: grid;
overflow-x: auto;
padding-block: ${({ theme }) => theme.spacing(4)};
padding-inline: ${({ theme }) => theme.spacing(3)};
`;
import { isTwoFirstDepths, JsonTree, useIcons } from 'twenty-ui';
export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
const { t, i18n } = useLingui();
const theme = useTheme();
const { getIcon } = useIcons();
const workflowRunId = useWorkflowRunIdOrThrow();
const workflowRun = useWorkflowRun({ workflowRunId });
const { t } = useLingui();
if (!isDefined(workflowRun?.output?.stepsOutput)) {
return null;
}
const stepOutput = workflowRun.output.stepsOutput[stepId];
const stepDefinition = getStepDefinitionOrThrow({
stepId,
trigger: workflowRun.output.flow.trigger,
steps: workflowRun.output.flow.steps,
});
if (stepDefinition?.type !== 'action') {
throw new Error('The output tab must be rendered with an action step.');
}
const headerTitle = stepDefinition.definition.name;
const headerIcon = getActionIcon(stepDefinition.definition.type);
const headerIconColor = getActionIconColorOrThrow({
theme,
actionType: stepDefinition.definition.type,
});
const headerType = getActionHeaderTypeOrThrow(stepDefinition.definition.type);
return (
<StyledContainer>
<JsonTree
value={stepOutput}
shouldExpandNodeInitially={isTwoFirstDepths}
emptyArrayLabel={t`Empty Array`}
emptyObjectLabel={t`Empty Object`}
emptyStringLabel={t`[empty string]`}
arrowButtonCollapsedLabel={t`Expand`}
arrowButtonExpandedLabel={t`Collapse`}
<>
<WorkflowStepHeader
disabled
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={i18n._(headerType)}
/>
</StyledContainer>
<WorkflowRunStepJsonContainer>
<JsonTree
value={stepOutput}
shouldExpandNodeInitially={isTwoFirstDepths}
emptyArrayLabel={t`Empty Array`}
emptyObjectLabel={t`Empty Object`}
emptyStringLabel={t`[empty string]`}
arrowButtonCollapsedLabel={t`Expand`}
arrowButtonExpandedLabel={t`Collapse`}
/>
</WorkflowRunStepJsonContainer>
</>
);
};
@@ -7,7 +7,8 @@ const StyledWorkflowStepBody = styled.div`
flex-direction: column;
height: 100%;
overflow-y: scroll;
padding: ${({ theme }) => theme.spacing(4)};
padding-block: ${({ theme }) => theme.spacing(4)};
padding-inline: ${({ theme }) => theme.spacing(3)};
row-gap: ${({ theme }) => theme.spacing(6)};
`;