Core views frontend (#13932)

Parallel code path to read and write core views when
IS_CORE_VIEW_ENABLED.

Migrated view key to an enum.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Raphaël Bosi
2025-08-20 19:04:58 +02:00
committed by GitHub
parent deec9c96da
commit 30c42345f0
114 changed files with 5019 additions and 254 deletions
@@ -20,11 +20,10 @@ import { ViewFieldRestApiExceptionFilter } from 'src/engine/core-modules/view/fi
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewFields')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewFieldRestApiExceptionFilter)
export class ViewFieldController {
constructor(private readonly viewFieldService: ViewFieldService) {}
@@ -20,11 +20,10 @@ import { ViewFilterGroupRestApiExceptionFilter } from 'src/engine/core-modules/v
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewFilterGroups')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewFilterGroupRestApiExceptionFilter)
export class ViewFilterGroupController {
constructor(
@@ -20,11 +20,10 @@ import { ViewFilterRestApiExceptionFilter } from 'src/engine/core-modules/view/f
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewFilters')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewFilterRestApiExceptionFilter)
export class ViewFilterController {
constructor(private readonly viewFilterService: ViewFilterService) {}
@@ -20,11 +20,10 @@ import { ViewGroupRestApiExceptionFilter } from 'src/engine/core-modules/view/fi
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewGroups')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewGroupRestApiExceptionFilter)
export class ViewGroupController {
constructor(private readonly viewGroupService: ViewGroupService) {}
@@ -20,11 +20,10 @@ import { ViewSortRestApiExceptionFilter } from 'src/engine/core-modules/view/fil
import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewSorts')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewSortRestApiExceptionFilter)
export class ViewSortController {
constructor(private readonly viewSortService: ViewSortService) {}
@@ -20,11 +20,10 @@ import { ViewRestApiExceptionFilter } from 'src/engine/core-modules/view/filters
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/views')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewRestApiExceptionFilter)
export class ViewController {
constructor(private readonly viewService: ViewService) {}
@@ -1,7 +1,8 @@
import { Field, InputType } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { RawJSONScalar } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/raw-json.scalar';
import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand';
import { ViewFilterValue } from 'src/engine/core-modules/view/types/view-filter-value.type';
@@ -13,7 +14,7 @@ export class CreateViewFilterInput {
@Field({ nullable: true, defaultValue: ViewFilterOperand.CONTAINS })
operand?: ViewFilterOperand;
@Field(() => RawJSONScalar, { nullable: false })
@Field(() => GraphQLJSON, { nullable: false })
value: ViewFilterValue;
@Field(() => UUIDScalarType, { nullable: true })
@@ -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 { 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,8 +17,8 @@ export class CreateViewInput {
@Field(() => ViewType, { nullable: true, defaultValue: ViewType.TABLE })
type?: ViewType;
@Field({ nullable: true, defaultValue: 'INDEX' })
key?: string;
@Field(() => ViewKey, { nullable: true })
key?: ViewKey;
@Field({ nullable: false })
icon: string;
@@ -1,11 +1,9 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import GraphQLJSON from 'graphql-type-json';
import {
RawJSONScalar,
UUIDScalarType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand';
import { ViewFilterValue } from 'src/engine/core-modules/view/types/view-filter-value.type';
@@ -24,7 +22,7 @@ export class ViewFilterDTO {
@Field({ nullable: false, defaultValue: ViewFilterOperand.CONTAINS })
operand: ViewFilterOperand;
@Field(() => RawJSONScalar, { nullable: false })
@Field(() => GraphQLJSON, { nullable: false })
value: ViewFilterValue;
@Field(() => UUIDScalarType, { nullable: true })
@@ -4,11 +4,13 @@ import { IDField } from '@ptc-org/nestjs-query-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 { 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';
registerEnumType(ViewOpenRecordIn, { name: 'ViewOpenRecordIn' });
registerEnumType(ViewType, { name: 'ViewType' });
registerEnumType(ViewKey, { name: 'ViewKey' });
@ObjectType('CoreView')
export class ViewDTO {
@@ -24,8 +26,8 @@ export class ViewDTO {
@Field(() => ViewType, { nullable: false, defaultValue: ViewType.TABLE })
type: ViewType;
@Field({ nullable: true, defaultValue: 'INDEX' })
key: string;
@Field(() => ViewKey, { nullable: true, defaultValue: ViewKey.INDEX })
key: ViewKey | null;
@Field({ nullable: false })
icon: string;
@@ -20,6 +20,7 @@ import { ViewFilterGroup } from 'src/engine/core-modules/view/entities/view-filt
import { ViewFilter } from 'src/engine/core-modules/view/entities/view-filter.entity';
import { ViewGroup } from 'src/engine/core-modules/view/entities/view-group.entity';
import { ViewSort } from 'src/engine/core-modules/view/entities/view-sort.entity';
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';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -48,8 +49,13 @@ export class View {
})
type: ViewType;
@Column({ nullable: true, type: 'text', default: 'INDEX' })
key: string;
@Column({
type: 'enum',
enum: Object.values(ViewKey),
nullable: true,
default: null,
})
key: ViewKey | null;
@Column({ nullable: false, type: 'text' })
icon: string;
@@ -0,0 +1,3 @@
export enum ViewKey {
INDEX = 'INDEX',
}
@@ -16,7 +16,17 @@ import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-contex
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { CreateViewInput } from 'src/engine/core-modules/view/dtos/inputs/create-view.input';
import { UpdateViewInput } from 'src/engine/core-modules/view/dtos/inputs/update-view.input';
import { ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto';
import { ViewFilterGroupDTO } from 'src/engine/core-modules/view/dtos/view-filter-group.dto';
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 { ViewDTO } from 'src/engine/core-modules/view/dtos/view.dto';
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/core-modules/view/utils/view-graphql-api-exception.filter';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -30,6 +40,11 @@ import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modul
export class ViewResolver {
constructor(
private readonly viewService: ViewService,
private readonly viewFieldService: ViewFieldService,
private readonly viewFilterService: ViewFilterService,
private readonly viewFilterGroupService: ViewFilterGroupService,
private readonly viewGroupService: ViewGroupService,
private readonly viewSortService: ViewSortService,
private readonly i18nService: I18nService,
) {}
@@ -152,4 +167,44 @@ export class ViewResolver {
return isDefined(deletedView);
}
@ResolveField(() => [ViewFieldDTO])
async viewFields(
@Parent() view: ViewDTO,
@AuthWorkspace() workspace: Workspace,
) {
return this.viewFieldService.findByViewId(workspace.id, view.id);
}
@ResolveField(() => [ViewFilterDTO])
async viewFilters(
@Parent() view: ViewDTO,
@AuthWorkspace() workspace: Workspace,
) {
return this.viewFilterService.findByViewId(workspace.id, view.id);
}
@ResolveField(() => [ViewFilterGroupDTO])
async viewFilterGroups(
@Parent() view: ViewDTO,
@AuthWorkspace() workspace: Workspace,
) {
return this.viewFilterGroupService.findByViewId(workspace.id, view.id);
}
@ResolveField(() => [ViewSortDTO])
async viewSorts(
@Parent() view: ViewDTO,
@AuthWorkspace() workspace: Workspace,
) {
return this.viewSortService.findByViewId(workspace.id, view.id);
}
@ResolveField(() => [ViewGroupDTO])
async viewGroups(
@Parent() view: ViewDTO,
@AuthWorkspace() workspace: Workspace,
) {
return this.viewGroupService.findByViewId(workspace.id, view.id);
}
}
@@ -6,6 +6,8 @@ export type RelationFilterValue = {
export type ViewFilterValue =
| string
| string[]
| boolean
| number
| RelationFilterValue
| Record<string, unknown>
| null
@@ -1,7 +1,6 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module';
import { ViewFieldController } from 'src/engine/core-modules/view/controllers/view-field.controller';
import { ViewFilterGroupController } from 'src/engine/core-modules/view/controllers/view-filter-group.controller';
@@ -36,7 +35,6 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
[View, ViewField, ViewFilter, ViewFilterGroup, ViewGroup, ViewSort],
'core',
),
AuthModule,
I18nModule,
WorkspaceCacheStorageModule,
WorkspaceMetadataCacheModule,