From c721fa8502d5384445570c38353affc8c340b766 Mon Sep 17 00:00:00 2001 From: Thomas des Francs Date: Thu, 21 May 2026 22:29:29 +0200 Subject: [PATCH] Add editor mode for text field widgets (#20779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Tested ↓ CleanShot 2026-05-20 at 21 14
04@2x - 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 --- .../components/LogicFunctionLogs.tsx | 1 + .../widgets/field/components/FieldWidget.tsx | 15 + .../components/FieldWidgetTextEditor.tsx | 97 + .../__stories__/FieldWidget.stories.tsx | 1813 ++++------------- .../field/constants/fieldWidgetConfig.ts | 4 + .../getFieldWidgetDisplayModeConfig.test.ts | 34 + .../ConfigVariableDatabaseInput.tsx | 1 + .../SettingsDataModelFieldDescriptionForm.tsx | 1 + .../SettingsDataModelObjectAboutForm.tsx | 1 + .../SettingsDevelopersWebhookForm.tsx | 1 + .../SettingsLogicFunctionNewForm.tsx | 1 + .../components/SettingsRoleSettings.tsx | 1 + .../modules/ui/input/components/TextArea.tsx | 47 +- .../__stories__/TextArea.stories.tsx | 19 +- .../pages/settings/ai/SettingsSkillForm.tsx | 1 + .../pages/settings/ai/SettingsToolDetail.tsx | 1 + 16 files changed, 568 insertions(+), 1470 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetTextEditor.tsx create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/field/utils/__tests__/getFieldWidgetDisplayModeConfig.test.ts diff --git a/packages/twenty-front/src/modules/logic-functions/components/LogicFunctionLogs.tsx b/packages/twenty-front/src/modules/logic-functions/components/LogicFunctionLogs.tsx index e215ad2503..aa1a8fb8d6 100644 --- a/packages/twenty-front/src/modules/logic-functions/components/LogicFunctionLogs.tsx +++ b/packages/twenty-front/src/modules/logic-functions/components/LogicFunctionLogs.tsx @@ -29,6 +29,7 @@ export const LogicFunctionLogs = ({ label={t`Logs`} value={value} height={height} + maxRows={5} readOnly /> { ); } + if ( + isFieldText(fieldDefinition) && + fieldDisplayMode === FieldDisplayMode.EDITOR + ) { + return ( + + ); + } + return ( { + const fieldName = fieldMetadataItem.name; + const { updateOneRecord } = useUpdateOneRecord(); + + const [fieldValue, setFieldValue] = useAtomFamilySelectorState( + recordStoreFamilySelector, + { + recordId, + fieldName, + }, + ); + + const isRecordFieldReadOnly = useIsRecordFieldReadOnly({ + recordId, + objectMetadataId: objectMetadataItem.id, + fieldMetadataId: fieldMetadataItem.id, + }); + + const textAreaId = `field-widget-text-editor-${recordId}-${fieldName}`; + + const persistTextDebounced = useDebouncedCallback((text: string) => { + if (isRecordFieldReadOnly === true) { + return; + } + + updateOneRecord({ + objectNameSingular: objectMetadataItem.nameSingular, + idToUpdate: recordId, + updateOneRecordInput: { + [fieldName]: text, + }, + }); + }, 300); + + useEffect(() => () => persistTextDebounced.flush(), [persistTextDebounced]); + + const handleChange = useCallback( + (text: string) => { + if (isRecordFieldReadOnly === true) { + return; + } + + setFieldValue(text); + + persistTextDebounced(text); + }, + [isRecordFieldReadOnly, persistTextDebounced, setFieldValue], + ); + + const handleBlur = useCallback(() => { + persistTextDebounced.flush(); + }, [persistTextDebounced]); + + const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : ''; + + return ( + +