diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index f3429607c2ef..596c19f4e2cd 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -192,6 +192,7 @@ export enum FieldMetadataType { Number = 'NUMBER', Numeric = 'NUMERIC', Phone = 'PHONE', + Position = 'POSITION', Probability = 'PROBABILITY', Rating = 'RATING', Relation = 'RELATION', diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 8b43fd60a380..9080adeca703 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -168,6 +168,7 @@ export enum FieldMetadataType { Number = 'NUMBER', Numeric = 'NUMERIC', Phone = 'PHONE', + Position = 'POSITION', Probability = 'PROBABILITY', Rating = 'RATING', Relation = 'RELATION', diff --git a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts index 1275379afd0d..e62507484366 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts @@ -32,6 +32,7 @@ export const generateEmptyFieldValue = ( } case FieldMetadataType.Number: case FieldMetadataType.Rating: + case FieldMetadataType.Position: case FieldMetadataType.Numeric: { return null; } diff --git a/packages/twenty-server/src/metadata/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/metadata/field-metadata/field-metadata.entity.ts index 6def6bad7b6d..762725a71f09 100644 --- a/packages/twenty-server/src/metadata/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/metadata/field-metadata/field-metadata.entity.ts @@ -35,6 +35,7 @@ export enum FieldMetadataType { SELECT = 'SELECT', MULTI_SELECT = 'MULTI_SELECT', RELATION = 'RELATION', + POSITION = 'POSITION', } @Entity('fieldMetadata') diff --git a/packages/twenty-server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts b/packages/twenty-server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts index b86064271f78..da192d86b729 100644 --- a/packages/twenty-server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts +++ b/packages/twenty-server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts @@ -41,6 +41,7 @@ type FieldMetadataDefaultValueMapping = { | FieldMetadataDynamicDefaultValueNow; [FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean; [FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber; + [FieldMetadataType.POSITION]: FieldMetadataDefaultValueNumber; [FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString; [FieldMetadataType.PROBABILITY]: FieldMetadataDefaultValueNumber; [FieldMetadataType.LINK]: FieldMetadataDefaultValueLink; diff --git a/packages/twenty-server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts b/packages/twenty-server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts index 78abf2ef0592..2ef171dd4d2b 100644 --- a/packages/twenty-server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts +++ b/packages/twenty-server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts @@ -34,6 +34,7 @@ export function generateTargetColumnMap( case FieldMetadataType.RATING: case FieldMetadataType.SELECT: case FieldMetadataType.MULTI_SELECT: + case FieldMetadataType.POSITION: return { value: columnName, }; diff --git a/packages/twenty-server/src/metadata/workspace-migration/factories/basic-column-action.factory.ts b/packages/twenty-server/src/metadata/workspace-migration/factories/basic-column-action.factory.ts index 46d515adbccc..8681c191b292 100644 --- a/packages/twenty-server/src/metadata/workspace-migration/factories/basic-column-action.factory.ts +++ b/packages/twenty-server/src/metadata/workspace-migration/factories/basic-column-action.factory.ts @@ -23,6 +23,7 @@ export type BasicFieldMetadataType = | FieldMetadataType.NUMBER | FieldMetadataType.PROBABILITY | FieldMetadataType.BOOLEAN + | FieldMetadataType.POSITION | FieldMetadataType.DATE_TIME; @Injectable() diff --git a/packages/twenty-server/src/metadata/workspace-migration/utils/field-metadata-type-to-column-type.util.ts b/packages/twenty-server/src/metadata/workspace-migration/utils/field-metadata-type-to-column-type.util.ts index 85647ae57424..ddee422a6853 100644 --- a/packages/twenty-server/src/metadata/workspace-migration/utils/field-metadata-type-to-column-type.util.ts +++ b/packages/twenty-server/src/metadata/workspace-migration/utils/field-metadata-type-to-column-type.util.ts @@ -19,6 +19,7 @@ export const fieldMetadataTypeToColumnType = ( return 'numeric'; case FieldMetadataType.NUMBER: case FieldMetadataType.PROBABILITY: + case FieldMetadataType.POSITION: return 'float'; case FieldMetadataType.BOOLEAN: return 'boolean'; diff --git a/packages/twenty-server/src/metadata/workspace-migration/workspace-migration.factory.ts b/packages/twenty-server/src/metadata/workspace-migration/workspace-migration.factory.ts index 8d2aa7ba8099..ccab76b29c70 100644 --- a/packages/twenty-server/src/metadata/workspace-migration/workspace-migration.factory.ts +++ b/packages/twenty-server/src/metadata/workspace-migration/workspace-migration.factory.ts @@ -66,6 +66,7 @@ export class WorkspaceMigrationFactory { ], [FieldMetadataType.NUMERIC, { factory: this.basicColumnActionFactory }], [FieldMetadataType.NUMBER, { factory: this.basicColumnActionFactory }], + [FieldMetadataType.POSITION, { factory: this.basicColumnActionFactory }], [ FieldMetadataType.PROBABILITY, { factory: this.basicColumnActionFactory }, diff --git a/packages/twenty-server/src/workspace/workspace-schema-builder/graphql-types/scalars/index.ts b/packages/twenty-server/src/workspace/workspace-schema-builder/graphql-types/scalars/index.ts index fa9b1f0f4a9d..32b0466f2060 100644 --- a/packages/twenty-server/src/workspace/workspace-schema-builder/graphql-types/scalars/index.ts +++ b/packages/twenty-server/src/workspace/workspace-schema-builder/graphql-types/scalars/index.ts @@ -1,3 +1,4 @@ +import { PositionScalarType } from './position.scalar'; import { CursorScalarType } from './cursor.scalar'; import { BigFloatScalarType } from './big-float.scalar'; import { BigIntScalarType } from './big-int.scalar'; @@ -22,4 +23,5 @@ export const scalars = [ TimeScalarType, UUIDScalarType, CursorScalarType, + PositionScalarType, ]; diff --git a/packages/twenty-server/src/workspace/workspace-schema-builder/graphql-types/scalars/position.scalar.ts b/packages/twenty-server/src/workspace/workspace-schema-builder/graphql-types/scalars/position.scalar.ts new file mode 100644 index 000000000000..4b5f9521f5f7 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-schema-builder/graphql-types/scalars/position.scalar.ts @@ -0,0 +1,38 @@ +import { GraphQLScalarType, Kind } from 'graphql'; + +type PositionType = 'first' | 'last' | number; + +const isValidStringPosition = (value: string): boolean => + typeof value === 'string' && (value === 'first' || value === 'last'); + +const isValidNumberPosition = (value: number): boolean => + typeof value === 'number' && value >= 0; + +const checkPosition = (value: any): PositionType => { + if (isValidNumberPosition(value) || isValidStringPosition(value)) { + return value; + } + throw new Error('Invalid position found'); +}; + +export const PositionScalarType = new GraphQLScalarType({ + name: 'Position', + description: + 'A custom scalar type for representing record position in a list', + serialize: checkPosition, + parseValue: checkPosition, + parseLiteral(ast): PositionType { + if ( + ast.kind == Kind.STRING && + (ast.value === 'first' || ast.value === 'last') + ) { + return ast.value; + } + + if (ast.kind == Kind.INT || ast.kind == Kind.FLOAT) { + return parseFloat(ast.value); + } + + throw new Error('Invalid position found'); + }, +}); diff --git a/packages/twenty-server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts index b54194317eb4..3ccc352768e4 100644 --- a/packages/twenty-server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts +++ b/packages/twenty-server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts @@ -34,6 +34,7 @@ import { } from 'src/workspace/workspace-schema-builder/graphql-types/input'; import { OrderByDirectionType } from 'src/workspace/workspace-schema-builder/graphql-types/enum'; import { BigFloatScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars'; +import { PositionScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars/position.scalar'; export interface TypeOptions { nullable?: boolean; @@ -66,6 +67,7 @@ export class TypeMapperService { [FieldMetadataType.NUMERIC, BigFloatScalarType], [FieldMetadataType.PROBABILITY, GraphQLFloat], [FieldMetadataType.RELATION, GraphQLID], + [FieldMetadataType.POSITION, PositionScalarType], ]); return typeScalarMapping.get(fieldMetadataType); @@ -96,6 +98,7 @@ export class TypeMapperService { [FieldMetadataType.NUMERIC, BigFloatFilterType], [FieldMetadataType.PROBABILITY, FloatFilterType], [FieldMetadataType.RELATION, UUIDFilterType], + [FieldMetadataType.POSITION, FloatFilterType], ]); return typeFilterMapping.get(fieldMetadataType); @@ -118,6 +121,7 @@ export class TypeMapperService { [FieldMetadataType.RATING, OrderByDirectionType], [FieldMetadataType.SELECT, OrderByDirectionType], [FieldMetadataType.MULTI_SELECT, OrderByDirectionType], + [FieldMetadataType.POSITION, OrderByDirectionType], ]); return typeOrderByMapping.get(fieldMetadataType);