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

View File

@ -4,6 +4,13 @@ import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hook
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 styled from '@emotion/styled';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
export const CommandMenuWorkflowEditStepContent = ({
workflow,
@ -19,12 +26,14 @@ export const CommandMenuWorkflowEditStepContent = ({
});
return (
<WorkflowStepDetail
stepId={workflowSelectedNode}
trigger={flow.trigger}
steps={flow.steps}
onActionUpdate={updateStep}
onTriggerUpdate={updateTrigger}
/>
<StyledContainer>
<WorkflowStepDetail
stepId={workflowSelectedNode}
trigger={flow.trigger}
steps={flow.steps}
onActionUpdate={updateStep}
onTriggerUpdate={updateTrigger}
/>
</StyledContainer>
);
};

View File

@ -20,17 +20,17 @@ import styled from '@emotion/styled';
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils';
const StyledTabList = styled(TabList)`
background-color: ${({ theme }) => theme.background.secondary};
padding-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
const StyledTabList = styled(TabList)`
background-color: ${({ theme }) => theme.background.secondary};
padding-left: ${({ theme }) => theme.spacing(2)};
`;
type TabId = WorkflowRunTabIdType;
export const CommandMenuWorkflowRunViewStep = () => {

View File

@ -2,20 +2,30 @@ import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
import { WorkflowStepContextProvider } from '@/workflow/states/context/WorkflowStepContext';
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
import styled from '@emotion/styled';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
export const CommandMenuWorkflowViewStep = () => {
const flow = useFlowOrThrow();
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
return (
<WorkflowStepContextProvider
value={{ workflowVersionId: flow.workflowVersionId }}
>
<WorkflowStepDetail
stepId={workflowSelectedNode}
trigger={flow.trigger}
steps={flow.steps}
readonly
/>
<StyledContainer>
<WorkflowStepDetail
stepId={workflowSelectedNode}
trigger={flow.trigger}
steps={flow.steps}
readonly
/>
</StyledContainer>
</WorkflowStepContextProvider>
);
};

View File

@ -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>
</>
);
};

View File

@ -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 };

View File

@ -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>
</>
);
};

View File

@ -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)};
`;

View File

@ -27,9 +27,10 @@ import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId';
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers';
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Monaco } from '@monaco-editor/react';
import { editor } from 'monaco-editor';
@ -40,12 +41,6 @@ import { CodeEditor, IconCode, IconPlayerPlay, useIcons } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
const StyledCodeEditorContainer = styled.div`
display: flex;
flex-direction: column;
@ -76,7 +71,6 @@ export const WorkflowEditActionServerlessFunction = ({
action,
actionOptions,
}: WorkflowEditActionServerlessFunctionProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
const activeTabId = useRecoilComponentValueV2(
@ -287,10 +281,12 @@ export const WorkflowEditActionServerlessFunction = ({
? action.name
: 'Code - Serverless Function';
const headerIcon = getActionIcon(action.type);
const headerIconColor = useActionIconColorOrThrow(action.type);
const headerType = useActionHeaderTypeOrThrow(action.type);
return (
!loading && (
<StyledContainer>
<>
<StyledTabList
tabs={tabs}
behaveAsLinks={false}
@ -303,9 +299,9 @@ export const WorkflowEditActionServerlessFunction = ({
updateAction({ name: newName });
}}
Icon={getIcon(headerIcon)}
iconColor={theme.color.orange}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType="Code"
headerType={headerType}
disabled={actionOptions.readonly}
/>
<WorkflowStepBody>
@ -373,7 +369,7 @@ export const WorkflowEditActionServerlessFunction = ({
]}
/>
)}
</StyledContainer>
</>
)
);
};

View File

@ -7,8 +7,9 @@ import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath'
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunctionFields';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers';
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Monaco } from '@monaco-editor/react';
import { editor } from 'monaco-editor';
@ -16,12 +17,6 @@ import { AutoTypings } from 'monaco-editor-auto-typings';
import { CodeEditor, useIcons } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
const StyledCodeEditorContainer = styled.div`
display: flex;
flex-direction: column;
@ -34,7 +29,6 @@ type WorkflowReadonlyActionServerlessFunctionProps = {
export const WorkflowReadonlyActionServerlessFunction = ({
action,
}: WorkflowReadonlyActionServerlessFunctionProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
const serverlessFunctionVersion =
@ -66,18 +60,20 @@ export const WorkflowReadonlyActionServerlessFunction = ({
? action.name
: 'Code - Serverless Function';
const headerIcon = getActionIcon(action.type);
const headerIconColor = useActionIconColorOrThrow(action.type);
const headerType = useActionHeaderTypeOrThrow(action.type);
if (loading) {
return null;
}
return (
<StyledContainer>
<>
<WorkflowStepHeader
Icon={getIcon(headerIcon)}
iconColor={theme.color.orange}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType="Code"
headerType={headerType}
disabled
/>
<WorkflowStepBody>
@ -99,6 +95,6 @@ export const WorkflowReadonlyActionServerlessFunction = ({
/>
</StyledCodeEditorContainer>
</WorkflowStepBody>
</StyledContainer>
</>
);
};

View File

@ -7,9 +7,10 @@ import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOr
import { WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useTheme } from '@emotion/react';
import { useEffect, useState } from 'react';
import { HorizontalSeparator, useIcons } from 'twenty-ui';
import { JsonValue } from 'type-fest';
@ -57,7 +58,6 @@ export const WorkflowEditActionCreateRecord = ({
action,
actionOptions,
}: WorkflowEditActionCreateRecordProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
@ -159,6 +159,8 @@ export const WorkflowEditActionCreateRecord = ({
const headerTitle = isDefined(action.name) ? action.name : `Create Record`;
const headerIcon = getActionIcon(action.type);
const headerIconColor = useActionIconColorOrThrow(action.type);
const headerType = useActionHeaderTypeOrThrow(action.type);
return (
<>
@ -174,9 +176,9 @@ export const WorkflowEditActionCreateRecord = ({
});
}}
Icon={getIcon(headerIcon)}
iconColor={theme.font.color.tertiary}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType="Action"
headerType={headerType}
disabled={isFormDisabled}
/>
<WorkflowStepBody>

View File

@ -3,10 +3,11 @@ import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowDeleteRecordAction } from '@/workflow/types/Workflow';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { WorkflowSingleRecordPicker } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordPicker';
import { useTheme } from '@emotion/react';
import { useEffect, useState } from 'react';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { HorizontalSeparator, useIcons } from 'twenty-ui';
import { JsonValue } from 'type-fest';
@ -34,7 +35,6 @@ export const WorkflowEditActionDeleteRecord = ({
action,
actionOptions,
}: WorkflowEditActionDeleteRecordProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
@ -108,6 +108,8 @@ export const WorkflowEditActionDeleteRecord = ({
const headerTitle = isDefined(action.name) ? action.name : `Delete Record`;
const headerIcon = getActionIcon(action.type);
const headerIconColor = useActionIconColorOrThrow(action.type);
const headerType = useActionHeaderTypeOrThrow(action.type);
return (
<>
@ -123,9 +125,9 @@ export const WorkflowEditActionDeleteRecord = ({
});
}}
Icon={getIcon(headerIcon)}
iconColor={theme.font.color.tertiary}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType="Action"
headerType={headerType}
disabled={isFormDisabled}
/>
<WorkflowStepBody>

View File

@ -2,11 +2,12 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilte
import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowFindRecordsAction } from '@/workflow/types/Workflow';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { useTheme } from '@emotion/react';
import { useEffect, useState } from 'react';
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { HorizontalSeparator, useIcons } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';
@ -33,7 +34,6 @@ export const WorkflowEditActionFindRecords = ({
action,
actionOptions,
}: WorkflowEditActionFindRecordsProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
@ -90,6 +90,8 @@ export const WorkflowEditActionFindRecords = ({
const headerTitle = isDefined(action.name) ? action.name : `Search Records`;
const headerIcon = getActionIcon(action.type);
const headerIconColor = useActionIconColorOrThrow(action.type);
const headerType = useActionHeaderTypeOrThrow(action.type);
return (
<>
@ -105,9 +107,9 @@ export const WorkflowEditActionFindRecords = ({
});
}}
Icon={getIcon(headerIcon)}
iconColor={theme.font.color.tertiary}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType="Action"
headerType={headerType}
disabled={isFormDisabled}
/>
<WorkflowStepBody>

View File

@ -12,9 +12,10 @@ import { workflowIdState } from '@/workflow/states/workflowIdState';
import { WorkflowSendEmailAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useTheme } from '@emotion/react';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { IconPlus, useIcons } from 'twenty-ui';
@ -47,7 +48,6 @@ export const WorkflowEditActionSendEmail = ({
action,
actionOptions,
}: WorkflowEditActionSendEmailProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { triggerApisOAuth } = useTriggerApisOAuth();
@ -188,6 +188,9 @@ export const WorkflowEditActionSendEmail = ({
const headerTitle = isDefined(action.name) ? action.name : 'Send Email';
const headerIcon = getActionIcon(action.type);
const headerIconColor = useActionIconColorOrThrow(action.type);
const headerType = useActionHeaderTypeOrThrow(action.type);
const navigate = useNavigateSettings();
const { closeCommandMenu } = useCommandMenu();
@ -206,9 +209,9 @@ export const WorkflowEditActionSendEmail = ({
});
}}
Icon={getIcon(headerIcon)}
iconColor={theme.color.blue}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType="Email"
headerType={headerType}
disabled={actionOptions.readonly}
/>
<WorkflowStepBody>

View File

@ -1,7 +1,6 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
import { useTheme } from '@emotion/react';
import { useEffect, useState } from 'react';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
@ -10,6 +9,8 @@ import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-typ
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { WorkflowSingleRecordPicker } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordPicker';
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { HorizontalSeparator, useIcons } from 'twenty-ui';
@ -59,7 +60,6 @@ export const WorkflowEditActionUpdateRecord = ({
action,
actionOptions,
}: WorkflowEditActionUpdateRecordProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
@ -160,6 +160,8 @@ export const WorkflowEditActionUpdateRecord = ({
const headerTitle = isDefined(action.name) ? action.name : `Update Record`;
const headerIcon = getActionIcon(action.type);
const headerIconColor = useActionIconColorOrThrow(action.type);
const headerType = useActionHeaderTypeOrThrow(action.type);
return (
<>
@ -175,9 +177,9 @@ export const WorkflowEditActionUpdateRecord = ({
});
}}
Icon={getIcon(headerIcon)}
iconColor={theme.font.color.tertiary}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType="Action"
headerType={headerType}
disabled={isFormDisabled}
/>

View File

@ -3,6 +3,7 @@ import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflo
import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, within } from '@storybook/test';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
@ -50,6 +51,7 @@ const meta: Meta<typeof WorkflowEditActionFindRecords> = {
SnackBarDecorator,
RouterDecorator,
WorkspaceDecorator,
I18nFrontDecorator,
],
};

View File

@ -1,8 +1,11 @@
import { WorkflowStepType } from '@/workflow/types/Workflow';
import { WorkflowActionType } from '@/workflow/types/Workflow';
export const OTHER_ACTIONS: Array<{
label: string;
type: WorkflowStepType;
type: Exclude<
WorkflowActionType,
'CREATE_RECORD' | 'UPDATE_RECORD' | 'DELETE_RECORD' | 'FIND_RECORDS'
>;
icon: string;
}> = [
{

View File

@ -1,8 +1,11 @@
import { WorkflowStepType } from '@/workflow/types/Workflow';
import { WorkflowActionType } from '@/workflow/types/Workflow';
export const RECORD_ACTIONS: Array<{
label: string;
type: WorkflowStepType;
type: Extract<
WorkflowActionType,
'CREATE_RECORD' | 'UPDATE_RECORD' | 'DELETE_RECORD' | 'FIND_RECORDS'
>;
icon: string;
}> = [
{

View File

@ -8,6 +8,8 @@ import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/Workflo
import { WorkflowEditActionFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings';
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
@ -99,6 +101,9 @@ export const WorkflowEditActionFormBuilder = ({
const headerTitle = isDefined(action.name) ? action.name : `Form`;
const headerIcon = getActionIcon(action.type);
const headerIconColor = useActionIconColorOrThrow(action.type);
const headerType = useActionHeaderTypeOrThrow(action.type);
const [selectedField, setSelectedField] = useState<string | null>(null);
const isFieldSelected = (fieldName: string) => selectedField === fieldName;
const handleFieldClick = (fieldName: string) => {
@ -161,9 +166,9 @@ export const WorkflowEditActionFormBuilder = ({
});
}}
Icon={getIcon(headerIcon)}
iconColor={theme.font.color.tertiary}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType="Action"
headerType={headerType}
disabled={actionOptions.readonly}
/>
<WorkflowStepBody>

View File

@ -0,0 +1,9 @@
import { WorkflowActionType } from '@/workflow/types/Workflow';
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
import { useLingui } from '@lingui/react';
export const useActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
const { _ } = useLingui();
return _(getActionHeaderTypeOrThrow(actionType));
};

View File

@ -0,0 +1,9 @@
import { WorkflowActionType } from '@/workflow/types/Workflow';
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
import { useTheme } from '@emotion/react';
export const useActionIconColorOrThrow = (actionType: WorkflowActionType) => {
const theme = useTheme();
return getActionIconColorOrThrow({ theme, actionType });
};

View File

@ -0,0 +1,20 @@
import { WorkflowActionType } from '@/workflow/types/Workflow';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
switch (actionType) {
case 'CODE':
return msg`Code`;
case 'CREATE_RECORD':
case 'UPDATE_RECORD':
case 'DELETE_RECORD':
case 'FIND_RECORDS':
case 'FORM':
return msg`Action`;
case 'SEND_EMAIL':
return msg`Email`;
default:
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
}
};

View File

@ -1,7 +1,8 @@
import { WorkflowActionType } from '@/workflow/types/Workflow';
import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions';
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
export const getActionIcon = (actionType: string) => {
export const getActionIcon = (actionType: WorkflowActionType) => {
switch (actionType) {
case 'CREATE_RECORD':
case 'UPDATE_RECORD':

View File

@ -0,0 +1,26 @@
import { WorkflowActionType } from '@/workflow/types/Workflow';
import { Theme } from '@emotion/react';
import { assertUnreachable } from 'twenty-shared/utils';
export const getActionIconColorOrThrow = ({
theme,
actionType,
}: {
theme: Theme;
actionType: WorkflowActionType;
}) => {
switch (actionType) {
case 'CODE':
return theme.color.orange;
case 'CREATE_RECORD':
case 'UPDATE_RECORD':
case 'DELETE_RECORD':
case 'FIND_RECORDS':
case 'FORM':
return theme.font.color.tertiary;
case 'SEND_EMAIL':
return theme.color.blue;
default:
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
}
};