mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Deprecate legacy RICH_TEXT field metadata type (#18623)
## Summary - Removes the deprecated `RICH_TEXT` (V1) field metadata type from the codebase entirely - Adds a 1.20 upgrade command that migrates existing `RICH_TEXT` fields to `TEXT` in `core.fieldMetadata` - Cleans up ~70 files across `twenty-shared`, `twenty-server`, `twenty-front`, `twenty-sdk`, and `twenty-zapier` ## Context `RICH_TEXT` was a legacy field type that stored rich text as a single `text` column. It was already **read-only** — writes threw errors directing users to `RICH_TEXT_V2` instead. `RICH_TEXT_V2` is the current approach: a composite type with `blocknote` (editor JSON) and `markdown` subfields. Keeping the deprecated type added maintenance burden without any value. Since the underlying database column type for `RICH_TEXT` was already `text` (same as `TEXT`), the migration only needs to update the metadata — no data migration or column changes required. ## Changes ### Upgrade command (new) - `1-20-migrate-rich-text-to-text.command.ts` — runs `UPDATE core."fieldMetadata" SET "type" = 'TEXT' WHERE "type" = 'RICH_TEXT'` per workspace, with cache invalidation ### Enum & shared types - Removed `RICH_TEXT` from `FieldMetadataType` enum - Removed from `FieldMetadataDefaultValueMapping`, `isFieldMetadataTextKind` ### Server (~30 files) - Removed from type mapper (scalar, filter, order-by), data processors, input transformer, filter operators, zod schemas, column type mapping, searchable fields, RLS matching, OpenAPI schema, fake value generators - Removed from field creation flow and field metadata type validator - Updated dev seeder Pet `bio` field to `TEXT` - Cleaned up mocks, snapshots, integration tests ### Frontend (~25 files) - Deleted: `RichTextFieldDisplay`, `isFieldRichText`, `isFieldRichTextValue`, `useRichTextFieldDisplay` - Removed from `FieldDisplay`, `usePersistField`, `isFieldValueEmpty`, `isRecordMatchingFilter`, `generateEmptyFieldValue`, `isFieldCellSupported`, spreadsheet import, workflow fake values - Removed from settings types, field type configs, and field creation exclusion list - Updated tests, mocks, and stories ### SDK & Zapier - Removed from generated GraphQL schema and TypeScript types - Removed from Zapier `computeInputFields`
This commit is contained in:
parent
49bdcd6bd5
commit
46e515436e
71 changed files with 165 additions and 368 deletions
|
|
@ -1825,7 +1825,6 @@ export enum FieldMetadataType {
|
|||
RATING = 'RATING',
|
||||
RAW_JSON = 'RAW_JSON',
|
||||
RELATION = 'RELATION',
|
||||
RICH_TEXT = 'RICH_TEXT',
|
||||
RICH_TEXT_V2 = 'RICH_TEXT_V2',
|
||||
SELECT = 'SELECT',
|
||||
TEXT = 'TEXT',
|
||||
|
|
|
|||
|
|
@ -96,12 +96,12 @@ describe('filterSortableFieldMetadataItems', () => {
|
|||
expect(filterSortableFieldMetadataItems(field)).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT allow unsortable field types like RICH_TEXT', () => {
|
||||
it('should NOT allow unsortable field types like RICH_TEXT_V2', () => {
|
||||
const field = {
|
||||
type: FieldMetadataType.RICH_TEXT,
|
||||
type: FieldMetadataType.RICH_TEXT_V2,
|
||||
isSystem: false,
|
||||
isActive: true,
|
||||
name: 'richText',
|
||||
name: 'richTextV2',
|
||||
};
|
||||
|
||||
expect(filterSortableFieldMetadataItems(field)).toBe(false);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export const isNonCompositeField = (type: FieldType) => {
|
|||
FieldMetadataType.MULTI_SELECT,
|
||||
FieldMetadataType.POSITION,
|
||||
FieldMetadataType.RAW_JSON,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.ARRAY,
|
||||
].includes(type as any);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { LinksFieldDisplay } from '@/object-record/record-field/ui/meta-types/di
|
|||
import { PhonesFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/PhonesFieldDisplay';
|
||||
import { RatingFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/RatingFieldDisplay';
|
||||
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/RelationFromManyFieldDisplay';
|
||||
import { RichTextFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/RichTextFieldDisplay';
|
||||
import { RichTextV2FieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/RichTextV2FieldDisplay';
|
||||
import { isFieldIdentifierDisplay } from '@/object-record/record-field/ui/meta-types/display/utils/isFieldIdentifierDisplay';
|
||||
import { isFieldActor } from '@/object-record/record-field/ui/types/guards/isFieldActor';
|
||||
|
|
@ -21,7 +20,6 @@ import { isFieldFiles } from '@/object-record/record-field/ui/types/guards/isFie
|
|||
import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks';
|
||||
import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones';
|
||||
import { isFieldRating } from '@/object-record/record-field/ui/types/guards/isFieldRating';
|
||||
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
|
||||
import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2';
|
||||
|
||||
import { MorphRelationManyToOneFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/MorphRelationManyToOneFieldDisplay';
|
||||
|
|
@ -112,8 +110,6 @@ export const FieldDisplay = () => {
|
|||
<BooleanFieldDisplay />
|
||||
) : isFieldRating(fieldDefinition) ? (
|
||||
<RatingFieldDisplay readonly={isRecordFieldReadOnly} />
|
||||
) : isFieldRichText(fieldDefinition) ? (
|
||||
<RichTextFieldDisplay />
|
||||
) : isFieldRichTextV2(fieldDefinition) ? (
|
||||
<RichTextV2FieldDisplay />
|
||||
) : isFieldActor(fieldDefinition) ? (
|
||||
|
|
|
|||
|
|
@ -49,9 +49,7 @@ import { isFieldRating } from '@/object-record/record-field/ui/types/guards/isFi
|
|||
import { isFieldRatingValue } from '@/object-record/record-field/ui/types/guards/isFieldRatingValue';
|
||||
import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
|
||||
import { isFieldRelationManyToOneValue } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOneValue';
|
||||
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
|
||||
import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2';
|
||||
import { isFieldRichTextValue } from '@/object-record/record-field/ui/types/guards/isFieldRichTextValue';
|
||||
import { isFieldRichTextV2Value } from '@/object-record/record-field/ui/types/guards/isFieldRichTextValueV2';
|
||||
import { isFieldText } from '@/object-record/record-field/ui/types/guards/isFieldText';
|
||||
import { isFieldTextValue } from '@/object-record/record-field/ui/types/guards/isFieldTextValue';
|
||||
|
|
@ -145,10 +143,6 @@ export const usePersistField = ({
|
|||
const fieldIsRawJson =
|
||||
isFieldRawJson(fieldDefinition) && isFieldRawJsonValue(valueToPersist);
|
||||
|
||||
const fieldIsRichText =
|
||||
isFieldRichText(fieldDefinition) &&
|
||||
isFieldRichTextValue(valueToPersist);
|
||||
|
||||
const fieldIsRichTextV2 =
|
||||
isFieldRichTextV2(fieldDefinition) &&
|
||||
isFieldRichTextV2Value(valueToPersist);
|
||||
|
|
@ -185,7 +179,6 @@ export const usePersistField = ({
|
|||
fieldIsRawJson ||
|
||||
fieldIsArray ||
|
||||
fieldIsFiles ||
|
||||
fieldIsRichText ||
|
||||
fieldIsRichTextV2;
|
||||
|
||||
if (isValuePersistable) {
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
import { useRichTextFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useRichTextFieldDisplay';
|
||||
import { getFirstNonEmptyLineOfRichText } from '@/blocknote-editor/utils/getFirstNonEmptyLineOfRichText';
|
||||
|
||||
export const RichTextFieldDisplay = () => {
|
||||
const { fieldValue } = useRichTextFieldDisplay();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>{getFirstNonEmptyLineOfRichText(fieldValue)}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { useContext } from 'react';
|
||||
|
||||
import { type FieldRichTextValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
|
||||
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
|
||||
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
|
||||
import type { PartialBlock } from '@blocknote/core';
|
||||
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
|
||||
export const useRichTextFieldDisplay = () => {
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
isFieldRichText,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const fieldValue = useRecordFieldValue<FieldRichTextValue | undefined>(
|
||||
recordId,
|
||||
fieldName,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
const fieldValueParsed = isDefined(fieldValue)
|
||||
? parseJson<PartialBlock[]>(fieldValue)
|
||||
: null;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue: fieldValueParsed,
|
||||
};
|
||||
};
|
||||
|
|
@ -65,7 +65,7 @@ const RichTextFieldInputWithContext = () => {
|
|||
fieldDefinition: {
|
||||
fieldMetadataId: 'richText',
|
||||
label: 'Rich Text',
|
||||
type: FieldMetadataType.RICH_TEXT,
|
||||
type: FieldMetadataType.RICH_TEXT_V2,
|
||||
iconName: 'IconRichText',
|
||||
metadata: {
|
||||
fieldName: 'richText',
|
||||
|
|
|
|||
|
|
@ -135,10 +135,6 @@ export type FieldRichTextV2Metadata = BaseFieldMetadata & {
|
|||
settings?: null;
|
||||
};
|
||||
|
||||
export type FieldRichTextMetadata = BaseFieldMetadata & {
|
||||
settings?: null;
|
||||
};
|
||||
|
||||
export type FieldPositionMetadata = BaseFieldMetadata & {
|
||||
settings?: null;
|
||||
};
|
||||
|
|
@ -229,8 +225,7 @@ export type FieldMetadata =
|
|||
| FieldArrayMetadata
|
||||
| FieldTsVectorMetadata
|
||||
| FieldRawJsonMetadata
|
||||
| FieldRichTextV2Metadata
|
||||
| FieldRichTextMetadata;
|
||||
| FieldRichTextV2Metadata;
|
||||
|
||||
export type FieldTextValue = string;
|
||||
export type FieldUUidValue = string; // TODO: can we replace with a template literal type, or maybe overkill ?
|
||||
|
|
@ -289,8 +284,6 @@ export type FieldRichTextV2Value = {
|
|||
markdown: string | null;
|
||||
};
|
||||
|
||||
export type FieldRichTextValue = null | string;
|
||||
|
||||
const FieldActorSourceSchema = z.union([
|
||||
z.literal('API'),
|
||||
z.literal('IMPORT'),
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import {
|
|||
type FieldRatingMetadata,
|
||||
type FieldRawJsonMetadata,
|
||||
type FieldRelationMetadata,
|
||||
type FieldRichTextMetadata,
|
||||
type FieldRichTextV2Metadata,
|
||||
type FieldSelectMetadata,
|
||||
type FieldTextMetadata,
|
||||
|
|
@ -77,15 +76,13 @@ type AssertFieldMetadataFunction = <
|
|||
? FieldRawJsonMetadata
|
||||
: E extends 'RICH_TEXT_V2'
|
||||
? FieldRichTextV2Metadata
|
||||
: E extends 'RICH_TEXT'
|
||||
? FieldRichTextMetadata
|
||||
: E extends 'ACTOR'
|
||||
? FieldActorMetadata
|
||||
: E extends 'ARRAY'
|
||||
? FieldArrayMetadata
|
||||
: E extends 'PHONES'
|
||||
? FieldPhonesMetadata
|
||||
: never,
|
||||
: E extends 'ACTOR'
|
||||
? FieldActorMetadata
|
||||
: E extends 'ARRAY'
|
||||
? FieldArrayMetadata
|
||||
: E extends 'PHONES'
|
||||
? FieldPhonesMetadata
|
||||
: never,
|
||||
>(
|
||||
fieldType: E,
|
||||
fieldTypeGuard: (
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import {
|
||||
type FieldMetadata,
|
||||
type FieldRichTextMetadata,
|
||||
} from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
export const isFieldRichText = (
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
|
||||
): field is FieldDefinition<FieldRichTextMetadata> =>
|
||||
field.type === FieldMetadataType.RICH_TEXT;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
import { type FieldRichTextValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
export const richTextSchema: z.ZodType<FieldRichTextValue> = z.union([
|
||||
z.null(),
|
||||
z.string(),
|
||||
]);
|
||||
|
||||
export const isFieldRichTextValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldRichTextValue =>
|
||||
richTextSchema.safeParse(fieldValue).success;
|
||||
|
|
@ -32,7 +32,6 @@ import { isFieldPosition } from '@/object-record/record-field/ui/types/guards/is
|
|||
import { isFieldRating } from '@/object-record/record-field/ui/types/guards/isFieldRating';
|
||||
import { isFieldRawJson } from '@/object-record/record-field/ui/types/guards/isFieldRawJson';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
|
||||
import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2';
|
||||
import { isFieldRichTextV2Value } from '@/object-record/record-field/ui/types/guards/isFieldRichTextValueV2';
|
||||
import { isFieldSelect } from '@/object-record/record-field/ui/types/guards/isFieldSelect';
|
||||
|
|
@ -63,7 +62,6 @@ export const isFieldValueEmpty = ({
|
|||
isFieldRating(fieldDefinition) ||
|
||||
isFieldBoolean(fieldDefinition) ||
|
||||
isFieldRawJson(fieldDefinition) ||
|
||||
isFieldRichText(fieldDefinition) ||
|
||||
isFieldPosition(fieldDefinition)
|
||||
) {
|
||||
return isValueEmpty(fieldValue);
|
||||
|
|
|
|||
|
|
@ -235,15 +235,6 @@ export const isRecordMatchingFilter = ({
|
|||
value: record[filterKey],
|
||||
});
|
||||
}
|
||||
case FieldMetadataType.RICH_TEXT: {
|
||||
// TODO: Implement a better rich text filter once it becomes a composite field
|
||||
// See this issue for more context: https://github.com/twentyhq/twenty/issues/7613#issuecomment-2408944585
|
||||
// This should be tackled in Q4'24
|
||||
return isMatchingStringFilter({
|
||||
stringFilter: filterValue as StringFilter,
|
||||
value: record[filterKey],
|
||||
});
|
||||
}
|
||||
case FieldMetadataType.RICH_TEXT_V2: {
|
||||
return isMatchingRichTextV2Filter({
|
||||
richTextV2Filter: filterValue as RichTextV2Filter,
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ export const useBuildSpreadsheetImportFields = () => {
|
|||
case FieldMetadataType.MORPH_RELATION:
|
||||
case FieldMetadataType.ACTOR:
|
||||
case FieldMetadataType.TS_VECTOR:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
return [];
|
||||
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -377,7 +377,6 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||
case FieldMetadataType.FILES:
|
||||
case FieldMetadataType.MORPH_RELATION:
|
||||
case FieldMetadataType.POSITION:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
case FieldMetadataType.TS_VECTOR:
|
||||
break;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -13,11 +13,9 @@ export const spreadsheetImportFilterAvailableFieldMetadataItems = (
|
|||
(!isHiddenSystemField(fieldMetadataItem) ||
|
||||
fieldMetadataItem.name === 'id') &&
|
||||
fieldMetadataItem.name !== 'deletedAt' &&
|
||||
(![
|
||||
FieldMetadataType.RELATION,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.ACTOR,
|
||||
].includes(fieldMetadataItem.type) ||
|
||||
(![FieldMetadataType.RELATION, FieldMetadataType.ACTOR].includes(
|
||||
fieldMetadataItem.type,
|
||||
) ||
|
||||
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE),
|
||||
)
|
||||
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
|
||||
|
|
|
|||
|
|
@ -95,9 +95,6 @@ export const generateEmptyFieldValue = ({
|
|||
case FieldMetadataType.RAW_JSON: {
|
||||
return null;
|
||||
}
|
||||
case FieldMetadataType.RICH_TEXT: {
|
||||
return null;
|
||||
}
|
||||
case FieldMetadataType.RICH_TEXT_V2: {
|
||||
return {
|
||||
blocknote: null,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,7 @@ export const isFieldCellSupported = (
|
|||
fieldMetadataItem: FieldMetadataItem,
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
) => {
|
||||
if (
|
||||
[FieldMetadataType.POSITION, FieldMetadataType.RICH_TEXT].includes(
|
||||
fieldMetadataItem.type,
|
||||
)
|
||||
) {
|
||||
if (fieldMetadataItem.type === FieldMetadataType.POSITION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ import { type PickLiteral } from '~/types/PickLiteral';
|
|||
|
||||
export type SettingsExcludedFieldType = PickLiteral<
|
||||
FieldType,
|
||||
'POSITION' | 'TS_VECTOR' | 'RICH_TEXT' | 'RICH_TEXT_V2' | 'NUMERIC'
|
||||
'POSITION' | 'TS_VECTOR' | 'RICH_TEXT_V2' | 'NUMERIC'
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -51,15 +51,6 @@ describe('getSortIconForFieldType', () => {
|
|||
).toBe(IconSortDescendingLetters);
|
||||
});
|
||||
|
||||
it('should return IconSortAscendingLetters for RICH_TEXT field ascending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.RICH_TEXT,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscendingLetters);
|
||||
});
|
||||
|
||||
it('should return IconSortAscendingLetters for RICH_TEXT_V2 field ascending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
|
|
|
|||
|
|
@ -141,15 +141,6 @@ describe('generateFakeValue', () => {
|
|||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should generate RICH_TEXT value', () => {
|
||||
const result = generateFakeValue(
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
'FieldMetadataType',
|
||||
);
|
||||
|
||||
expect(result).toBe('My rich text');
|
||||
});
|
||||
|
||||
it('should generate UUID value', () => {
|
||||
const result = generateFakeValue(
|
||||
FieldMetadataType.UUID,
|
||||
|
|
|
|||
|
|
@ -68,8 +68,6 @@ const generateFieldMetadataTypeValue = (
|
|||
return 'Tim Cook';
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
return null;
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
return 'My rich text';
|
||||
case FieldMetadataType.UUID:
|
||||
return '123e4567-e89b-12d3-a456-426614174000';
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ export const DEFAULT_ICONS_BY_FIELD_TYPE: Record<FieldMetadataType, string> = {
|
|||
[FieldMetadataType.ACTOR]: 'IconUsers',
|
||||
[FieldMetadataType.NUMERIC]: 'IconUsers',
|
||||
[FieldMetadataType.POSITION]: 'IconUsers',
|
||||
[FieldMetadataType.RICH_TEXT]: 'IconUsers',
|
||||
[FieldMetadataType.RICH_TEXT_V2]: 'IconUsers',
|
||||
[FieldMetadataType.TS_VECTOR]: 'IconUsers',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ export const SettingsObjectNewFieldSelect = () => {
|
|||
const excludedFieldTypes: FieldType[] = (
|
||||
[
|
||||
FieldMetadataType.NUMERIC,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
FieldMetadataType.ACTOR,
|
||||
FieldMetadataType.UUID,
|
||||
|
|
|
|||
|
|
@ -13456,7 +13456,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||
"__typename": "Field",
|
||||
"id": "7700d2ac-0643-44a2-acd0-f0bb446bc08c",
|
||||
"universalIdentifier": "107ed3e9-2ae0-43ef-a2d6-3f24bc54c2a5",
|
||||
"type": "RICH_TEXT",
|
||||
"type": "TEXT",
|
||||
"name": "bio",
|
||||
"label": "Bio",
|
||||
"description": "",
|
||||
|
|
|
|||
|
|
@ -374,7 +374,6 @@ enum FieldMetadataType {
|
|||
RATING
|
||||
RAW_JSON
|
||||
RELATION
|
||||
RICH_TEXT
|
||||
RICH_TEXT_V2
|
||||
SELECT
|
||||
TEXT
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ export interface Field {
|
|||
|
||||
|
||||
/** Type of the field */
|
||||
export type FieldMetadataType = 'ACTOR' | 'ADDRESS' | 'ARRAY' | 'BOOLEAN' | 'CURRENCY' | 'DATE' | 'DATE_TIME' | 'EMAILS' | 'FILES' | 'FULL_NAME' | 'LINKS' | 'MORPH_RELATION' | 'MULTI_SELECT' | 'NUMBER' | 'NUMERIC' | 'PHONES' | 'POSITION' | 'RATING' | 'RAW_JSON' | 'RELATION' | 'RICH_TEXT' | 'RICH_TEXT_V2' | 'SELECT' | 'TEXT' | 'TS_VECTOR' | 'UUID'
|
||||
export type FieldMetadataType = 'ACTOR' | 'ADDRESS' | 'ARRAY' | 'BOOLEAN' | 'CURRENCY' | 'DATE' | 'DATE_TIME' | 'EMAILS' | 'FILES' | 'FULL_NAME' | 'LINKS' | 'MORPH_RELATION' | 'MULTI_SELECT' | 'NUMBER' | 'NUMERIC' | 'PHONES' | 'POSITION' | 'RATING' | 'RAW_JSON' | 'RELATION' | 'RICH_TEXT_V2' | 'SELECT' | 'TEXT' | 'TS_VECTOR' | 'UUID'
|
||||
|
||||
export interface IndexField {
|
||||
id: Scalars['UUID']
|
||||
|
|
@ -8290,7 +8290,6 @@ export const enumFieldMetadataType = {
|
|||
RATING: 'RATING' as const,
|
||||
RAW_JSON: 'RAW_JSON' as const,
|
||||
RELATION: 'RELATION' as const,
|
||||
RICH_TEXT: 'RICH_TEXT' as const,
|
||||
RICH_TEXT_V2: 'RICH_TEXT_V2' as const,
|
||||
SELECT: 'SELECT' as const,
|
||||
TEXT: 'TEXT' as const,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { type WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:migrate-rich-text-to-text',
|
||||
description:
|
||||
'Migrate deprecated RICH_TEXT field metadata type to TEXT. The underlying column type is already text, so only the metadata needs updating.',
|
||||
})
|
||||
export class MigrateRichTextToTextCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const dryRun = options?.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? '[DRY RUN] ' : ''}Migrating RICH_TEXT fields to TEXT in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would update RICH_TEXT -> TEXT in core.fieldMetadata for workspace ${workspaceId}. Skipping.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
|
||||
try {
|
||||
const result = await queryRunner.query(
|
||||
`UPDATE core."fieldMetadata"
|
||||
SET "type" = 'TEXT'
|
||||
WHERE "workspaceId" = $1
|
||||
AND "type" = 'RICH_TEXT'
|
||||
RETURNING "id"`,
|
||||
[workspaceId],
|
||||
);
|
||||
|
||||
const updatedCount = result.length;
|
||||
|
||||
if (updatedCount > 0) {
|
||||
this.logger.log(
|
||||
`Migrated ${updatedCount} RICH_TEXT field(s) to TEXT in workspace ${workspaceId}`,
|
||||
);
|
||||
await this.invalidateCaches(workspaceId);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`No RICH_TEXT fields found in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
private async invalidateCaches(workspaceId: string): Promise<void> {
|
||||
const modifiedMetadataNames = ['fieldMetadata'] as const;
|
||||
|
||||
const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [
|
||||
...new Set(
|
||||
modifiedMetadataNames
|
||||
.flatMap((name) => [name, ...getMetadataRelatedMetadataNames(name)])
|
||||
.map(getMetadataFlatEntityMapsKey),
|
||||
),
|
||||
'ORMEntityMetadatas',
|
||||
];
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(
|
||||
workspaceId,
|
||||
cacheKeysToInvalidate,
|
||||
);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspaceCacheStorageService.flush(workspaceId);
|
||||
|
||||
this.logger.log(
|
||||
`Cache invalidated and metadata version incremented for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
|
||||
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
|
||||
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command';
|
||||
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
|
@ -25,7 +26,15 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
|||
WorkspaceMigrationModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [BackfillCommandMenuItemsCommand, BackfillPageLayoutsCommand],
|
||||
exports: [BackfillCommandMenuItemsCommand, BackfillPageLayoutsCommand],
|
||||
providers: [
|
||||
BackfillCommandMenuItemsCommand,
|
||||
BackfillPageLayoutsCommand,
|
||||
MigrateRichTextToTextCommand,
|
||||
],
|
||||
exports: [
|
||||
BackfillCommandMenuItemsCommand,
|
||||
BackfillPageLayoutsCommand,
|
||||
MigrateRichTextToTextCommand,
|
||||
],
|
||||
})
|
||||
export class V1_20_UpgradeVersionCommandModule {}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { BackfillMissingStandardViewsCommand } from 'src/database/commands/upgra
|
|||
import { SeedServerIdCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-seed-server-id.command';
|
||||
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
|
||||
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command';
|
||||
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
|
|
@ -87,6 +88,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
|||
// 1.20 Commands
|
||||
protected readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand,
|
||||
protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand,
|
||||
protected readonly migrateRichTextToTextCommand: MigrateRichTextToTextCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
|
|
@ -133,6 +135,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
|||
];
|
||||
|
||||
const commands_1200: VersionCommands = [
|
||||
this.migrateRichTextToTextCommand,
|
||||
this.backfillCommandMenuItemsCommand,
|
||||
this.backfillPageLayoutsCommand,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -335,20 +335,6 @@ export const fieldRawJsonMock = getMockFieldMetadataEntity({
|
|||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
export const fieldRichTextMock = getMockFieldMetadataEntity({
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
id: 'fieldRichTextId',
|
||||
name: 'fieldRichText',
|
||||
type: FieldMetadataType.RICH_TEXT,
|
||||
label: 'Field Rich Text',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
export const fieldActorMock = getMockFieldMetadataEntity({
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
|
|
@ -436,7 +422,6 @@ const FIELDS_MOCK = [
|
|||
fieldPositionMock,
|
||||
fieldAddressMock,
|
||||
fieldRawJsonMock,
|
||||
fieldRichTextMock,
|
||||
fieldActorMock,
|
||||
fieldArrayMock,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -172,8 +172,6 @@ exports[`DataArgProcessorService failing inputs validation RELATION should throw
|
|||
|
||||
exports[`DataArgProcessorService failing inputs validation RELATION should throw for invalid input #5: "non-uuid" 1`] = `"Invalid UUID value 'non-uuid' for field "manyToOneRelationFieldId""`;
|
||||
|
||||
exports[`DataArgProcessorService failing inputs validation RICH_TEXT should throw for invalid input #1: "test" 1`] = `"richTextField RICH_TEXT-typed field does not support write operations"`;
|
||||
|
||||
exports[`DataArgProcessorService failing inputs validation RICH_TEXT_V2 should throw for invalid input #1: "not-a-rich-text" 1`] = `"Invalid rich text v2 value 'not-a-rich-text' for field "richTextV2Field" - Should be an object"`;
|
||||
|
||||
exports[`DataArgProcessorService failing inputs validation RICH_TEXT_V2 should throw for invalid input #2: 1 1`] = `"Invalid rich text v2 value 1 for field "richTextV2Field" - Should be an object"`;
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ export const failingInputsByFieldMetadataType: {
|
|||
{ input: { booleanField: 'string' } },
|
||||
{ input: { booleanField: 1 } },
|
||||
],
|
||||
[FieldMetadataType.RICH_TEXT]: [{ input: { richTextField: 'test' } }],
|
||||
[FieldMetadataType.ADDRESS]: [
|
||||
{ input: { addressField: 'not-an-address' } },
|
||||
{ input: { addressField: 1 } },
|
||||
|
|
|
|||
|
|
@ -139,11 +139,6 @@ export const fieldMetadataConfigByFieldName: Record<
|
|||
type: FieldMetadataType.RICH_TEXT_V2,
|
||||
isNullable: true,
|
||||
},
|
||||
richTextField: {
|
||||
name: 'richTextField',
|
||||
type: FieldMetadataType.RICH_TEXT,
|
||||
isNullable: true,
|
||||
},
|
||||
position: {
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
|
|
|
|||
|
|
@ -312,7 +312,6 @@ export class DataArgProcessorService {
|
|||
|
||||
return transformLinksValue(validatedValue);
|
||||
}
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
case FieldMetadataType.TS_VECTOR:
|
||||
throw new CommonQueryRunnerException(
|
||||
`${key} ${fieldMetadata.type}-typed field does not support write operations`,
|
||||
|
|
|
|||
|
|
@ -140,10 +140,6 @@ exports[`FilterArgProcessorService failing filter inputs validation RELATION sho
|
|||
|
||||
exports[`FilterArgProcessorService failing filter inputs validation RELATION should throw for invalid filter #3: {"manyToOneRelationFieldId":{}} 1`] = `"Filter for field "manyToOneRelationFieldId" must have exactly one operator"`;
|
||||
|
||||
exports[`FilterArgProcessorService failing filter inputs validation RICH_TEXT should throw for invalid filter #1: {"richTextField":{"invalidOperator":"test"}} 1`] = `"Operator "invalidOperator" is not valid for field "richTextField" of type RICH_TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`;
|
||||
|
||||
exports[`FilterArgProcessorService failing filter inputs validation RICH_TEXT should throw for invalid filter #2: {"richTextField":{}} 1`] = `"Filter for field "richTextField" must have exactly one operator"`;
|
||||
|
||||
exports[`FilterArgProcessorService failing filter inputs validation RICH_TEXT_V2 should throw for invalid filter #1: {"richTextV2Field":{"invalidOperator":"test"}} 1`] = `"Sub field "invalidOperator" not found for composite type: RICH_TEXT_V2"`;
|
||||
|
||||
exports[`FilterArgProcessorService failing filter inputs validation RICH_TEXT_V2 should throw for invalid filter #2: {"richTextV2Field":{"markdown":{"invalidOperator":"test"}}} 1`] = `"Operator "invalidOperator" is not valid for field "richTextV2Field.markdown" of type TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`;
|
||||
|
|
|
|||
|
|
@ -11,10 +11,6 @@ export const failingFilterInputsByFieldMetadataType: {
|
|||
{ filter: { textField: { eq: 'test', neq: 'test' } } },
|
||||
{ filter: { textField: {} } },
|
||||
],
|
||||
[FieldMetadataType.RICH_TEXT]: [
|
||||
{ filter: { richTextField: { invalidOperator: 'test' } } },
|
||||
{ filter: { richTextField: {} } },
|
||||
],
|
||||
[FieldMetadataType.NUMBER]: [
|
||||
{ filter: { numberField: { eq: 'not-a-number' } } },
|
||||
{ filter: { numberField: { eq: {} } } },
|
||||
|
|
|
|||
|
|
@ -21,13 +21,6 @@ export const successfulFilterInputsByFieldMetadataType: {
|
|||
{ filter: { textField: { is: 'NOT_NULL' } } },
|
||||
{ filter: { textField: { eq: null } } },
|
||||
],
|
||||
[FieldMetadataType.RICH_TEXT]: [
|
||||
{ filter: { richTextField: { eq: 'test' } } },
|
||||
{ filter: { richTextField: { like: '%test%' } } },
|
||||
{ filter: { richTextField: { ilike: '%test%' } } },
|
||||
{ filter: { richTextField: { is: 'NULL' } } },
|
||||
{ filter: { richTextField: { is: 'NOT_NULL' } } },
|
||||
],
|
||||
[FieldMetadataType.NUMBER]: [
|
||||
{ filter: { numberField: { eq: 1 } } },
|
||||
{ filter: { numberField: { neq: 1 } } },
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ export const getOperatorsForFieldType = (
|
|||
): FilterOperator[] => {
|
||||
switch (fieldType) {
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
return STRING_FILTER_OPERATORS;
|
||||
|
||||
case FieldMetadataType.NUMBER:
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ export class TypeMapperService {
|
|||
[FieldMetadataType.POSITION, PositionScalarType],
|
||||
[FieldMetadataType.RAW_JSON, GraphQLJSON],
|
||||
[FieldMetadataType.ARRAY, StringArrayScalarType],
|
||||
[FieldMetadataType.RICH_TEXT, GraphQLString],
|
||||
[FieldMetadataType.TS_VECTOR, TSVectorScalarType],
|
||||
]);
|
||||
|
||||
|
|
@ -179,7 +178,6 @@ export class TypeMapperService {
|
|||
[FieldMetadataType.POSITION, FloatFilterType],
|
||||
[FieldMetadataType.FILES, RawJsonFilterType],
|
||||
[FieldMetadataType.RAW_JSON, RawJsonFilterType],
|
||||
[FieldMetadataType.RICH_TEXT, StringFilterType],
|
||||
[FieldMetadataType.RICH_TEXT_V2, RichTextV2FilterType],
|
||||
[FieldMetadataType.ARRAY, ArrayFilterType],
|
||||
[FieldMetadataType.MULTI_SELECT, MultiSelectFilterType],
|
||||
|
|
@ -209,7 +207,6 @@ export class TypeMapperService {
|
|||
[FieldMetadataType.POSITION, OrderByDirectionType],
|
||||
[FieldMetadataType.FILES, OrderByDirectionType],
|
||||
[FieldMetadataType.RAW_JSON, OrderByDirectionType],
|
||||
[FieldMetadataType.RICH_TEXT, OrderByDirectionType],
|
||||
[FieldMetadataType.ARRAY, OrderByDirectionType],
|
||||
[FieldMetadataType.TS_VECTOR, OrderByDirectionType], // TODO: Add TSVectorOrderByType
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -295,10 +295,6 @@ describe('computeSchemaComponents', () => {
|
|||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"fieldRichText": {
|
||||
"description": "Default field metadata entity description",
|
||||
"type": "string",
|
||||
},
|
||||
"fieldSelect": {
|
||||
"description": "Default field metadata entity description",
|
||||
"enum": [
|
||||
|
|
@ -540,10 +536,6 @@ describe('computeSchemaComponents', () => {
|
|||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"fieldRichText": {
|
||||
"description": "Default field metadata entity description",
|
||||
"type": "string",
|
||||
},
|
||||
"fieldSelect": {
|
||||
"description": "Default field metadata entity description",
|
||||
"enum": [
|
||||
|
|
@ -796,10 +788,6 @@ describe('computeSchemaComponents', () => {
|
|||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"fieldRichText": {
|
||||
"description": "Default field metadata entity description",
|
||||
"type": "string",
|
||||
},
|
||||
"fieldSelect": {
|
||||
"description": "Default field metadata entity description",
|
||||
"enum": [
|
||||
|
|
|
|||
|
|
@ -124,7 +124,6 @@ export const generateRandomFieldValue = ({
|
|||
return {};
|
||||
}
|
||||
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
case FieldMetadataType.RICH_TEXT_V2: {
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ export const generateFieldFilterZodSchema = (
|
|||
.describe(`Filter by ${field.name} (UUID field)`);
|
||||
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
case FieldMetadataType.RICH_TEXT_V2:
|
||||
return z
|
||||
.object({
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ const getFieldZodType = (field: FlatFieldMetadata): z.ZodTypeAny => {
|
|||
return z.string().uuidv4();
|
||||
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
return z.string();
|
||||
|
||||
case FieldMetadataType.DATE_TIME:
|
||||
|
|
|
|||
|
|
@ -78,10 +78,6 @@ export class RecordInputTransformerService {
|
|||
return value || null;
|
||||
case FieldMetadataType.NUMBER:
|
||||
return value === null ? null : Number(value);
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
throw new Error(
|
||||
'Rich text is not supported, please use RICH_TEXT_V2 instead',
|
||||
);
|
||||
case FieldMetadataType.RICH_TEXT_V2:
|
||||
return await transformRichTextV2Value(value);
|
||||
case FieldMetadataType.LINKS:
|
||||
|
|
|
|||
|
|
@ -61,8 +61,6 @@ type PositionFieldMetadata = FieldMetadataEntity<FieldMetadataType.POSITION>;
|
|||
|
||||
type RawJsonFieldMetadata = FieldMetadataEntity<FieldMetadataType.RAW_JSON>;
|
||||
|
||||
type RichTextFieldMetadata = FieldMetadataEntity<FieldMetadataType.RICH_TEXT>;
|
||||
|
||||
type ActorFieldMetadata = FieldMetadataEntity<FieldMetadataType.ACTOR>;
|
||||
|
||||
type ArrayFieldMetadata = FieldMetadataEntity<FieldMetadataType.ARRAY>;
|
||||
|
|
@ -93,7 +91,6 @@ type RelationAssertions = [
|
|||
Expect<HasAllProperties<MultiSelectFieldMetadata, NotDefinedRelationRecord>>,
|
||||
Expect<HasAllProperties<PositionFieldMetadata, NotDefinedRelationRecord>>,
|
||||
Expect<HasAllProperties<RawJsonFieldMetadata, NotDefinedRelationRecord>>,
|
||||
Expect<HasAllProperties<RichTextFieldMetadata, NotDefinedRelationRecord>>,
|
||||
Expect<HasAllProperties<ActorFieldMetadata, NotDefinedRelationRecord>>,
|
||||
Expect<HasAllProperties<ArrayFieldMetadata, NotDefinedRelationRecord>>,
|
||||
Expect<HasAllProperties<PhonesFieldMetadata, NotDefinedRelationRecord>>,
|
||||
|
|
@ -124,7 +121,6 @@ type SettingsAssertions = [
|
|||
Expect<HasAllProperties<MultiSelectFieldMetadata, NotDefinedSettings>>,
|
||||
Expect<HasAllProperties<PositionFieldMetadata, NotDefinedSettings>>,
|
||||
Expect<HasAllProperties<RawJsonFieldMetadata, NotDefinedSettings>>,
|
||||
Expect<HasAllProperties<RichTextFieldMetadata, NotDefinedSettings>>,
|
||||
Expect<HasAllProperties<ActorFieldMetadata, NotDefinedSettings>>,
|
||||
Expect<HasAllProperties<UUIDFieldMetadata, NotDefinedSettings>>,
|
||||
Expect<HasAllProperties<BooleanFieldMetadata, NotDefinedSettings>>,
|
||||
|
|
@ -371,16 +367,6 @@ type DefaultValueAssertions = [
|
|||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
RichTextFieldMetadata,
|
||||
{
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.RICH_TEXT]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
ActorFieldMetadata,
|
||||
|
|
@ -484,7 +470,6 @@ type OptionsAssertions = [
|
|||
Expect<HasAllProperties<FullNameFieldMetadata, NotDefinedOptions>>,
|
||||
Expect<HasAllProperties<PositionFieldMetadata, NotDefinedOptions>>,
|
||||
Expect<HasAllProperties<RawJsonFieldMetadata, NotDefinedOptions>>,
|
||||
Expect<HasAllProperties<RichTextFieldMetadata, NotDefinedOptions>>,
|
||||
Expect<HasAllProperties<ActorFieldMetadata, NotDefinedOptions>>,
|
||||
Expect<HasAllProperties<ArrayFieldMetadata, NotDefinedOptions>>,
|
||||
Expect<HasAllProperties<PhonesFieldMetadata, NotDefinedOptions>>,
|
||||
|
|
|
|||
|
|
@ -742,7 +742,7 @@ export const PET_FLAT_FIELDS_MOCK = {
|
|||
bio: getFlatFieldMetadataMock({
|
||||
id: 'fa0ec409-0f29-4b19-b304-31d1018a2344',
|
||||
objectMetadataId: 'd34e0f07-1b8c-4de0-938e-599cf05e1f7f',
|
||||
type: FieldMetadataType.RICH_TEXT,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'bio',
|
||||
label: 'Bio',
|
||||
defaultValue: null,
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ export class FlatFieldMetadataTypeValidatorService {
|
|||
PHONES: DEFAULT_NO_VALIDATION,
|
||||
POSITION: validatePositionFlatFieldMetadata,
|
||||
RAW_JSON: DEFAULT_NO_VALIDATION,
|
||||
RICH_TEXT: DEFAULT_NO_VALIDATION,
|
||||
RICH_TEXT_V2: DEFAULT_NO_VALIDATION,
|
||||
TEXT: DEFAULT_NO_VALIDATION,
|
||||
TS_VECTOR: validateTsVectorFlatFieldMetadata,
|
||||
|
|
|
|||
|
|
@ -105,7 +105,6 @@ const _assertion: Record<string, AbstractFlatFieldMetadata> = {
|
|||
// JSON/Array types
|
||||
rawJson: {} as FlatFieldMetadata<FieldMetadataType.RAW_JSON>,
|
||||
array: {} as FlatFieldMetadata<FieldMetadataType.ARRAY>,
|
||||
richText: {} as FlatFieldMetadata<FieldMetadataType.RICH_TEXT>,
|
||||
richTextV2: {} as FlatFieldMetadata<FieldMetadataType.RICH_TEXT_V2>,
|
||||
|
||||
// Relation types
|
||||
|
|
|
|||
|
|
@ -183,7 +183,6 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
|
|||
case FieldMetadataType.POSITION:
|
||||
case FieldMetadataType.ADDRESS:
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
case FieldMetadataType.RICH_TEXT_V2:
|
||||
case FieldMetadataType.ACTOR:
|
||||
case FieldMetadataType.ARRAY: {
|
||||
|
|
|
|||
|
|
@ -243,15 +243,6 @@ export const isRecordMatchingRLSRowLevelPermissionPredicate = ({
|
|||
value: recordFieldValue,
|
||||
});
|
||||
}
|
||||
case FieldMetadataType.RICH_TEXT: {
|
||||
// TODO: Implement a better rich text filter once it becomes a composite field
|
||||
// See this issue for more context: https://github.com/twentyhq/twenty/issues/7613#issuecomment-2408944585
|
||||
// This should be tackled in Q4'24
|
||||
return isMatchingStringFilter({
|
||||
stringFilter: filterValue as StringFilter,
|
||||
value: recordFieldValue,
|
||||
});
|
||||
}
|
||||
case FieldMetadataType.RICH_TEXT_V2: {
|
||||
return isMatchingRichTextV2Filter({
|
||||
richTextV2Filter: filterValue as RichTextV2Filter,
|
||||
|
|
|
|||
|
|
@ -49,8 +49,7 @@ const getFieldProperties = (field: FieldMetadataEntity): SchemaObject => {
|
|||
case FieldMetadataType.UUID: {
|
||||
return { type: 'string', format: 'uuid' };
|
||||
}
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.RICH_TEXT: {
|
||||
case FieldMetadataType.TEXT: {
|
||||
return { type: 'string' };
|
||||
}
|
||||
case FieldMetadataType.DATE_TIME: {
|
||||
|
|
|
|||
|
|
@ -68,8 +68,6 @@ const generateFieldMetadataTypeValue = (
|
|||
return 'Tim Cook';
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
return null;
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
return 'My rich text';
|
||||
case FieldMetadataType.UUID:
|
||||
return '123e4567-e89b-12d3-a456-426614174000';
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export const PET_CUSTOM_FIELD_SEEDS: FieldMetadataSeed[] = [
|
|||
name: 'soundSwag',
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.RICH_TEXT,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Bio',
|
||||
name: 'bio',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ You help users manage their workspace data model by creating, updating, and orga
|
|||
- **CURRENCY**: Monetary values
|
||||
- **RATING**: Star ratings
|
||||
- **RELATION**: Links to other objects
|
||||
- **RICH_TEXT**: Formatted text content
|
||||
- **RICH_TEXT_V2**: Formatted rich text content
|
||||
|
||||
## Best Practices
|
||||
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ describe('getTsVectorColumnExpressionFromFields', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should handle rich text fields', () => {
|
||||
it('should handle text fields', () => {
|
||||
const fields = [
|
||||
{ name: 'body', type: FieldMetadataType.RICH_TEXT },
|
||||
{ name: 'body', type: FieldMetadataType.TEXT },
|
||||
] as FieldTypeAndNameMetadata[];
|
||||
const result = getTsVectorColumnExpressionFromFields(fields);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ const SEARCHABLE_FIELD_TYPES = [
|
|||
FieldMetadataType.ADDRESS,
|
||||
FieldMetadataType.LINKS,
|
||||
FieldMetadataType.PHONES,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
FieldMetadataType.UUID,
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
// 'RICH_TEXT' kept as a raw string for pre-upgrade workspaces that haven't
|
||||
// yet run the 1.20 migrate-rich-text-to-text command.
|
||||
export const isTextColumnType = (type: FieldMetadataType) => {
|
||||
return (
|
||||
type === FieldMetadataType.TEXT ||
|
||||
type === FieldMetadataType.RICH_TEXT ||
|
||||
type === FieldMetadataType.ARRAY
|
||||
type === FieldMetadataType.ARRAY ||
|
||||
(type as string) === 'RICH_TEXT'
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ export const TEST_OBJECT_GQL_FIELDS = `
|
|||
}
|
||||
rawJsonField
|
||||
arrayField
|
||||
richTextField
|
||||
richTextV2Field {
|
||||
blocknote
|
||||
markdown
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`Create input validation - RICH_TEXT Gql create input - failure RICH_TEXT - should fail with : {"richTextField":"test"} 1`] = `"richTextField RICH_TEXT-typed field does not support write operations"`;
|
||||
|
||||
exports[`Create input validation - RICH_TEXT Rest create input - failure RICH_TEXT - should fail with : {"richTextField":"test"} 1`] = `"["richTextField RICH_TEXT-typed field does not support write operations"]"`;
|
||||
|
|
@ -138,13 +138,6 @@ export const failingCreateInputByFieldMetadataType: {
|
|||
},
|
||||
},
|
||||
],
|
||||
[FieldMetadataType.RICH_TEXT]: [
|
||||
{
|
||||
input: {
|
||||
richTextField: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
[FieldMetadataType.ADDRESS]: [
|
||||
{
|
||||
input: {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
|||
export const successfulCreateInputByFieldMetadataType: {
|
||||
[K in Exclude<
|
||||
FieldMetadataTypesToTestForCreateInputValidation,
|
||||
FieldMetadataType.RICH_TEXT | FieldMetadataType.FILES // Done in files-field-sync.integration-spec.ts
|
||||
FieldMetadataType.FILES // Done in files-field-sync.integration-spec.ts
|
||||
>]: {
|
||||
input: any;
|
||||
validateInput: (record: Record<string, any>) => boolean;
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
import { failingCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant';
|
||||
import { expectGqlCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-error.util';
|
||||
import { expectRestCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-error.util';
|
||||
import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/inputs-validation/utils/destroy-many-objects-metadata';
|
||||
import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
const FIELD_METADATA_TYPE = FieldMetadataType.RICH_TEXT;
|
||||
|
||||
const failingTestCases =
|
||||
failingCreateInputByFieldMetadataType[FIELD_METADATA_TYPE];
|
||||
|
||||
describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => {
|
||||
let objectMetadataId: string;
|
||||
let objectMetadataSingularName: string;
|
||||
let objectMetadataPluralName: string;
|
||||
let targetObjectMetadata1Id: string;
|
||||
let targetObjectMetadata2Id: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const setupTest = await setupTestObjectsWithAllFieldTypes();
|
||||
|
||||
objectMetadataId = setupTest.objectMetadataId;
|
||||
objectMetadataSingularName = setupTest.objectMetadataSingularName;
|
||||
objectMetadataPluralName = setupTest.objectMetadataPluralName;
|
||||
targetObjectMetadata1Id = setupTest.targetObjectMetadata1Id;
|
||||
targetObjectMetadata2Id = setupTest.targetObjectMetadata2Id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await destroyManyObjectsMetadata([
|
||||
objectMetadataId,
|
||||
targetObjectMetadata1Id,
|
||||
targetObjectMetadata2Id,
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Gql create input - failure', () => {
|
||||
it.each(
|
||||
failingTestCases.map((testCase) => ({
|
||||
...testCase,
|
||||
stringifiedInput: JSON.stringify(testCase.input),
|
||||
})),
|
||||
)(
|
||||
`${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`,
|
||||
async ({ input }) => {
|
||||
await expectGqlCreateInputValidationError(
|
||||
objectMetadataSingularName,
|
||||
input,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Rest create input - failure', () => {
|
||||
it.each(
|
||||
failingTestCases.map((testCase) => ({
|
||||
...testCase,
|
||||
stringifiedInput: JSON.stringify(testCase.input),
|
||||
})),
|
||||
)(
|
||||
`${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`,
|
||||
async ({ input }) => {
|
||||
await expectRestCreateInputValidationError(
|
||||
objectMetadataPluralName,
|
||||
input,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,6 @@ import { type FieldMetadataType } from 'twenty-shared/types';
|
|||
|
||||
type FieldMetadataTypesNotTestedForFilterInputValidation =
|
||||
| 'TS_VECTOR'
|
||||
| 'RICH_TEXT'
|
||||
| 'POSITION'
|
||||
| 'ACTOR'
|
||||
| 'NUMERIC'
|
||||
|
|
|
|||
|
|
@ -37,12 +37,6 @@ export const getFieldMetadataCreationInputs = (
|
|||
'ACTOR' | 'POSITION'
|
||||
>]: FieldMetadataCreationInput | FieldMetadataCreationInput[];
|
||||
} = {
|
||||
[FieldMetadataType.RICH_TEXT]: {
|
||||
name: 'richTextField',
|
||||
label: 'richTextField',
|
||||
type: FieldMetadataType.RICH_TEXT,
|
||||
objectMetadataId,
|
||||
},
|
||||
[FieldMetadataType.RICH_TEXT_V2]: {
|
||||
name: 'richTextV2Field',
|
||||
label: 'richTextV2Field',
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ export type FieldMetadataDefaultValueMapping = {
|
|||
[FieldMetadataType.SELECT]: string | null;
|
||||
[FieldMetadataType.MULTI_SELECT]: string[] | null;
|
||||
[FieldMetadataType.RAW_JSON]: object | null;
|
||||
[FieldMetadataType.RICH_TEXT]: string | null;
|
||||
[FieldMetadataType.RICH_TEXT_V2]: FieldMetadataDefaultValueRichTextV2 | null;
|
||||
[FieldMetadataType.ACTOR]: FieldMetadataDefaultActor | null;
|
||||
[FieldMetadataType.ARRAY]: string[] | null;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ export enum FieldMetadataType {
|
|||
RATING = 'RATING',
|
||||
RAW_JSON = 'RAW_JSON',
|
||||
RELATION = 'RELATION',
|
||||
RICH_TEXT = 'RICH_TEXT',
|
||||
RICH_TEXT_V2 = 'RICH_TEXT_V2',
|
||||
SELECT = 'SELECT',
|
||||
TEXT = 'TEXT',
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import { FieldMetadataType } from '@/types';
|
|||
import { isFieldMetadataTextKind } from '../isFieldMetadataTextKind';
|
||||
|
||||
describe('isFieldMetadataTextKind', () => {
|
||||
it.each([
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
])('should return true for %s', (type) => {
|
||||
expect(isFieldMetadataTextKind(type)).toBe(true);
|
||||
});
|
||||
it.each([FieldMetadataType.TEXT, FieldMetadataType.RICH_TEXT_V2])(
|
||||
'should return true for %s',
|
||||
(type) => {
|
||||
expect(isFieldMetadataTextKind(type)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([FieldMetadataType.NUMBER, FieldMetadataType.SELECT])(
|
||||
'should return false for %s',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { FieldMetadataType } from '@/types';
|
|||
|
||||
const TEXT_FIELD_TYPES: FieldMetadataType[] = [
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ const getTypeFromFieldMetadataType = (
|
|||
switch (fieldMetadataType) {
|
||||
case FieldMetadataType.UUID:
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
case FieldMetadataType.ARRAY:
|
||||
case FieldMetadataType.RATING:
|
||||
return 'string';
|
||||
|
|
@ -242,7 +241,6 @@ export const computeInputFields = (
|
|||
break;
|
||||
case FieldMetadataType.UUID:
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
case FieldMetadataType.DATE_TIME:
|
||||
case FieldMetadataType.DATE:
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
|
|
|
|||
Loading…
Reference in a new issue