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

@ -1,5 +1,5 @@
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };

View File

@ -49,6 +49,10 @@ export const ActivityRichTextEditor = ({
const cache = useApolloClient().cache;
const activity = activityInStore as Task | Note | null;
const isRichTextV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsRichTextV2Enabled,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
@ -67,13 +71,20 @@ export const ActivityRichTextEditor = ({
activityObjectNameSingular: activityObjectNameSingular,
});
const persistBodyDebounced = useDebouncedCallback((newBody: string) => {
const persistBodyDebounced = useDebouncedCallback((blocknote: string) => {
const input = isRichTextV2Enabled
? {
bodyV2: {
blocknote,
markdown: null,
},
}
: { body: blocknote };
if (isDefined(activity)) {
upsertActivity({
activity,
input: {
body: newBody,
},
input,
});
}
}, 300);
@ -163,14 +174,18 @@ export const ActivityRichTextEditor = ({
};
const initialBody = useMemo(() => {
const blocknote = isRichTextV2Enabled
? activity?.bodyV2?.blocknote
: activity?.body;
if (
isDefined(activity) &&
isNonEmptyString(activity.body) &&
activity?.body !== '{}'
isNonEmptyString(blocknote) &&
blocknote !== '{}'
) {
return JSON.parse(activity.body);
return JSON.parse(blocknote);
}
}, [activity]);
}, [activity, isRichTextV2Enabled]);
const handleEditorBuiltInUploadFile = async (file: File) => {
const { attachmentAbsoluteURL } = await handleUploadAttachment(file);

View File

@ -94,7 +94,11 @@ const task = {
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
title: 'Task title',
body: null,
body: '',
bodyV2: {
blocknote: null,
markdown: null,
},
assigneeId: null,
status: null,
dueAt: '2023-04-26T10:12:42.33625+00:00',

View File

@ -6,6 +6,8 @@ import { Note } from '@/activities/types/Note';
import { getActivityPreview } from '@/activities/utils/getActivityPreview';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledCard = styled.div<{ isSingleNote: boolean }>`
align-items: flex-start;
@ -71,7 +73,12 @@ export const NoteCard = ({
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Note,
});
const body = getActivityPreview(note.body);
const isRichTextV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsRichTextV2Enabled,
);
const body = getActivityPreview(
isRichTextV2Enabled ? (note?.bodyV2?.blocknote ?? null) : note?.body,
);
const { FieldContextProvider: NoteTargetsContextProvider } = useFieldContext({
objectNameSingular: CoreObjectNameSingular.Note,

View File

@ -16,6 +16,8 @@ import { ActivityRow } from '@/activities/components/ActivityRow';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { useCompleteTask } from '../hooks/useCompleteTask';
const StyledTaskBody = styled.div`
@ -82,7 +84,14 @@ export const TaskRow = ({ task }: { task: Task }) => {
objectNameSingular: CoreObjectNameSingular.Task,
});
const body = getActivitySummary(task.body);
const isRichTextV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsRichTextV2Enabled,
);
const body = getActivitySummary(
isRichTextV2Enabled ? (task?.bodyV2?.blocknote ?? null) : task?.body,
);
const { completeTask } = useCompleteTask(task);
const { FieldContextProvider: TaskTargetsContextProvider } = useFieldContext({

View File

@ -11,6 +11,10 @@ const task: Task = {
status: 'DONE',
title: 'Test',
body: 'Test',
bodyV2: {
blocknote: 'Test',
markdown: 'Test',
},
dueAt: '2024-03-15T07:33:14.212Z',
createdAt: '2024-03-15T07:33:14.212Z',
updatedAt: '2024-03-15T07:33:14.212Z',

View File

@ -4,4 +4,8 @@ export type Activity = {
updatedAt: string;
title: string;
body: string | null;
bodyV2?: {
blocknote: string | null;
markdown: string | null;
};
};

View File

@ -10,12 +10,14 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useMultiObjectSearch } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
import { useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap } from '@/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap';
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
import isEmpty from 'lodash.isempty';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { Avatar, IconCheckbox, IconNotes } from 'twenty-ui';
import { useDebounce } from 'use-debounce';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { getLogoUrlFromDomainName } from '~/utils';
const MAX_SEARCH_RESULTS_PER_OBJECT = 8;
@ -23,6 +25,10 @@ const MAX_SEARCH_RESULTS_PER_OBJECT = 8;
export const useSearchRecords = () => {
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
const isRichTextV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsRichTextV2Enabled,
);
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300);
const {
@ -45,7 +51,13 @@ export const useSearchRecords = () => {
filter: deferredCommandMenuSearch
? makeOrFilterVariables([
{ title: { ilike: `%${deferredCommandMenuSearch}%` } },
{ body: { ilike: `%${deferredCommandMenuSearch}%` } },
isRichTextV2Enabled
? {
bodyV2: {
markdown: { ilike: `%${deferredCommandMenuSearch}%` },
},
}
: { body: { ilike: `%${deferredCommandMenuSearch}%` } },
])
: undefined,
limit: MAX_SEARCH_RESULTS_PER_OBJECT,
@ -56,7 +68,13 @@ export const useSearchRecords = () => {
filter: deferredCommandMenuSearch
? makeOrFilterVariables([
{ title: { ilike: `%${deferredCommandMenuSearch}%` } },
{ body: { ilike: `%${deferredCommandMenuSearch}%` } },
isRichTextV2Enabled
? {
bodyV2: {
markdown: { ilike: `%${deferredCommandMenuSearch}%` },
},
}
: { body: { ilike: `%${deferredCommandMenuSearch}%` } },
])
: undefined,
limit: MAX_SEARCH_RESULTS_PER_OBJECT,

View File

@ -163,5 +163,13 @@ ${mapObjectMetadataToGraphQLQuery({
}`;
}
if (fieldType === FieldMetadataType.RICH_TEXT_V2) {
return `${field.name}
{
blocknote
markdown
}`;
}
return '';
};

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',

View File

@ -7,6 +7,7 @@ import {
FieldFullNameValue,
FieldLinksValue,
FieldPhonesValue,
FieldRichTextV2Value,
} from '@/object-record/record-field/types/FieldMetadata';
import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
@ -18,6 +19,7 @@ import {
IllustrationIconMap,
IllustrationIconPhone,
IllustrationIconSetting,
IllustrationIconText,
IllustrationIconUser,
} from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -185,4 +187,19 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
context: { provider: ConnectedAccountProvider.GOOGLE },
},
} as const satisfies SettingsCompositeFieldTypeConfig<FieldActorValue>,
[FieldMetadataType.RICH_TEXT_V2]: {
label: 'Rich Text',
Icon: IllustrationIconText,
subFields: ['blocknote', 'markdown'],
filterableSubFields: [],
labelBySubField: {
blocknote: 'BlockNote',
markdown: 'Markdown',
},
exampleValue: {
blocknote: '[{"type":"heading","content":"Hello"}]',
markdown: '# Hello',
},
category: 'Basic',
} as const satisfies SettingsCompositeFieldTypeConfig<FieldRichTextV2Value>,
} as const satisfies SettingsCompositeFieldTypeConfigArray;

View File

@ -6,7 +6,6 @@ import {
IllustrationIconJson,
IllustrationIconNumbers,
IllustrationIconOneToMany,
IllustrationIconSetting,
IllustrationIconStar,
IllustrationIconTag,
IllustrationIconTags,
@ -25,7 +24,6 @@ import {
FieldNumberValue,
FieldRatingValue,
FieldRelationValue,
FieldRichTextValue,
FieldSelectValue,
FieldTextValue,
FieldUUidValue,
@ -122,12 +120,6 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel
exampleValue: { key: 'value' },
category: 'Advanced',
} as const satisfies SettingsFieldTypeConfig<FieldJsonValue>,
[FieldMetadataType.RICH_TEXT]: {
label: 'Rich Text',
Icon: IllustrationIconSetting,
exampleValue: "{ key: 'value' }",
category: 'Basic',
} as const satisfies SettingsFieldTypeConfig<FieldRichTextValue>,
[FieldMetadataType.ARRAY]: {
label: 'Array',
Icon: IllustrationIconArray,

View File

@ -7,6 +7,7 @@ import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/Setting
import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues';
import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues';
import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues';
import { FieldType } from '@/settings/data-model/types/FieldType';
import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
import { SettingsPath } from '@/types/SettingsPath';
import { TextInput } from '@/ui/input/components/TextInput';
@ -22,7 +23,7 @@ import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
type SettingsObjectNewFieldSelectorProps = {
className?: string;
excludedFieldTypes?: SettingsFieldType[];
excludedFieldTypes?: FieldType[];
fieldMetadataItem?: Pick<
FieldMetadataItem,
'defaultValue' | 'options' | 'type'

View File

@ -10,6 +10,7 @@ export const COMPOSITE_FIELD_TYPES = [
'PHONES',
'FULL_NAME',
'ACTOR',
'RICH_TEXT_V2',
] as const;
type CompositeFieldTypeBaseLiteral = (typeof COMPOSITE_FIELD_TYPES)[number];

View File

@ -1,8 +1,7 @@
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
import { PickLiteral } from '~/types/PickLiteral';
export type SettingsCompositeFieldType = PickLiteral<
export type SettingsCompositeFieldType = Extract<
SettingsFieldType,
CompositeFieldType
>;

View File

@ -3,5 +3,5 @@ import { PickLiteral } from '~/types/PickLiteral';
export type SettingsExcludedFieldType = PickLiteral<
FieldType,
'POSITION' | 'TS_VECTOR' | 'RICH_TEXT_V2'
'POSITION' | 'TS_VECTOR' | 'RICH_TEXT' | 'RICH_TEXT_V2'
>;

View File

@ -1,8 +1,7 @@
import { NonCompositeFieldType } from '@/settings/data-model/types/NonCompositeFieldType';
import { SettingsExcludedFieldType } from '@/settings/data-model/types/SettingsExcludedFieldType';
import { ExcludeLiteral } from '~/types/ExcludeLiteral';
import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
export type SettingsNonCompositeFieldType = ExcludeLiteral<
export type SettingsNonCompositeFieldType = Extract<
NonCompositeFieldType,
SettingsExcludedFieldType
SettingsFieldType
>;

View File

@ -4,6 +4,7 @@ import {
Fields,
SpreadsheetImportDialogOptions,
} from '@/spreadsheet-import/types';
import { FieldMetadataType } from 'twenty-shared';
import { sleep } from '~/utils/sleep';
const fields = [
@ -22,6 +23,7 @@ const fields = [
errorMessage: 'Name is required',
},
],
fieldMetadataType: FieldMetadataType.TEXT,
},
{
icon: null,

View File

@ -4,6 +4,9 @@ import { IconForbid } from 'twenty-ui';
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FieldMetadataType } from 'twenty-shared';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { Columns, ColumnType } from '../MatchColumnsStep';
const StyledContainer = styled.div`
@ -24,26 +27,35 @@ export const TemplateColumn = <T extends string>({
columnIndex,
onChange,
}: TemplateColumnProps<T>) => {
const isRichTextV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsRichTextV2Enabled,
);
const { fields } = useSpreadsheetImportInternal<T>();
const column = columns[columnIndex];
const isIgnored = column.type === ColumnType.ignored;
const fieldOptions = fields.map(({ icon, label, key }) => {
const isSelected =
columns.findIndex((column) => {
if ('value' in column) {
return column.value === key;
}
return false;
}) !== -1;
const fieldOptions = fields
.filter((field) =>
isRichTextV2Enabled
? field.fieldMetadataType !== FieldMetadataType.RICH_TEXT
: true,
)
.map(({ icon, label, key }) => {
const isSelected =
columns.findIndex((column) => {
if ('value' in column) {
return column.value === key;
}
return false;
}) !== -1;
return {
icon: icon,
value: key,
label: label,
disabled: isSelected,
} as const;
});
return {
icon: icon,
value: key,
label: label,
disabled: isSelected,
} as const;
});
const selectOptions = [
{

View File

@ -4,6 +4,7 @@ import { ReadonlyDeep } from 'type-fest';
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { ImportedStructuredRowMetadata } from '@/spreadsheet-import/steps/components/ValidationStep/types';
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
import { FieldMetadataType } from 'twenty-shared';
export type SpreadsheetImportDialogOptions<FieldNames extends string> = {
// Is modal visible.
@ -122,6 +123,8 @@ export type Field<T extends string> = {
fieldValidationDefinitions?: FieldValidationDefinition[];
// Field entry component, default: Input
fieldType: SpreadsheetImportFieldType;
// Field metadata type
fieldMetadataType: FieldMetadataType;
// UI-facing values shown to user as field examples pre-upload phase
example?: string;
};

View File

@ -6,6 +6,7 @@ import {
TableHook,
} from '@/spreadsheet-import/types';
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
import { FieldMetadataType } from 'twenty-shared';
describe('addErrorsAndRunHooks', () => {
type FullData = ImportedStructuredRow<'name' | 'age' | 'country'>;
@ -15,6 +16,7 @@ describe('addErrorsAndRunHooks', () => {
fieldValidationDefinitions: [{ rule: 'required' }],
icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
};
const regexField: Field<'age'> = {
@ -25,6 +27,7 @@ describe('addErrorsAndRunHooks', () => {
],
icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.NUMBER,
};
const uniqueField: Field<'country'> = {
@ -33,6 +36,7 @@ describe('addErrorsAndRunHooks', () => {
fieldValidationDefinitions: [{ rule: 'unique' }],
icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.SELECT,
};
const functionValidationFieldTrue: Field<'email'> = {
@ -47,6 +51,7 @@ describe('addErrorsAndRunHooks', () => {
],
icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.EMAILS,
};
const functionValidationFieldFalse: Field<'email'> = {
@ -61,6 +66,7 @@ describe('addErrorsAndRunHooks', () => {
],
icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.EMAILS,
};
const validData: ImportedStructuredRow<'name' | 'age'> = {

View File

@ -1,6 +1,6 @@
import { Field } from '@/spreadsheet-import/types';
import { findMatch } from '@/spreadsheet-import/utils/findMatch';
import { FieldMetadataType } from 'twenty-shared';
describe('findMatch', () => {
const defaultField: Field<'defaultField'> = {
key: 'defaultField',
@ -9,6 +9,7 @@ describe('findMatch', () => {
fieldType: {
type: 'input',
},
fieldMetadataType: FieldMetadataType.TEXT,
alternateMatches: ['Full Name', 'First Name'],
};
@ -19,6 +20,7 @@ describe('findMatch', () => {
fieldType: {
type: 'input',
},
fieldMetadataType: FieldMetadataType.TEXT,
};
const fields = [defaultField, secondaryField];

View File

@ -4,6 +4,7 @@ import {
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field, FieldValidationDefinition } from '@/spreadsheet-import/types';
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
import { FieldMetadataType } from 'twenty-shared';
const nameField: Field<'Name'> = {
key: 'Name',
@ -12,6 +13,7 @@ const nameField: Field<'Name'> = {
fieldType: {
type: 'input',
},
fieldMetadataType: FieldMetadataType.TEXT,
};
const ageField: Field<'Age'> = {
@ -21,7 +23,9 @@ const ageField: Field<'Age'> = {
fieldType: {
type: 'input',
},
fieldMetadataType: FieldMetadataType.NUMBER,
};
const validations: FieldValidationDefinition[] = [{ rule: 'required' }];
const nameFieldWithValidations: Field<'Name'> = {
...nameField,

View File

@ -1,5 +1,6 @@
import { Field } from '@/spreadsheet-import/types';
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';
import { FieldMetadataType } from 'twenty-shared';
describe('generateExampleRow', () => {
const defaultField: Field<'defaultField'> = {
@ -9,6 +10,7 @@ describe('generateExampleRow', () => {
fieldType: {
type: 'input',
},
fieldMetadataType: FieldMetadataType.TEXT,
};
it('should generate an example row from input field type', () => {
@ -24,6 +26,7 @@ describe('generateExampleRow', () => {
{
...defaultField,
fieldType: { type: 'checkbox' },
fieldMetadataType: FieldMetadataType.BOOLEAN,
},
];
@ -37,6 +40,7 @@ describe('generateExampleRow', () => {
{
...defaultField,
fieldType: { type: 'select', options: [] },
fieldMetadataType: FieldMetadataType.SELECT,
},
];
@ -50,6 +54,7 @@ describe('generateExampleRow', () => {
{
...defaultField,
example: 'Example',
fieldMetadataType: FieldMetadataType.TEXT,
},
];

View File

@ -1,5 +1,6 @@
import { Field } from '@/spreadsheet-import/types';
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
import { FieldMetadataType } from 'twenty-shared';
describe('getFieldOptions', () => {
const optionsArray = [
@ -25,6 +26,7 @@ describe('getFieldOptions', () => {
type: 'select',
options: optionsArray,
},
fieldMetadataType: FieldMetadataType.SELECT,
},
{
key: 'Name',
@ -33,6 +35,7 @@ describe('getFieldOptions', () => {
fieldType: {
type: 'input',
},
fieldMetadataType: FieldMetadataType.TEXT,
},
];

View File

@ -4,6 +4,7 @@ import {
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field } from '@/spreadsheet-import/types';
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
import { FieldMetadataType } from 'twenty-shared';
describe('getMatchedColumns', () => {
const columns: Column<string>[] = [
@ -27,15 +28,23 @@ describe('getMatchedColumns', () => {
key: 'Name',
label: 'Name',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
icon: null,
},
{
key: 'Location',
label: 'Location',
fieldType: { type: 'select', options: [] },
fieldMetadataType: FieldMetadataType.POSITION,
icon: null,
},
{
key: 'Age',
label: 'Age',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.NUMBER,
icon: null,
},
{ key: 'Age', label: 'Age', fieldType: { type: 'input' }, icon: null },
];
const data = [
@ -110,12 +119,14 @@ describe('getMatchedColumns', () => {
key: 'Hobby',
label: 'Hobby',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
icon: null,
},
{
key: 'Interest',
label: 'Interest',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
icon: null,
},
];

View File

@ -4,6 +4,7 @@ import {
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field } from '@/spreadsheet-import/types';
import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableData';
import { FieldMetadataType } from 'twenty-shared';
describe('normalizeTableData', () => {
const columns: Column<string>[] = [
@ -18,14 +19,27 @@ describe('normalizeTableData', () => {
];
const fields: Field<string>[] = [
{ key: 'name', label: 'Name', fieldType: { type: 'input' }, icon: null },
{ key: 'age', label: 'Age', fieldType: { type: 'input' }, icon: null },
{
key: 'name',
label: 'Name',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
icon: null,
},
{
key: 'age',
label: 'Age',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.NUMBER,
icon: null,
},
{
key: 'active',
label: 'Active',
fieldType: {
type: 'checkbox',
},
fieldMetadataType: FieldMetadataType.BOOLEAN,
icon: null,
},
];
@ -64,6 +78,7 @@ describe('normalizeTableData', () => {
type: 'checkbox',
booleanMatches: { yes: true, no: false },
},
fieldMetadataType: FieldMetadataType.BOOLEAN,
icon: null,
},
];
@ -100,6 +115,7 @@ describe('normalizeTableData', () => {
{ label: 'Two', value: '2' },
],
},
fieldMetadataType: FieldMetadataType.SELECT,
icon: null,
},
];

View File

@ -4,6 +4,7 @@ import {
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field } from '@/spreadsheet-import/types';
import { setColumn } from '@/spreadsheet-import/utils/setColumn';
import { FieldMetadataType } from 'twenty-shared';
describe('setColumn', () => {
const defaultField: Field<'Name'> = {
@ -11,6 +12,7 @@ describe('setColumn', () => {
label: 'label',
key: 'Name',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
};
const oldColumn: Column<'oldValue'> = {

View File

@ -2,14 +2,14 @@ import { PartialBlock } from '@blocknote/core';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const getFirstNonEmptyLineOfRichText = (
fieldValue: PartialBlock[] | null,
blocks: PartialBlock[] | null,
): string => {
if (fieldValue === null) {
if (blocks === null) {
return '';
}
for (const node of fieldValue) {
if (!isUndefinedOrNull(node.content)) {
const contentArray = node.content as Array<
for (const block of blocks) {
if (!isUndefinedOrNull(block.content)) {
const contentArray = block.content as Array<
{ text: string } | { link: string }
>;
if (contentArray.length > 0) {

View File

@ -4,6 +4,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { SettingsDataModelNewFieldBreadcrumbDropDown } from '@/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown';
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
import { SettingsObjectNewFieldSelector } from '@/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector';
import { FieldType } from '@/settings/data-model/types/FieldType';
import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath';
@ -44,10 +45,11 @@ export const SettingsObjectNewFieldSelect = () => {
type: FieldMetadataType.TEXT,
},
});
const excludedFieldTypes: SettingsFieldType[] = (
const excludedFieldTypes: FieldType[] = (
[
FieldMetadataType.NUMERIC,
FieldMetadataType.RICH_TEXT,
FieldMetadataType.RICH_TEXT_V2,
FieldMetadataType.ACTOR,
] as const
).filter(isDefined);

View File

@ -10,6 +10,10 @@ export const mockedNotes: Array<MockedNote> = [
updatedAt: '2023-04-26T10:23:42.33625+00:00',
title: 'My very first note',
body: null,
bodyV2: {
blocknote: null,
markdown: null,
},
noteTargets: [
{
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
@ -65,6 +69,10 @@ export const mockedNotes: Array<MockedNote> = [
updatedAt: new Date().toISOString(),
title: 'Another note',
body: null,
bodyV2: {
blocknote: null,
markdown: null,
},
noteTargets: [
{
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278t',

View File

@ -27,6 +27,10 @@ export const mockedTasks: Array<MockedTask> = [
updatedAt: '2023-04-26T10:23:42.33625+00:00',
title: 'My very first note',
body: null,
bodyV2: {
blocknote: null,
markdown: null,
},
dueAt: '2023-04-26T10:12:42.33625+00:00',
status: null,
assignee: workspaceMember,

View File

@ -35,7 +35,20 @@ export class ActivityQueryResultGetterHandler
return activity;
}
const blocknote: RichTextBody = JSON.parse(blocknoteJson);
let blocknote: RichTextBody = [];
try {
blocknote = JSON.parse(blocknoteJson);
} catch (error) {
blocknote = [];
// TODO: Remove this once we have removed the old rich text
// eslint-disable-next-line no-console
console.warn(
`Failed to parse body for activity ${activity.id} in workspace ${workspaceId}, for rich text version ${isRichTextV2Enabled ? 'v2' : 'v1'}`,
);
// eslint-disable-next-line no-console
console.warn(blocknoteJson);
}
const blocknoteWithSignedPayload = await Promise.all(
blocknote.map(async (block: RichTextBlock) => {