Add editor mode for text field widgets (#20779)
## Summary Tested ↓ <img width="3456" height="1990" alt="CleanShot 2026-05-20 at 21 14 04@2x" src="https://github.com/user-attachments/assets/b4e0d3d3-715f-4ad7-bd03-e8e1922b3c6c" /> - Enable `FieldDisplayMode.EDITOR` for plain `TEXT` field widgets while keeping `FIELD` as the default display mode. - Add a plain multiline text editor renderer for `TEXT + EDITOR` field widgets with optimistic record-store/cache updates and debounced persistence. - Reuse the shared `TextArea` component through a transparent, uncapped variant so the editor has no input chrome and lets the widget grow. - Add unit coverage for text display-mode config and a Storybook scenario for a text field widget in editor mode. ## useEffect cleanup note `FieldWidgetTextEditor` flushes the debounced persist callback in a `useEffect` cleanup: ```ts useEffect(() => () => persistTextDebounced.flush(), [persistTextDebounced]); ``` This follows the existing debounced autosave cleanup pattern already used in `WorkflowEditActionHttpRequest`. It ensures pending text changes are persisted when the widget unmounts, while `onBlur` still flushes immediately for normal editor exits. ## Validation - `npx nx test twenty-front --testPathPattern=page-layout` - `npx nx typecheck twenty-front` - `npx nx lint twenty-front` - Browser check on `http://apple.localhost:3001/object/company/20202020-a305-41e7-8c72-ba44072a4c58` for transparent textarea, no internal max-height/scroll, equal padding, and widget growth. Note: lint passes with two unrelated existing warnings in `NavigationDrawerItem.tsx` and `ConfigVariableEdit.tsx`. --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
committed by
GitHub
parent
b0a1002838
commit
c721fa8502
@@ -9,7 +9,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const MAX_ROWS = 5;
|
||||
type TextAreaVariant = 'default' | 'transparent';
|
||||
|
||||
export type TextAreaProps = {
|
||||
textAreaId: string;
|
||||
@@ -24,6 +24,7 @@ export type TextAreaProps = {
|
||||
className?: string;
|
||||
onBlur?: () => void;
|
||||
readOnly?: boolean;
|
||||
variant?: TextAreaVariant;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -40,26 +41,43 @@ const StyledLabel = styled.label`
|
||||
margin-bottom: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledTextAreaContainer = styled.div`
|
||||
const StyledTextAreaContainer = styled.div<{ variant: TextAreaVariant }>`
|
||||
> textarea {
|
||||
background-color: ${themeCssVariables.background.transparent.lighter};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
background-color: ${({ variant }) =>
|
||||
variant === 'transparent'
|
||||
? 'transparent'
|
||||
: themeCssVariables.background.transparent.lighter};
|
||||
border: ${({ variant }) =>
|
||||
variant === 'transparent'
|
||||
? 'none'
|
||||
: `1px solid ${themeCssVariables.border.color.medium}`};
|
||||
border-radius: ${({ variant }) =>
|
||||
variant === 'transparent' ? '0' : themeCssVariables.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: block;
|
||||
font-family: inherit;
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.regular};
|
||||
line-height: 16px;
|
||||
overflow: auto;
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
line-height: ${({ variant }) =>
|
||||
variant === 'transparent' ? 'inherit' : '16px'};
|
||||
overflow: ${({ variant }) =>
|
||||
variant === 'transparent' ? 'hidden' : 'auto'};
|
||||
padding: ${({ variant }) =>
|
||||
variant === 'transparent' ? '0' : themeCssVariables.spacing[2]};
|
||||
resize: none;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0px 0px 0px 3px ${themeCssVariables.color.transparent.blue2};
|
||||
border-color: ${themeCssVariables.color.blue};
|
||||
box-shadow: ${({ variant }) =>
|
||||
variant === 'transparent'
|
||||
? 'none'
|
||||
: `0px 0px 0px 3px ${themeCssVariables.color.transparent.blue2}`};
|
||||
border-color: ${({ variant }) =>
|
||||
variant === 'transparent'
|
||||
? 'transparent'
|
||||
: themeCssVariables.color.blue};
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
@@ -80,14 +98,17 @@ export const TextArea = ({
|
||||
height,
|
||||
placeholder,
|
||||
minRows = 1,
|
||||
maxRows = MAX_ROWS,
|
||||
maxRows,
|
||||
value = '',
|
||||
className,
|
||||
onChange,
|
||||
onBlur,
|
||||
readOnly = false,
|
||||
variant = 'default',
|
||||
}: TextAreaProps) => {
|
||||
const computedMinRows = Math.min(minRows, maxRows);
|
||||
const computedMinRows = isDefined(maxRows)
|
||||
? Math.min(minRows, maxRows)
|
||||
: minRows;
|
||||
|
||||
const instanceId = useId();
|
||||
|
||||
@@ -117,7 +138,7 @@ export const TextArea = ({
|
||||
<StyledContainer>
|
||||
{label && <StyledLabel htmlFor={instanceId}>{label}</StyledLabel>}
|
||||
|
||||
<StyledTextAreaContainer>
|
||||
<StyledTextAreaContainer variant={variant}>
|
||||
<TextareaAutosize
|
||||
id={instanceId}
|
||||
placeholder={placeholder}
|
||||
|
||||
Reference in New Issue
Block a user