From 1de3338551570b07f66e3fb7872a6e4c22b7a1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Tue, 5 Aug 2025 15:38:54 +0200 Subject: [PATCH] Improve code editor style and enable full screen code editor (#13618) Screenshot 2025-08-05 at 09 30 44 Screenshot 2025-08-05 at 09 30 36 --- .../WorkflowEditActionServerlessFunction.tsx | 181 ++++++++++++++++-- ...flowEditActionServerlessFunctionFields.tsx | 20 +- .../WorkflowServerlessFunctionCodeEditor.tsx | 72 +++++++ 3 files changed, 251 insertions(+), 22 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowServerlessFunctionCodeEditor.tsx diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx index 8e59e90bce..a426830fa3 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx @@ -1,6 +1,13 @@ import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction'; +import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices'; +import { PageHeader } from '@/ui/layout/page/components/PageHeader'; +import { PAGE_BAR_MIN_HEIGHT } from '@/ui/layout/page/constants/PageBarMinHeight'; +import { + Breadcrumb, + BreadcrumbProps, +} from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; @@ -20,10 +27,13 @@ import { TextArea } from '@/ui/input/components/TextArea'; import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunctionFields'; +import { WorkflowServerlessFunctionCodeEditor } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowServerlessFunctionCodeEditor'; import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId'; import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId'; import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers'; @@ -32,19 +42,59 @@ import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-ac 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'; + import { Monaco } from '@monaco-editor/react'; import { editor } from 'monaco-editor'; import { AutoTypings } from 'monaco-editor-auto-typings'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { useRecoilState } from 'recoil'; +import { Key } from 'ts-key-enum'; import { isDefined } from 'twenty-shared/utils'; import { IconCode, IconPlayerPlay, useIcons } from 'twenty-ui/display'; import { CodeEditor } from 'twenty-ui/input'; +import { useIsMobile } from 'twenty-ui/utilities'; import { useDebouncedCallback } from 'use-debounce'; +const CODE_EDITOR_MIN_HEIGHT = 343; + const StyledCodeEditorContainer = styled.div` display: flex; flex-direction: column; + position: relative; + flex: 1; + min-height: ${CODE_EDITOR_MIN_HEIGHT}px; + overflow: hidden; +`; + +const StyledFullScreenOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: ${({ theme }) => theme.background.noisy}; + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; + z-index: ${RootStackingContextZIndices.Dialog}; +`; + +const StyledFullScreenHeader = styled(PageHeader)` + padding-left: ${({ theme }) => theme.spacing(3)}; +`; + +const StyledFullScreenContent = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(3)}; + height: calc( + 100% - ${PAGE_BAR_MIN_HEIGHT}px - ${({ theme }) => theme.spacing(2 * 2 + 5)} + ); + padding: ${({ theme }) => + `0 ${theme.spacing(3)} ${theme.spacing(3)} ${theme.spacing(3)}`}; `; const StyledTabList = styled(TabList)` @@ -52,6 +102,11 @@ const StyledTabList = styled(TabList)` padding-left: ${({ theme }) => theme.spacing(2)}; `; +const StyledFullScreenCodeEditorContainer = styled.div` + flex: 1; + min-height: 0; +`; + type WorkflowEditActionServerlessFunctionProps = { action: WorkflowCodeAction; actionOptions: @@ -73,7 +128,12 @@ export const WorkflowEditActionServerlessFunction = ({ actionOptions, }: WorkflowEditActionServerlessFunctionProps) => { const { getIcon } = useIcons(); + const { t } = useLingui(); + const [isFullScreen, setIsFullScreen] = useState(false); + const isMobile = useIsMobile(); + const fullScreenOverlayRef = useRef(null); const serverlessFunctionId = action.settings.input.serverlessFunctionId; + const fullScreenFocusId = `code-editor-fullscreen-${serverlessFunctionId}`; const activeTabId = useRecoilComponentValueV2( activeTabIdComponentState, WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID, @@ -280,6 +340,28 @@ export const WorkflowEditActionServerlessFunction = ({ setFunctionInput(action.settings.input.serverlessFunctionInput); }, [action]); + useHotkeysOnFocusedElement({ + keys: [Key.Escape], + callback: () => { + if (isFullScreen) { + handleExitFullScreen(); + } + }, + focusId: fullScreenFocusId, + dependencies: [isFullScreen], + }); + + useListenClickOutside({ + refs: [fullScreenOverlayRef], + callback: () => { + if (isFullScreen) { + handleExitFullScreen(); + } + }, + listenerId: `full-screen-overlay-${serverlessFunctionId}`, + enabled: isFullScreen, + }); + const headerTitle = isDefined(action.name) ? action.name : 'Code - Serverless Function'; @@ -289,6 +371,74 @@ export const WorkflowEditActionServerlessFunction = ({ const testLogsTextAreaId = `${serverlessFunctionId}-test-logs`; + const handleEnterFullScreen = () => { + setIsFullScreen(true); + setTimeout(() => { + if (isDefined(fullScreenOverlayRef.current)) { + fullScreenOverlayRef.current.focus(); + } + }, 0); + }; + + const handleExitFullScreen = () => { + setIsFullScreen(false); + }; + + const breadcrumbLinks: BreadcrumbProps['links'] = [ + { + children: workflow?.name?.trim() || t`Untitled Workflow`, + href: '#', + }, + { + children: headerTitle, + href: '#', + }, + { + children: t`Code Editor`, + }, + ]; + + const fullScreenOverlay = isFullScreen + ? createPortal( + + } + hasClosePageButton={!isMobile} + onClosePage={handleExitFullScreen} + /> + + + + + + + , + document.body, + ) + : null; + return ( !loading && ( <> @@ -318,20 +468,20 @@ export const WorkflowEditActionServerlessFunction = ({ onInputChange={handleInputChange} readonly={actionOptions.readonly} /> - - - + )} {activeTabId === WorkflowServerlessFunctionTabId.TEST && ( @@ -375,6 +525,7 @@ export const WorkflowEditActionServerlessFunction = ({ ]} /> )} + {fullScreenOverlay} ) ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunctionFields.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunctionFields.tsx index dfee3b3d8d..8e4609b89b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunctionFields.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunctionFields.tsx @@ -7,15 +7,21 @@ import styled from '@emotion/styled'; import { isObject } from '@sniptt/guards'; const StyledContainer = styled.div` - display: inline-flex; - flex-direction: column; + display: flex; + gap: ${({ theme }) => theme.spacing(3)}; + flex-wrap: wrap; + + > * { + flex: 1; + min-width: 200px; + } `; type WorkflowEditActionServerlessFunctionFieldsProps = { functionInput: FunctionInput; path?: string[]; readonly?: boolean; - onInputChange?: (value: any, path: string[]) => void; + onInputChange?: (value: any, path: string[]) => void | Promise; VariablePicker?: VariablePickerComponent; }; @@ -27,14 +33,14 @@ export const WorkflowEditActionServerlessFunctionFields = ({ VariablePicker, }: WorkflowEditActionServerlessFunctionFieldsProps) => { return ( - <> + {Object.entries(functionInput).map(([inputKey, inputValue]) => { const currentPath = [...path, inputKey]; const pathKey = currentPath.join('.'); if (inputValue !== null && isObject(inputValue)) { return ( - +
{inputKey} - +
); } @@ -61,6 +67,6 @@ export const WorkflowEditActionServerlessFunctionFields = ({ /> ); })} - +
); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowServerlessFunctionCodeEditor.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowServerlessFunctionCodeEditor.tsx new file mode 100644 index 0000000000..f6ad6eb80c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowServerlessFunctionCodeEditor.tsx @@ -0,0 +1,72 @@ +import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers'; +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { Monaco } from '@monaco-editor/react'; +import { editor } from 'monaco-editor'; +import { IconMaximize } from 'twenty-ui/display'; +import { CodeEditor, LightIconButton } from 'twenty-ui/input'; + +const CODE_EDITOR_MIN_HEIGHT = 343; + +const StyledCodeEditorContainer = styled.div` + display: flex; + flex-direction: column; + position: relative; + flex: 1; + min-height: ${CODE_EDITOR_MIN_HEIGHT}px; + overflow: hidden; +`; + +const StyledFullScreenButtonContainer = styled.div` + position: absolute; + top: ${({ theme }) => theme.spacing(2)}; + right: ${({ theme }) => theme.spacing(2)}; + z-index: 1; +`; + +type WorkflowServerlessFunctionCodeEditorProps = { + value?: string; + onChange: (value: string) => void; + onMount: (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => void; + options: editor.IStandaloneEditorConstructionOptions; + readonly?: boolean; + fullScreenMode?: boolean; + onEnterFullScreen?: () => void; +}; + +export const WorkflowServerlessFunctionCodeEditor = ({ + value, + onChange, + onMount, + options, + readonly = false, + fullScreenMode = false, + onEnterFullScreen, +}: WorkflowServerlessFunctionCodeEditorProps) => { + const { t } = useLingui(); + + return ( + + {!readonly && !fullScreenMode && onEnterFullScreen && ( + + + + )} + + + ); +};