diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts index 053d9b36d..d5f5dba58 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts @@ -95,6 +95,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { } const previouslySelectedKanbanField = availableFieldsForKanban.find( (fieldsForKanban) => + // TODO: replace with viewGroups.fieldMetadataId fieldsForKanban.id === currentView.kanbanFieldMetadataId, ); @@ -103,6 +104,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { : availableFieldsForKanban[0]; if (!isDefined(previouslySelectedKanbanField)) { + // TODO: replace with viewGroups.fieldMetadataId updateCurrentViewParams.kanbanFieldMetadataId = kanbanField.id; } diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts index 1b415f3fb..e906eb3e1 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts @@ -194,6 +194,7 @@ export const useLoadRecordIndexStates = () => { setRecordIndexViewType(view.type); setRecordIndexOpenRecordIn(view.openRecordIn); setRecordIndexViewKanbanFieldMetadataIdState( + // TODO: replace with viewGroups.fieldMetadataId view.kanbanFieldMetadataId, ); const kanbanAggregateOperationFieldMetadataType = diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx index 5b73f4f06..69952e4ca 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx @@ -132,6 +132,7 @@ export const SettingsObjectFieldItemTableRow = ({ const deletedViewIds = prefetchViews .map((view) => { + // TODO: replace with viewGroups.fieldMetadataId if (view.kanbanFieldMetadataId === activeFieldMetadatItem.id) { deleteViewFromCache(view); return view.id; diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEffect.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEffect.tsx index a6bb2755f..3494ad7ec 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEffect.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEffect.tsx @@ -94,6 +94,7 @@ export const ViewPickerContentEffect = () => { viewPickerKanbanFieldMetadataId === '' ) { setViewPickerKanbanFieldMetadataId( + // TODO: replace with viewGroups.fieldMetadataId referenceView.kanbanFieldMetadataId !== '' ? referenceView.kanbanFieldMetadataId : availableFieldsForKanban[0].id, diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1753104458798-createViewTablesInCoreSchema.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1753104458798-createViewTablesInCoreSchema.ts new file mode 100644 index 000000000..38ece0f1b --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1753104458798-createViewTablesInCoreSchema.ts @@ -0,0 +1,171 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateViewTablesInCoreSchema1753104458798 + implements MigrationInterface +{ + name = 'CreateViewTablesInCoreSchema1753104458798'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "core"."viewField_aggregateoperation_enum" AS ENUM('MIN', 'MAX', 'AVG', 'SUM', 'COUNT', 'COUNT_UNIQUE_VALUES', 'COUNT_EMPTY', 'COUNT_NOT_EMPTY', 'COUNT_TRUE', 'COUNT_FALSE', 'PERCENTAGE_EMPTY', 'PERCENTAGE_NOT_EMPTY')`, + ); + await queryRunner.query( + `CREATE TABLE "core"."viewField" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fieldMetadataId" uuid NOT NULL, "isVisible" boolean NOT NULL DEFAULT true, "size" integer NOT NULL DEFAULT '0', "position" integer NOT NULL DEFAULT '0', "aggregateOperation" "core"."viewField_aggregateoperation_enum", "viewId" uuid NOT NULL, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "IDX_VIEW_FIELD_FIELD_METADATA_ID_VIEW_ID_UNIQUE" UNIQUE ("fieldMetadataId", "viewId"), CONSTRAINT "PK_ba2a5aa5f0bd7ac82788fae921e" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_FIELD_WORKSPACE_ID_VIEW_ID" ON "core"."viewField" ("workspaceId", "viewId") `, + ); + await queryRunner.query( + `CREATE TYPE "core"."viewFilterGroup_logicaloperator_enum" AS ENUM('AND', 'OR', 'NOT')`, + ); + await queryRunner.query( + `CREATE TABLE "core"."viewFilterGroup" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "parentViewFilterGroupId" uuid, "logicalOperator" "core"."viewFilterGroup_logicaloperator_enum" NOT NULL DEFAULT 'NOT', "positionInViewFilterGroup" integer, "viewId" uuid NOT NULL, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_16f55359d609168b826405ed307" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_FILTER_GROUP_WORKSPACE_ID_VIEW_ID" ON "core"."viewFilterGroup" ("workspaceId", "viewId") `, + ); + await queryRunner.query( + `CREATE TABLE "core"."viewFilter" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fieldMetadataId" uuid NOT NULL, "operand" character varying NOT NULL DEFAULT 'Contains', "value" jsonb NOT NULL, "viewFilterGroupId" uuid, "positionInViewFilterGroup" integer, "subFieldName" text, "viewId" uuid NOT NULL, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_09f9ffa2f66263b9eb301460137" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_FILTER_FIELD_METADATA_ID" ON "core"."viewFilter" ("fieldMetadataId") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_FILTER_WORKSPACE_ID_VIEW_ID" ON "core"."viewFilter" ("workspaceId", "viewId") `, + ); + await queryRunner.query( + `CREATE TABLE "core"."viewGroup" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fieldMetadataId" uuid NOT NULL, "isVisible" boolean NOT NULL DEFAULT true, "fieldValue" text NOT NULL, "position" integer NOT NULL DEFAULT '0', "viewId" uuid NOT NULL, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_d2aa8cad01e9d5e99c23f9ccec3" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_GROUP_WORKSPACE_ID_VIEW_ID" ON "core"."viewGroup" ("workspaceId", "viewId") `, + ); + await queryRunner.query( + `CREATE TYPE "core"."viewSort_direction_enum" AS ENUM('ASC', 'DESC')`, + ); + await queryRunner.query( + `CREATE TABLE "core"."viewSort" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fieldMetadataId" uuid NOT NULL, "direction" "core"."viewSort_direction_enum" NOT NULL DEFAULT 'ASC', "viewId" uuid NOT NULL, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "IDX_VIEW_SORT_FIELD_METADATA_ID_VIEW_ID_UNIQUE" UNIQUE ("fieldMetadataId", "viewId"), CONSTRAINT "PK_eceb74d297f926313af6463d496" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_SORT_WORKSPACE_ID_VIEW_ID" ON "core"."viewSort" ("workspaceId", "viewId") `, + ); + await queryRunner.query( + `CREATE TYPE "core"."view_openrecordin_enum" AS ENUM('SIDE_PANEL', 'RECORD_PAGE')`, + ); + await queryRunner.query( + `CREATE TYPE "core"."view_kanbanaggregateoperation_enum" AS ENUM('MIN', 'MAX', 'AVG', 'SUM', 'COUNT', 'COUNT_UNIQUE_VALUES', 'COUNT_EMPTY', 'COUNT_NOT_EMPTY', 'COUNT_TRUE', 'COUNT_FALSE', 'PERCENTAGE_EMPTY', 'PERCENTAGE_NOT_EMPTY')`, + ); + await queryRunner.query( + `CREATE TABLE "core"."view" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" text NOT NULL DEFAULT '', "objectMetadataId" uuid NOT NULL, "type" character varying NOT NULL DEFAULT 'table', "key" text DEFAULT 'INDEX', "icon" text NOT NULL, "position" integer NOT NULL DEFAULT '0', "isCompact" boolean NOT NULL DEFAULT false, "openRecordIn" "core"."view_openrecordin_enum" NOT NULL DEFAULT 'SIDE_PANEL', "kanbanAggregateOperation" "core"."view_kanbanaggregateoperation_enum", "kanbanAggregateOperationFieldMetadataId" uuid, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_86cfb9e426c77d60b900fe2b543" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_WORKSPACE_ID_OBJECT_METADATA_ID" ON "core"."view" ("workspaceId", "objectMetadataId") `, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewField" ADD CONSTRAINT "FK_96158de54c78944b5340b6f708e" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewField" ADD CONSTRAINT "FK_c5ab40cd4debb51d588752a4857" FOREIGN KEY ("viewId") REFERENCES "core"."view"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFilterGroup" ADD CONSTRAINT "FK_dce74ab06fa7a2effcbf1b98dff" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFilterGroup" ADD CONSTRAINT "FK_8919a390f4022ab1e40182a5ac3" FOREIGN KEY ("viewId") REFERENCES "core"."view"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFilter" ADD CONSTRAINT "FK_32cabc67e40d24acab541c469a8" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFilter" ADD CONSTRAINT "FK_06858adf0fb54ec88fa602198ca" FOREIGN KEY ("viewId") REFERENCES "core"."view"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" ADD CONSTRAINT "FK_61053f5509cc31e5d7139fba1cb" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" ADD CONSTRAINT "FK_2d7cfc4748058a0ca648835d046" FOREIGN KEY ("viewId") REFERENCES "core"."view"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewSort" ADD CONSTRAINT "FK_5f3278d6791aa4c58423e556ae6" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewSort" ADD CONSTRAINT "FK_2b36c6adea4542b4844d9fb1806" FOREIGN KEY ("viewId") REFERENCES "core"."view"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."view" ADD CONSTRAINT "FK_580dad12c8b92f3a3c307c4e66d" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."view" DROP CONSTRAINT "FK_580dad12c8b92f3a3c307c4e66d"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewSort" DROP CONSTRAINT "FK_2b36c6adea4542b4844d9fb1806"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewSort" DROP CONSTRAINT "FK_5f3278d6791aa4c58423e556ae6"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" DROP CONSTRAINT "FK_2d7cfc4748058a0ca648835d046"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" DROP CONSTRAINT "FK_61053f5509cc31e5d7139fba1cb"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFilter" DROP CONSTRAINT "FK_06858adf0fb54ec88fa602198ca"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFilter" DROP CONSTRAINT "FK_32cabc67e40d24acab541c469a8"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFilterGroup" DROP CONSTRAINT "FK_8919a390f4022ab1e40182a5ac3"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFilterGroup" DROP CONSTRAINT "FK_dce74ab06fa7a2effcbf1b98dff"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewField" DROP CONSTRAINT "FK_c5ab40cd4debb51d588752a4857"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewField" DROP CONSTRAINT "FK_96158de54c78944b5340b6f708e"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_VIEW_WORKSPACE_ID_OBJECT_METADATA_ID"`, + ); + await queryRunner.query(`DROP TABLE "core"."view"`); + await queryRunner.query( + `DROP TYPE "core"."view_kanbanaggregateoperation_enum"`, + ); + await queryRunner.query(`DROP TYPE "core"."view_openrecordin_enum"`); + await queryRunner.query( + `DROP INDEX "core"."IDX_VIEW_SORT_WORKSPACE_ID_VIEW_ID"`, + ); + await queryRunner.query(`DROP TABLE "core"."viewSort"`); + await queryRunner.query(`DROP TYPE "core"."viewSort_direction_enum"`); + await queryRunner.query( + `DROP INDEX "core"."IDX_VIEW_GROUP_WORKSPACE_ID_VIEW_ID"`, + ); + await queryRunner.query(`DROP TABLE "core"."viewGroup"`); + await queryRunner.query( + `DROP INDEX "core"."IDX_VIEW_FILTER_WORKSPACE_ID_VIEW_ID"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_VIEW_FILTER_FIELD_METADATA_ID"`, + ); + await queryRunner.query(`DROP TABLE "core"."viewFilter"`); + await queryRunner.query( + `DROP INDEX "core"."IDX_VIEW_FILTER_GROUP_WORKSPACE_ID_VIEW_ID"`, + ); + await queryRunner.query(`DROP TABLE "core"."viewFilterGroup"`); + await queryRunner.query( + `DROP TYPE "core"."viewFilterGroup_logicaloperator_enum"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_VIEW_FIELD_WORKSPACE_ID_VIEW_ID"`, + ); + await queryRunner.query(`DROP TABLE "core"."viewField"`); + await queryRunner.query( + `DROP TYPE "core"."viewField_aggregateoperation_enum"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/enums/view-filter-group-logical-operator.ts b/packages/twenty-server/src/engine/metadata-modules/view/enums/view-filter-group-logical-operator.ts new file mode 100644 index 000000000..d5dc58489 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view/enums/view-filter-group-logical-operator.ts @@ -0,0 +1,5 @@ +export enum ViewFilterGroupLogicalOperator { + AND = 'AND', + OR = 'OR', + NOT = 'NOT', +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/enums/view-open-record-in.ts b/packages/twenty-server/src/engine/metadata-modules/view/enums/view-open-record-in.ts new file mode 100644 index 000000000..31fa4b17d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view/enums/view-open-record-in.ts @@ -0,0 +1,4 @@ +export enum ViewOpenRecordIn { + SIDE_PANEL = 'SIDE_PANEL', + RECORD_PAGE = 'RECORD_PAGE', +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/enums/view-sort-direction.ts b/packages/twenty-server/src/engine/metadata-modules/view/enums/view-sort-direction.ts new file mode 100644 index 000000000..6c240e07c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view/enums/view-sort-direction.ts @@ -0,0 +1,4 @@ +export enum ViewSortDirection { + ASC = 'ASC', + DESC = 'DESC', +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/view-field.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view/view-field.entity.ts new file mode 100644 index 000000000..f89198656 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view/view-field.entity.ts @@ -0,0 +1,77 @@ +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Relation, + Unique, + UpdateDateColumn, +} from 'typeorm'; + +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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { View } from 'src/engine/metadata-modules/view/view.entity'; + +@Entity({ name: 'viewField', schema: 'core' }) +@Index('IDX_VIEW_FIELD_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) +@Unique('IDX_VIEW_FIELD_FIELD_METADATA_ID_VIEW_ID_UNIQUE', [ + 'fieldMetadataId', + 'viewId', +]) +export class ViewField { + @IDField(() => UUIDScalarType) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: false, type: 'uuid' }) + fieldMetadataId: string; + + @Column({ nullable: false, default: true }) + isVisible: boolean; + + @Column({ nullable: false, type: 'int', default: 0 }) + size: number; + + @Column({ nullable: false, type: 'int', default: 0 }) + position: number; + + @Column({ + type: 'enum', + enum: AggregateOperations, + nullable: true, + }) + aggregateOperation?: AggregateOperations | null; + + @Column({ nullable: false, type: 'uuid' }) + viewId: string; + + @Column({ nullable: false, type: 'uuid' }) + workspaceId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt?: Date | null; + + @ManyToOne(() => Workspace, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'workspaceId' }) + workspace: Relation; + + @ManyToOne(() => View, (view) => view.viewFields, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'viewId' }) + view: Relation; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/view-filter-group.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view/view-filter-group.entity.ts new file mode 100644 index 000000000..1e2869c8b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view/view-filter-group.entity.ts @@ -0,0 +1,73 @@ +import { registerEnumType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Relation, + UpdateDateColumn, +} from 'typeorm'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { ViewFilterGroupLogicalOperator } from 'src/engine/metadata-modules/view/enums/view-filter-group-logical-operator'; +import { View } from 'src/engine/metadata-modules/view/view.entity'; + +registerEnumType(ViewFilterGroupLogicalOperator, { + name: 'ViewFilterGroupLogicalOperator', +}); + +@Entity({ name: 'viewFilterGroup', schema: 'core' }) +@Index('IDX_VIEW_FILTER_GROUP_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) +export class ViewFilterGroup { + @IDField(() => UUIDScalarType) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: true, type: 'uuid' }) + parentViewFilterGroupId?: string | null; + + @Column({ + type: 'enum', + enum: ViewFilterGroupLogicalOperator, + nullable: false, + default: ViewFilterGroupLogicalOperator.NOT, + }) + logicalOperator: ViewFilterGroupLogicalOperator; + + @Column({ nullable: true, type: 'int' }) + positionInViewFilterGroup?: number | null; + + @Column({ nullable: false, type: 'uuid' }) + viewId: string; + + @Column({ nullable: false, type: 'uuid' }) + workspaceId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt?: Date | null; + + @ManyToOne(() => Workspace, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'workspaceId' }) + workspace: Relation; + + @ManyToOne(() => View, (view) => view.viewFilterGroups, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'viewId' }) + view: Relation; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/view-filter.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view/view-filter.entity.ts new file mode 100644 index 000000000..bef6cae54 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view/view-filter.entity.ts @@ -0,0 +1,71 @@ +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Relation, + UpdateDateColumn, +} from 'typeorm'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { View } from 'src/engine/metadata-modules/view/view.entity'; + +@Entity({ name: 'viewFilter', schema: 'core' }) +@Index('IDX_VIEW_FILTER_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) +@Index('IDX_VIEW_FILTER_FIELD_METADATA_ID', ['fieldMetadataId']) +export class ViewFilter { + @IDField(() => UUIDScalarType) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: false, type: 'uuid' }) + fieldMetadataId: string; + + @Column({ nullable: false, default: 'Contains' }) + operand: string; + + @Column({ nullable: false, type: 'jsonb' }) + value: JSON; + + @Column({ nullable: true, type: 'uuid' }) + viewFilterGroupId?: string | null; + + @Column({ nullable: true, type: 'int' }) + positionInViewFilterGroup?: number | null; + + @Column({ nullable: true, type: 'text', default: null }) + subFieldName?: string | null; + + @Column({ nullable: false, type: 'uuid' }) + viewId: string; + + @Column({ nullable: false, type: 'uuid' }) + workspaceId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt?: Date | null; + + @ManyToOne(() => Workspace, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'workspaceId' }) + workspace: Relation; + + @ManyToOne(() => View, (view) => view.viewFilters, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'viewId' }) + view: Relation; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/view-group.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view/view-group.entity.ts new file mode 100644 index 000000000..a6088e3f1 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view/view-group.entity.ts @@ -0,0 +1,64 @@ +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Relation, + UpdateDateColumn, +} from 'typeorm'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { View } from 'src/engine/metadata-modules/view/view.entity'; + +@Entity({ name: 'viewGroup', schema: 'core' }) +@Index('IDX_VIEW_GROUP_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) +export class ViewGroup { + @IDField(() => UUIDScalarType) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: false, type: 'uuid' }) + fieldMetadataId: string; + + @Column({ nullable: false, default: true }) + isVisible: boolean; + + @Column({ nullable: false, type: 'text' }) + fieldValue: string; + + @Column({ nullable: false, type: 'int', default: 0 }) + position: number; + + @Column({ nullable: false, type: 'uuid' }) + viewId: string; + + @Column({ nullable: false, type: 'uuid' }) + workspaceId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt?: Date | null; + + @ManyToOne(() => Workspace, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'workspaceId' }) + workspace: Relation; + + @ManyToOne(() => View, (view) => view.viewGroups, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'viewId' }) + view: Relation; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/view-sort.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view/view-sort.entity.ts new file mode 100644 index 000000000..1b2a5aba4 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view/view-sort.entity.ts @@ -0,0 +1,73 @@ +import { registerEnumType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Relation, + Unique, + UpdateDateColumn, +} from 'typeorm'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { ViewSortDirection } from 'src/engine/metadata-modules/view/enums/view-sort-direction'; +import { View } from 'src/engine/metadata-modules/view/view.entity'; + +registerEnumType(ViewSortDirection, { name: 'ViewSortDirection' }); + +@Entity({ name: 'viewSort', schema: 'core' }) +@Index('IDX_VIEW_SORT_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) +@Unique('IDX_VIEW_SORT_FIELD_METADATA_ID_VIEW_ID_UNIQUE', [ + 'fieldMetadataId', + 'viewId', +]) +export class ViewSort { + @IDField(() => UUIDScalarType) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: false, type: 'uuid' }) + fieldMetadataId: string; + + @Column({ + nullable: false, + type: 'enum', + enum: ViewSortDirection, + default: ViewSortDirection.ASC, + }) + direction: ViewSortDirection; + + @Column({ nullable: false, type: 'uuid' }) + viewId: string; + + @Column({ nullable: false, type: 'uuid' }) + workspaceId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt?: Date | null; + + @ManyToOne(() => Workspace, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'workspaceId' }) + workspace: Relation; + + @ManyToOne(() => View, (view) => view.viewSorts, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'viewId' }) + view: Relation; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/view.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view/view.entity.ts new file mode 100644 index 000000000..f92235fa0 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view/view.entity.ts @@ -0,0 +1,111 @@ +import { registerEnumType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + Relation, + UpdateDateColumn, +} from 'typeorm'; + +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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { ViewOpenRecordIn } from 'src/engine/metadata-modules/view/enums/view-open-record-in'; +import { ViewField } from 'src/engine/metadata-modules/view/view-field.entity'; +import { ViewFilterGroup } from 'src/engine/metadata-modules/view/view-filter-group.entity'; +import { ViewFilter } from 'src/engine/metadata-modules/view/view-filter.entity'; +import { ViewGroup } from 'src/engine/metadata-modules/view/view-group.entity'; +import { ViewSort } from 'src/engine/metadata-modules/view/view-sort.entity'; + +registerEnumType(ViewOpenRecordIn, { name: 'ViewOpenRecordIn' }); + +@Entity({ name: 'view', schema: 'core' }) +@Index('IDX_VIEW_WORKSPACE_ID_OBJECT_METADATA_ID', [ + 'workspaceId', + 'objectMetadataId', +]) +export class View { + @IDField(() => UUIDScalarType) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: false, type: 'text', default: '' }) + name: string; + + @Column({ nullable: false, type: 'uuid' }) + objectMetadataId: string; + + @Column({ nullable: false, default: 'table' }) + type: string; + + @Column({ nullable: true, type: 'text', default: 'INDEX' }) + key: string; + + @Column({ nullable: false, type: 'text' }) + icon: string; + + @Column({ nullable: false, type: 'int', default: 0 }) + position: number; + + @Column({ nullable: false, default: false, type: 'boolean' }) + isCompact: boolean; + + @Column({ + type: 'enum', + enum: ViewOpenRecordIn, + nullable: false, + default: ViewOpenRecordIn.SIDE_PANEL, + }) + openRecordIn: ViewOpenRecordIn; + + @Column({ + type: 'enum', + enum: AggregateOperations, + nullable: true, + }) + kanbanAggregateOperation?: AggregateOperations | null; + + @Column({ nullable: true, type: 'uuid' }) + kanbanAggregateOperationFieldMetadataId?: string | null; + + @Column({ nullable: false, type: 'uuid' }) + workspaceId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt?: Date | null; + + @ManyToOne(() => Workspace, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'workspaceId' }) + workspace: Relation; + + @OneToMany(() => ViewField, (viewField) => viewField.view) + viewFields: Relation; + + @OneToMany(() => ViewFilter, (viewFilter) => viewFilter.view) + viewFilters: Relation; + + @OneToMany(() => ViewSort, (viewSort) => viewSort.view) + viewSorts: Relation; + + @OneToMany(() => ViewGroup, (viewGroup) => viewGroup.view) + viewGroups: Relation; + + @OneToMany(() => ViewFilterGroup, (viewFilterGroup) => viewFilterGroup.view) + viewFilterGroups: Relation; +}