Improve code editor style and enable full screen code editor (#13618)

<img width="1501" height="757" alt="Screenshot 2025-08-05 at 09 30 44"
src="https://github.com/user-attachments/assets/535e6dfb-a1b5-4812-b5a9-c19ab7bb7240"
/>
<img width="486" height="717" alt="Screenshot 2025-08-05 at 09 30 36"
src="https://github.com/user-attachments/assets/189407b3-fdec-4795-b115-7e00a11e6bc5"
/>
This commit is contained in:
Félix Malfait
2025-08-05 15:38:54 +02:00
committed by GitHub
parent 4ccd9e21e7
commit 1de3338551
3 changed files with 251 additions and 22 deletions
@@ -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<HTMLDivElement>(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(
<StyledFullScreenOverlay
ref={fullScreenOverlayRef}
data-globally-prevent-click-outside="true"
tabIndex={-1}
>
<StyledFullScreenHeader
title={<Breadcrumb links={breadcrumbLinks} />}
hasClosePageButton={!isMobile}
onClosePage={handleExitFullScreen}
/>
<StyledFullScreenContent data-globally-prevent-click-outside="true">
<WorkflowEditActionServerlessFunctionFields
functionInput={functionInput}
VariablePicker={WorkflowVariablePicker}
onInputChange={handleInputChange}
readonly={actionOptions.readonly}
/>
<StyledFullScreenCodeEditorContainer>
<CodeEditor
height="100%"
value={formValues.code?.[INDEX_FILE_PATH]}
language={'typescript'}
onChange={handleCodeChange}
onMount={handleEditorDidMount}
setMarkers={getWrongExportedFunctionMarkers}
options={{
readOnly: actionOptions.readonly,
domReadOnly: actionOptions.readonly,
scrollBeyondLastLine: false,
padding: { top: 4, bottom: 4 },
}}
/>
</StyledFullScreenCodeEditorContainer>
</StyledFullScreenContent>
</StyledFullScreenOverlay>,
document.body,
)
: null;
return (
!loading && (
<>
@@ -318,20 +468,20 @@ export const WorkflowEditActionServerlessFunction = ({
onInputChange={handleInputChange}
readonly={actionOptions.readonly}
/>
<StyledCodeEditorContainer>
<CodeEditor
height={343}
value={formValues.code?.[INDEX_FILE_PATH]}
language={'typescript'}
onChange={handleCodeChange}
onMount={handleEditorDidMount}
setMarkers={getWrongExportedFunctionMarkers}
options={{
readOnly: actionOptions.readonly,
domReadOnly: actionOptions.readonly,
}}
/>
</StyledCodeEditorContainer>
<WorkflowServerlessFunctionCodeEditor
value={formValues.code?.[INDEX_FILE_PATH]}
onChange={handleCodeChange}
onMount={handleEditorDidMount}
options={{
readOnly: actionOptions.readonly,
domReadOnly: actionOptions.readonly,
scrollBeyondLastLine: false,
padding: { top: 4, bottom: 4 },
lineNumbersMinChars: 2,
}}
readonly={actionOptions.readonly}
onEnterFullScreen={handleEnterFullScreen}
/>
</>
)}
{activeTabId === WorkflowServerlessFunctionTabId.TEST && (
@@ -375,6 +525,7 @@ export const WorkflowEditActionServerlessFunction = ({
]}
/>
)}
{fullScreenOverlay}
</>
)
);
@@ -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<void>;
VariablePicker?: VariablePickerComponent;
};
@@ -27,14 +33,14 @@ export const WorkflowEditActionServerlessFunctionFields = ({
VariablePicker,
}: WorkflowEditActionServerlessFunctionFieldsProps) => {
return (
<>
<StyledContainer>
{Object.entries(functionInput).map(([inputKey, inputValue]) => {
const currentPath = [...path, inputKey];
const pathKey = currentPath.join('.');
if (inputValue !== null && isObject(inputValue)) {
return (
<StyledContainer key={pathKey}>
<div key={pathKey}>
<InputLabel>{inputKey}</InputLabel>
<FormNestedFieldInputContainer>
<WorkflowEditActionServerlessFunctionFields
@@ -45,7 +51,7 @@ export const WorkflowEditActionServerlessFunctionFields = ({
VariablePicker={VariablePicker}
/>
</FormNestedFieldInputContainer>
</StyledContainer>
</div>
);
}
@@ -61,6 +67,6 @@ export const WorkflowEditActionServerlessFunctionFields = ({
/>
);
})}
</>
</StyledContainer>
);
};
@@ -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 (
<StyledCodeEditorContainer>
{!readonly && !fullScreenMode && onEnterFullScreen && (
<StyledFullScreenButtonContainer>
<LightIconButton
Icon={IconMaximize}
onClick={onEnterFullScreen}
title={t`Expand to Full Screen`}
size="small"
accent="tertiary"
/>
</StyledFullScreenButtonContainer>
)}
<CodeEditor
height="100%"
value={value}
language={'typescript'}
onChange={onChange}
onMount={onMount}
setMarkers={getWrongExportedFunctionMarkers}
options={options}
/>
</StyledCodeEditorContainer>
);
};