nitin
2026-02-03 20:40:34 +05:30
committed by GitHub
parent 19b56c5d3a
commit c31ffef510
5 changed files with 207 additions and 131 deletions
@@ -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,
@@ -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>
);
@@ -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}
/>
</>
);
};
@@ -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,6 +1,5 @@
import { useEffect } from 'react';
import { type BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { useEffect } from 'react';
type StandaloneRichTextWidgetAutoFocusEffectProps = {
shouldFocus: boolean;