Add Edit Rich Text functionality to table view (#11390)
Fixes https://github.com/twentyhq/core-team-issues/issues/729 [recording.webm](https://github.com/user-attachments/assets/ea95d67b-64a3-4fef-91ed-b06318099a78) --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -2,6 +2,7 @@ import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
|||||||
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
|
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
|
||||||
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
|
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
|
||||||
import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage';
|
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 { CommandMenuSearchRecordsPage } from '@/command-menu/pages/search/components/CommandMenuSearchRecordsPage';
|
||||||
import { CommandMenuWorkflowSelectAction } from '@/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction';
|
import { CommandMenuWorkflowSelectAction } from '@/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction';
|
||||||
import { CommandMenuWorkflowEditStep } from '@/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStep';
|
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, <CommandMenuRecordPage />],
|
[CommandMenuPages.ViewRecord, <CommandMenuRecordPage />],
|
||||||
[CommandMenuPages.ViewEmailThread, <CommandMenuMessageThreadPage />],
|
[CommandMenuPages.ViewEmailThread, <CommandMenuMessageThreadPage />],
|
||||||
[CommandMenuPages.ViewCalendarEvent, <CommandMenuCalendarEventPage />],
|
[CommandMenuPages.ViewCalendarEvent, <CommandMenuCalendarEventPage />],
|
||||||
|
[CommandMenuPages.EditRichText, <CommandMenuEditRichTextPage />],
|
||||||
[
|
[
|
||||||
CommandMenuPages.WorkflowStepSelectTriggerType,
|
CommandMenuPages.WorkflowStepSelectTriggerType,
|
||||||
<CommandMenuWorkflowSelectTriggerType />,
|
<CommandMenuWorkflowSelectTriggerType />,
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<StyledContainer>
|
||||||
|
<ActivityRichTextEditor
|
||||||
|
activityId={activityId}
|
||||||
|
activityObjectNameSingular={activityObjectNameSingular}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { atom, RecoilState } from 'recoil';
|
||||||
|
|
||||||
|
export const viewableRichTextComponentState: RecoilState<{
|
||||||
|
activityId: string;
|
||||||
|
activityObjectNameSingular: string;
|
||||||
|
}> = atom({
|
||||||
|
key: 'viewableRichTextComponentState',
|
||||||
|
default: {
|
||||||
|
activityId: '',
|
||||||
|
activityObjectNameSingular: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -3,6 +3,7 @@ export enum CommandMenuPages {
|
|||||||
ViewRecord = 'view-record',
|
ViewRecord = 'view-record',
|
||||||
ViewEmailThread = 'view-email-thread',
|
ViewEmailThread = 'view-email-thread',
|
||||||
ViewCalendarEvent = 'view-calendar-event',
|
ViewCalendarEvent = 'view-calendar-event',
|
||||||
|
EditRichText = 'edit-rich-text',
|
||||||
Copilot = 'copilot',
|
Copilot = 'copilot',
|
||||||
WorkflowStepSelectTriggerType = 'workflow-step-select-trigger-type',
|
WorkflowStepSelectTriggerType = 'workflow-step-select-trigger-type',
|
||||||
WorkflowStepSelectAction = 'workflow-step-select-action',
|
WorkflowStepSelectAction = 'workflow-step-select-action',
|
||||||
|
|||||||
@ -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 { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
|
||||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
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 { 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 { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
||||||
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||||
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
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 { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
|
||||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
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 { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
|
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 { RelationToOneFieldInput } from '../meta-types/input/components/RelationToOneFieldInput';
|
||||||
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
|
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
|
||||||
import { FieldInputEvent } from '../types/FieldInputEvent';
|
import { FieldInputEvent } from '../types/FieldInputEvent';
|
||||||
|
import { FieldRichTextV2Metadata } from '../types/FieldMetadata';
|
||||||
import { isFieldText } from '../types/guards/isFieldText';
|
import { isFieldText } from '../types/guards/isFieldText';
|
||||||
|
|
||||||
type FieldInputProps = {
|
type FieldInputProps = {
|
||||||
@ -64,7 +68,7 @@ export const FieldInput = ({
|
|||||||
onClickOutside,
|
onClickOutside,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
}: FieldInputProps) => {
|
}: FieldInputProps) => {
|
||||||
const { fieldDefinition } = useContext(FieldContext);
|
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -161,6 +165,22 @@ export const FieldInput = ({
|
|||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||||
/>
|
/>
|
||||||
|
) : isFieldRichTextV2(fieldDefinition) ? (
|
||||||
|
<RichTextFieldInput
|
||||||
|
targetableObject={{
|
||||||
|
id: recordId,
|
||||||
|
targetObjectNameSingular: (
|
||||||
|
fieldDefinition as {
|
||||||
|
metadata: FieldRichTextV2Metadata;
|
||||||
|
}
|
||||||
|
).metadata.objectMetadataNameSingular as
|
||||||
|
| CoreObjectNameSingular.Note
|
||||||
|
| CoreObjectNameSingular.Task,
|
||||||
|
}}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
|
onEscape={onEscape}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,60 +1,89 @@
|
|||||||
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
|
import { ActivityRichTextEditor } from '@/activities/components/ActivityRichTextEditor';
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { useRichTextField } from '@/object-record/record-field/meta-types/hooks/useRichTextField';
|
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 { 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 { 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 styled from '@emotion/styled';
|
||||||
|
import { useRef } from 'react';
|
||||||
import { useContext, useRef } from 'react';
|
import { IconLayoutSidebarLeftCollapse } from 'twenty-ui/display';
|
||||||
|
import { FloatingIconButton } from 'twenty-ui/input';
|
||||||
const StyledRichTextContainer = styled.div`
|
|
||||||
height: 400px;
|
|
||||||
width: 500px;
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type RichTextFieldInputProps = {
|
export type RichTextFieldInputProps = {
|
||||||
onClickOutside?: FieldInputClickOutsideEvent;
|
onClickOutside?: FieldInputClickOutsideEvent;
|
||||||
|
onCancel?: () => void;
|
||||||
|
onEnter?: FieldInputEvent;
|
||||||
|
onEscape?: FieldInputEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RichTextFieldInput = ({
|
const StyledContainer = styled.div`
|
||||||
onClickOutside,
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
}: RichTextFieldInputProps) => {
|
width: 480px;
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
const { recordId } = useContext(FieldContext);
|
margin: 0 0 0 ${({ theme }) => theme.spacing(-6)};
|
||||||
const { draftValue, persistRichTextField, fieldDefinition } =
|
display: flex;
|
||||||
useRichTextField();
|
`;
|
||||||
|
|
||||||
const editor = useCreateBlockNote({
|
const StyledCollapseButton = styled.div`
|
||||||
initialContent: draftValue,
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
domAttributes: { editor: { class: 'editor' } },
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
schema: BLOCK_SCHEMA,
|
cursor: pointer;
|
||||||
});
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const RichTextFieldInput = ({
|
||||||
|
targetableObject,
|
||||||
|
onClickOutside,
|
||||||
|
onEscape,
|
||||||
|
}: {
|
||||||
|
targetableObject: Pick<ActivityTargetableObject, 'id'> & {
|
||||||
|
targetObjectNameSingular:
|
||||||
|
| CoreObjectNameSingular.Note
|
||||||
|
| CoreObjectNameSingular.Task;
|
||||||
|
};
|
||||||
|
} & RichTextFieldInputProps) => {
|
||||||
|
const { editRichText } = useRichTextCommandMenu();
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||||
onClickOutside?.(() => persistRichTextField(editor.document), event);
|
onClickOutside?.(() => {}, event);
|
||||||
};
|
};
|
||||||
|
|
||||||
useRegisterInputEvents<PartialBlock[]>({
|
const handleEscape = () => {
|
||||||
|
onEscape?.(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
useRegisterInputEvents({
|
||||||
inputRef: containerRef,
|
inputRef: containerRef,
|
||||||
inputValue: draftValue,
|
inputValue: null,
|
||||||
onClickOutside: handleClickOutside,
|
onClickOutside: handleClickOutside,
|
||||||
|
onEscape: handleEscape,
|
||||||
hotkeyScope: DEFAULT_CELL_SCOPE.scope,
|
hotkeyScope: DEFAULT_CELL_SCOPE.scope,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRichTextContainer ref={containerRef}>
|
<StyledContainer ref={containerRef}>
|
||||||
<BlockEditorComponentInstanceContext.Provider
|
<ActivityRichTextEditor
|
||||||
value={{ instanceId: `${recordId}-${fieldDefinition.fieldMetadataId}` }}
|
activityId={targetableObject.id}
|
||||||
>
|
activityObjectNameSingular={targetableObject.targetObjectNameSingular}
|
||||||
<BlockEditor editor={editor} />
|
/>
|
||||||
</BlockEditorComponentInstanceContext.Provider>
|
<StyledCollapseButton>
|
||||||
</StyledRichTextContainer>
|
<FloatingIconButton
|
||||||
|
Icon={IconLayoutSidebarLeftCollapse}
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
onEscape?.(() => {});
|
||||||
|
editRichText(
|
||||||
|
targetableObject.id,
|
||||||
|
targetableObject.targetObjectNameSingular,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledCollapseButton>
|
||||||
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 <Story />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RichTextFieldInputWithContext = ({
|
||||||
|
targetableObjectId = 'test-id',
|
||||||
|
onClickOutside,
|
||||||
|
onEscape,
|
||||||
|
}: RichTextFieldInputWithContextProps) => {
|
||||||
|
const setHotKeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHotKeyScope(DEFAULT_CELL_SCOPE.scope);
|
||||||
|
}, [setHotKeyScope]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecordFieldComponentInstanceContext.Provider
|
||||||
|
value={{
|
||||||
|
instanceId: 'record-field-component-instance-id',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FieldContext.Provider
|
||||||
|
value={{
|
||||||
|
recordId: targetableObjectId,
|
||||||
|
fieldDefinition: {
|
||||||
|
fieldMetadataId: 'richText',
|
||||||
|
label: 'Rich Text',
|
||||||
|
type: FieldMetadataType.RICH_TEXT,
|
||||||
|
iconName: 'IconRichText',
|
||||||
|
metadata: {
|
||||||
|
fieldName: 'richText',
|
||||||
|
objectMetadataNameSingular: 'note',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isLabelIdentifier: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RichTextFieldInput
|
||||||
|
targetableObject={{
|
||||||
|
id: targetableObjectId,
|
||||||
|
targetObjectNameSingular: CoreObjectNameSingular.Note,
|
||||||
|
}}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
|
onEscape={onEscape}
|
||||||
|
/>
|
||||||
|
</FieldContext.Provider>
|
||||||
|
<div data-testid="click-outside-element" />
|
||||||
|
</RecordFieldComponentInstanceContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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<typeof RichTextFieldInputWithContext>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -54,12 +54,12 @@ describe('isFieldValueReadOnly', () => {
|
|||||||
expect(result).toBe(true);
|
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({
|
const result = isFieldValueReadOnly({
|
||||||
fieldType: FieldMetadataType.RICH_TEXT_V2,
|
fieldType: FieldMetadataType.RICH_TEXT_V2,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true if fieldType is ACTOR', () => {
|
it('should return true if fieldType is ACTOR', () => {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
|
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
|
||||||
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
|
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
|
||||||
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
|
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 { isDefined } from 'twenty-shared/utils';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
@ -61,9 +61,7 @@ export const isFieldValueReadOnly = ({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
isDefined(fieldType) &&
|
isDefined(fieldType) &&
|
||||||
(isFieldActor({ type: fieldType }) ||
|
(isFieldActor({ type: fieldType }) || isFieldRichText({ type: fieldType }))
|
||||||
isFieldRichText({ type: fieldType }) ||
|
|
||||||
isFieldRichTextV2({ type: fieldType }))
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,11 +55,16 @@ export const FieldsCard = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy(
|
const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy(
|
||||||
availableFieldMetadataItems.filter(
|
availableFieldMetadataItems
|
||||||
(fieldMetadataItem) =>
|
.filter(
|
||||||
fieldMetadataItem.name !== 'createdAt' &&
|
(fieldMetadataItem) =>
|
||||||
fieldMetadataItem.name !== 'deletedAt',
|
fieldMetadataItem.name !== 'createdAt' &&
|
||||||
),
|
fieldMetadataItem.name !== 'deletedAt',
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(fieldMetadataItem) =>
|
||||||
|
fieldMetadataItem.type !== FieldMetadataType.RICH_TEXT_V2,
|
||||||
|
),
|
||||||
(fieldMetadataItem) =>
|
(fieldMetadataItem) =>
|
||||||
fieldMetadataItem.type === FieldMetadataType.RELATION
|
fieldMetadataItem.type === FieldMetadataType.RELATION
|
||||||
? 'relationFieldMetadataItems'
|
? 'relationFieldMetadataItems'
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
||||||
|
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
import {
|
import {
|
||||||
FieldInputClickOutsideEvent,
|
FieldInputClickOutsideEvent,
|
||||||
@ -12,7 +13,6 @@ import { useRecoilCallback } from 'recoil';
|
|||||||
|
|
||||||
export const RecordTableCellFieldInput = () => {
|
export const RecordTableCellFieldInput = () => {
|
||||||
const { onMoveFocus, onCloseTableCell } = useRecordTableBodyContextOrThrow();
|
const { onMoveFocus, onCloseTableCell } = useRecordTableBodyContextOrThrow();
|
||||||
|
|
||||||
const { isReadOnly } = useContext(FieldContext);
|
const { isReadOnly } = useContext(FieldContext);
|
||||||
|
|
||||||
const handleEnter: FieldInputEvent = (persistField) => {
|
const handleEnter: FieldInputEvent = (persistField) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user