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:
+2
-25
@@ -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
-43
@@ -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`}
|
||||
|
||||
+6
-1
@@ -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);
|
||||
|
||||
+94
@@ -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();
|
||||
});
|
||||
});
|
||||
+34
@@ -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 };
|
||||
};
|
||||
-39
@@ -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
|
||||
|
||||
+2
-25
@@ -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 && (
|
||||
<>
|
||||
|
||||
-21
@@ -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}
|
||||
|
||||
-30
@@ -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}
|
||||
|
||||
-27
@@ -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"
|
||||
|
||||
+2
-30
@@ -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: '#',
|
||||
},
|
||||
{
|
||||
|
||||
-27
@@ -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"
|
||||
|
||||
-31
@@ -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}
|
||||
|
||||
-9
@@ -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);
|
||||
|
||||
-27
@@ -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);
|
||||
|
||||
-9
@@ -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);
|
||||
|
||||
-3
@@ -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();
|
||||
|
||||
-27
@@ -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);
|
||||
|
||||
-29
@@ -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"
|
||||
|
||||
-30
@@ -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,
|
||||
|
||||
-27
@@ -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"
|
||||
|
||||
-29
@@ -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" />
|
||||
|
||||
-19
@@ -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') {
|
||||
|
||||
-9
@@ -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');
|
||||
|
||||
-223
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
-38
@@ -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
-24
@@ -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>
|
||||
|
||||
-4
@@ -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');
|
||||
|
||||
-29
@@ -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`}
|
||||
|
||||
Reference in New Issue
Block a user