Reset Tab Page Layout (#19453)
## Context - Add "Reset to default" for page layout tabs and widgets — backend mutation resets overrides, reactivates deactivated children, and deletes custom children - Fix override write bug where mutating an overridable property (e.g. widget title) on a standard-app entity incorrectly overwrote the base column instead of writing to the overrides JSONB — PageLayoutUpdateService now uses resolveFlatEntityOverridableProperties for accurate diffing and routes properties through sanitizeOverridableEntityInput - Deprecate isOverridden - We need more time to think about this feature. Currently this adds too much complexity for a very small benefit https://github.com/user-attachments/assets/a84546c8-1e15-4d9e-a489-0825cf8b8ed2
This commit is contained in:
+2
-3
@@ -20,7 +20,6 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input';
|
||||
import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
|
||||
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import {
|
||||
generateViewFieldExceptionMessage,
|
||||
generateViewFieldUserFriendlyExceptionMessage,
|
||||
@@ -45,7 +44,7 @@ export class ViewFieldController {
|
||||
async findMany(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@Query('viewId') viewId?: string,
|
||||
): Promise<ViewFieldEntity[]> {
|
||||
): Promise<ViewFieldDTO[]> {
|
||||
if (viewId) {
|
||||
return this.viewFieldService.findByViewId(workspace.id, viewId);
|
||||
}
|
||||
@@ -58,7 +57,7 @@ export class ViewFieldController {
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ViewFieldEntity> {
|
||||
): Promise<ViewFieldDTO> {
|
||||
const viewField = await this.viewFieldService.findById(id, workspace.id);
|
||||
|
||||
if (!isDefined(viewField)) {
|
||||
|
||||
+5
-2
@@ -51,8 +51,11 @@ export class ViewFieldDTO {
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt?: Date | null;
|
||||
|
||||
@Field(() => Boolean, { nullable: false })
|
||||
isOverridden: boolean;
|
||||
@Field(() => Boolean, {
|
||||
nullable: true,
|
||||
deprecationReason: 'isOverridden is deprecated',
|
||||
})
|
||||
isOverridden?: boolean;
|
||||
|
||||
@HideField()
|
||||
overrides?: ViewFieldOverrides | null;
|
||||
|
||||
+3
-54
@@ -1,30 +1,16 @@
|
||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Float,
|
||||
Int,
|
||||
Mutation,
|
||||
Parent,
|
||||
Query,
|
||||
ResolveField,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { AggregateOperations } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { resolveOverridableEntityProperty } from 'src/engine/metadata-modules/utils/resolve-overridable-entity-property.util';
|
||||
import { CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input';
|
||||
import { DeleteViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/delete-view-field.input';
|
||||
import { DestroyViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/destroy-view-field.input';
|
||||
import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
|
||||
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { CreateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-field-permission.guard';
|
||||
import { DeleteViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-field-permission.guard';
|
||||
@@ -38,49 +24,12 @@ import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/
|
||||
export class ViewFieldResolver {
|
||||
constructor(private readonly viewFieldService: ViewFieldService) {}
|
||||
|
||||
@ResolveField(() => Boolean)
|
||||
isVisible(@Parent() viewField: ViewFieldDTO): boolean {
|
||||
return resolveOverridableEntityProperty(viewField, 'isVisible');
|
||||
}
|
||||
|
||||
@ResolveField(() => Int)
|
||||
size(@Parent() viewField: ViewFieldDTO): number {
|
||||
return resolveOverridableEntityProperty(viewField, 'size');
|
||||
}
|
||||
|
||||
@ResolveField(() => Float)
|
||||
position(@Parent() viewField: ViewFieldDTO): number {
|
||||
return resolveOverridableEntityProperty(viewField, 'position');
|
||||
}
|
||||
|
||||
@ResolveField(() => AggregateOperations, { nullable: true })
|
||||
aggregateOperation(
|
||||
@Parent() viewField: ViewFieldDTO,
|
||||
): AggregateOperations | null | undefined {
|
||||
return resolveOverridableEntityProperty(viewField, 'aggregateOperation');
|
||||
}
|
||||
|
||||
@ResolveField(() => UUIDScalarType, { nullable: true })
|
||||
viewFieldGroupId(
|
||||
@Parent() viewField: ViewFieldDTO,
|
||||
): string | null | undefined {
|
||||
return resolveOverridableEntityProperty(viewField, 'viewFieldGroupId');
|
||||
}
|
||||
|
||||
@ResolveField(() => Boolean)
|
||||
isOverridden(@Parent() viewField: ViewFieldDTO): boolean {
|
||||
return (
|
||||
isDefined(viewField.overrides) &&
|
||||
Object.keys(viewField.overrides).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
@Query(() => [ViewFieldDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
async getViewFields(
|
||||
@Args('viewId', { type: () => String }) viewId: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ViewFieldEntity[]> {
|
||||
): Promise<ViewFieldDTO[]> {
|
||||
return this.viewFieldService.findByViewId(workspace.id, viewId);
|
||||
}
|
||||
|
||||
@@ -89,7 +38,7 @@ export class ViewFieldResolver {
|
||||
async getViewField(
|
||||
@Args('id', { type: () => String }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ViewFieldEntity | null> {
|
||||
): Promise<ViewFieldDTO | null> {
|
||||
return this.viewFieldService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
|
||||
+51
-35
@@ -1,11 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { fromCreateViewFieldInputToFlatViewFieldToCreate } from 'src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util';
|
||||
@@ -17,7 +16,6 @@ import { DeleteViewFieldInput } from 'src/engine/metadata-modules/view-field/dto
|
||||
import { DestroyViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/destroy-view-field.input';
|
||||
import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
|
||||
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import {
|
||||
ViewFieldException,
|
||||
ViewFieldExceptionCode,
|
||||
@@ -31,8 +29,6 @@ export class ViewFieldService {
|
||||
constructor(
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
@InjectRepository(ViewFieldEntity)
|
||||
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
@@ -345,47 +341,67 @@ export class ViewFieldService {
|
||||
});
|
||||
}
|
||||
|
||||
async findByWorkspaceId(workspaceId: string): Promise<ViewFieldEntity[]> {
|
||||
return this.viewFieldRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
isActive: true,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
async findByWorkspaceId(workspaceId: string): Promise<ViewFieldDTO[]> {
|
||||
const { flatViewFieldMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatViewFieldMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return Object.values(flatViewFieldMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
.filter((field) => field.isActive && !isDefined(field.deletedAt))
|
||||
.map(fromFlatViewFieldToViewFieldDto)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
}
|
||||
|
||||
async findByViewId(
|
||||
workspaceId: string,
|
||||
viewId: string,
|
||||
): Promise<ViewFieldEntity[]> {
|
||||
return this.viewFieldRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
viewId,
|
||||
isActive: true,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
): Promise<ViewFieldDTO[]> {
|
||||
const { flatViewFieldMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatViewFieldMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return Object.values(flatViewFieldMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(field) =>
|
||||
field.viewId === viewId &&
|
||||
field.isActive &&
|
||||
!isDefined(field.deletedAt),
|
||||
)
|
||||
.map(fromFlatViewFieldToViewFieldDto)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<ViewFieldEntity | null> {
|
||||
const viewField = await this.viewFieldRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['workspace', 'view'],
|
||||
): Promise<ViewFieldDTO | null> {
|
||||
const { flatViewFieldMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatViewFieldMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatViewField = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatViewFieldMaps,
|
||||
});
|
||||
|
||||
return viewField || null;
|
||||
if (!isDefined(flatViewField) || isDefined(flatViewField.deletedAt)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fromFlatViewFieldToViewFieldDto(flatViewField);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,17 +1,15 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { type ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
|
||||
export const fromFlatViewFieldToViewFieldDto = (
|
||||
flatViewField: FlatViewField,
|
||||
): ViewFieldDTO => {
|
||||
const { createdAt, updatedAt, deletedAt, ...rest } = flatViewField;
|
||||
const { createdAt, updatedAt, deletedAt, overrides, ...rest } = flatViewField;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
isOverridden:
|
||||
isDefined(rest.overrides) && Object.keys(rest.overrides).length > 0,
|
||||
...(overrides ?? {}),
|
||||
isOverridden: false,
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
deletedAt: deletedAt ? new Date(deletedAt) : null,
|
||||
|
||||
Reference in New Issue
Block a user