diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 202b1d9d17..b3e0aa8beb 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -529,6 +529,7 @@ export type ConnectionParametersOutput = { export type CoreView = { __typename?: 'CoreView'; anyFieldFilterValue?: Maybe; + calendarLayout?: Maybe; createdAt: Scalars['DateTime']; deletedAt?: Maybe; icon: Scalars['String']; @@ -808,6 +809,7 @@ export type CreateViewGroupInput = { export type CreateViewInput = { anyFieldFilterValue?: InputMaybe; + calendarLayout?: InputMaybe; icon: Scalars['String']; id?: InputMaybe; isCompact?: InputMaybe; @@ -1032,6 +1034,7 @@ export type FeatureFlagDto = { export enum FeatureFlagKey { IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED', IS_AI_ENABLED = 'IS_AI_ENABLED', + IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED', IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED', IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED', IS_DATABASE_EVENT_TRIGGER_ENABLED = 'IS_DATABASE_EVENT_TRIGGER_ENABLED', @@ -3654,6 +3657,7 @@ export type UpdateViewGroupInput = { export type UpdateViewInput = { anyFieldFilterValue?: InputMaybe; + calendarLayout?: InputMaybe; icon?: InputMaybe; id?: InputMaybe; isCompact?: InputMaybe; @@ -3836,6 +3840,12 @@ export type VersionInfo = { latestVersion: Scalars['String']; }; +export enum ViewCalendarLayout { + DAY = 'DAY', + MONTH = 'MONTH', + WEEK = 'WEEK' +} + export enum ViewFilterGroupLogicalOperator { AND = 'AND', NOT = 'NOT', @@ -3876,6 +3886,7 @@ export enum ViewSortDirection { } export enum ViewType { + CALENDAR = 'CALENDAR', KANBAN = 'KANBAN', TABLE = 'TABLE' } diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index b226cb6ead..7d9a51844a 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -529,6 +529,7 @@ export type ConnectionParametersOutput = { export type CoreView = { __typename?: 'CoreView'; anyFieldFilterValue?: Maybe; + calendarLayout?: Maybe; createdAt: Scalars['DateTime']; deletedAt?: Maybe; icon: Scalars['String']; @@ -772,6 +773,7 @@ export type CreateViewGroupInput = { export type CreateViewInput = { anyFieldFilterValue?: InputMaybe; + calendarLayout?: InputMaybe; icon: Scalars['String']; id?: InputMaybe; isCompact?: InputMaybe; @@ -996,6 +998,7 @@ export type FeatureFlagDto = { export enum FeatureFlagKey { IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED', IS_AI_ENABLED = 'IS_AI_ENABLED', + IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED', IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED', IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED', IS_DATABASE_EVENT_TRIGGER_ENABLED = 'IS_DATABASE_EVENT_TRIGGER_ENABLED', @@ -3492,6 +3495,7 @@ export type UpdateViewGroupInput = { export type UpdateViewInput = { anyFieldFilterValue?: InputMaybe; + calendarLayout?: InputMaybe; icon?: InputMaybe; id?: InputMaybe; isCompact?: InputMaybe; @@ -3664,6 +3668,12 @@ export type VersionInfo = { latestVersion: Scalars['String']; }; +export enum ViewCalendarLayout { + DAY = 'DAY', + MONTH = 'MONTH', + WEEK = 'WEEK' +} + export enum ViewFilterGroupLogicalOperator { AND = 'AND', NOT = 'NOT', @@ -3704,6 +3714,7 @@ export enum ViewSortDirection { } export enum ViewType { + CALENDAR = 'CALENDAR', KANBAN = 'KANBAN', TABLE = 'TABLE' } diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1757858496548-addCalendarTypeToViewTable.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1757858496548-addCalendarTypeToViewTable.ts new file mode 100644 index 0000000000..a6e7573860 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1757858496548-addCalendarTypeToViewTable.ts @@ -0,0 +1,61 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AddCalendarTypeToViewTable1757858496548 + implements MigrationInterface +{ + name = 'AddCalendarTypeToViewTable1757858496548'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "core"."view_calendarlayout_enum" AS ENUM('DAY', 'WEEK', 'MONTH')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."view" ADD "calendarLayout" "core"."view_calendarlayout_enum"`, + ); + await queryRunner.query( + `ALTER TYPE "core"."view_type_enum" RENAME TO "view_type_enum_old"`, + ); + await queryRunner.query( + `CREATE TYPE "core"."view_type_enum" AS ENUM('TABLE', 'KANBAN', 'CALENDAR')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."view" ALTER COLUMN "type" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "core"."view" ALTER COLUMN "type" TYPE "core"."view_type_enum" USING "type"::"text"::"core"."view_type_enum"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."view" ALTER COLUMN "type" SET DEFAULT 'TABLE'`, + ); + await queryRunner.query(`DROP TYPE "core"."view_type_enum_old"`); + await queryRunner.query( + `ALTER TABLE "core"."view" ADD CONSTRAINT "CHK_VIEW_CALENDAR_LAYOUT_NOT_NULL_WHEN_TYPE_CALENDAR" CHECK (("type" != 'CALENDAR' OR "calendarLayout" IS NOT NULL))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."view" DROP CONSTRAINT "CHK_VIEW_CALENDAR_LAYOUT_NOT_NULL_WHEN_TYPE_CALENDAR"`, + ); + await queryRunner.query( + `CREATE TYPE "core"."view_type_enum_old" AS ENUM('KANBAN', 'TABLE')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."view" ALTER COLUMN "type" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "core"."view" ALTER COLUMN "type" TYPE "core"."view_type_enum_old" USING "type"::"text"::"core"."view_type_enum_old"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."view" ALTER COLUMN "type" SET DEFAULT 'TABLE'`, + ); + await queryRunner.query(`DROP TYPE "core"."view_type_enum"`); + await queryRunner.query( + `ALTER TYPE "core"."view_type_enum_old" RENAME TO "view_type_enum"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."view" DROP COLUMN "calendarLayout"`, + ); + await queryRunner.query(`DROP TYPE "core"."view_calendarlayout_enum"`); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 2e537f1dc1..429f7ac7e9 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -15,4 +15,5 @@ export enum FeatureFlagKey { IS_MESSAGE_FOLDER_CONTROL_ENABLED = 'IS_MESSAGE_FOLDER_CONTROL_ENABLED', IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED', IS_DATABASE_EVENT_TRIGGER_ENABLED = 'IS_DATABASE_EVENT_TRIGGER_ENABLED', + IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED', } diff --git a/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/create-view.input.ts b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/create-view.input.ts index 0dcd1baec0..5c25972ac1 100644 --- a/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/create-view.input.ts +++ b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/create-view.input.ts @@ -2,6 +2,7 @@ import { Field, InputType } from '@nestjs/graphql'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { ViewCalendarLayout } from 'src/engine/core-modules/view/enums/view-calendar-layout.enum'; import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum'; import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in'; import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; @@ -46,4 +47,7 @@ export class CreateViewInput { @Field({ nullable: true }) anyFieldFilterValue?: string; + + @Field(() => ViewCalendarLayout, { nullable: true }) + calendarLayout?: ViewCalendarLayout; } diff --git a/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/update-view.input.ts b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/update-view.input.ts index 965936b365..1b01315315 100644 --- a/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/update-view.input.ts +++ b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/update-view.input.ts @@ -2,6 +2,7 @@ import { Field, InputType } from '@nestjs/graphql'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { ViewCalendarLayout } from 'src/engine/core-modules/view/enums/view-calendar-layout.enum'; import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in'; import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; @@ -40,4 +41,7 @@ export class UpdateViewInput { @Field({ nullable: true }) anyFieldFilterValue?: string; + + @Field(() => ViewCalendarLayout, { nullable: true }) + calendarLayout?: ViewCalendarLayout; } diff --git a/packages/twenty-server/src/engine/core-modules/view/dtos/view.dto.ts b/packages/twenty-server/src/engine/core-modules/view/dtos/view.dto.ts index d40a47930b..8bb247774a 100644 --- a/packages/twenty-server/src/engine/core-modules/view/dtos/view.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/view/dtos/view.dto.ts @@ -9,6 +9,7 @@ import { ViewFilterGroupDTO } from 'src/engine/core-modules/view/dtos/view-filte import { ViewFilterDTO } from 'src/engine/core-modules/view/dtos/view-filter.dto'; import { ViewGroupDTO } from 'src/engine/core-modules/view/dtos/view-group.dto'; import { ViewSortDTO } from 'src/engine/core-modules/view/dtos/view-sort.dto'; +import { ViewCalendarLayout } from 'src/engine/core-modules/view/enums/view-calendar-layout.enum'; import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum'; import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in'; import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; @@ -16,6 +17,7 @@ import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; registerEnumType(ViewOpenRecordIn, { name: 'ViewOpenRecordIn' }); registerEnumType(ViewType, { name: 'ViewType' }); registerEnumType(ViewKey, { name: 'ViewKey' }); +registerEnumType(ViewCalendarLayout, { name: 'ViewCalendarLayout' }); @ObjectType('CoreView') export class ViewDTO { @@ -64,6 +66,9 @@ export class ViewDTO { @Field(() => String, { nullable: true }) anyFieldFilterValue?: string | null; + @Field(() => ViewCalendarLayout, { nullable: true }) + calendarLayout: ViewCalendarLayout | null; + @Field() createdAt: Date; diff --git a/packages/twenty-server/src/engine/core-modules/view/entities/view.entity.ts b/packages/twenty-server/src/engine/core-modules/view/entities/view.entity.ts index 60e4181be9..ba9471df94 100644 --- a/packages/twenty-server/src/engine/core-modules/view/entities/view.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/view/entities/view.entity.ts @@ -1,4 +1,5 @@ import { + Check, Column, CreateDateColumn, DeleteDateColumn, @@ -20,6 +21,7 @@ import { ViewFilterGroupEntity } from 'src/engine/core-modules/view/entities/vie import { ViewFilterEntity } from 'src/engine/core-modules/view/entities/view-filter.entity'; import { ViewGroupEntity } from 'src/engine/core-modules/view/entities/view-group.entity'; import { ViewSortEntity } from 'src/engine/core-modules/view/entities/view-sort.entity'; +import { ViewCalendarLayout } from 'src/engine/core-modules/view/enums/view-calendar-layout.enum'; import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum'; import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in'; import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; @@ -31,6 +33,10 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat 'workspaceId', 'objectMetadataId', ]) +@Check( + 'CHK_VIEW_CALENDAR_LAYOUT_NOT_NULL_WHEN_TYPE_CALENDAR', + `("type" != 'CALENDAR' OR "calendarLayout" IS NOT NULL)`, +) export class ViewEntity extends SyncableEntity implements Required { @PrimaryGeneratedColumn('uuid') id: string; @@ -94,6 +100,14 @@ export class ViewEntity extends SyncableEntity implements Required { @Column({ nullable: true, type: 'uuid' }) kanbanAggregateOperationFieldMetadataId: string | null; + @Column({ + type: 'enum', + enum: Object.values(ViewCalendarLayout), + nullable: true, + default: null, + }) + calendarLayout: ViewCalendarLayout | null; + @Column({ nullable: false, type: 'uuid' }) workspaceId: string; diff --git a/packages/twenty-server/src/engine/core-modules/view/enums/view-calendar-layout.enum.ts b/packages/twenty-server/src/engine/core-modules/view/enums/view-calendar-layout.enum.ts new file mode 100644 index 0000000000..b921beada7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/enums/view-calendar-layout.enum.ts @@ -0,0 +1,5 @@ +export enum ViewCalendarLayout { + DAY = 'DAY', + WEEK = 'WEEK', + MONTH = 'MONTH', +} diff --git a/packages/twenty-server/src/engine/core-modules/view/enums/view-type.enum.ts b/packages/twenty-server/src/engine/core-modules/view/enums/view-type.enum.ts index ece12a631c..f40be54d22 100644 --- a/packages/twenty-server/src/engine/core-modules/view/enums/view-type.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/view/enums/view-type.enum.ts @@ -1,4 +1,5 @@ export enum ViewType { TABLE = 'TABLE', KANBAN = 'KANBAN', + CALENDAR = 'CALENDAR', } diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-to-flat-view-to-with-default.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-to-flat-view-to-with-default.util.ts index a98e7658bd..bf4e2f4f26 100644 --- a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-to-flat-view-to-with-default.util.ts +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-to-flat-view-to-with-default.util.ts @@ -32,5 +32,6 @@ export const fromPartialFlatViewToFlatViewWithDefault = ( anyFieldFilterValue: partialFlatView.anyFieldFilterValue ?? null, universalIdentifier: partialFlatView.universalIdentifier ?? viewId, viewFieldIds: partialFlatView.viewFieldIds ?? [], + calendarLayout: partialFlatView.calendarLayout ?? null, }; }; diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 25cfa88c54..df36dfe5ac 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -138,6 +138,7 @@ describe('WorkspaceEntityManager', () => { IS_MESSAGE_FOLDER_CONTROL_ENABLED: false, IS_WORKFLOW_ITERATOR_ENABLED: false, IS_DATABASE_EVENT_TRIGGER_ENABLED: false, + IS_CALENDAR_VIEW_ENABLED: false, }, eventEmitterService: { emitMutationEvent: jest.fn(),