RICH_TEXT_V2 frontend (#10083)

Adds task and note support for the new `bodyV2` field. (Field metadata
type of `bodyV2` is `RICH_TEXT_V2`.)

Related to issue https://github.com/twentyhq/twenty/issues/7613

Upgrade commands will be in separate PRs.

Fixes https://github.com/twentyhq/twenty/issues/10084

---------

Co-authored-by: ad-elias <elias@autodiligence.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
eliasylonen
2025-02-11 11:21:03 +01:00
committed by GitHub
parent de91a5e39e
commit 4f06b83d7f
55 changed files with 545 additions and 4576 deletions

View File

@ -128,6 +128,15 @@ export type RawJsonFilter = {
is?: IsFilter;
};
export type RichTextV2LeafFilter = {
ilike?: string;
};
export type RichTextV2Filter = {
blocknote?: RichTextV2LeafFilter;
markdown?: RichTextV2LeafFilter;
};
export type LeafFilter =
| UUIDFilter
| StringFilter
@ -143,6 +152,7 @@ export type LeafFilter =
| PhonesFilter
| ArrayFilter
| RawJsonFilter
| RichTextV2Filter
| undefined;
export type AndObjectRecordFilter = {

View File

@ -15,6 +15,7 @@ import {
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
@ -93,6 +94,11 @@ export const ObjectOptionsDropdownMenuContent = () => {
viewType,
});
// TODO: Remove this once we have implemented Rich Text v2 and removed the old rich text
const canImportOrExport =
objectMetadataItem.nameSingular !== CoreObjectNameSingular.Note &&
objectMetadataItem.nameSingular !== CoreObjectNameSingular.Task;
return (
<>
<DropdownMenuHeader StartIcon={CurrentViewIcon ?? IconList}>
@ -151,16 +157,20 @@ export const ObjectOptionsDropdownMenuContent = () => {
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItem
onClick={download}
LeftIcon={IconFileExport}
text={displayedExportProgress(progress)}
/>
<MenuItem
onClick={() => openObjectRecordsSpreasheetImportDialog()}
LeftIcon={IconFileImport}
text="Import"
/>
{canImportOrExport && (
<>
<MenuItem
onClick={download}
LeftIcon={IconFileExport}
text={displayedExportProgress(progress)}
/>
<MenuItem
onClick={() => openObjectRecordsSpreasheetImportDialog()}
LeftIcon={IconFileImport}
text="Import"
/>
</>
)}
<MenuItem
onClick={() => {
handleToggleTrashColumnFilter();

View File

@ -9,6 +9,7 @@ import { PhonesFieldDisplay } from '@/object-record/record-field/meta-types/disp
import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
import { RichTextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextFieldDisplay';
import { RichTextV2FieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextV2FieldDisplay';
import { isFieldIdentifierDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldIdentifierDisplay';
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
@ -20,6 +21,7 @@ import { isFieldRating } from '@/object-record/record-field/types/guards/isField
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
import { FieldContext } from '../contexts/FieldContext';
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
@ -90,6 +92,8 @@ export const FieldDisplay = () => {
<RatingFieldDisplay />
) : isFieldRichText(fieldDefinition) ? (
<RichTextFieldDisplay />
) : isFieldRichTextV2(fieldDefinition) ? (
<RichTextV2FieldDisplay />
) : isFieldActor(fieldDefinition) ? (
<ActorFieldDisplay />
) : isFieldArray(fieldDefinition) ? (

View File

@ -29,7 +29,9 @@ import { RecordForSelect } from '@/object-record/relation-picker/types/RecordFor
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
import { isFieldArrayValue } from '@/object-record/record-field/types/guards/isFieldArrayValue';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2';
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
@ -118,6 +120,10 @@ export const usePersistField = () => {
isFieldRichText(fieldDefinition) &&
isFieldRichTextValue(valueToPersist);
const fieldIsRichTextV2 =
isFieldRichTextV2(fieldDefinition) &&
isFieldRichTextV2Value(valueToPersist);
const fieldIsArray =
isFieldArray(fieldDefinition) && isFieldArrayValue(valueToPersist);
@ -139,7 +145,8 @@ export const usePersistField = () => {
fieldIsAddress ||
fieldIsRawJson ||
fieldIsArray ||
fieldIsRichText;
fieldIsRichText ||
fieldIsRichTextV2;
if (isValuePersistable) {
const fieldName = fieldDefinition.metadata.fieldName;

View File

@ -0,0 +1,16 @@
import { useRichTextV2FieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRichTextV2FieldDisplay';
import { getFirstNonEmptyLineOfRichText } from '@/ui/input/editor/utils/getFirstNonEmptyLineOfRichText';
import { PartialBlock } from '@blocknote/core';
import { parseJson } from '~/utils/parseJson';
export const RichTextV2FieldDisplay = () => {
const { fieldValue } = useRichTextV2FieldDisplay();
const blocks = parseJson<PartialBlock[]>(fieldValue?.blocknote);
return (
<div>
<span>{getFirstNonEmptyLineOfRichText(blocks)}</span>
</div>
);
};

View File

@ -0,0 +1,73 @@
import { useContext } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
import {
FieldRichTextV2Value,
FieldRichTextValue,
} from '@/object-record/record-field/types/FieldMetadata';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2';
import { PartialBlock } from '@blocknote/core';
import { isNonEmptyString } from '@sniptt/guards';
import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
export const useRichTextV2Field = () => {
const { recordId, fieldDefinition, hotkeyScope, maxWidth } =
useContext(FieldContext);
assertFieldMetadata(
FieldMetadataType.RICH_TEXT_V2,
isFieldRichTextV2,
fieldDefinition,
);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldRichTextValue>(
recordStoreFamilySelector({
recordId,
fieldName: fieldName,
}),
);
const fieldRichTextV2Value = isFieldRichTextV2Value(fieldValue)
? fieldValue
: ({ blocknote: null, markdown: null } as FieldRichTextV2Value);
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldRichTextValue>(`${recordId}-${fieldName}`);
const draftValue = useRecoilValue(getDraftValueSelector());
const draftValueParsed: PartialBlock[] = isNonEmptyString(draftValue)
? JSON.parse(draftValue)
: draftValue;
const persistField = usePersistField();
const persistRichTextField = (nextValue: PartialBlock[]) => {
if (!nextValue) {
persistField(null);
} else {
const parsedValueToPersist = JSON.stringify(nextValue);
persistField(parsedValueToPersist);
}
};
return {
draftValue: draftValueParsed,
setDraftValue,
maxWidth,
fieldDefinition,
fieldValue: fieldRichTextV2Value,
setFieldValue,
hotkeyScope,
persistRichTextField,
};
};

View File

@ -0,0 +1,32 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldRichTextV2Value } from '@/object-record/record-field/types/FieldMetadata';
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '../../contexts/FieldContext';
export const useRichTextV2FieldDisplay = () => {
const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata(
FieldMetadataType.RICH_TEXT_V2,
isFieldRichTextV2,
fieldDefinition,
);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldRichTextV2Value | undefined>(
recordId,
fieldName,
);
return {
fieldDefinition,
fieldValue,
hotkeyScope,
};
};

View File

@ -128,6 +128,12 @@ export type FieldRawJsonMetadata = {
settings?: null;
};
export type FieldRichTextV2Metadata = {
objectMetadataNameSingular?: string;
fieldName: string;
settings?: null;
};
export type FieldRichTextMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
@ -212,7 +218,9 @@ export type FieldMetadata =
| FieldAddressMetadata
| FieldActorMetadata
| FieldArrayMetadata
| FieldTsVectorMetadata;
| FieldTsVectorMetadata
| FieldRichTextV2Metadata
| FieldRichTextMetadata;
export type FieldTextValue = string;
export type FieldUUidValue = string; // TODO: can we replace with a template literal type, or maybe overkill ?
@ -264,6 +272,11 @@ export type FieldRelationValue<
export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[];
export type FieldJsonValue = Record<string, Json> | Json[] | null;
export type FieldRichTextV2Value = {
blocknote: string | null;
markdown: string | null;
};
export type FieldRichTextValue = null | string;
export type FieldActorValue = {

View File

@ -23,6 +23,7 @@ import {
FieldRawJsonMetadata,
FieldRelationMetadata,
FieldRichTextMetadata,
FieldRichTextV2Metadata,
FieldSelectMetadata,
FieldTextMetadata,
FieldUuidMetadata,
@ -68,15 +69,17 @@ type AssertFieldMetadataFunction = <
? FieldAddressMetadata
: E extends 'RAW_JSON'
? FieldRawJsonMetadata
: E extends 'RICH_TEXT'
? FieldRichTextMetadata
: E extends 'ACTOR'
? FieldActorMetadata
: E extends 'ARRAY'
? FieldArrayMetadata
: E extends 'PHONES'
? FieldPhonesMetadata
: never,
: E extends 'RICH_TEXT_V2'
? FieldRichTextV2Metadata
: E extends 'RICH_TEXT'
? FieldRichTextMetadata
: E extends 'ACTOR'
? FieldActorMetadata
: E extends 'ARRAY'
? FieldArrayMetadata
: E extends 'PHONES'
? FieldPhonesMetadata
: never,
>(
fieldType: E,
fieldTypeGuard: (

View File

@ -0,0 +1,9 @@
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldRichTextV2Metadata } from '../FieldMetadata';
export const isFieldRichTextV2 = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldRichTextV2Metadata> =>
field.type === FieldMetadataType.RICH_TEXT_V2;

View File

@ -2,7 +2,7 @@ import { z } from 'zod';
import { FieldRichTextValue } from '../FieldMetadata';
export const richTextSchema: z.ZodType<FieldRichTextValue> = z.union([
z.null(), // Exclude literal values other than null
z.null(),
z.string(),
]);

View File

@ -0,0 +1,12 @@
import { FieldRichTextV2Value } from '@/object-record/record-field/types/FieldMetadata';
import { z } from 'zod';
export const richTextV2Schema: z.ZodType<FieldRichTextV2Value> = z.object({
blocknote: z.string().nullable(),
markdown: z.string().nullable(),
});
export const isFieldRichTextV2Value = (
fieldValue: unknown,
): fieldValue is FieldRichTextV2Value =>
richTextV2Schema.safeParse(fieldValue).success;

View File

@ -29,6 +29,8 @@ import { isFieldRating } from '@/object-record/record-field/types/guards/isField
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
@ -142,6 +144,14 @@ export const isFieldValueEmpty = ({
return false;
}
if (isFieldRichTextV2(fieldDefinition)) {
return (
!isFieldRichTextV2Value(fieldValue) ||
(isValueEmpty(fieldValue?.blocknote) &&
isValueEmpty(fieldValue?.markdown))
);
}
throw new Error(
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
);

View File

@ -2,6 +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';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -49,7 +50,9 @@ export const isFieldValueReadOnly = ({
if (
isDefined(fieldType) &&
(isFieldActor({ type: fieldType }) || isFieldRichText({ type: fieldType }))
(isFieldActor({ type: fieldType }) ||
isFieldRichText({ type: fieldType }) ||
isFieldRichTextV2({ type: fieldType }))
) {
return true;
}

View File

@ -0,0 +1,25 @@
import { RichTextV2Filter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import escapeRegExp from 'lodash.escaperegexp';
export const isMatchingRichTextV2Filter = ({
richTextV2Filter,
value,
}: {
richTextV2Filter: RichTextV2Filter;
value: string;
}) => {
switch (true) {
case richTextV2Filter.markdown !== undefined: {
const escapedPattern = escapeRegExp(richTextV2Filter.markdown.ilike);
const regexPattern = escapedPattern.replace(/%/g, '.*');
const regexCaseInsensitive = new RegExp(`^${regexPattern}$`, 'i');
return regexCaseInsensitive.test(value);
}
default: {
throw new Error(
`Unexpected value for RICH_TEXT_V2 filter : ${JSON.stringify(richTextV2Filter)}`,
);
}
}
};

View File

@ -21,6 +21,7 @@ import {
RatingFilter,
RawJsonFilter,
RecordGqlOperationFilter,
RichTextV2Filter,
SelectFilter,
StringFilter,
UUIDFilter,
@ -33,6 +34,7 @@ import { isMatchingFloatFilter } from '@/object-record/record-filter/utils/isMat
import { isMatchingMultiSelectFilter } from '@/object-record/record-filter/utils/isMatchingMultiSelectFilter';
import { isMatchingRatingFilter } from '@/object-record/record-filter/utils/isMatchingRatingFilter';
import { isMatchingRawJsonFilter } from '@/object-record/record-filter/utils/isMatchingRawJsonFilter';
import { isMatchingRichTextV2Filter } from '@/object-record/record-filter/utils/isMatchingRichTextV2Filter';
import { isMatchingSelectFilter } from '@/object-record/record-filter/utils/isMatchingSelectFilter';
import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter';
import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter';
@ -199,6 +201,12 @@ export const isRecordMatchingFilter = ({
value: record[filterKey],
});
}
case FieldMetadataType.RICH_TEXT_V2: {
return isMatchingRichTextV2Filter({
richTextV2Filter: filterValue as RichTextV2Filter,
value: record[filterKey],
});
}
case FieldMetadataType.SELECT:
return isMatchingSelectFilter({
selectFilter: filterValue as SelectFilter,

View File

@ -5,6 +5,7 @@ import {
FieldFullNameValue,
FieldLinksValue,
FieldPhonesValue,
FieldRichTextV2Value,
} from '@/object-record/record-field/types/FieldMetadata';
import { CompositeFieldLabels } from '@/object-record/spreadsheet-import/types/CompositeFieldLabels';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -39,6 +40,10 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = {
primaryPhoneCountryCodeLabel: 'Phone country code',
primaryPhoneNumberLabel: 'Phone number',
} satisfies Partial<CompositeFieldLabels<FieldPhonesValue>>,
[FieldMetadataType.RICH_TEXT_V2]: {
blocknoteLabel: 'BlockNote',
markdownLabel: 'Markdown',
} satisfies Partial<CompositeFieldLabels<FieldRichTextV2Value>>,
[FieldMetadataType.ACTOR]: {
sourceLabel: 'Source',
},

View File

@ -28,6 +28,7 @@ export const useBuildAvailableFieldsForImport = () => {
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${firstNameLabel} (${fieldMetadataItem.label})`,
@ -41,6 +42,7 @@ export const useBuildAvailableFieldsForImport = () => {
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${lastNameLabel} (${fieldMetadataItem.label})`,
@ -54,6 +56,7 @@ export const useBuildAvailableFieldsForImport = () => {
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label + ' (ID)',
@ -70,6 +73,7 @@ export const useBuildAvailableFieldsForImport = () => {
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${currencyCodeLabel} (${fieldMetadataItem.label})`,
@ -83,6 +87,7 @@ export const useBuildAvailableFieldsForImport = () => {
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
FieldMetadataType.NUMBER,
`${amountMicrosLabel} (${fieldMetadataItem.label})`,
@ -99,6 +104,7 @@ export const useBuildAvailableFieldsForImport = () => {
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
@ -117,6 +123,7 @@ export const useBuildAvailableFieldsForImport = () => {
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
@ -138,6 +145,7 @@ export const useBuildAvailableFieldsForImport = () => {
color: option.color,
})) || [],
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label + ' (ID)',
@ -157,6 +165,7 @@ export const useBuildAvailableFieldsForImport = () => {
color: option.color,
})) || [],
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label + ' (ID)',
@ -170,6 +179,7 @@ export const useBuildAvailableFieldsForImport = () => {
fieldType: {
type: 'checkbox',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label,
@ -186,6 +196,7 @@ export const useBuildAvailableFieldsForImport = () => {
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
@ -204,6 +215,7 @@ export const useBuildAvailableFieldsForImport = () => {
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
@ -211,6 +223,20 @@ export const useBuildAvailableFieldsForImport = () => {
),
});
});
} else if (fieldMetadataItem.type === FieldMetadataType.RICH_TEXT_V2) {
Object.entries(
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.RICH_TEXT_V2],
).forEach(([_, fieldLabel]) => {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${fieldLabel} (${fieldMetadataItem.label})`,
key: `${fieldLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
});
});
} else {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
@ -219,6 +245,7 @@ export const useBuildAvailableFieldsForImport = () => {
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label,

View File

@ -3,11 +3,12 @@ import {
SpreadsheetImportFieldType,
} from '@/spreadsheet-import/types';
import { IconComponent } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export type AvailableFieldForImport = {
icon: IconComponent;
label: string;
key: string;
fieldType: SpreadsheetImportFieldType;
fieldValidationDefinitions?: FieldValidationDefinition[];
fieldMetadataType: FieldMetadataType;
};

View File

@ -4,6 +4,7 @@ import {
FieldEmailsValue,
FieldLinksValue,
FieldPhonesValue,
FieldRichTextV2Value,
} from '@/object-record/record-field/types/FieldMetadata';
import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
@ -36,6 +37,7 @@ export const buildRecordFromImportedStructuredRow = (
LINKS: { primaryLinkUrlLabel },
EMAILS: { primaryEmailLabel },
PHONES: { primaryPhoneNumberLabel, primaryPhoneCountryCodeLabel },
RICH_TEXT_V2: { blocknoteLabel, markdownLabel },
} = COMPOSITE_FIELD_IMPORT_LABELS;
for (const field of fields) {
@ -158,6 +160,24 @@ export const buildRecordFromImportedStructuredRow = (
}
break;
}
case FieldMetadataType.RICH_TEXT_V2: {
if (
isDefined(
importedStructuredRow[`${blocknoteLabel} (${field.name})`] ||
importedStructuredRow[`${markdownLabel} (${field.name})`],
)
) {
recordToBuild[field.name] = {
blocknote: castToString(
importedStructuredRow[`${blocknoteLabel} (${field.name})`],
),
markdown: castToString(
importedStructuredRow[`${markdownLabel} (${field.name})`],
),
} satisfies FieldRichTextV2Value;
}
break;
}
case FieldMetadataType.EMAILS: {
if (
isDefined(

View File

@ -84,6 +84,12 @@ export const generateEmptyFieldValue = (
case FieldMetadataType.RICH_TEXT: {
return null;
}
case FieldMetadataType.RICH_TEXT_V2: {
return {
blocknote: null,
markdown: null,
};
}
case FieldMetadataType.ACTOR: {
return {
source: 'MANUAL',