From c31ffef510efd0ca78d5f5e07117ea3802963847 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:40:34 +0530 Subject: [PATCH] [Dashboard] fix rich text widget bugs + text selection in non edit mode (#17624) closes - https://discord.com/channels/1130383047699738754/1467809736715141211 and https://discord.com/channels/1130383047699738754/1467810112453345333 before - text selection -- didn't work - https://github.com/user-attachments/assets/a12c5301-5eb1-4641-8386-87e534675c67 tripple click color picker bug - https://github.com/user-attachments/assets/8f2ebdd9-53c1-4635-8b28-6c82110b88a0 immediate cancel (before 300ms) - draft persist -- have to refresh to get correct oncancel data https://github.com/user-attachments/assets/d56e5738-d42f-4ba8-a9d3-b83acb8546ca after - text selection - https://github.com/user-attachments/assets/e358035a-1424-45c5-a062-bb2ae6b6ca47 tripple click color picker bug - https://github.com/user-attachments/assets/d70947b0-b68b-4a85-93d4-8bed50e308fd immediate cancel (before 300ms) - https://github.com/user-attachments/assets/f3cfb013-1d48-4f2d-b535-4b8bc5bb1c50 immediate save (before 300ms) - https://github.com/user-attachments/assets/3b017755-8523-429e-a511-f65732edec34 --- .../page-layout/hooks/useSavePageLayout.ts | 14 +- .../components/DashboardsBlockEditor.tsx | 31 ++-- .../StandaloneRichTextEditorContent.tsx | 168 ++++++++++++++++++ .../components/StandaloneRichTextWidget.tsx | 122 ++----------- ...tandaloneRichTextWidgetAutoFocusEffect.tsx | 3 +- 5 files changed, 207 insertions(+), 131 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts b/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts index 57e19ec342..96d7451638 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts @@ -1,7 +1,7 @@ -import { usePageLayoutDraftState } from '@/page-layout/hooks/usePageLayoutDraftState'; import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; import { type PageLayout } from '@/page-layout/types/PageLayout'; import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput'; @@ -28,14 +28,20 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => { pageLayoutId, ); - const { pageLayoutDraft } = usePageLayoutDraftState(pageLayoutId); + const pageLayoutDraftCallbackState = useRecoilComponentCallbackState( + pageLayoutDraftComponentState, + pageLayoutId, + ); const { updatePageLayoutWithTabsAndWidgets } = useUpdatePageLayoutWithTabsAndWidgets(); const savePageLayout = useRecoilCallback( - ({ set }) => + ({ set, snapshot }) => async () => { + const pageLayoutDraft = snapshot + .getLoadable(pageLayoutDraftCallbackState) + .getValue(); const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft); @@ -64,7 +70,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => { }, [ pageLayoutCurrentLayoutsCallbackState, - pageLayoutDraft, + pageLayoutDraftCallbackState, pageLayoutId, pageLayoutPersistedCallbackState, updatePageLayoutWithTabsAndWidgets, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor.tsx index c3d7291341..f9e57eb1c0 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor.tsx @@ -35,6 +35,7 @@ const StyledEditor = styled.div` background: transparent; font-size: 13px; color: ${({ theme }) => theme.font.color.primary}; + user-select: text; } & .editor [class^='_inlineContent']:before { color: ${({ theme }) => theme.font.color.tertiary}; @@ -179,19 +180,23 @@ export const DashboardsBlockEditor = ({ formattingToolbar={false} editable={!readonly} > - - - { - const items = getSlashMenu(editor); - return filterSuggestionItems(items, query); - }} - suggestionMenuComponent={CustomSlashMenu} - /> + {!readonly && ( + <> + + + { + const items = getSlashMenu(editor); + return filterSuggestionItems(items, query); + }} + suggestionMenuComponent={CustomSlashMenu} + /> + + )} ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx new file mode 100644 index 0000000000..00263ea161 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx @@ -0,0 +1,168 @@ +import { useCallback, useMemo } from 'react'; + +import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema'; +import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; +import { type Attachment } from '@/activities/files/types/Attachment'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; +import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; +import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { DashboardsBlockEditor } from '@/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor'; +import { StandaloneRichTextWidgetAutoFocusEffect } from '@/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidgetAutoFocusEffect'; +import { BLOCK_EDITOR_GLOBAL_HOTKEYS_CONFIG } from '@/ui/input/editor/constants/BlockEditorGlobalHotkeysConfig'; +import { useAttachmentSync } from '@/ui/input/editor/hooks/useAttachmentSync'; +import { parseInitialBlocknote } from '@/ui/input/editor/utils/parseInitialBlocknote'; +import { prepareBodyWithSignedUrls } from '@/ui/input/editor/utils/prepareBodyWithSignedUrls'; +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 { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import '@blocknote/core/fonts/inter.css'; +import '@blocknote/mantine/style.css'; +import { useCreateBlockNote } from '@blocknote/react'; +import '@blocknote/react/style.css'; +import { useRecoilCallback } from 'recoil'; +import { useDebouncedCallback } from 'use-debounce'; +import { WidgetConfigurationType } from '~/generated/graphql'; + +type StandaloneRichTextEditorContentProps = { + widget: PageLayoutWidget; + dashboardId: string; + currentBody: string; + attachments: Attachment[]; + isEditable: boolean; + containerElement: HTMLDivElement | null; +}; + +export const StandaloneRichTextEditorContent = ({ + widget, + dashboardId, + currentBody, + attachments, + isEditable, + containerElement, +}: StandaloneRichTextEditorContentProps) => { + const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(); + const { uploadAttachmentFile } = useUploadAttachmentFile(); + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); + const { removeFocusItemFromFocusStackById } = + useRemoveFocusItemFromFocusStackById(); + const isPageLayoutInEditModeState = useRecoilComponentCallbackState( + isPageLayoutInEditModeComponentState, + ); + const pageLayoutEditingWidgetIdState = useRecoilComponentCallbackState( + pageLayoutEditingWidgetIdComponentState, + ); + + const { syncAttachments } = useAttachmentSync(attachments); + + const shouldPersistDraft = useRecoilCallback( + ({ snapshot }) => + () => { + const isPageLayoutInEditMode = snapshot + .getLoadable(isPageLayoutInEditModeState) + .getValue(); + const editingWidgetId = snapshot + .getLoadable(pageLayoutEditingWidgetIdState) + .getValue(); + + return isPageLayoutInEditMode && editingWidgetId === widget.id; + }, + [isPageLayoutInEditModeState, pageLayoutEditingWidgetIdState, widget.id], + ); + + const handleUploadAttachment = async (file: File) => { + return await uploadAttachmentFile(file, { + id: dashboardId, + targetObjectNameSingular: CoreObjectNameSingular.Dashboard, + }); + }; + + const handleEditorBuiltInUploadFile = async (file: File) => { + const { attachmentAbsoluteURL } = await handleUploadAttachment(file); + return attachmentAbsoluteURL; + }; + + const initialContent = useMemo( + () => parseInitialBlocknote(currentBody), + [currentBody], + ); + + const editor = useCreateBlockNote({ + initialContent, + domAttributes: { editor: { class: 'editor' } }, + schema: BLOCK_SCHEMA, + uploadFile: handleEditorBuiltInUploadFile, + sideMenuDetection: 'editor', + }); + + const handlePersistBody = useDebouncedCallback((blocknote: string) => { + if (!shouldPersistDraft()) { + return; + } + updatePageLayoutWidget(widget.id, { + configuration: { + configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT, + body: { + blocknote, + markdown: null, + }, + }, + }); + }, 300); + + const handleAttachmentSync = useDebouncedCallback( + async (newStringifiedBody: string, previousBody: string) => { + if (!shouldPersistDraft()) { + return; + } + await syncAttachments(newStringifiedBody, previousBody); + }, + 500, + ); + + const handleEditorChange = () => { + const newStringifiedBody = JSON.stringify(editor.document) ?? ''; + const preparedBody = prepareBodyWithSignedUrls(newStringifiedBody); + + handlePersistBody(preparedBody); + handleAttachmentSync(newStringifiedBody, currentBody); + }; + + const handleBlockEditorFocus = useCallback(() => { + pushFocusItemToFocusStack({ + component: { + instanceId: widget.id, + type: FocusComponentType.STANDALONE_RICH_TEXT_WIDGET, + }, + focusId: widget.id, + globalHotkeysConfig: BLOCK_EDITOR_GLOBAL_HOTKEYS_CONFIG, + }); + }, [pushFocusItemToFocusStack, widget.id]); + + const handleBlockEditorBlur = useCallback(() => { + handlePersistBody.flush(); + removeFocusItemFromFocusStackById({ + focusId: widget.id, + }); + }, [handlePersistBody, removeFocusItemFromFocusStackById, widget.id]); + + return ( + <> + + + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx index 215163a4ba..5675d0dac2 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx @@ -1,39 +1,22 @@ -import { useCallback, useMemo, useRef } from 'react'; +import { useRef } from 'react'; -import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema'; -import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { type Attachment } from '@/activities/files/types/Attachment'; import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; -import { DashboardsBlockEditor } from '@/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor'; -import { StandaloneRichTextWidgetAutoFocusEffect } from '@/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidgetAutoFocusEffect'; -import { BLOCK_EDITOR_GLOBAL_HOTKEYS_CONFIG } from '@/ui/input/editor/constants/BlockEditorGlobalHotkeysConfig'; -import { useAttachmentSync } from '@/ui/input/editor/hooks/useAttachmentSync'; -import { parseInitialBlocknote } from '@/ui/input/editor/utils/parseInitialBlocknote'; -import { prepareBodyWithSignedUrls } from '@/ui/input/editor/utils/prepareBodyWithSignedUrls'; +import { StandaloneRichTextEditorContent } from '@/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; -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 { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import '@blocknote/core/fonts/inter.css'; -import '@blocknote/mantine/style.css'; -import { useCreateBlockNote } from '@blocknote/react'; -import '@blocknote/react/style.css'; import styled from '@emotion/styled'; import { isDefined } from 'twenty-shared/utils'; -import { useDebouncedCallback } from 'use-debounce'; import { FeatureFlagKey, PageLayoutType, - WidgetConfigurationType, type StandaloneRichTextConfiguration, } from '~/generated/graphql'; @@ -64,17 +47,11 @@ export const StandaloneRichTextWidget = ({ pageLayoutEditingWidgetIdComponentState, ); - const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(); const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext(); - const { uploadAttachmentFile } = useUploadAttachmentFile(); const isAttachmentMigrated = useIsFeatureEnabled( FeatureFlagKey.IS_ATTACHMENT_MIGRATED, ); - const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); - const { removeFocusItemFromFocusStackById } = - useRemoveFocusItemFromFocusStackById(); - const isDashboard = layoutType === PageLayoutType.DASHBOARD; const dashboardId = isDashboard ? targetRecordIdentifier?.id : undefined; const attachmentTargetFieldIdName = getActivityTargetObjectFieldIdName({ @@ -96,84 +73,9 @@ export const StandaloneRichTextWidget = ({ skip: !isDefined(dashboardId), }); - const { syncAttachments } = useAttachmentSync(attachments); - - const handleUploadAttachment = async (file: File) => { - if (!isDefined(dashboardId)) return { attachmentAbsoluteURL: '' }; - - return await uploadAttachmentFile(file, { - id: dashboardId, - targetObjectNameSingular: CoreObjectNameSingular.Dashboard, - }); - }; - - const handleEditorBuiltInUploadFile = async (file: File) => { - const { attachmentAbsoluteURL } = await handleUploadAttachment(file); - return attachmentAbsoluteURL; - }; - - const initialContent = useMemo(() => { - if (isDefined(configuration) && 'body' in configuration) { - return parseInitialBlocknote(configuration.body?.blocknote); - } - return undefined; - }, [configuration]); - - const editor = useCreateBlockNote({ - initialContent, - domAttributes: { editor: { class: 'editor' } }, - schema: BLOCK_SCHEMA, - uploadFile: handleEditorBuiltInUploadFile, - sideMenuDetection: 'editor', - }); - - const handlePersistBody = useDebouncedCallback((blocknote: string) => { - updatePageLayoutWidget(widget.id, { - configuration: { - configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT, - body: { - blocknote, - markdown: null, - }, - }, - }); - }, 300); - - const handleAttachmentSync = useDebouncedCallback( - async (newStringifiedBody: string, previousBody: string) => { - await syncAttachments(newStringifiedBody, previousBody); - }, - 500, - ); - - const handleEditorChange = () => { - const newStringifiedBody = JSON.stringify(editor.document) ?? ''; - const preparedBody = prepareBodyWithSignedUrls(newStringifiedBody); - - handlePersistBody(preparedBody); - handleAttachmentSync(newStringifiedBody, currentBody); - }; - const isThisWidgetBeingEdited = editingWidgetId === widget.id; const isEditable = isPageLayoutInEditMode && isThisWidgetBeingEdited; - const handleBlockEditorFocus = useCallback(() => { - pushFocusItemToFocusStack({ - component: { - instanceId: widget.id, - type: FocusComponentType.STANDALONE_RICH_TEXT_WIDGET, - }, - focusId: widget.id, - globalHotkeysConfig: BLOCK_EDITOR_GLOBAL_HOTKEYS_CONFIG, - }); - }, [pushFocusItemToFocusStack, widget.id]); - - const handleBlockEditorBlur = useCallback(() => { - removeFocusItemFromFocusStackById({ - focusId: widget.id, - }); - }, [removeFocusItemFromFocusStackById, widget.id]); - if (!isDefined(dashboardId)) { return null; } @@ -183,21 +85,17 @@ export const StandaloneRichTextWidget = ({ ref={containerElementRef} isPageLayoutInEditMode={isPageLayoutInEditMode} > - - diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidgetAutoFocusEffect.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidgetAutoFocusEffect.tsx index 748f870b02..122e77cf2a 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidgetAutoFocusEffect.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidgetAutoFocusEffect.tsx @@ -1,6 +1,5 @@ -import { useEffect } from 'react'; - import { type BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema'; +import { useEffect } from 'react'; type StandaloneRichTextWidgetAutoFocusEffectProps = { shouldFocus: boolean;