From 0d023e5e7735f896a089d367c5e2504a489c2ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Wed, 1 May 2024 14:56:55 +0200 Subject: [PATCH] feat: update links field (#5212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #5113 --------- Co-authored-by: Jérémy Magrin --- .../utils/mapFieldMetadataToGraphQLQuery.ts | 78 ++++++++++++------- .../input/components/LinksFieldInput.tsx | 6 ++ .../types/FieldInputDraftValue.ts | 2 +- .../record-field/types/FieldMetadata.ts | 2 +- .../types/guards/isFieldLinksValue.ts | 4 +- .../graphql-types/scalars/index.ts | 4 +- .../graphql-types/scalars/json.scalar.ts | 14 ---- .../graphql-types/scalars/raw-json.scalar.ts | 74 ++++++++++++++++++ .../services/type-mapper.service.ts | 4 +- .../composite-types/links.composite-type.ts | 2 +- .../dtos/default-value.input.ts | 12 +-- 11 files changed, 146 insertions(+), 56 deletions(-) delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/raw-json.scalar.ts diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts index 34e80fe1f8f7..983741c6cbb4 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -2,7 +2,10 @@ import { isUndefined } from '@sniptt/guards'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { + FieldMetadataType, + RelationMetadataType, +} from '~/generated-metadata/graphql'; import { FieldMetadataItem } from '../types/FieldMetadataItem'; @@ -23,29 +26,29 @@ export const mapFieldMetadataToGraphQLQuery = ({ }): any => { const fieldType = field.type; - const fieldIsSimpleValue = ( - [ - 'UUID', - 'TEXT', - 'PHONE', - 'DATE_TIME', - 'DATE', - 'EMAIL', - 'NUMBER', - 'BOOLEAN', - 'RATING', - 'SELECT', - 'MULTI_SELECT', - 'POSITION', - 'RAW_JSON', - ] as FieldMetadataType[] - ).includes(fieldType); + const fieldIsSimpleValue = [ + FieldMetadataType.Uuid, + FieldMetadataType.Text, + FieldMetadataType.Phone, + FieldMetadataType.DateTime, + FieldMetadataType.Date, + FieldMetadataType.Email, + FieldMetadataType.Number, + FieldMetadataType.Boolean, + FieldMetadataType.Rating, + FieldMetadataType.Select, + FieldMetadataType.MultiSelect, + FieldMetadataType.Position, + FieldMetadataType.RawJson, + ].includes(fieldType); if (fieldIsSimpleValue) { return field.name; - } else if ( - fieldType === 'RELATION' && - field.toRelationMetadata?.relationType === 'ONE_TO_MANY' + } + + if ( + fieldType === FieldMetadataType.Relation && + field.toRelationMetadata?.relationType === RelationMetadataType.OneToMany ) { const relationMetadataItem = objectMetadataItems.find( (objectMetadataItem) => @@ -65,9 +68,11 @@ ${mapObjectMetadataToGraphQLQuery({ computeReferences: computeReferences, isRootLevel: false, })}`; - } else if ( - fieldType === 'RELATION' && - field.fromRelationMetadata?.relationType === 'ONE_TO_MANY' + } + + if ( + fieldType === FieldMetadataType.Relation && + field.fromRelationMetadata?.relationType === RelationMetadataType.OneToMany ) { const relationMetadataItem = objectMetadataItems.find( (objectMetadataItem) => @@ -91,26 +96,43 @@ ${mapObjectMetadataToGraphQLQuery({ })} } }`; - } else if (fieldType === 'LINK') { + } + + if (fieldType === FieldMetadataType.Link) { return `${field.name} { label url }`; - } else if (fieldType === 'CURRENCY') { + } + + if (fieldType === FieldMetadataType.Links) { + return `${field.name} +{ + primaryLinkUrl + primaryLinkLabel + secondaryLinks +}`; + } + + if (fieldType === FieldMetadataType.Currency) { return `${field.name} { amountMicros currencyCode } `; - } else if (fieldType === 'FULL_NAME') { + } + + if (fieldType === FieldMetadataType.FullName) { return `${field.name} { firstName lastName }`; - } else if (fieldType === 'ADDRESS') { + } + + if (fieldType === FieldMetadataType.Address) { return `${field.name} { addressStreet1 diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx index 0f4a449dad49..26dd57f20d2a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx @@ -27,6 +27,7 @@ export const LinksFieldInput = ({ persistLinksField({ primaryLinkUrl: url, primaryLinkLabel: '', + secondaryLinks: [], }), ); }; @@ -36,6 +37,7 @@ export const LinksFieldInput = ({ persistLinksField({ primaryLinkUrl: url, primaryLinkLabel: '', + secondaryLinks: [], }), ); }; @@ -45,6 +47,7 @@ export const LinksFieldInput = ({ persistLinksField({ primaryLinkUrl: url, primaryLinkLabel: '', + secondaryLinks: [], }), ); }; @@ -54,6 +57,7 @@ export const LinksFieldInput = ({ persistLinksField({ primaryLinkUrl: url, primaryLinkLabel: '', + secondaryLinks: [], }), ); }; @@ -63,6 +67,7 @@ export const LinksFieldInput = ({ persistLinksField({ primaryLinkUrl: url, primaryLinkLabel: '', + secondaryLinks: [], }), ); }; @@ -71,6 +76,7 @@ export const LinksFieldInput = ({ setDraftValue({ primaryLinkUrl: url, primaryLinkLabel: '', + secondaryLinks: [], }); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts index c4a60d98d5fa..7cee957d5224 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts @@ -30,7 +30,7 @@ export type FieldLinkDraftValue = { url: string; label: string }; export type FieldLinksDraftValue = { primaryLinkLabel: string; primaryLinkUrl: string; - secondaryLinks?: string | null; + secondaryLinks?: { label: string; url: string }[] | null; }; export type FieldCurrencyDraftValue = { currencyCode: CurrencyCode; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index 2a4d1d5f8b99..2e83a5bff645 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -151,7 +151,7 @@ export type FieldLinkValue = { url: string; label: string }; export type FieldLinksValue = { primaryLinkLabel: string; primaryLinkUrl: string; - secondaryLinks?: string | null; + secondaryLinks?: { label: string; url: string }[] | null; }; export type FieldCurrencyValue = { currencyCode: CurrencyCode; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldLinksValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldLinksValue.ts index c2d8f2db409d..6f519b676cda 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldLinksValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldLinksValue.ts @@ -7,7 +7,9 @@ import { FieldLinksValue } from '../FieldMetadata'; export const linksSchema = z.object({ primaryLinkLabel: z.string(), primaryLinkUrl: absoluteUrlSchema, - secondaryLinks: z.string().optional().nullable(), + secondaryLinks: z + .array(z.object({ label: z.string(), url: absoluteUrlSchema })) + .nullable(), }) satisfies z.ZodType; export const isFieldLinksValue = ( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts index b6c4f05a8474..071e99b1dba3 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts @@ -1,4 +1,4 @@ -import { JsonScalarType } from './json.scalar'; +import { RawJSONScalar } from './raw-json.scalar'; import { PositionScalarType } from './position.scalar'; import { CursorScalarType } from './cursor.scalar'; import { BigFloatScalarType } from './big-float.scalar'; @@ -25,5 +25,5 @@ export const scalars = [ UUIDScalarType, CursorScalarType, PositionScalarType, - JsonScalarType, + RawJSONScalar, ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar.ts deleted file mode 100644 index 1f03304e715c..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { isString } from 'class-validator'; -import { GraphQLScalarType } from 'graphql'; -import GraphQLJSON from 'graphql-type-json'; - -export const JsonScalarType = new GraphQLScalarType({ - ...GraphQLJSON, - parseValue: (value) => { - if (isString(value) && isString(JSON.parse(value))) { - throw new Error(`Strings are not supported as JSON: ${value}`); - } - - return GraphQLJSON.parseValue(value); - }, -}); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/raw-json.scalar.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/raw-json.scalar.ts new file mode 100644 index 000000000000..92b502f0500c --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/raw-json.scalar.ts @@ -0,0 +1,74 @@ +import { GraphQLScalarType } from 'graphql'; +import { Maybe } from 'graphql-yoga'; +import { ObjMap } from 'graphql/jsutils/ObjMap'; +import { ASTNode, Kind, ValueNode } from 'graphql/language'; + +const parseLiteral = ( + ast: ValueNode, + variables?: Maybe>, +): any => { + switch (ast.kind) { + case Kind.STRING: + case Kind.BOOLEAN: + return ast.value; + case Kind.INT: + case Kind.FLOAT: + return parseFloat(ast.value); + case Kind.OBJECT: + return parseObject(ast as any, variables); + case Kind.LIST: + return (ast as any).values.map((n: ValueNode) => + parseLiteral(n, variables), + ); + case Kind.NULL: + return null; + case Kind.VARIABLE: + return variables ? variables[ast.name.value] : undefined; + default: + throw new TypeError( + `JSONStringify cannot represent value: ${JSON.stringify(ast)}`, + ); + } +}; + +const parseObject = ( + ast: ASTNode, + variables?: Maybe>, +): object => { + const value = Object.create(null); + + if ('fields' in ast) { + ast.fields?.forEach((field: any) => { + value[field.name.value] = parseLiteral(field.value, variables); + }); + } + + return value; +}; + +const stringify = (value: any): string => { + return JSON.stringify(value); +}; + +const parseJSON = (value: string): object => { + try { + return JSON.parse(value); + } catch (e) { + throw new TypeError(`Value is not valid JSON: ${value}`); + } +}; + +export const RawJSONScalar = new GraphQLScalarType({ + name: 'RawJSONScalar', + description: + 'The `RawJSONScalar` scalar type represents JSON values, but stringifies inputs and parses outputs.', + serialize: parseJSON, + parseValue: stringify, + parseLiteral: (ast, variables) => { + if (ast.kind === Kind.STRING) { + return stringify(ast.value); + } else { + return stringify(parseLiteral(ast, variables)); + } + }, +}); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts index 4ff99c7c2b89..7ab60a4fd599 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts @@ -34,7 +34,7 @@ import { UUIDScalarType, } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { PositionScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/position.scalar'; -import { JsonScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar'; +import { RawJSONScalar } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/raw-json.scalar'; import { IDFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/id-filter.input-type'; export interface TypeOptions { @@ -76,7 +76,7 @@ export class TypeMapperService { [FieldMetadataType.NUMERIC, BigFloatScalarType], [FieldMetadataType.PROBABILITY, GraphQLFloat], [FieldMetadataType.POSITION, PositionScalarType], - [FieldMetadataType.RAW_JSON, JsonScalarType], + [FieldMetadataType.RAW_JSON, RawJSONScalar], ]); return typeScalarMapping.get(fieldMetadataType); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/links.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/links.composite-type.ts index e04affd1687e..2238e2175847 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/links.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/links.composite-type.ts @@ -29,5 +29,5 @@ export const linksCompositeType: CompositeType = { export type LinksMetadata = { primaryLinkLabel: string; primaryLinkUrl: string; - secondaryLinks: JSON | null; + secondaryLinks: object | null; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts index 1fcc000f3fc3..39f6f7556458 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts @@ -2,10 +2,10 @@ import { IsArray, IsBoolean, IsDate, - IsJSON, IsNotEmpty, IsNumber, IsNumberString, + IsObject, IsString, Matches, ValidateIf, @@ -28,9 +28,9 @@ export class FieldMetadataDefaultValueString { } export class FieldMetadataDefaultValueRawJson { - @ValidateIf((object, value) => value !== null) - @IsJSON() - value: JSON | null; + @ValidateIf((_object, value) => value !== null) + @IsObject() + value: object | null; } export class FieldMetadataDefaultValueNumber { @@ -149,6 +149,6 @@ export class FieldMetadataDefaultValueLinks { primaryLinkUrl: string | null; @ValidateIf((_object, value) => value !== null) - @IsJSON() - secondaryLinks: JSON | null; + @IsObject() + secondaryLinks: object | null; }