feat: fix Command Menu Side Panel Layout (#15883)

[Figma
Design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=81380-344641&t=FpjWNOK2gZuDQQfr-0)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds a side panel layout for the Command Menu, routes modals into a
local container, updates the top bar and context chips, and standardizes
small button sizes.
> 
> - **Command Menu**:
> - **Side Panel Layout**: Introduces `CommandMenuSidePanelLayout` with
animated width, hosts `CommandMenuRouter`, and provides a modal
container via `ModalContainerContext`.
> - **Top Bar**: Redesign (`CommandMenuTopBar`) with back icon, optional
AI sparkles action, compact height
(`COMMAND_MENU_SEARCH_BAR_HEIGHT=40`), and updated placeholder.
> - **Context Chips**: Adds `CommandMenuLastContextChip` and
`CommandMenuRecordInfo`; extends `CommandMenuContextChip` with `page`
prop; updates `CommandMenuContextChipGroups` to render last chip as
record info when applicable.
> - **Container Simplification**: `CommandMenuContainer` simplified to
just provide contexts and `AgentChatProvider`.
> - **Modal System**:
> - Adds `ModalContainerContext` and updates `Modal` to portal into
provided container; `Modal.Backdrop` supports `isInContainer`.
> - Updates usages (e.g., `UserOrMetadataLoader`, `ActionModal`) to
align with new modal behavior.
> - **Page Integration**:
> - Replaces `PageBody` with `CommandMenuSidePanelLayout` in
`RecordShowPage` and `RecordIndexContainerGater`.
> - Removes global `CommandMenuRouter` from `DefaultLayout` (keeps
keyboard shortcuts).
> - **UI/Styling**:
> - Standardizes several buttons to `size="small"` (e.g., command
actions, open record, options, reply, workflow footer).
> - Adjusts `ShowPageSubContainer` styling when rendered inside command
menu.
>   - Storybook tests updated for new placeholder text.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
81fcaa145618a2fa1c3e11e2dc88fe832c51374c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
Co-authored-by: Aman Raj <92664006+araj00@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-11-21 20:42:41 +05:30
committed by GitHub
parent 28b8a4f7ec
commit 04b0a65e73
102 changed files with 1913 additions and 1548 deletions
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { workflowRunIteratorSubStepIterationIndexComponentState } from '@/command-menu/pages/workflow/step/view-run/states/workflowRunIteratorSubStepIterationIndexComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
@@ -8,13 +7,9 @@ import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/componen
import { getIsDescendantOfIterator } from '@/workflow/workflow-steps/utils/getIsDescendantOfIterator';
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
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 { isDefined } from 'twenty-shared/utils';
import { IconBrackets, useIcons } from 'twenty-ui/display';
import { IconBrackets } from 'twenty-ui/display';
import {
type GetJsonNodeHighlighting,
JsonNestedNode,
@@ -25,9 +20,7 @@ import { type JsonValue } from 'type-fest';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
const { t, i18n } = useLingui();
const { getIcon } = useIcons();
const theme = useTheme();
const { t } = useLingui();
const { copyToClipboard } = useCopyToClipboard();
const workflowRunId = useWorkflowRunIdOrThrow();
@@ -61,14 +54,6 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
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,
});
@@ -115,14 +100,6 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
return (
<>
<SidePanelHeader
disabled
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={i18n._(headerType)}
/>
<WorkflowRunStepJsonContainer>
<JsonTreeContextProvider
value={{
@@ -1,21 +1,11 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
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 { useWorkflowRunStepInfo } from '@/workflow/workflow-steps/hooks/useWorkflowRunStepInfo';
import { getWorkflowRunStepInfoToDisplayAsOutput } from '@/workflow/workflow-steps/utils/getWorkflowRunStepInfoToDisplayAsOutput';
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 { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import {
type GetJsonNodeHighlighting,
isTwoFirstDepths,
@@ -24,9 +14,7 @@ import {
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
const { t, i18n } = useLingui();
const theme = useTheme();
const { getIcon } = useIcons();
const { t } = useLingui();
const { copyToClipboard } = useCopyToClipboard();
const workflowRunId = useWorkflowRunIdOrThrow();
@@ -51,26 +39,6 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
throw new Error('The step is expected to be properly shaped.');
}
const headerTitle = stepDefinition.definition.name;
const headerIcon =
stepDefinition.type === 'trigger'
? getTriggerIcon(stepDefinition.definition)
: getActionIcon(stepDefinition.definition.type);
const headerIconColor =
stepDefinition.type === 'trigger'
? getTriggerIconColor({
theme,
triggerType: stepDefinition.definition.type,
})
: getActionIconColorOrThrow({
theme,
actionType: stepDefinition.definition.type,
});
const headerType =
stepDefinition.type === 'trigger'
? getTriggerHeaderType(stepDefinition.definition)
: i18n._(getActionHeaderTypeOrThrow(stepDefinition.definition.type));
const setRedHighlightingForEveryNode: GetJsonNodeHighlighting = (keyPath) => {
if (keyPath.startsWith('error')) {
return 'red';
@@ -81,16 +49,6 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
return (
<>
<SidePanelHeader
disabled
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={
headerTitle ?? getTriggerDefaultLabel(workflowRun.state.flow.trigger)
}
headerType={headerType}
/>
<WorkflowRunStepJsonContainer>
<JsonTree
value={stepInfoToDisplay ?? t`No output available`}
@@ -44,7 +44,11 @@ export const WorkflowStepFooter = ({
dropdownId={dropdownId}
data-select-disable
clickableComponent={
<Button title={t`Options`} hotkeys={[getOsControlSymbol(), 'O']} />
<Button
size="small"
title={t`Options`}
hotkeys={[getOsControlSymbol(), 'O']}
/>
}
dropdownPlacement="top-end"
dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
@@ -99,6 +103,7 @@ export const WorkflowStepFooter = ({
const deleteButton = (
<Button
size="small"
title={t`Delete`}
onClick={() => {
deleteStep(stepId);
@@ -0,0 +1,94 @@
import { renderHook } from '@testing-library/react';
import { useUpdateAgentLabel } from '../useUpdateAgentLabel';
const mockUpdateAgent = jest.fn();
const mockUseFindOneAgentQuery = jest.fn();
jest.mock('~/generated-metadata/graphql', () => ({
useFindOneAgentQuery: jest.fn(),
useUpdateOneAgentMutation: () => [mockUpdateAgent],
}));
describe('useUpdateAgentLabel', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseFindOneAgentQuery.mockReturnValue({
data: undefined,
});
(
require('~/generated-metadata/graphql').useFindOneAgentQuery as jest.Mock
).mockImplementation(mockUseFindOneAgentQuery);
});
it('should skip query when agentId is undefined', () => {
renderHook(() => useUpdateAgentLabel(undefined));
expect(mockUseFindOneAgentQuery).toHaveBeenCalledWith({
variables: { id: '' },
skip: true,
});
});
it('should fetch agent when agentId is provided', () => {
const mockAgent = {
id: 'agent-123',
label: 'Original Label',
};
mockUseFindOneAgentQuery.mockReturnValue({
data: {
findOneAgent: mockAgent,
},
});
renderHook(() => useUpdateAgentLabel('agent-123'));
expect(mockUseFindOneAgentQuery).toHaveBeenCalledWith({
variables: { id: 'agent-123' },
skip: false,
});
});
it('should update agent label when agent is defined', async () => {
const mockAgent = {
id: 'agent-123',
label: 'Original Label',
};
mockUseFindOneAgentQuery.mockReturnValue({
data: {
findOneAgent: mockAgent,
},
});
mockUpdateAgent.mockResolvedValue({});
const { result } = renderHook(() => useUpdateAgentLabel('agent-123'));
await result.current.updateAgentLabel('New Label');
expect(mockUpdateAgent).toHaveBeenCalledWith({
variables: {
input: {
id: 'agent-123',
label: 'New Label',
},
},
refetchQueries: ['FindOneAgent'],
});
});
it('should not update agent label when agent is undefined', async () => {
mockUseFindOneAgentQuery.mockReturnValue({
data: {
findOneAgent: undefined,
},
});
const { result } = renderHook(() => useUpdateAgentLabel('agent-123'));
await result.current.updateAgentLabel('New Label');
expect(mockUpdateAgent).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,34 @@
import { isDefined } from 'twenty-shared/utils';
import {
useFindOneAgentQuery,
useUpdateOneAgentMutation,
} from '~/generated-metadata/graphql';
export const useUpdateAgentLabel = (agentId: string | undefined) => {
const { data: agentData } = useFindOneAgentQuery({
variables: { id: agentId || '' },
skip: !isDefined(agentId),
});
const [updateAgent] = useUpdateOneAgentMutation();
const agent = agentData?.findOneAgent;
const updateAgentLabel = async (newLabel: string) => {
if (!isDefined(agent)) {
return;
}
await updateAgent({
variables: {
input: {
id: agent.id,
label: newLabel,
},
},
refetchQueries: ['FindOneAgent'],
});
};
return { updateAgentLabel };
};
@@ -1,5 +1,4 @@
import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions';
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { Select } from '@/ui/input/components/Select';
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
@@ -7,8 +6,6 @@ import { type WorkflowAiAgentAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep';
import { AI_AGENT_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/AiAgentAction';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { t } from '@lingui/core/macro';
import {
@@ -16,7 +13,6 @@ import {
type ModelConfiguration,
} from 'twenty-shared/ai';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { useDebouncedCallback } from 'use-debounce';
import {
useFindOneAgentQuery,
@@ -40,13 +36,6 @@ export const WorkflowEditActionAiAgent = ({
action,
actionOptions,
}: WorkflowEditActionAiAgentProps) => {
const { getIcon } = useIcons();
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: AI_AGENT_ACTION.defaultLabel,
});
const agentId = action.settings.input.agentId;
const { data: agentData, loading: agentLoading } = useFindOneAgentQuery({
variables: { id: agentId || '' },
@@ -157,34 +146,6 @@ export const WorkflowEditActionAiAgent = ({
<RightDrawerSkeletonLoader />
) : (
<>
<SidePanelHeader
onTitleChange={async (newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate?.({ ...action, name: newName });
// Also update agent label
if (isDefined(agent)) {
await updateAgent({
variables: {
input: {
id: agent.id,
label: newName,
},
},
refetchQueries: ['FindOneAgent'],
});
}
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={actionOptions.readonly}
iconTooltip={AI_AGENT_ACTION.defaultLabel}
/>
<WorkflowStepBody>
<FormTextFieldInput
multiline
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { useFullScreenModal } from '@/ui/layout/fullscreen/hooks/useFullScreenModal';
@@ -29,9 +28,6 @@ import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/w
import { serverlessFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/serverlessFunctionTestDataFamilyState';
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 styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
@@ -49,7 +45,7 @@ import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { buildOutputSchemaFromValue } from 'twenty-shared/workflow';
import { IconCode, IconPlayerPlay, useIcons } from 'twenty-ui/display';
import { IconCode, IconPlayerPlay } from 'twenty-ui/display';
import { CodeEditor } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
import { useDebouncedCallback } from 'use-debounce';
@@ -95,7 +91,6 @@ export const WorkflowEditActionServerlessFunction = ({
action,
actionOptions,
}: WorkflowEditActionServerlessFunctionProps) => {
const { getIcon } = useIcons();
const { t } = useLingui();
const [isFullScreen, setIsFullScreen] = useState(false);
const isMobile = useIsMobile();
@@ -329,13 +324,6 @@ export const WorkflowEditActionServerlessFunction = ({
dependencies: [isFullScreen],
});
const headerTitle = isDefined(action.name)
? action.name
: CODE_ACTION.defaultLabel;
const headerIcon = getActionIcon(action.type);
const headerIconColor = useActionIconColorOrThrow(action.type);
const headerType = useActionHeaderTypeOrThrow(action.type);
const testLogsTextAreaId = `${serverlessFunctionId}-test-logs`;
const breadcrumbLinks: BreadcrumbProps['links'] = [
@@ -344,7 +332,7 @@ export const WorkflowEditActionServerlessFunction = ({
href: '#',
},
{
children: headerTitle,
children: isDefined(action.name) ? action.name : CODE_ACTION.defaultLabel,
href: '#',
},
{
@@ -427,17 +415,6 @@ export const WorkflowEditActionServerlessFunction = ({
WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID
}
/>
<SidePanelHeader
onTitleChange={(newName: string) => {
updateAction({ name: newName });
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={actionOptions.readonly}
iconTooltip={CODE_ACTION.defaultLabel}
/>
<WorkflowStepBody>
{activeTabId === WorkflowServerlessFunctionTabId.CODE && (
<>
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { type WorkflowCodeAction } from '@/workflow/types/Workflow';
@@ -7,15 +6,10 @@ import { INDEX_FILE_NAME } from '@/serverless-functions/constants/IndexFileName'
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 styled from '@emotion/styled';
import { type Monaco } from '@monaco-editor/react';
import { type editor } from 'monaco-editor';
import { AutoTypings } from 'monaco-editor-auto-typings';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { CodeEditor } from 'twenty-ui/input';
import { SOURCE_FOLDER_NAME } from '@/serverless-functions/constants/SourceFolderName';
@@ -31,7 +25,6 @@ type WorkflowReadonlyActionServerlessFunctionProps = {
export const WorkflowReadonlyActionServerlessFunction = ({
action,
}: WorkflowReadonlyActionServerlessFunctionProps) => {
const { getIcon } = useIcons();
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
const serverlessFunctionVersion =
action.settings.input.serverlessFunctionVersion;
@@ -58,13 +51,6 @@ export const WorkflowReadonlyActionServerlessFunction = ({
});
};
const headerTitle = isDefined(action.name)
? action.name
: 'Code - Serverless Function';
const headerIcon = getActionIcon(action.type);
const headerIconColor = useActionIconColorOrThrow(action.type);
const headerType = useActionHeaderTypeOrThrow(action.type);
if (loading) {
return null;
}
@@ -77,13 +63,6 @@ export const WorkflowReadonlyActionServerlessFunction = ({
return (
<>
<SidePanelHeader
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled
/>
<WorkflowStepBody>
<WorkflowEditActionServerlessFunctionFields
functionInput={action.settings.input.serverlessFunctionInput}
@@ -1,13 +1,9 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { type WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import {
WorkflowCreateRecordBody,
type CreateRecordFormData,
} from '@/workflow/workflow-steps/workflow-actions/components/WorkflowCreateRecordBody';
import { CREATE_RECORD_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CreateRecordAction';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { useIcons } from 'twenty-ui/display';
type WorkflowEditActionCreateRecordProps = {
action: WorkflowCreateRecordAction;
@@ -25,16 +21,8 @@ export const WorkflowEditActionCreateRecord = ({
action,
actionOptions,
}: WorkflowEditActionCreateRecordProps) => {
const { getIcon } = useIcons();
const isFormDisabled = actionOptions.readonly;
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: CREATE_RECORD_ACTION.defaultLabel,
});
const handleUpdate = (formData: CreateRecordFormData) => {
if (actionOptions.readonly === true) {
return;
@@ -56,24 +44,6 @@ export const WorkflowEditActionCreateRecord = ({
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={isFormDisabled}
iconTooltip={CREATE_RECORD_ACTION.defaultLabel}
/>
<WorkflowCreateRecordBody
defaultObjectName={action.settings.input.objectName}
defaultObjectRecord={action.settings.input.objectRecord}
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
import { Select } from '@/ui/input/components/Select';
@@ -8,8 +7,6 @@ import { useEffect, useState } from 'react';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { DELETE_RECORD_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/DeleteRecordAction';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useTheme } from '@emotion/react';
import { isDefined } from 'twenty-shared/utils';
@@ -116,32 +113,8 @@ export const WorkflowEditActionDeleteRecord = ({
};
}, [saveAction]);
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: DELETE_RECORD_ACTION.defaultLabel,
});
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={isFormDisabled}
iconTooltip={DELETE_RECORD_ACTION.defaultLabel}
/>
<WorkflowStepBody>
<Select
dropdownId="workflow-edit-action-record-delete-object-name"
@@ -4,7 +4,6 @@ import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { WorkflowSendEmailAttachments } from '@/advanced-text-editor/components/WorkflowSendEmailAttachments';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
@@ -20,8 +19,6 @@ import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/wo
import { type WorkflowSendEmailAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { SEND_EMAIL_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/SendEmailAction';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
@@ -29,7 +26,7 @@ import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { ConnectedAccountProvider, SettingsPath } from 'twenty-shared/types';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { IconPlus, useIcons } from 'twenty-ui/display';
import { IconPlus } from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
import { type JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce';
@@ -72,7 +69,6 @@ export const WorkflowEditActionSendEmail = ({
actionOptions,
}: WorkflowEditActionSendEmailProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { triggerApisOAuth } = useTriggerApisOAuth();
const { enqueueErrorSnackBar } = useSnackBar();
@@ -257,12 +253,6 @@ export const WorkflowEditActionSendEmail = ({
}
});
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: SEND_EMAIL_ACTION.defaultLabel,
});
const navigate = useNavigateSettings();
const { closeCommandMenu } = useCommandMenu();
@@ -270,24 +260,6 @@ export const WorkflowEditActionSendEmail = ({
return (
!loading && (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={actionOptions.readonly}
iconTooltip={SEND_EMAIL_ACTION.defaultLabel}
/>
<WorkflowStepBody>
<Select
dropdownId="select-connected-account-id"
@@ -346,7 +318,7 @@ export const WorkflowEditActionSendEmail = ({
href: '#',
},
{
children: headerTitle,
children: isDefined(action.name) ? action.name : t`Send Email`,
href: '#',
},
{
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
@@ -10,8 +9,6 @@ import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpd
import { type WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { UPDATE_RECORD_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/UpdateRecordAction';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { type UpdateRecordFormData } from '@/workflow/workflow-steps/workflow-actions/types/update-record-form-data.type';
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
@@ -71,12 +68,6 @@ export const WorkflowEditActionUpdateRecord = ({
const isFormDisabled = actionOptions.readonly === true;
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: UPDATE_RECORD_ACTION.defaultLabel,
});
const handleFieldChange = (
fieldName: keyof UpdateRecordFormData,
updatedValue: JsonValue,
@@ -156,24 +147,6 @@ export const WorkflowEditActionUpdateRecord = ({
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={isFormDisabled}
iconTooltip={UPDATE_RECORD_ACTION.defaultLabel}
/>
<WorkflowStepBody>
<Select
dropdownId="workflow-update-record-object-name"
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { type WorkflowUpsertRecordAction } from '@/workflow/types/Workflow';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import {
@@ -6,10 +5,6 @@ import {
type CreateRecordFormData,
} from '@/workflow/workflow-steps/workflow-actions/components/WorkflowCreateRecordBody';
import { UPSERT_RECORD_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/UpsertRecordAction';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { useIcons } from 'twenty-ui/display';
type WorkflowEditActionUpsertRecordProps = {
action: WorkflowUpsertRecordAction;
actionOptions:
@@ -26,14 +21,6 @@ export const WorkflowEditActionUpsertRecord = ({
action,
actionOptions,
}: WorkflowEditActionUpsertRecordProps) => {
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: UPSERT_RECORD_ACTION.defaultLabel,
});
const { getIcon } = useIcons();
const isFormDisabled = actionOptions.readonly === true;
const handleUpdate = (formData: CreateRecordFormData) => {
@@ -57,24 +44,6 @@ export const WorkflowEditActionUpsertRecord = ({
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={isFormDisabled}
iconTooltip={UPSERT_RECORD_ACTION.defaultLabel}
/>
<WorkflowCreateRecordBody
defaultObjectName={action.settings.input.objectName}
defaultObjectRecord={action.settings.input.objectRecord}
@@ -72,15 +72,6 @@ export const Disabled: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleText = await canvas.findByText('Create Record');
expect(window.getComputedStyle(titleText).cursor).toBe('default');
await userEvent.click(titleText);
const titleInput = canvas.queryByDisplayValue('Create Record');
expect(titleInput).not.toBeInTheDocument();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
@@ -77,15 +77,6 @@ export const DisabledWithEmptyValues: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleText = await canvas.findByText('Delete Record');
expect(window.getComputedStyle(titleText).cursor).toBe('default');
await userEvent.click(titleText);
const titleInput = canvas.queryByDisplayValue('Delete Record');
expect(titleInput).not.toBeInTheDocument();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
@@ -128,15 +119,6 @@ export const DisabledWithDefaultStaticValues: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleText = await canvas.findByText('Delete Record');
expect(window.getComputedStyle(titleText).cursor).toBe('default');
await userEvent.click(titleText);
const titleInput = canvas.queryByDisplayValue('Delete Record');
expect(titleInput).not.toBeInTheDocument();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
@@ -187,15 +169,6 @@ export const DisabledWithDefaultVariableValues: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleText = await canvas.findByText('Delete Record');
expect(window.getComputedStyle(titleText).cursor).toBe('default');
await userEvent.click(titleText);
const titleInput = canvas.queryByDisplayValue('Delete Record');
expect(titleInput).not.toBeInTheDocument();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
@@ -76,15 +76,6 @@ export const DisabledWithEmptyValues: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleText = await canvas.findByText('Search Records');
expect(window.getComputedStyle(titleText).cursor).toBe('default');
await userEvent.click(titleText);
const titleInput = canvas.queryByDisplayValue('Search Records');
expect(titleInput).not.toBeInTheDocument();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
@@ -112,7 +112,6 @@ export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Send Email')).toBeVisible();
expect(await canvas.findByText('Account')).toBeVisible();
expect(await canvas.findByText('Subject')).toBeVisible();
expect(await canvas.findByText('Body')).toBeVisible();
@@ -129,8 +128,6 @@ export const Configured: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Send Welcome Email')).toBeVisible();
expect(await canvas.findByText('Account')).toBeVisible();
expect(await canvas.findByText('Subject')).toBeVisible();
expect(await canvas.findByText('Body')).toBeVisible();
@@ -90,15 +90,6 @@ export const DisabledWithEmptyValues: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleText = await canvas.findByText('Update Record');
expect(window.getComputedStyle(titleText).cursor).toBe('default');
await userEvent.click(titleText);
const titleInput = canvas.queryByDisplayValue('Update Record');
expect(titleInput).not.toBeInTheDocument();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
@@ -152,15 +143,6 @@ export const DisabledWithDefaultStaticValues: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleText = await canvas.findByText('Update Record');
expect(window.getComputedStyle(titleText).cursor).toBe('default');
await userEvent.click(titleText);
const titleInput = canvas.queryByDisplayValue('Update Record');
expect(titleInput).not.toBeInTheDocument();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
@@ -216,15 +198,6 @@ export const DisabledWithDefaultVariableValues: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleText = await canvas.findByText('Update Record');
expect(window.getComputedStyle(titleText).cursor).toBe('default');
await userEvent.click(titleText);
const titleInput = canvas.queryByDisplayValue('Update Record');
expect(titleInput).not.toBeInTheDocument();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { FormDateTimeFieldInput } from '@/object-record/record-field/ui/form-types/components/FormDateTimeFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
import { Select } from '@/ui/input/components/Select';
@@ -6,8 +5,6 @@ import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/Gene
import { type WorkflowDelayAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { DELAY_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/DelayAction';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { t } from '@lingui/core/macro';
import {
@@ -32,12 +29,6 @@ export const WorkflowEditActionDelay = ({
action,
actionOptions,
}: WorkflowEditActionDelayProps) => {
const { headerTitle, headerIcon, headerIconColor, headerType, getIcon } =
useWorkflowActionHeader({
action,
defaultTitle: DELAY_ACTION.defaultLabel,
});
const delayOptions: Array<SelectOption<'SCHEDULED_DATE' | 'DURATION'>> = [
{
label: t`At a specific date or time`,
@@ -139,28 +130,8 @@ export const WorkflowEditActionDelay = ({
});
};
const HeaderIcon = getIcon(headerIcon ?? 'IconPlayerPause');
return (
<>
<SidePanelHeader
initialTitle={headerTitle}
Icon={HeaderIcon}
iconColor={headerIconColor}
headerType={headerType}
onTitleChange={(newTitle: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newTitle,
});
}}
iconTooltip={DELAY_ACTION.defaultLabel}
/>
<WorkflowStepBody>
<Select
dropdownId="workflow-edit-action-delay-type"
@@ -1,14 +1,10 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { type WorkflowFilterAction } from '@/workflow/types/Workflow';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { FILTER_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/FilterAction';
import { WorkflowEditActionFilterBody } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody';
import { WorkflowEditActionFilterBodyEffect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBodyEffect';
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFiltersComponentInstanceContext';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
import { useIcons } from 'twenty-ui/display';
type WorkflowEditActionFilterProps = {
action: WorkflowFilterAction;
@@ -31,34 +27,8 @@ export const WorkflowEditActionFilter = ({
action,
actionOptions,
}: WorkflowEditActionFilterProps) => {
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: FILTER_ACTION.defaultLabel,
});
const { getIcon } = useIcons();
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={actionOptions.readonly}
iconTooltip={FILTER_ACTION.defaultLabel}
/>
<StepFiltersComponentInstanceContext.Provider
value={{
instanceId: action.id,
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select } from '@/ui/input/components/Select';
import { type WorkflowFindRecordsAction } from '@/workflow/types/Workflow';
@@ -17,11 +16,9 @@ import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { FIND_RECORDS_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/FindRecordsAction';
import { WorkflowFindRecordsFilters } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFilters';
import { WorkflowFindRecordsFiltersEffect } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFiltersEffect';
import { WorkflowFindRecordsSorts } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsSorts';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { useLingui } from '@lingui/react/macro';
import { isNumber } from '@sniptt/guards';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
@@ -147,32 +144,8 @@ export const WorkflowEditActionFindRecords = ({
};
}, [saveAction]);
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: FIND_RECORDS_ACTION.defaultLabel,
});
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={isFormDisabled}
iconTooltip={FIND_RECORDS_ACTION.defaultLabel}
/>
<WorkflowStepBody>
<Select
dropdownId="workflow-edit-action-record-find-records-object-name"
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer';
@@ -9,12 +8,10 @@ import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableLi
import { type WorkflowFormAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { FORM_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/FormAction';
import { WorkflowEditActionFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings';
import { WorkflowFormEmptyMessage } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormEmptyMessage';
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { type OnDragEndResponder } from '@hello-pangea/dnd';
@@ -28,7 +25,6 @@ import {
IconGripVertical,
IconPlus,
IconTrash,
useIcons,
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { useDebouncedCallback } from 'use-debounce';
@@ -136,17 +132,10 @@ export const WorkflowEditActionFormBuilder = ({
actionOptions,
}: WorkflowEditActionFormBuilderProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const { t } = useLingui();
const [formData, setFormData] = useState<FormData>(action.settings.input);
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: FORM_ACTION.defaultLabel,
});
const [selectedField, setSelectedField] = useState<string | null>(null);
const [hoveredField, setHoveredField] = useState<string | null>(null);
@@ -223,24 +212,6 @@ export const WorkflowEditActionFormBuilder = ({
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={actionOptions.readonly}
iconTooltip={FORM_ACTION.defaultLabel}
/>
<StyledWorkflowStepBody>
{formData.length === 0 && (
<WorkflowFormEmptyMessage data-testid="empty-form-message" />
@@ -1,5 +1,4 @@
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
@@ -10,15 +9,12 @@ import { type WorkflowFormAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { useUpdateWorkflowRunStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowRunStep';
import { WorkflowFormFieldInput } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowFormFieldInput';
import { FORM_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/FormAction';
import { useSubmitFormStep } from '@/workflow/workflow-steps/workflow-actions/form-action/hooks/useSubmitFormStep';
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { useDebouncedCallback } from 'use-debounce';
export type WorkflowEditActionFormFillerProps = {
@@ -35,7 +31,6 @@ export const WorkflowEditActionFormFiller = ({
actionOptions,
}: WorkflowEditActionFormFillerProps) => {
const { t } = useLingui();
const { getIcon } = useIcons();
const { submitFormStep } = useSubmitFormStep();
const [formData, setFormData] = useState<FormData>(action.settings.input);
const workflowRunId = useWorkflowRunIdOrThrow();
@@ -45,12 +40,6 @@ export const WorkflowEditActionFormFiller = ({
const canSubmit = !actionOptions.readonly && !isDefined(error);
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: FORM_ACTION.defaultLabel,
});
const onFieldUpdate = ({
fieldId,
value,
@@ -111,14 +100,6 @@ export const WorkflowEditActionFormFiller = ({
return (
<>
<SidePanelHeader
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled
iconTooltip={FORM_ACTION.defaultLabel}
/>
<WorkflowStepBody>
{formData.map((field) => {
if (field.type === 'RECORD') {
@@ -159,15 +159,6 @@ export const DisabledWithEmptyValues: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleText = await canvas.findByText('Form');
expect(window.getComputedStyle(titleText).cursor).toBe('default');
await userEvent.click(titleText);
const titleInput = canvas.queryByDisplayValue('Form');
expect(titleInput).not.toBeInTheDocument();
await canvas.findByText('Company');
const addFieldButton = canvas.queryByText('Add Field');
@@ -1,223 +0,0 @@
import {
type WorkflowFormAction,
type WorkflowHttpRequestAction,
type WorkflowSendEmailAction,
} from '@/workflow/types/Workflow';
import { renderHook } from '@testing-library/react';
import { FieldMetadataType } from 'twenty-shared/types';
import {
workflowFormActionSettingsSchema,
workflowHttpRequestActionSettingsSchema,
workflowSendEmailActionSettingsSchema,
} from 'twenty-shared/workflow';
import { useWorkflowActionHeader } from '../useWorkflowActionHeader';
jest.mock('../useActionIconColorOrThrow', () => ({
useActionIconColorOrThrow: jest.fn().mockReturnValue('blue'),
}));
jest.mock('../useActionHeaderTypeOrThrow', () => ({
useActionHeaderTypeOrThrow: jest.fn().mockReturnValue('Action'),
}));
jest.mock('../../utils/getActionIcon', () => ({
getActionIcon: jest.fn().mockReturnValue('IconHttp'),
}));
const mockGetIcon = jest.fn().mockReturnValue('IconComponent');
jest.mock('twenty-ui/display', () => ({
useIcons: () => ({
getIcon: mockGetIcon,
}),
}));
describe('useWorkflowActionHeader', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('when action name is not defined', () => {
it('should return default title', () => {
const action = {
id: '1',
name: '',
type: 'HTTP_REQUEST',
settings: workflowHttpRequestActionSettingsSchema.parse({
input: {
url: 'https://example.com',
method: 'GET',
headers: {},
body: {},
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
}),
valid: true,
} satisfies WorkflowHttpRequestAction;
const { result } = renderHook(() =>
useWorkflowActionHeader({
action,
defaultTitle: 'HTTP Request',
}),
);
expect(result.current.headerTitle).toBe('HTTP Request');
expect(result.current.headerIcon).toBe('IconHttp');
expect(result.current.headerIconColor).toBe('blue');
expect(result.current.headerType).toBe('Action');
expect(result.current.getIcon).toBe(mockGetIcon);
});
});
describe('when action name is defined', () => {
it('should return the action name', () => {
const action = {
id: '1',
name: 'Test Action',
type: 'HTTP_REQUEST',
settings: workflowHttpRequestActionSettingsSchema.parse({
input: {
url: 'https://example.com',
method: 'GET',
headers: {},
body: {},
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
}),
valid: true,
} satisfies WorkflowHttpRequestAction;
const { result } = renderHook(() =>
useWorkflowActionHeader({
action,
defaultTitle: 'HTTP Request',
}),
);
expect(result.current.headerTitle).toBe('Test Action');
expect(result.current.headerIcon).toBe('IconHttp');
expect(result.current.headerIconColor).toBe('blue');
expect(result.current.headerType).toBe('Action');
expect(result.current.getIcon).toBe(mockGetIcon);
});
});
describe('when action type is defined', () => {
it('should return default title for HTTP request action', () => {
const action = {
id: '1',
name: '',
type: 'HTTP_REQUEST',
settings: workflowHttpRequestActionSettingsSchema.parse({
input: {
url: 'https://example.com',
method: 'GET',
headers: {},
body: {},
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
}),
valid: true,
} satisfies WorkflowHttpRequestAction;
const { result } = renderHook(() =>
useWorkflowActionHeader({
action,
defaultTitle: 'HTTP Request',
}),
);
expect(result.current.headerTitle).toBe('HTTP Request');
expect(result.current.headerIcon).toBe('IconHttp');
expect(result.current.headerIconColor).toBe('blue');
expect(result.current.headerType).toBe('Action');
expect(result.current.getIcon).toBe(mockGetIcon);
});
it('should return default title for form action', () => {
const action = {
id: '1',
name: '',
type: 'FORM',
settings: workflowFormActionSettingsSchema.parse({
input: [
{
id: '1',
name: 'field1',
label: 'Field 1',
type: FieldMetadataType.TEXT,
required: false,
settings: {},
},
],
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
}),
valid: true,
} satisfies WorkflowFormAction;
const { result } = renderHook(() =>
useWorkflowActionHeader({
action,
defaultTitle: 'Form',
}),
);
expect(result.current.headerTitle).toBe('Form');
expect(result.current.headerIcon).toBe('IconHttp');
expect(result.current.headerIconColor).toBe('blue');
expect(result.current.headerType).toBe('Action');
expect(result.current.getIcon).toBe(mockGetIcon);
});
it('should return default title for email action', () => {
const action = {
id: '1',
name: '',
type: 'SEND_EMAIL',
settings: workflowSendEmailActionSettingsSchema.parse({
input: {
connectedAccountId: '1',
email: 'test@example.com',
subject: 'Test Subject',
body: 'Test Body',
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
}),
valid: true,
} satisfies WorkflowSendEmailAction;
const { result } = renderHook(() =>
useWorkflowActionHeader({
action,
defaultTitle: 'Email',
}),
);
expect(result.current.headerTitle).toBe('Email');
expect(result.current.headerIcon).toBe('IconHttp');
expect(result.current.headerIconColor).toBe('blue');
expect(result.current.headerType).toBe('Action');
expect(result.current.getIcon).toBe(mockGetIcon);
});
});
});
@@ -1,38 +0,0 @@
import { type WorkflowAction } from '@/workflow/types/Workflow';
import { type IconComponent, useIcons } from 'twenty-ui/display';
import { getActionIcon } from '../utils/getActionIcon';
import { useActionHeaderTypeOrThrow } from './useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from './useActionIconColorOrThrow';
type UseWorkflowActionHeaderProps = {
action: WorkflowAction;
defaultTitle: string;
};
type UseWorkflowActionHeaderReturn = {
headerTitle: string;
headerIcon: string | undefined;
headerIconColor: string;
headerType: string;
getIcon: (iconName: string) => IconComponent;
};
export const useWorkflowActionHeader = ({
action,
defaultTitle,
}: UseWorkflowActionHeaderProps): UseWorkflowActionHeaderReturn => {
const { getIcon } = useIcons();
const headerTitle = action.name ? action.name : defaultTitle;
const headerIcon = getActionIcon(action.type);
const headerIconColor = useActionIconColorOrThrow(action.type);
const headerType = useActionHeaderTypeOrThrow(action.type);
return {
headerTitle,
headerIcon,
headerIconColor,
headerType,
getIcon,
};
};
@@ -1,11 +1,8 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { FormRawJsonFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRawJsonFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { Select } from '@/ui/input/components/Select';
import { type WorkflowHttpRequestAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { HTTP_REQUEST_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/HttpRequestAction';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
@@ -20,7 +17,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useEffect } from 'react';
import { IconPlayerPlay, IconSettings, useIcons } from 'twenty-ui/display';
import { IconPlayerPlay, IconSettings } from 'twenty-ui/display';
import {
HTTP_METHODS,
JSON_RESPONSE_PLACEHOLDER,
@@ -93,16 +90,10 @@ export const WorkflowEditActionHttpRequest = ({
}: WorkflowEditActionHttpRequestProps) => {
const { t } = useLingui();
const theme = useTheme();
const { getIcon } = useIcons();
const activeTabId = useRecoilComponentValue(
activeTabIdComponentState,
WORKFLOW_HTTP_REQUEST_TAB_LIST_COMPONENT_ID,
);
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: HTTP_REQUEST_ACTION.defaultLabel,
});
const { formData, handleFieldChange, saveAction } = useHttpRequestForm({
action,
@@ -143,20 +134,6 @@ export const WorkflowEditActionHttpRequest = ({
behaveAsLinks={false}
componentInstanceId={WORKFLOW_HTTP_REQUEST_TAB_LIST_COMPONENT_ID}
/>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate?.({ ...action, name: newName });
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={actionOptions.readonly}
iconTooltip={HTTP_REQUEST_ACTION.defaultLabel}
/>
<WorkflowStepBody>
{activeTabId === WorkflowHttpRequestTabId.CONFIGURATION && (
<StyledConfigurationTabContent>
@@ -118,10 +118,6 @@ export const Configured: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const header = await canvas.findByTestId('side-panel-header');
const headerCanvas = within(header);
expect(await headerCanvas.findByText('API Call')).toBeVisible();
const urlLabel = await canvas.findByText('URL');
const urlInputContainer = urlLabel.closest('div')?.nextElementSibling;
const urlEditor = urlInputContainer?.querySelector('.ProseMirror');
@@ -1,12 +1,9 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { FormArrayFieldInput } from '@/object-record/record-field/ui/form-types/components/FormArrayFieldInput';
import { type FieldArrayValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { type WorkflowIteratorAction } from '@/workflow/types/Workflow';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { ITERATOR_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/IteratorAction';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useLingui } from '@lingui/react/macro';
import { isArray, isString } from '@sniptt/guards';
@@ -44,12 +41,6 @@ export const WorkflowEditActionIterator = ({
action,
actionOptions,
}: WorkflowEditActionIteratorProps) => {
const { headerTitle, headerIcon, headerIconColor, headerType, getIcon } =
useWorkflowActionHeader({
action,
defaultTitle: ITERATOR_ACTION.defaultLabel,
});
const { t } = useLingui();
const defaultItems = isDefined(action.settings.input.items)
@@ -100,28 +91,8 @@ export const WorkflowEditActionIterator = ({
saveAction(updatedFormData);
};
const handleTitleChange = (newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate?.({
...action,
name: newName,
});
};
return (
<>
<SidePanelHeader
onTitleChange={handleTitleChange}
Icon={getIcon(headerIcon || 'IconRepeat')}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={actionOptions.readonly}
iconTooltip={ITERATOR_ACTION.defaultLabel}
/>
<WorkflowStepBody>
<FormArrayFieldInput
label={t`Items to iterate over`}