diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx
index 2ecbad5ae..f4ecbdb70 100644
--- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx
+++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx
@@ -2,6 +2,7 @@ import { CommandMenu } from '@/command-menu/components/CommandMenu';
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage';
+import { CommandMenuEditRichTextPage } from '@/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage';
import { CommandMenuSearchRecordsPage } from '@/command-menu/pages/search/components/CommandMenuSearchRecordsPage';
import { CommandMenuWorkflowSelectAction } from '@/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction';
import { CommandMenuWorkflowEditStep } from '@/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStep';
@@ -18,6 +19,7 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
[CommandMenuPages.ViewRecord, ],
[CommandMenuPages.ViewEmailThread, ],
[CommandMenuPages.ViewCalendarEvent, ],
+ [CommandMenuPages.EditRichText, ],
[
CommandMenuPages.WorkflowStepSelectTriggerType,
,
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useRichTextCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useRichTextCommandMenu.ts
new file mode 100644
index 000000000..efddebfc7
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/hooks/useRichTextCommandMenu.ts
@@ -0,0 +1,45 @@
+import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
+import { viewableRichTextComponentState } from '@/command-menu/pages/rich-text-page/states/viewableRichTextComponentState';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
+import { useCallback } from 'react';
+import { useRecoilCallback } from 'recoil';
+import { IconPencil } from 'twenty-ui/display';
+
+export const useRichTextCommandMenu = () => {
+ const { navigateCommandMenu, openCommandMenu } = useCommandMenu();
+
+ const openRichTextInCommandMenu = useRecoilCallback(
+ ({ set }) =>
+ ({
+ activityId,
+ activityObjectNameSingular,
+ }: {
+ activityId: string;
+ activityObjectNameSingular: string;
+ }) => {
+ set(viewableRichTextComponentState, {
+ activityId,
+ activityObjectNameSingular,
+ });
+
+ openCommandMenu();
+ navigateCommandMenu({
+ page: CommandMenuPages.EditRichText,
+ pageTitle: 'Rich Text',
+ pageIcon: IconPencil,
+ });
+ },
+ [navigateCommandMenu, openCommandMenu],
+ );
+
+ const editRichText = useCallback(
+ (activityId: string, activityObjectNameSingular: string) => {
+ openRichTextInCommandMenu({ activityId, activityObjectNameSingular });
+ },
+ [openRichTextInCommandMenu],
+ );
+
+ return {
+ editRichText,
+ };
+};
diff --git a/packages/twenty-front/src/modules/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage.tsx b/packages/twenty-front/src/modules/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage.tsx
new file mode 100644
index 000000000..2aa1066b3
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage.tsx
@@ -0,0 +1,33 @@
+import { ActivityRichTextEditor } from '@/activities/components/ActivityRichTextEditor';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import styled from '@emotion/styled';
+import { useRecoilValue } from 'recoil';
+import { viewableRichTextComponentState } from '../states/viewableRichTextComponentState';
+
+const StyledContainer = styled.div`
+ margin: ${({ theme }) => theme.spacing(4)} ${({ theme }) => theme.spacing(-2)};
+`;
+
+export const CommandMenuEditRichTextPage = () => {
+ const { activityId, activityObjectNameSingular } = useRecoilValue(
+ viewableRichTextComponentState,
+ );
+
+ if (
+ activityObjectNameSingular !== CoreObjectNameSingular.Note &&
+ activityObjectNameSingular !== CoreObjectNameSingular.Task
+ ) {
+ throw new Error(
+ `Invalid activity object name singular: ${activityObjectNameSingular}`,
+ );
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu/pages/rich-text-page/states/viewableRichTextComponentState.ts b/packages/twenty-front/src/modules/command-menu/pages/rich-text-page/states/viewableRichTextComponentState.ts
new file mode 100644
index 000000000..39f003c2f
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/pages/rich-text-page/states/viewableRichTextComponentState.ts
@@ -0,0 +1,12 @@
+import { atom, RecoilState } from 'recoil';
+
+export const viewableRichTextComponentState: RecoilState<{
+ activityId: string;
+ activityObjectNameSingular: string;
+}> = atom({
+ key: 'viewableRichTextComponentState',
+ default: {
+ activityId: '',
+ activityObjectNameSingular: '',
+ },
+});
diff --git a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts
index 57bf842fd..6341ceebf 100644
--- a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts
+++ b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts
@@ -3,6 +3,7 @@ export enum CommandMenuPages {
ViewRecord = 'view-record',
ViewEmailThread = 'view-email-thread',
ViewCalendarEvent = 'view-calendar-event',
+ EditRichText = 'edit-rich-text',
Copilot = 'copilot',
WorkflowStepSelectTriggerType = 'workflow-step-select-trigger-type',
WorkflowStepSelectAction = 'workflow-step-select-action',
diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx
index 0b1453f1e..edca66ba9 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx
@@ -13,7 +13,9 @@ import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ArrayFieldInput } from '@/object-record/record-field/meta-types/input/components/ArrayFieldInput';
+import { RichTextFieldInput } from '@/object-record/record-field/meta-types/input/components/RichTextFieldInput';
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
@@ -28,6 +30,7 @@ import { isFieldNumber } from '@/object-record/record-field/types/guards/isField
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
+import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { FieldContext } from '../contexts/FieldContext';
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
@@ -38,6 +41,7 @@ import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInpu
import { RelationToOneFieldInput } from '../meta-types/input/components/RelationToOneFieldInput';
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
import { FieldInputEvent } from '../types/FieldInputEvent';
+import { FieldRichTextV2Metadata } from '../types/FieldMetadata';
import { isFieldText } from '../types/guards/isFieldText';
type FieldInputProps = {
@@ -64,7 +68,7 @@ export const FieldInput = ({
onClickOutside,
isReadOnly,
}: FieldInputProps) => {
- const { fieldDefinition } = useContext(FieldContext);
+ const { fieldDefinition, recordId } = useContext(FieldContext);
return (
<>
@@ -161,6 +165,22 @@ export const FieldInput = ({
onCancel={onCancel}
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
/>
+ ) : isFieldRichTextV2(fieldDefinition) ? (
+
) : (
<>>
)}
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx
index 2cbc0ba19..7956192f9 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx
@@ -1,60 +1,89 @@
-import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
-import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
-import { useRichTextField } from '@/object-record/record-field/meta-types/hooks/useRichTextField';
+import { ActivityRichTextEditor } from '@/activities/components/ActivityRichTextEditor';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { useRichTextCommandMenu } from '@/command-menu/hooks/useRichTextCommandMenu';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
-import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
+import {
+ FieldInputClickOutsideEvent,
+ FieldInputEvent,
+} from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
-import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
-import { BlockEditorComponentInstanceContext } from '@/ui/input/editor/contexts/BlockEditorCompoponeInstanceContext';
-import { PartialBlock } from '@blocknote/core';
-import { useCreateBlockNote } from '@blocknote/react';
import styled from '@emotion/styled';
-
-import { useContext, useRef } from 'react';
-
-const StyledRichTextContainer = styled.div`
- height: 400px;
- width: 500px;
-
- overflow: auto;
-`;
+import { useRef } from 'react';
+import { IconLayoutSidebarLeftCollapse } from 'twenty-ui/display';
+import { FloatingIconButton } from 'twenty-ui/input';
export type RichTextFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;
+ onCancel?: () => void;
+ onEnter?: FieldInputEvent;
+ onEscape?: FieldInputEvent;
};
-export const RichTextFieldInput = ({
- onClickOutside,
-}: RichTextFieldInputProps) => {
- const containerRef = useRef(null);
- const { recordId } = useContext(FieldContext);
- const { draftValue, persistRichTextField, fieldDefinition } =
- useRichTextField();
+const StyledContainer = styled.div`
+ background-color: ${({ theme }) => theme.background.primary};
+ width: 480px;
+ padding: ${({ theme }) => theme.spacing(2)};
+ margin: 0 0 0 ${({ theme }) => theme.spacing(-6)};
+ display: flex;
+`;
- const editor = useCreateBlockNote({
- initialContent: draftValue,
- domAttributes: { editor: { class: 'editor' } },
- schema: BLOCK_SCHEMA,
- });
+const StyledCollapseButton = styled.div`
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ color: ${({ theme }) => theme.font.color.light};
+ cursor: pointer;
+ display: flex;
+`;
+
+export const RichTextFieldInput = ({
+ targetableObject,
+ onClickOutside,
+ onEscape,
+}: {
+ targetableObject: Pick & {
+ targetObjectNameSingular:
+ | CoreObjectNameSingular.Note
+ | CoreObjectNameSingular.Task;
+ };
+} & RichTextFieldInputProps) => {
+ const { editRichText } = useRichTextCommandMenu();
+ const containerRef = useRef(null);
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
- onClickOutside?.(() => persistRichTextField(editor.document), event);
+ onClickOutside?.(() => {}, event);
};
- useRegisterInputEvents({
+ const handleEscape = () => {
+ onEscape?.(() => {});
+ };
+
+ useRegisterInputEvents({
inputRef: containerRef,
- inputValue: draftValue,
+ inputValue: null,
onClickOutside: handleClickOutside,
+ onEscape: handleEscape,
hotkeyScope: DEFAULT_CELL_SCOPE.scope,
});
return (
-
-
-
-
-
+
+
+
+ {
+ onEscape?.(() => {});
+ editRichText(
+ targetableObject.id,
+ targetableObject.targetObjectNameSingular,
+ );
+ }}
+ />
+
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RichTextFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RichTextFieldInput.stories.tsx
new file mode 100644
index 000000000..d61e1e3b7
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RichTextFieldInput.stories.tsx
@@ -0,0 +1,132 @@
+import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
+import { useEffect } from 'react';
+
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
+import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
+import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
+import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
+import { Decorator, Meta, StoryObj } from '@storybook/react';
+import { FieldMetadataType } from '~/generated/graphql';
+import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
+import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
+import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
+import { RichTextFieldInput } from '../RichTextFieldInput';
+
+const clickOutsideJestFn = fn();
+const escapeJestFn = fn();
+
+type RichTextFieldInputWithContextProps = {
+ targetableObjectId?: string;
+ onClickOutside?: typeof clickOutsideJestFn;
+ onEscape?: typeof escapeJestFn;
+};
+
+const clearMocksDecorator: Decorator = (Story, context) => {
+ if (context.parameters.clearMocks !== false) {
+ clickOutsideJestFn.mockClear();
+ escapeJestFn.mockClear();
+ }
+ return ;
+};
+
+const RichTextFieldInputWithContext = ({
+ targetableObjectId = 'test-id',
+ onClickOutside,
+ onEscape,
+}: RichTextFieldInputWithContextProps) => {
+ const setHotKeyScope = useSetHotkeyScope();
+
+ useEffect(() => {
+ setHotKeyScope(DEFAULT_CELL_SCOPE.scope);
+ }, [setHotKeyScope]);
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+const meta: Meta = {
+ title: 'UI/Data/Field/Input/RichTextFieldInput',
+ component: RichTextFieldInputWithContext,
+ args: {
+ targetableObjectId: 'test-id',
+ onClickOutside: clickOutsideJestFn,
+ onEscape: escapeJestFn,
+ },
+ argTypes: {
+ onClickOutside: { control: false },
+ onEscape: { control: false },
+ },
+ decorators: [
+ clearMocksDecorator,
+ SnackBarDecorator,
+ I18nFrontDecorator,
+ ObjectMetadataItemsDecorator,
+ ],
+ parameters: {
+ clearMocks: true,
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Escape: Story = {
+ play: async () => {
+ expect(escapeJestFn).toHaveBeenCalledTimes(0);
+
+ await waitFor(() => {
+ userEvent.keyboard('{esc}');
+ expect(escapeJestFn).toHaveBeenCalledTimes(1);
+ });
+ },
+};
+
+export const ClickOutside: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
+
+ await waitFor(() => {
+ const outsideElement = canvas.getByTestId('click-outside-element');
+ userEvent.click(outsideElement);
+ expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
+ });
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts
index 69f49bb33..67dcd1243 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts
@@ -54,12 +54,12 @@ describe('isFieldValueReadOnly', () => {
expect(result).toBe(true);
});
- it('should return true if fieldType is RICH_TEXT_V2', () => {
+ it('should return false if fieldType is RICH_TEXT_V2', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.RICH_TEXT_V2,
});
- expect(result).toBe(true);
+ expect(result).toBe(false);
});
it('should return true if fieldType is ACTOR', () => {
diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts
index 321ba81cd..8db90998a 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts
@@ -2,7 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
-import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
+
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -61,9 +61,7 @@ export const isFieldValueReadOnly = ({
if (
isDefined(fieldType) &&
- (isFieldActor({ type: fieldType }) ||
- isFieldRichText({ type: fieldType }) ||
- isFieldRichTextV2({ type: fieldType }))
+ (isFieldActor({ type: fieldType }) || isFieldRichText({ type: fieldType }))
) {
return true;
}
diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx
index 04b2846c2..faae2cd10 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx
@@ -55,11 +55,16 @@ export const FieldsCard = ({
);
const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy(
- availableFieldMetadataItems.filter(
- (fieldMetadataItem) =>
- fieldMetadataItem.name !== 'createdAt' &&
- fieldMetadataItem.name !== 'deletedAt',
- ),
+ availableFieldMetadataItems
+ .filter(
+ (fieldMetadataItem) =>
+ fieldMetadataItem.name !== 'createdAt' &&
+ fieldMetadataItem.name !== 'deletedAt',
+ )
+ .filter(
+ (fieldMetadataItem) =>
+ fieldMetadataItem.type !== FieldMetadataType.RICH_TEXT_V2,
+ ),
(fieldMetadataItem) =>
fieldMetadataItem.type === FieldMetadataType.RELATION
? 'relationFieldMetadataItems'
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx
index 0716666d7..e60e79f17 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx
@@ -1,4 +1,5 @@
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
+
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import {
FieldInputClickOutsideEvent,
@@ -12,7 +13,6 @@ import { useRecoilCallback } from 'recoil';
export const RecordTableCellFieldInput = () => {
const { onMoveFocus, onCloseTableCell } = useRecordTableBodyContextOrThrow();
-
const { isReadOnly } = useContext(FieldContext);
const handleEnter: FieldInputEvent = (persistField) => {