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:
+166
-15
@@ -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}
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
+13
-7
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+72
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user