Increase size of input in test setting logic function tab (#18369)

## Before
<img width="1031" height="836" alt="image"
src="https://github.com/user-attachments/assets/475ca1be-f7c4-49d0-b329-649dbe8da489"
/>


## After

<img width="1195" height="862" alt="image"
src="https://github.com/user-attachments/assets/b1bac131-e562-4439-8f8e-bda4d6e2a646"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
martmull
2026-03-04 16:49:13 +01:00
committed by GitHub
parent b11f77df2a
commit 999dcd4468
15 changed files with 249 additions and 64 deletions
@@ -8,11 +8,9 @@ import { LogicFunctionExecutionStatus } from '~/generated-metadata/graphql';
export const LogicFunctionExecutionResult = ({
logicFunctionTestData,
maxHeight,
isTesting = false,
}: {
logicFunctionTestData: LogicFunctionTestData;
maxHeight?: number;
isTesting?: boolean;
}) => {
const result =
@@ -39,10 +37,7 @@ export const LogicFunctionExecutionResult = ({
<WorkflowStepExecutionResult
result={result}
language={logicFunctionTestData.language}
height={Math.min(
logicFunctionTestData.height,
maxHeight ?? logicFunctionTestData.height,
)}
height={150}
status={status}
isTesting={isTesting}
loadingMessage={t`Running function`}
@@ -0,0 +1,41 @@
import { useLingui } from '@lingui/react/macro';
import { ResizeHandle, useResizeHandle } from 'twenty-ui/layout';
import { TextArea } from '@/ui/input/components/TextArea';
type LogicFunctionLogsProps = {
componentInstanceId: string;
value: string;
};
export const LogicFunctionLogs = ({
componentInstanceId,
value,
}: LogicFunctionLogsProps) => {
const { t } = useLingui();
const {
size: height,
handleResizeStart,
handleResizeMove,
handleResizeEnd,
} = useResizeHandle({
initialSize: 150,
});
return (
<>
<TextArea
textAreaId={`logs-${componentInstanceId}`}
label={t`Logs`}
value={value}
height={height}
readOnly
/>
<ResizeHandle
onPointerDown={handleResizeStart}
onPointerMove={handleResizeMove}
onPointerUp={handleResizeEnd}
/>
</>
);
};
@@ -75,7 +75,6 @@ export const useExecuteLogicFunction = ({
setLogicFunctionTestData((prev) => ({
...prev,
language: 'json',
height: 300,
output: {
data: executionResult?.data
? JSON.stringify(executionResult.data, null, 4)
@@ -6,6 +6,7 @@ import {
IconRadiusTopLeft,
IconRadiusTopRight,
} from 'twenty-ui/display';
import { ResizeHandle } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { ThemeContext } from 'twenty-ui/theme';
@@ -67,28 +68,6 @@ const StyledVerticalHandle = styled.div`
width: ${themeCssVariables.icon.stroke.lg}px;
`;
const StyledHorizontalHandle = styled.div`
border-radius: ${themeCssVariables.border.radius.sm};
height: ${themeCssVariables.icon.stroke.lg}px;
width: ${themeCssVariables.spacing[5]};
`;
const StyledHorizontalHandleWrapper = styled.div<{
widgetHandleAxis: WidgetHorizontalHandleAxis;
}>`
border-radius: ${themeCssVariables.border.radius.sm};
cursor: row-resize;
transform: ${({ widgetHandleAxis }) =>
widgetHandleAxis === 'n' ? 'translateY(-50%)' : 'translateY(50%)'};
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]};
:hover {
& > div {
background-color: ${themeCssVariables.font.color.tertiary};
}
}
`;
const StyledVerticalHandleWrapper = styled.div<{
widgetHandleAxis: WidgetVerticalHandleAxis;
}>`
@@ -201,9 +180,15 @@ export const PageLayoutGridResizeHandle = forwardRef<
</StyledVerticalHandleWrapper>
)}
{isHorizontalHandle(widgetHandleAxis) && (
<StyledHorizontalHandleWrapper widgetHandleAxis={widgetHandleAxis}>
<StyledHorizontalHandle />
</StyledHorizontalHandleWrapper>
<ResizeHandle
style={{
cursor: 'row-resize',
transform:
widgetHandleAxis === 'n'
? 'translateY(-50%)'
: 'translateY(50%)',
}}
/>
)}
{widgetHandleAxis === 'ne' && (
<StyledCornerIconWrapper cursor="nesw-resize" position="ne">
@@ -1,4 +1,5 @@
import { LogicFunctionExecutionResult } from '@/logic-functions/components/LogicFunctionExecutionResult';
import { LogicFunctionLogs } from '@/logic-functions/components/LogicFunctionLogs';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
import { logicFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/logicFunctionTestDataFamilyState';
@@ -8,8 +9,6 @@ import { H2Title, IconPlayerPlay } from 'twenty-ui/display';
import { Button, CodeEditor, CoreEditorHeader } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { TextArea } from '@/ui/input/components/TextArea';
const StyledInputsContainer = styled.div`
display: flex;
@@ -48,8 +47,6 @@ export const SettingsLogicFunctionTestTab = ({
}));
};
const testLogsTextAreaId = `${logicFunctionId}-test-logs`;
return (
<Section>
<H2Title
@@ -78,23 +75,18 @@ export const SettingsLogicFunctionTestTab = ({
height={100}
onChange={onChange}
variant="with-header"
resizable
/>
</StyledCodeEditorContainer>
<LogicFunctionExecutionResult
logicFunctionTestData={logicFunctionTestData}
maxHeight={
logicFunctionTestData.output.logs.length > 0 ? 200 : undefined
}
isTesting={isTesting}
/>
{logicFunctionTestData.output.logs.length > 0 && (
<StyledCodeEditorContainer>
<InputLabel>{t`Logs`}</InputLabel>
<TextArea
textAreaId={testLogsTextAreaId}
<LogicFunctionLogs
componentInstanceId={`settings-logic-function-logs-${logicFunctionId}`}
value={isTesting ? '' : logicFunctionTestData.output.logs}
maxRows={20}
disabled
/>
</StyledCodeEditorContainer>
)}
@@ -5,6 +5,7 @@ import TextareaAutosize from 'react-textarea-autosize';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { isDefined } from 'twenty-shared/utils';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -14,6 +15,7 @@ export type TextAreaProps = {
textAreaId: string;
label?: string;
disabled?: boolean;
height?: number;
minRows?: number;
maxRows?: number;
onChange?: (value: string) => void;
@@ -73,6 +75,7 @@ export const TextArea = ({
textAreaId,
label,
disabled,
height,
placeholder,
minRows = 1,
maxRows = MAX_ROWS,
@@ -126,6 +129,7 @@ export const TextArea = ({
disabled={disabled}
className={className}
readOnly={readOnly}
style={isDefined(height) ? { height } : undefined}
/>
</StyledContainer>
);
@@ -16,14 +16,12 @@ const StyledContainer = styled.div`
display: flex;
flex-direction: column;
flex: 1;
min-height: 200px;
`;
const StyledCodeEditorWrapper = styled.div`
display: flex;
flex: 1;
flex-direction: column;
min-height: 200px;
`;
type OutputAccent = 'default' | 'success' | 'error';
@@ -137,6 +135,7 @@ export const WorkflowStepExecutionResult = ({
/>
<StyledCodeEditorWrapper>
<CodeEditor
resizable={true}
value={result}
language={language}
height={height}
@@ -10,9 +10,9 @@ import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/code-
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
import { LogicFunctionExecutionResult } from '@/logic-functions/components/LogicFunctionExecutionResult';
import { LogicFunctionLogs } from '@/logic-functions/components/LogicFunctionLogs';
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { TextArea } from '@/ui/input/components/TextArea';
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';
@@ -50,15 +50,10 @@ import { useDebouncedCallback } from 'use-debounce';
import { getFunctionInputFromInputSchema } from 'twenty-shared/workflow';
import { themeCssVariables } from 'twenty-ui/theme-constants';
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 StyledTabList = styled(TabList)`
@@ -304,8 +299,6 @@ export const WorkflowEditActionCode = ({
dependencies: [isFullScreen],
});
const testLogsTextAreaId = `${logicFunctionId}-test-logs`;
const breadcrumbLinks: BreadcrumbProps['links'] = [
{
children: workflow?.name?.trim() || t`Untitled Workflow`,
@@ -429,12 +422,9 @@ export const WorkflowEditActionCode = ({
</StyledCodeEditorContainer>
{logicFunctionTestData.output.logs.length > 0 && (
<StyledCodeEditorContainer>
<InputLabel>{t`Logs`}</InputLabel>
<TextArea
textAreaId={testLogsTextAreaId}
<LogicFunctionLogs
componentInstanceId={`workflow-edit-action-logs-${action.id}`}
value={isExecuting ? '' : logicFunctionTestData.output.logs}
maxRows={20}
disabled
/>
</StyledCodeEditorContainer>
)}
@@ -12,7 +12,6 @@ export type LogicFunctionTestData = {
error?: string;
};
language: 'plaintext' | 'json';
height: number;
};
export const DEFAULT_OUTPUT_VALUE = {
@@ -28,7 +27,6 @@ export const logicFunctionTestDataFamilyState = createAtomFamilyState<
key: 'logicFunctionTestDataFamilyState',
defaultValue: {
language: 'plaintext',
height: 64,
input: {},
shouldInitInput: true,
output: DEFAULT_OUTPUT_VALUE,
@@ -96,7 +96,7 @@ export const SettingsAIMCP = () => {
top: 12,
},
}}
height="220px"
height={230}
/>
</StyledEditorContainer>
</StyledWrapper>
@@ -1,7 +1,9 @@
import { styled } from '@linaria/react';
import Editor, { type EditorProps, type Monaco } from '@monaco-editor/react';
import { Loader } from '@ui/feedback/loader/components/Loader';
import { ResizeHandle } from '@ui/layout/resize-handle/components/ResizeHandle';
import { BASE_CODE_EDITOR_THEME_ID } from '@ui/input/code-editor/constants/BaseCodeEditorThemeId';
import { useResizeHandle } from '@ui/layout/resize-handle/hooks/useResizeHandle';
import { getBaseCodeEditorTheme } from '@ui/input/code-editor/theme/utils/getBaseCodeEditorTheme';
import { ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
@@ -20,6 +22,7 @@ type CodeEditorProps = Pick<
variant?: CodeEditorVariant;
isLoading?: boolean;
transparentBackground?: boolean;
resizable?: boolean;
};
const StyledEditorLoader = styled.div<{
@@ -118,6 +121,7 @@ export const CodeEditor = ({
transparentBackground,
isLoading = false,
options,
resizable = false,
}: CodeEditorProps) => {
const { theme } = useContext(ThemeContext);
const [monaco, setMonaco] = useState<Monaco | undefined>(undefined);
@@ -126,6 +130,18 @@ export const CodeEditor = ({
>(undefined);
const [isEditorFocused, setIsEditorFocused] = useState(false);
const numericHeight = typeof height === 'number' ? height : 450;
const {
size: resizableHeight,
handleResizeStart,
handleResizeMove,
handleResizeEnd,
} = useResizeHandle({
initialSize: numericHeight,
});
const currentHeight = resizable ? resizableHeight : height;
const setModelMarkers = (
editor: editor.IStandaloneCodeEditor | undefined,
monaco: Monaco | undefined,
@@ -147,7 +163,7 @@ export const CodeEditor = ({
};
return isLoading ? (
<StyledEditorLoader height={height} variant={variant}>
<StyledEditorLoader height={currentHeight} variant={variant}>
<Loader />
</StyledEditorLoader>
) : (
@@ -163,7 +179,7 @@ export const CodeEditor = ({
transparentBackground={transparentBackground}
>
<Editor
height={height}
height={currentHeight}
value={value}
language={language}
loading=""
@@ -213,6 +229,13 @@ export const CodeEditor = ({
}}
/>
</StyledEditorWrapper>
{resizable && (
<ResizeHandle
onPointerDown={handleResizeStart}
onPointerMove={handleResizeMove}
onPointerUp={handleResizeEnd}
/>
)}
</StyledCodeEditorContainer>
);
};
+2
View File
@@ -51,6 +51,8 @@ export type { ModalOverlay } from './modal/types/ModalOverlay';
export type { ModalPadding } from './modal/types/ModalPadding';
export type { ModalProps } from './modal/types/ModalProps';
export type { ModalSize } from './modal/types/ModalSize';
export { ResizeHandle } from './resize-handle/components/ResizeHandle';
export { useResizeHandle } from './resize-handle/hooks/useResizeHandle';
export {
SectionAlignment,
SectionFontColor,
@@ -0,0 +1,47 @@
import { styled } from '@linaria/react';
import { type CSSProperties } from 'react';
import { themeCssVariables } from '@ui/theme-constants';
type ResizeHandleProps = {
onPointerDown?: (event: React.PointerEvent<HTMLDivElement>) => void;
onPointerMove?: (event: React.PointerEvent<HTMLDivElement>) => void;
onPointerUp?: (event: React.PointerEvent<HTMLDivElement>) => void;
style?: CSSProperties;
};
const StyledResizeHandleArea = styled.div`
align-items: center;
cursor: ns-resize;
display: flex;
height: 8px;
justify-content: center;
user-select: none;
&:hover > div {
background-color: ${themeCssVariables.font.color.tertiary};
}
`;
const StyledResizeHandleBar = styled.div`
background-color: ${themeCssVariables.background.quaternary};
border-radius: ${themeCssVariables.border.radius.pill};
height: 3px;
transition: background-color ${themeCssVariables.animation.duration.fast}s;
width: 32px;
`;
export const ResizeHandle = ({
onPointerDown,
onPointerMove,
onPointerUp,
style,
}: ResizeHandleProps) => (
<StyledResizeHandleArea
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
style={style}
>
<StyledResizeHandleBar />
</StyledResizeHandleArea>
);
@@ -0,0 +1,66 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { useResizeHandle } from '@ui/layout/resize-handle/hooks/useResizeHandle';
import { ComponentDecorator } from '@ui/testing';
import { themeCssVariables } from '@ui/theme-constants';
import { expect, userEvent, within } from 'storybook/test';
import { ResizeHandle } from '../ResizeHandle';
const INITIAL_HEIGHT = 150;
const ResizableDemo = () => {
const { size, handleResizeStart, handleResizeMove, handleResizeEnd } =
useResizeHandle({ initialSize: INITIAL_HEIGHT });
return (
<div>
<div
data-testid="resizable-area"
style={{
background: themeCssVariables.background.secondary,
height: size,
width: 300,
}}
/>
<ResizeHandle
onPointerDown={handleResizeStart}
onPointerMove={handleResizeMove}
onPointerUp={handleResizeEnd}
/>
</div>
);
};
const meta: Meta<typeof ResizeHandle> = {
title: 'UI/Layout/ResizeHandle',
component: ResizeHandle,
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof ResizeHandle>;
export const Resizable: Story = {
render: () => <ResizableDemo />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const resizableArea = canvas.getByTestId('resizable-area');
const handle = resizableArea.nextElementSibling as HTMLElement;
await expect(resizableArea).toHaveStyle({ height: `${INITIAL_HEIGHT}px` });
// Split pointer calls so React flushes state between pointerdown and pointermove
await userEvent.pointer({
keys: '[MouseLeft>]',
target: handle,
coords: { x: 0, y: 100 },
});
await userEvent.pointer({ target: handle, coords: { x: 0, y: 200 } });
await userEvent.pointer({ keys: '[/MouseLeft]' });
await expect(resizableArea).toHaveStyle({
height: `${INITIAL_HEIGHT + 100}px`,
});
},
};
@@ -0,0 +1,44 @@
import { useState, type PointerEvent } from 'react';
import { isDefined } from 'twenty-shared/utils';
const RESIZE_MIN_SIZE = 50;
const RESIZE_MAX_SIZE = 500;
export const useResizeHandle = ({
initialSize,
axis = 'y',
}: {
initialSize: number;
axis?: 'x' | 'y';
}) => {
const [size, setSize] = useState(initialSize);
const [resizeStartState, setResizeStartState] = useState<{
startPosition: number;
startSize: number;
} | null>(null);
const handleResizeStart = (event: PointerEvent) => {
(event.target as HTMLElement).setPointerCapture(event.pointerId);
const startPosition = axis === 'y' ? event.clientY : event.clientX;
setResizeStartState({ startPosition, startSize: size });
};
const handleResizeMove = (event: PointerEvent) => {
if (!isDefined(resizeStartState)) {
return;
}
const currentPosition = axis === 'y' ? event.clientY : event.clientX;
const delta = currentPosition - resizeStartState.startPosition;
const newSize = Math.min(
RESIZE_MAX_SIZE,
Math.max(RESIZE_MIN_SIZE, resizeStartState.startSize + delta),
);
setSize(newSize);
};
const handleResizeEnd = () => {
setResizeStartState(null);
};
return { size, handleResizeStart, handleResizeMove, handleResizeEnd };
};