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:
Weiko
2026-04-08 18:43:57 +02:00
committed by GitHub
parent 2267c56f15
commit 238018dc7b
94 changed files with 1000 additions and 619 deletions
@@ -55,8 +55,11 @@ export class PageLayoutTabDTO {
@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?: PageLayoutTabOverrides | null;
@@ -7,7 +7,6 @@ import {
import {
Args,
Context,
Float,
Mutation,
Parent,
Query,
@@ -15,7 +14,6 @@ import {
} from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@@ -32,7 +30,6 @@ import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dt
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
import { resolvePageLayoutTabTitle } from 'src/engine/metadata-modules/page-layout-tab/utils/resolve-page-layout-tab-title.util';
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
import { resolveOverridableEntityProperty } from 'src/engine/metadata-modules/utils/resolve-overridable-entity-property.util';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
@MetadataResolver(() => PageLayoutTabDTO)
@@ -51,31 +48,15 @@ export class PageLayoutTabResolver {
@Parent() tab: PageLayoutTabDTO,
@Context() context: I18nContext,
): Promise<string> {
const resolvedTitle = resolveOverridableEntityProperty(tab, 'title');
const i18n = this.i18nService.getI18nInstance(context.req.locale);
return resolvePageLayoutTabTitle({
title: resolvedTitle,
title: tab.title,
applicationId: tab.applicationId,
i18nInstance: i18n,
});
}
@ResolveField(() => Float)
position(@Parent() tab: PageLayoutTabDTO): number {
return resolveOverridableEntityProperty(tab, 'position');
}
@ResolveField(() => String, { nullable: true })
icon(@Parent() tab: PageLayoutTabDTO): string | null | undefined {
return resolveOverridableEntityProperty(tab, 'icon');
}
@ResolveField(() => Boolean)
isOverridden(@Parent() tab: PageLayoutTabDTO): boolean {
return isDefined(tab.overrides) && Object.keys(tab.overrides).length > 0;
}
@Query(() => [PageLayoutTabDTO])
@UseGuards(NoPermissionGuard)
async getPageLayoutTabs(
@@ -1,5 +1,3 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
@@ -11,13 +9,14 @@ export const fromFlatPageLayoutTabToPageLayoutTabDto = (
updatedAt,
deletedAt,
widgetIds: _widgetIds,
overrides,
...rest
} = flatPageLayoutTab;
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,
@@ -74,8 +74,11 @@ export class PageLayoutWidgetDTO {
@Field(() => Date, { nullable: true })
deletedAt?: Date;
@Field(() => Boolean, { nullable: false })
isOverridden: boolean;
@Field(() => Boolean, {
nullable: true,
deprecationReason: 'isOverridden is deprecated',
})
isOverridden?: boolean;
@HideField()
overrides?: PageLayoutWidgetOverrides | null;
@@ -6,9 +6,7 @@ import {
} from '@nestjs/common';
import { Args, Mutation, Parent, Query, ResolveField } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -23,7 +21,6 @@ import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-wid
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service';
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
import { resolveOverridableEntityProperty } from 'src/engine/metadata-modules/utils/resolve-overridable-entity-property.util';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
@MetadataResolver(() => PageLayoutWidgetDTO)
@@ -98,30 +95,8 @@ export class PageLayoutWidgetResolver {
});
}
@ResolveField(() => String)
title(@Parent() widget: PageLayoutWidgetDTO): string {
return resolveOverridableEntityProperty(widget, 'title');
}
@ResolveField(() => GraphQLJSON, { nullable: true })
position(@Parent() widget: PageLayoutWidgetDTO) {
return resolveOverridableEntityProperty(widget, 'position');
}
@ResolveField(() => GraphQLJSON, { nullable: true })
conditionalDisplay(@Parent() widget: PageLayoutWidgetDTO) {
return resolveOverridableEntityProperty(widget, 'conditionalDisplay');
}
@ResolveField(() => WidgetConfiguration, { nullable: true })
configuration(@Parent() widget: PageLayoutWidgetDTO) {
return widget.configuration;
}
@ResolveField(() => Boolean)
isOverridden(@Parent() widget: PageLayoutWidgetDTO): boolean {
return (
isDefined(widget.overrides) && Object.keys(widget.overrides).length > 0
);
}
}
@@ -1,18 +1,22 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
export const fromFlatPageLayoutWidgetToPageLayoutWidgetDto = (
flatPageLayoutWidget: FlatPageLayoutWidget,
): PageLayoutWidgetDTO => {
const { createdAt, updatedAt, deletedAt, objectMetadataId, ...rest } =
flatPageLayoutWidget;
const {
createdAt,
updatedAt,
deletedAt,
objectMetadataId,
overrides,
...rest
} = flatPageLayoutWidget;
return {
...rest,
isOverridden:
isDefined(rest.overrides) && Object.keys(rest.overrides).length > 0,
...(overrides ?? {}),
isOverridden: false,
objectMetadataId: objectMetadataId ?? undefined,
createdAt: new Date(createdAt),
updatedAt: new Date(updatedAt),
@@ -15,6 +15,7 @@ import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorato
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
import { CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input';
import { UpdatePageLayoutWithTabsInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input';
@@ -136,4 +137,16 @@ export class PageLayoutResolver {
workspaceId: workspace.id,
});
}
@Mutation(() => PageLayoutTabDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async resetPageLayoutTabToDefault(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
return this.pageLayoutResetService.resetPageLayoutTabToDefault({
id,
workspaceId: workspace.id,
});
}
}
@@ -7,10 +7,19 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { splitEntitiesByResetStrategy } from 'src/engine/metadata-modules/flat-entity/utils/split-entities-by-reset-strategy.util';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { isFlatPageLayoutWidgetConfigurationOfType } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/is-flat-page-layout-widget-configuration-of-type.util';
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
import {
PageLayoutTabException,
PageLayoutTabExceptionCode,
PageLayoutTabExceptionMessageKey,
generatePageLayoutTabExceptionMessage,
} from 'src/engine/metadata-modules/page-layout-tab/exceptions/page-layout-tab.exception';
import { fromFlatPageLayoutTabToPageLayoutTabDto } from 'src/engine/metadata-modules/page-layout-tab/utils/from-flat-page-layout-tab-to-page-layout-tab-dto.util';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import {
PageLayoutWidgetException,
@@ -20,6 +29,7 @@ import {
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { DashboardSyncService } from 'src/modules/dashboard-sync/services/dashboard-sync.service';
@@ -33,6 +43,7 @@ export class PageLayoutResetService {
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
private readonly dashboardSyncService: DashboardSyncService,
private readonly viewService: ViewService,
) {}
async resetPageLayoutWidgetToDefault({
@@ -187,6 +198,303 @@ export class PageLayoutResetService {
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(updatedWidget);
}
async resetPageLayoutTabToDefault({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
const {
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
} =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
'flatViewFieldGroupMaps',
'flatViewFieldMaps',
],
},
);
const tab = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatPageLayoutTabMaps,
});
if (!isDefined(tab) || isDefined(tab.deletedAt)) {
throw new PageLayoutTabException(
generatePageLayoutTabExceptionMessage(
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND,
id,
),
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
);
}
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
if (
tab.applicationUniversalIdentifier ===
workspaceCustomFlatApplication.universalIdentifier
) {
throw new PageLayoutTabException(
`Custom tab "${id}" cannot be reset to default`,
PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA,
);
}
const now = new Date().toISOString();
const tabToUpdate: FlatPageLayoutTab = {
...tab,
overrides: null,
updatedAt: now,
};
const {
widgetsToUpdate,
widgetsToDelete,
viewFieldGroupsToUpdate,
viewFieldGroupsToDelete,
viewFieldsToUpdate,
viewFieldsToDelete,
orphanedViewIds,
} = this.computeTabChildResetOperations({
tabId: id,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
now,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutTab: {
flatEntityToCreate: [],
flatEntityToUpdate: [tabToUpdate],
flatEntityToDelete: [],
},
pageLayoutWidget: {
flatEntityToCreate: [],
flatEntityToUpdate: widgetsToUpdate,
flatEntityToDelete: widgetsToDelete,
},
viewFieldGroup: {
flatEntityToCreate: [],
flatEntityToUpdate: viewFieldGroupsToUpdate,
flatEntityToDelete: viewFieldGroupsToDelete,
},
viewField: {
flatEntityToCreate: [],
flatEntityToUpdate: viewFieldsToUpdate,
flatEntityToDelete: viewFieldsToDelete,
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while resetting page layout tab to default',
);
}
await this.destroyOrphanedFieldsWidgetViews({
viewIds: orphanedViewIds,
workspaceId,
});
const { flatPageLayoutTabMaps: recomputedTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutTabMaps'],
},
);
const updatedTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedTabMaps,
});
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByTabId({
tabId: id,
workspaceId,
updatedAt: new Date(updatedTab.updatedAt),
});
return fromFlatPageLayoutTabToPageLayoutTabDto(updatedTab);
}
private computeTabChildResetOperations({
tabId,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier,
now,
}: {
tabId: string;
flatPageLayoutWidgetMaps: {
byUniversalIdentifier: Record<string, FlatPageLayoutWidget | undefined>;
};
flatViewFieldGroupMaps: {
byUniversalIdentifier: Record<string, FlatViewFieldGroup | undefined>;
};
flatViewFieldMaps: {
byUniversalIdentifier: Record<string, FlatViewField | undefined>;
};
workspaceCustomApplicationUniversalIdentifier: string;
now: string;
}): {
widgetsToUpdate: FlatPageLayoutWidget[];
widgetsToDelete: FlatPageLayoutWidget[];
viewFieldGroupsToUpdate: FlatViewFieldGroup[];
viewFieldGroupsToDelete: FlatViewFieldGroup[];
viewFieldsToUpdate: FlatViewField[];
viewFieldsToDelete: FlatViewField[];
orphanedViewIds: string[];
} {
const existingWidgets = Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(widget) =>
widget.pageLayoutTabId === tabId && !isDefined(widget.deletedAt),
);
const { toHardDelete: widgetsToDelete, toReset: widgetsToReset } =
splitEntitiesByResetStrategy({
entities: existingWidgets,
workspaceCustomApplicationUniversalIdentifier,
now,
});
let allViewFieldGroupsToUpdate: FlatViewFieldGroup[] = [];
let allViewFieldGroupsToDelete: FlatViewFieldGroup[] = [];
let allViewFieldsToUpdate: FlatViewField[] = [];
let allViewFieldsToDelete: FlatViewField[] = [];
for (const widget of widgetsToReset) {
if (
!isFlatPageLayoutWidgetConfigurationOfType(
widget,
WidgetConfigurationType.FIELDS,
)
) {
continue;
}
const viewId = widget.configuration.viewId;
if (!isDefined(viewId)) {
continue;
}
const {
viewFieldGroupsToUpdate,
viewFieldGroupsToDelete,
viewFieldsToUpdate,
viewFieldsToDelete,
} = this.computeFieldsWidgetChildResetOperations({
viewId,
flatViewFieldGroupMaps,
flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier,
now,
});
allViewFieldGroupsToUpdate = [
...allViewFieldGroupsToUpdate,
...viewFieldGroupsToUpdate,
];
allViewFieldGroupsToDelete = [
...allViewFieldGroupsToDelete,
...viewFieldGroupsToDelete,
];
allViewFieldsToUpdate = [...allViewFieldsToUpdate, ...viewFieldsToUpdate];
allViewFieldsToDelete = [...allViewFieldsToDelete, ...viewFieldsToDelete];
}
const orphanedViewIds =
this.collectOrphanedViewIdsFromDeletedWidgets(widgetsToDelete);
return {
widgetsToUpdate: widgetsToReset,
widgetsToDelete,
viewFieldGroupsToUpdate: allViewFieldGroupsToUpdate,
viewFieldGroupsToDelete: allViewFieldGroupsToDelete,
viewFieldsToUpdate: allViewFieldsToUpdate,
viewFieldsToDelete: allViewFieldsToDelete,
orphanedViewIds,
};
}
private collectOrphanedViewIdsFromDeletedWidgets(
widgets: FlatPageLayoutWidget[],
): string[] {
const viewIds: string[] = [];
for (const widget of widgets) {
if (
widget.configuration.configurationType !==
WidgetConfigurationType.FIELDS
) {
continue;
}
const viewId = (widget.configuration as { viewId?: string | null })
.viewId;
if (typeof viewId === 'string') {
viewIds.push(viewId);
}
}
return viewIds;
}
private async destroyOrphanedFieldsWidgetViews({
viewIds,
workspaceId,
}: {
viewIds: string[];
workspaceId: string;
}): Promise<void> {
for (const viewId of viewIds) {
try {
await this.viewService.destroyOne({
destroyViewInput: { id: viewId },
workspaceId,
});
} catch (error) {
this.logger.warn(
`Failed to destroy orphaned view ${viewId} during tab reset: ${error}`,
);
}
}
}
private computeFieldsWidgetChildResetOperations({
viewId,
flatViewFieldGroupMaps,
@@ -33,6 +33,9 @@ import {
generatePageLayoutExceptionMessage,
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
import { fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util';
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
import { resolveFlatEntityOverridableProperties } from 'src/engine/metadata-modules/utils/resolve-flat-entity-overridable-properties.util';
import { sanitizeOverridableEntityInput } from 'src/engine/metadata-modules/utils/sanitize-overridable-entity-input.util';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@@ -277,6 +280,10 @@ export class PageLayoutUpdateService {
.filter(isDefined)
.filter((tab) => tab.pageLayoutId === existingPageLayout.id);
const resolvedExistingTabs = existingTabs.map(
resolveFlatEntityOverridableProperties,
);
const {
toCreate: entitiesToCreate,
toUpdate: entitiesToUpdate,
@@ -286,7 +293,7 @@ export class PageLayoutUpdateService {
FlatPageLayoutTab,
UpdatePageLayoutTabWithWidgetsInput
>({
existingObjects: existingTabs,
existingObjects: resolvedExistingTabs,
receivedObjects: tabs,
propertiesToCompare: FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES,
isEntityIncluded: (entity) => entity.isActive,
@@ -329,11 +336,33 @@ export class PageLayoutUpdateService {
flatEntityMaps: flatPageLayoutTabMaps,
});
return {
...existingTab,
const shouldOverride = isCallerOverridingEntity({
callerApplicationUniversalIdentifier:
workspaceCustomApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier:
existingTab.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
});
const editableProperties = {
title: tabInput.title,
position: tabInput.position,
...(tabInput.icon !== undefined && { icon: tabInput.icon }),
layoutMode: tabInput.layoutMode ?? existingTab.layoutMode,
};
const { overrides, updatedEditableProperties } =
sanitizeOverridableEntityInput({
metadataName: 'pageLayoutTab',
existingFlatEntity: existingTab,
updatedEditableProperties: editableProperties,
shouldOverride,
});
return {
...existingTab,
...updatedEditableProperties,
overrides,
updatedAt: now.toISOString(),
};
},
@@ -346,11 +375,33 @@ export class PageLayoutUpdateService {
flatEntityMaps: flatPageLayoutTabMaps,
});
return {
...existingTab,
const shouldOverride = isCallerOverridingEntity({
callerApplicationUniversalIdentifier:
workspaceCustomApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier:
existingTab.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
});
const editableProperties = {
title: tabInput.title,
position: tabInput.position,
...(tabInput.icon !== undefined && { icon: tabInput.icon }),
layoutMode: tabInput.layoutMode ?? existingTab.layoutMode,
};
const { overrides, updatedEditableProperties } =
sanitizeOverridableEntityInput({
metadataName: 'pageLayoutTab',
existingFlatEntity: existingTab,
updatedEditableProperties: editableProperties,
shouldOverride,
});
return {
...existingTab,
...updatedEditableProperties,
overrides,
isActive: true,
updatedAt: now.toISOString(),
};
@@ -493,6 +544,10 @@ export class PageLayoutUpdateService {
.filter(isDefined)
.filter((widget) => widget.pageLayoutTabId === tabId);
const resolvedExistingWidgets = existingWidgets.map(
resolveFlatEntityOverridableProperties,
);
const {
toCreate: entitiesToCreate,
toUpdate: entitiesToUpdate,
@@ -502,7 +557,7 @@ export class PageLayoutUpdateService {
FlatPageLayoutWidget,
UpdatePageLayoutWidgetWithIdInput
>({
existingObjects: existingWidgets,
existingObjects: resolvedExistingWidgets,
receivedObjects: widgets,
propertiesToCompare: FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES,
isEntityIncluded: (entity) => entity.isActive,
@@ -555,16 +610,37 @@ export class PageLayoutUpdateService {
flatEntityMaps: flatPageLayoutWidgetMaps,
});
const shouldOverride = isCallerOverridingEntity({
callerApplicationUniversalIdentifier:
workspaceCustomApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier:
existingWidget.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
});
const commonProperties = buildFlatPageLayoutWidgetCommonProperties({
widgetInput,
flatPageLayoutTabMaps,
flatObjectMetadataMaps,
});
const updatedConfiguration = widgetInput.configuration ?? null;
const { overrides, updatedEditableProperties } =
sanitizeOverridableEntityInput({
metadataName: 'pageLayoutWidget',
existingFlatEntity: existingWidget,
updatedEditableProperties: {
...commonProperties,
configuration: updatedConfiguration,
},
shouldOverride,
});
return {
...existingWidget,
...buildFlatPageLayoutWidgetCommonProperties({
widgetInput,
flatPageLayoutTabMaps,
flatObjectMetadataMaps,
}),
configuration: updatedConfiguration,
...updatedEditableProperties,
overrides,
updatedAt: now.toISOString(),
...(isDefined(updatedConfiguration) && {
universalConfiguration:
@@ -591,16 +667,37 @@ export class PageLayoutUpdateService {
flatEntityMaps: flatPageLayoutWidgetMaps,
});
const shouldOverride = isCallerOverridingEntity({
callerApplicationUniversalIdentifier:
workspaceCustomApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier:
existingWidget.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
});
const commonProperties = buildFlatPageLayoutWidgetCommonProperties({
widgetInput,
flatPageLayoutTabMaps,
flatObjectMetadataMaps,
});
const restoredConfiguration = widgetInput.configuration ?? null;
const { overrides, updatedEditableProperties } =
sanitizeOverridableEntityInput({
metadataName: 'pageLayoutWidget',
existingFlatEntity: existingWidget,
updatedEditableProperties: {
...commonProperties,
configuration: restoredConfiguration,
},
shouldOverride,
});
return {
...existingWidget,
...buildFlatPageLayoutWidgetCommonProperties({
widgetInput,
flatPageLayoutTabMaps,
flatObjectMetadataMaps,
}),
configuration: restoredConfiguration,
...updatedEditableProperties,
overrides,
isActive: true,
updatedAt: now.toISOString(),
...(isDefined(restoredConfiguration) && {
@@ -0,0 +1,21 @@
import { isDefined } from 'twenty-shared/utils';
type FlatEntityWithOverrides = {
[key: string]: unknown;
overrides: Record<string, unknown> | null;
};
export const resolveFlatEntityOverridableProperties = <
T extends FlatEntityWithOverrides,
>(
flatEntity: T,
): T => {
if (!isDefined(flatEntity.overrides)) {
return flatEntity;
}
return {
...flatEntity,
...flatEntity.overrides,
};
};
@@ -38,7 +38,10 @@ export class ViewFieldGroupDTO {
@Field(() => [ViewFieldDTO])
viewFields?: ViewFieldDTO[];
@Field(() => Boolean, { nullable: false })
@Field(() => Boolean, {
nullable: false,
deprecationReason: 'isOverridden is deprecated',
})
isOverridden: boolean;
@HideField()
@@ -2,7 +2,6 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import {
Args,
Context,
Float,
Mutation,
Parent,
Query,
@@ -10,7 +9,6 @@ import {
} from '@nestjs/graphql';
import { isArray } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@@ -19,14 +17,12 @@ import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
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 { CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input';
import { DeleteViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input';
import { DestroyViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input';
import { UpdateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input';
import { UpsertFieldsWidgetInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/upsert-fields-widget.input';
import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
import { FieldsWidgetUpsertService } from 'src/engine/metadata-modules/view-field-group/services/fields-widget-upsert.service';
import { ViewFieldGroupService } from 'src/engine/metadata-modules/view-field-group/services/view-field-group.service';
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
@@ -43,35 +39,12 @@ export class ViewFieldGroupResolver {
private readonly fieldsWidgetUpsertService: FieldsWidgetUpsertService,
) {}
@ResolveField(() => String)
name(@Parent() viewFieldGroup: ViewFieldGroupDTO): string {
return resolveOverridableEntityProperty(viewFieldGroup, 'name');
}
@ResolveField(() => Float)
position(@Parent() viewFieldGroup: ViewFieldGroupDTO): number {
return resolveOverridableEntityProperty(viewFieldGroup, 'position');
}
@ResolveField(() => Boolean)
isVisible(@Parent() viewFieldGroup: ViewFieldGroupDTO): boolean {
return resolveOverridableEntityProperty(viewFieldGroup, 'isVisible');
}
@ResolveField(() => Boolean)
isOverridden(@Parent() viewFieldGroup: ViewFieldGroupDTO): boolean {
return (
isDefined(viewFieldGroup.overrides) &&
Object.keys(viewFieldGroup.overrides).length > 0
);
}
@Query(() => [ViewFieldGroupDTO])
@UseGuards(NoPermissionGuard)
async getViewFieldGroups(
@Args('viewId', { type: () => String }) viewId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldGroupEntity[]> {
): Promise<ViewFieldGroupDTO[]> {
return this.viewFieldGroupService.findByViewId(workspace.id, viewId);
}
@@ -80,7 +53,7 @@ export class ViewFieldGroupResolver {
async getViewFieldGroup(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldGroupEntity | null> {
): Promise<ViewFieldGroupDTO | null> {
return this.viewFieldGroupService.findById(id, workspace.id);
}
@@ -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 { fromCreateViewFieldGroupInputToFlatViewFieldGroupToCreate } from 'src/engine/metadata-modules/flat-view-field-group/utils/from-create-view-field-group-input-to-flat-view-field-group-to-create.util';
@@ -17,7 +16,6 @@ import { DeleteViewFieldGroupInput } from 'src/engine/metadata-modules/view-fiel
import { DestroyViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input';
import { UpdateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input';
import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
import {
ViewFieldGroupException,
ViewFieldGroupExceptionCode,
@@ -31,8 +29,6 @@ export class ViewFieldGroupService {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
@InjectRepository(ViewFieldGroupEntity)
private readonly viewFieldGroupRepository: Repository<ViewFieldGroupEntity>,
private readonly applicationService: ApplicationService,
) {}
@@ -342,30 +338,51 @@ export class ViewFieldGroupService {
async findByViewId(
workspaceId: string,
viewId: string,
): Promise<ViewFieldGroupEntity[]> {
return this.viewFieldGroupRepository.find({
where: {
workspaceId,
viewId,
isActive: true,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
});
): Promise<ViewFieldGroupDTO[]> {
const { flatViewFieldGroupMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldGroupMaps'],
},
);
return Object.values(flatViewFieldGroupMaps.byUniversalIdentifier)
.filter(isDefined)
.filter(
(group) =>
group.viewId === viewId &&
group.isActive &&
!isDefined(group.deletedAt),
)
.map(fromFlatViewFieldGroupToViewFieldGroupDto)
.sort((a, b) => a.position - b.position);
}
async findById(
id: string,
workspaceId: string,
): Promise<ViewFieldGroupEntity | null> {
const viewFieldGroup = await this.viewFieldGroupRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
): Promise<ViewFieldGroupDTO | null> {
const { flatViewFieldGroupMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldGroupMaps'],
},
);
const flatViewFieldGroup = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatViewFieldGroupMaps,
});
return viewFieldGroup || null;
if (
!isDefined(flatViewFieldGroup) ||
isDefined(flatViewFieldGroup.deletedAt)
) {
return null;
}
return fromFlatViewFieldGroupToViewFieldGroupDto(flatViewFieldGroup);
}
}
@@ -1,17 +1,16 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
export const fromFlatViewFieldGroupToViewFieldGroupDto = (
flatViewFieldGroup: FlatViewFieldGroup,
): ViewFieldGroupDTO => {
const { createdAt, updatedAt, deletedAt, ...rest } = flatViewFieldGroup;
const { createdAt, updatedAt, deletedAt, overrides, ...rest } =
flatViewFieldGroup;
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,
@@ -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)) {
@@ -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;
@@ -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);
}
@@ -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);
}
}
@@ -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,