[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
This commit is contained in:
@@ -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,
|
||||
|
||||
+18
-13
@@ -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}
|
||||
>
|
||||
<DashboardFormattingToolbar boundaryElement={boundaryElement} />
|
||||
<DashboardEditorSideMenu
|
||||
editor={editor}
|
||||
boundaryElement={boundaryElement}
|
||||
/>
|
||||
<SuggestionMenuController
|
||||
triggerCharacter="/"
|
||||
getItems={async (query) => {
|
||||
const items = getSlashMenu(editor);
|
||||
return filterSuggestionItems<SuggestionItem>(items, query);
|
||||
}}
|
||||
suggestionMenuComponent={CustomSlashMenu}
|
||||
/>
|
||||
{!readonly && (
|
||||
<>
|
||||
<DashboardFormattingToolbar boundaryElement={boundaryElement} />
|
||||
<DashboardEditorSideMenu
|
||||
editor={editor}
|
||||
boundaryElement={boundaryElement}
|
||||
/>
|
||||
<SuggestionMenuController
|
||||
triggerCharacter="/"
|
||||
getItems={async (query) => {
|
||||
const items = getSlashMenu(editor);
|
||||
return filterSuggestionItems<SuggestionItem>(items, query);
|
||||
}}
|
||||
suggestionMenuComponent={CustomSlashMenu}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BlockNoteView>
|
||||
</StyledEditor>
|
||||
);
|
||||
|
||||
+168
@@ -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 (
|
||||
<>
|
||||
<StandaloneRichTextWidgetAutoFocusEffect
|
||||
shouldFocus={isEditable}
|
||||
editor={editor}
|
||||
containerElement={containerElement}
|
||||
/>
|
||||
<DashboardsBlockEditor
|
||||
onFocus={handleBlockEditorFocus}
|
||||
onBlur={handleBlockEditorBlur}
|
||||
onChange={handleEditorChange}
|
||||
editor={editor}
|
||||
readonly={!isEditable}
|
||||
boundaryElement={containerElement}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+10
-112
@@ -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}
|
||||
>
|
||||
<StandaloneRichTextWidgetAutoFocusEffect
|
||||
shouldFocus={isEditable}
|
||||
editor={editor}
|
||||
containerElement={containerElementRef.current}
|
||||
/>
|
||||
<ScrollWrapper
|
||||
componentInstanceId={`scroll-wrapper-rich-text-widget-${widget.id}`}
|
||||
>
|
||||
<DashboardsBlockEditor
|
||||
onFocus={handleBlockEditorFocus}
|
||||
onBlur={handleBlockEditorBlur}
|
||||
onChange={handleEditorChange}
|
||||
editor={editor}
|
||||
readonly={!isEditable}
|
||||
boundaryElement={containerElementRef.current}
|
||||
<StandaloneRichTextEditorContent
|
||||
key={isEditable ? 'editing' : 'readonly'}
|
||||
widget={widget}
|
||||
dashboardId={dashboardId}
|
||||
currentBody={currentBody}
|
||||
attachments={attachments}
|
||||
isEditable={isEditable}
|
||||
containerElement={containerElementRef.current}
|
||||
/>
|
||||
</ScrollWrapper>
|
||||
</StyledContainer>
|
||||
|
||||
+1
-2
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user