Implement page layout override (#18472)
This commit is contained in:
+9
-1
@@ -145,8 +145,12 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"title",
|
||||
"position",
|
||||
"deletedAt",
|
||||
"icon",
|
||||
"overrides",
|
||||
],
|
||||
"propertiesToStringify": [
|
||||
"overrides",
|
||||
],
|
||||
"propertiesToStringify": [],
|
||||
},
|
||||
"pageLayoutWidget": {
|
||||
"propertiesToCompare": [
|
||||
@@ -157,11 +161,15 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"position",
|
||||
"universalConfiguration",
|
||||
"deletedAt",
|
||||
"conditionalDisplay",
|
||||
"overrides",
|
||||
],
|
||||
"propertiesToStringify": [
|
||||
"gridPosition",
|
||||
"position",
|
||||
"universalConfiguration",
|
||||
"conditionalDisplay",
|
||||
"overrides",
|
||||
],
|
||||
},
|
||||
"role": {
|
||||
|
||||
+28
-2
@@ -34,6 +34,7 @@ type MetadataEntityPropertyConfiguration<
|
||||
: HasObjectInUnion<MetadataEntity<TMetadataName>[K]>
|
||||
: boolean;
|
||||
toCompare: boolean;
|
||||
isOverridable?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -834,6 +835,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
isOverridable: true,
|
||||
},
|
||||
type: { toCompare: true, toStringify: false, universalProperty: undefined },
|
||||
objectMetadataId: {
|
||||
@@ -850,6 +852,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
universalProperty: undefined,
|
||||
isOverridable: true,
|
||||
},
|
||||
configuration: {
|
||||
toCompare: true,
|
||||
@@ -877,7 +880,13 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
universalProperty: 'pageLayoutTabUniversalIdentifier',
|
||||
},
|
||||
conditionalDisplay: {
|
||||
toCompare: false,
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
universalProperty: undefined,
|
||||
isOverridable: true,
|
||||
},
|
||||
overrides: {
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
@@ -887,11 +896,13 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
isOverridable: true,
|
||||
},
|
||||
position: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
isOverridable: true,
|
||||
},
|
||||
deletedAt: {
|
||||
toCompare: true,
|
||||
@@ -899,9 +910,10 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
universalProperty: undefined,
|
||||
},
|
||||
icon: {
|
||||
toCompare: false,
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
isOverridable: true,
|
||||
},
|
||||
createdAt: {
|
||||
toCompare: false,
|
||||
@@ -923,6 +935,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
overrides: {
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
},
|
||||
skill: {
|
||||
name: { toCompare: true, toStringify: false, universalProperty: undefined },
|
||||
@@ -1341,3 +1358,12 @@ export type MetadataEntityComparablePropertyName<T extends AllMetadataName> =
|
||||
FilterComparableKeys<
|
||||
(typeof ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME)[T]
|
||||
>;
|
||||
|
||||
type FilterOverridableKeys<TConfig> = {
|
||||
[P in keyof TConfig]: TConfig[P] extends { isOverridable: true } ? P : never;
|
||||
}[keyof TConfig];
|
||||
|
||||
export type MetadataEntityOverridablePropertyName<T extends AllMetadataName> =
|
||||
FilterOverridableKeys<
|
||||
(typeof ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME)[T]
|
||||
>;
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
ALL_METADATA_NAME,
|
||||
type AllMetadataName,
|
||||
} from 'twenty-shared/metadata';
|
||||
|
||||
import {
|
||||
ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME,
|
||||
type MetadataEntityOverridablePropertyName,
|
||||
} from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
|
||||
|
||||
const computeOverridableProperties = <T extends AllMetadataName>(
|
||||
metadataName: T,
|
||||
): MetadataEntityOverridablePropertyName<T>[] => {
|
||||
const config =
|
||||
ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME[metadataName];
|
||||
|
||||
return (Object.entries(config) as [string, { isOverridable?: boolean }][])
|
||||
.filter(([_, conf]) => conf.isOverridable === true)
|
||||
.map(([property]) => property as MetadataEntityOverridablePropertyName<T>);
|
||||
};
|
||||
|
||||
export const ALL_OVERRIDABLE_PROPERTIES_BY_METADATA_NAME = Object.values(
|
||||
ALL_METADATA_NAME,
|
||||
).reduce(
|
||||
(acc, metadataName) => ({
|
||||
...acc,
|
||||
[metadataName]: computeOverridableProperties(metadataName),
|
||||
}),
|
||||
{} as {
|
||||
[P in AllMetadataName]: MetadataEntityOverridablePropertyName<P>[];
|
||||
},
|
||||
);
|
||||
+1
@@ -3,4 +3,5 @@ import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/fla
|
||||
export const FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES = [
|
||||
'title',
|
||||
'position',
|
||||
'icon',
|
||||
] as const satisfies MetadataEntityPropertyName<'pageLayoutTab'>[];
|
||||
|
||||
+1
@@ -57,5 +57,6 @@ export const fromCreatePageLayoutTabInputToFlatPageLayoutTabToCreate = ({
|
||||
widgetUniversalIdentifiers: [],
|
||||
icon: null,
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
overrides: null,
|
||||
};
|
||||
};
|
||||
|
||||
+29
-5
@@ -13,6 +13,8 @@ import {
|
||||
PageLayoutTabException,
|
||||
PageLayoutTabExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout-tab/exceptions/page-layout-tab.exception';
|
||||
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
|
||||
import { sanitizeOverridableEntityInput } from 'src/engine/metadata-modules/utils/sanitize-overridable-entity-input.util';
|
||||
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
|
||||
|
||||
export type UpdatePageLayoutTabInputWithId = {
|
||||
@@ -23,9 +25,13 @@ export type UpdatePageLayoutTabInputWithId = {
|
||||
export const fromUpdatePageLayoutTabInputToFlatPageLayoutTabToUpdateOrThrow = ({
|
||||
updatePageLayoutTabInput: rawUpdatePageLayoutTabInput,
|
||||
flatPageLayoutTabMaps,
|
||||
callerApplicationUniversalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
}: {
|
||||
updatePageLayoutTabInput: UpdatePageLayoutTabInputWithId;
|
||||
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
|
||||
callerApplicationUniversalIdentifier: string;
|
||||
workspaceCustomApplicationUniversalIdentifier: string;
|
||||
}): FlatPageLayoutTab => {
|
||||
const { id: pageLayoutTabToUpdateId } = extractAndSanitizeObjectStringFields(
|
||||
rawUpdatePageLayoutTabInput,
|
||||
@@ -44,14 +50,32 @@ export const fromUpdatePageLayoutTabInputToFlatPageLayoutTabToUpdateOrThrow = ({
|
||||
);
|
||||
}
|
||||
|
||||
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
|
||||
const editableProperties = extractAndSanitizeObjectStringFields(
|
||||
rawUpdatePageLayoutTabInput.update,
|
||||
FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES,
|
||||
);
|
||||
|
||||
return mergeUpdateInExistingRecord({
|
||||
existing: existingFlatPageLayoutTabToUpdate,
|
||||
properties: [...FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES],
|
||||
update: updatedEditableFieldProperties,
|
||||
const shouldOverride = isCallerOverridingEntity({
|
||||
callerApplicationUniversalIdentifier,
|
||||
entityApplicationUniversalIdentifier:
|
||||
existingFlatPageLayoutTabToUpdate.applicationUniversalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const { overrides, updatedEditableProperties } =
|
||||
sanitizeOverridableEntityInput({
|
||||
metadataName: 'pageLayoutTab',
|
||||
existingFlatEntity: existingFlatPageLayoutTabToUpdate,
|
||||
updatedEditableProperties: editableProperties,
|
||||
shouldOverride,
|
||||
});
|
||||
|
||||
return {
|
||||
...mergeUpdateInExistingRecord({
|
||||
existing: existingFlatPageLayoutTabToUpdate,
|
||||
properties: [...FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES],
|
||||
update: updatedEditableProperties,
|
||||
}),
|
||||
overrides,
|
||||
} as FlatPageLayoutTab;
|
||||
};
|
||||
|
||||
+1
@@ -53,5 +53,6 @@ export const transformPageLayoutTabEntityToFlatPageLayoutTab = ({
|
||||
widgetUniversalIdentifiers: pageLayoutTabEntity.widgets.map(
|
||||
(widget) => widget.universalIdentifier,
|
||||
),
|
||||
overrides: pageLayoutTabEntity.overrides ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
+1
@@ -7,4 +7,5 @@ export const FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES = [
|
||||
'gridPosition',
|
||||
'position',
|
||||
'configuration',
|
||||
'conditionalDisplay',
|
||||
] as const satisfies MetadataEntityPropertyName<'pageLayoutWidget'>[];
|
||||
|
||||
+1
@@ -73,6 +73,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
conditionalDisplay: null,
|
||||
overrides: null,
|
||||
universalConfiguration:
|
||||
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
|
||||
configuration: createPageLayoutWidgetInput.configuration,
|
||||
|
||||
+33
-12
@@ -17,6 +17,8 @@ import {
|
||||
PageLayoutWidgetExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { validateWidgetConfigurationInput } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util';
|
||||
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
|
||||
import { sanitizeOverridableEntityInput } from 'src/engine/metadata-modules/utils/sanitize-overridable-entity-input.util';
|
||||
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
|
||||
|
||||
export type UpdatePageLayoutWidgetInputWithId = {
|
||||
@@ -33,9 +35,13 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro
|
||||
flatFrontComponentMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewMaps,
|
||||
callerApplicationUniversalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
}: {
|
||||
updatePageLayoutWidgetInput: UpdatePageLayoutWidgetInputWithId;
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
callerApplicationUniversalIdentifier: string;
|
||||
workspaceCustomApplicationUniversalIdentifier: string;
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
| 'flatObjectMetadataMaps'
|
||||
@@ -62,29 +68,44 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro
|
||||
);
|
||||
}
|
||||
|
||||
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
|
||||
const editableProperties = extractAndSanitizeObjectStringFields(
|
||||
rawUpdatePageLayoutWidgetInput.update,
|
||||
FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES,
|
||||
);
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
updatedEditableFieldProperties,
|
||||
'configuration',
|
||||
)
|
||||
Object.prototype.hasOwnProperty.call(editableProperties, 'configuration')
|
||||
) {
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: updatedEditableFieldProperties.configuration,
|
||||
configuration: editableProperties.configuration,
|
||||
});
|
||||
}
|
||||
|
||||
const flatPageLayoutWidgetToUpdate = mergeUpdateInExistingRecord({
|
||||
existing: existingFlatPageLayoutWidgetToUpdate,
|
||||
properties: FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES,
|
||||
update: updatedEditableFieldProperties,
|
||||
const shouldOverride = isCallerOverridingEntity({
|
||||
callerApplicationUniversalIdentifier,
|
||||
entityApplicationUniversalIdentifier:
|
||||
existingFlatPageLayoutWidgetToUpdate.applicationUniversalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
if (updatedEditableFieldProperties.objectMetadataId !== undefined) {
|
||||
const { overrides, updatedEditableProperties } =
|
||||
sanitizeOverridableEntityInput({
|
||||
metadataName: 'pageLayoutWidget',
|
||||
existingFlatEntity: existingFlatPageLayoutWidgetToUpdate,
|
||||
updatedEditableProperties: editableProperties,
|
||||
shouldOverride,
|
||||
});
|
||||
|
||||
const flatPageLayoutWidgetToUpdate = {
|
||||
...mergeUpdateInExistingRecord({
|
||||
existing: existingFlatPageLayoutWidgetToUpdate,
|
||||
properties: FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES,
|
||||
update: updatedEditableProperties,
|
||||
}),
|
||||
overrides,
|
||||
};
|
||||
|
||||
if (updatedEditableProperties.objectMetadataId !== undefined) {
|
||||
const { objectMetadataUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'pageLayoutWidget',
|
||||
@@ -98,7 +119,7 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro
|
||||
objectMetadataUniversalIdentifier;
|
||||
}
|
||||
|
||||
if (isDefined(updatedEditableFieldProperties.configuration)) {
|
||||
if (isDefined(updatedEditableProperties.configuration)) {
|
||||
flatPageLayoutWidgetToUpdate.universalConfiguration =
|
||||
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
|
||||
configuration: flatPageLayoutWidgetToUpdate.configuration,
|
||||
|
||||
+1
@@ -97,6 +97,7 @@ const buildFieldsWidget = ({
|
||||
},
|
||||
},
|
||||
universalConfiguration: null,
|
||||
overrides: null,
|
||||
});
|
||||
|
||||
const buildFlatPageLayoutWidgetMaps = (
|
||||
|
||||
+2
@@ -70,6 +70,7 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({
|
||||
deletedAt: null,
|
||||
icon: tabProps.icon,
|
||||
layoutMode: tabProps.layoutMode,
|
||||
overrides: null,
|
||||
});
|
||||
|
||||
const isFieldsWidget = widgetKey === 'fields';
|
||||
@@ -120,6 +121,7 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
conditionalDisplay: null,
|
||||
overrides: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -5,6 +5,7 @@ import {
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
@@ -29,6 +30,11 @@ export class UpdatePageLayoutTabWithWidgetsInput {
|
||||
@IsNotEmpty()
|
||||
position: number;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
icon?: string | null;
|
||||
|
||||
@Field(() => [UpdatePageLayoutWidgetWithIdInput])
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
|
||||
+5
@@ -13,4 +13,9 @@ export class UpdatePageLayoutTabInput {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
position?: number;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
icon?: string | null;
|
||||
}
|
||||
|
||||
+14
-1
@@ -1,9 +1,16 @@
|
||||
import { Field, Float, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import {
|
||||
Field,
|
||||
Float,
|
||||
HideField,
|
||||
ObjectType,
|
||||
registerEnumType,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { type PageLayoutTabOverrides } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
|
||||
|
||||
registerEnumType(PageLayoutTabLayoutMode, {
|
||||
@@ -47,4 +54,10 @@ export class PageLayoutTabDTO {
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt?: Date | null;
|
||||
|
||||
@Field(() => Boolean, { nullable: false })
|
||||
isOverridden: boolean;
|
||||
|
||||
@HideField()
|
||||
overrides?: PageLayoutTabOverrides | null;
|
||||
}
|
||||
|
||||
+8
-2
@@ -17,7 +17,13 @@ import {
|
||||
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { OverridableEntity } from 'src/engine/workspace-manager/types/overridable-entity';
|
||||
|
||||
export type PageLayoutTabOverrides = {
|
||||
title?: string;
|
||||
position?: number;
|
||||
icon?: string | null;
|
||||
};
|
||||
|
||||
@Entity({ name: 'pageLayoutTab', schema: 'core' })
|
||||
@ObjectType('PageLayoutTab')
|
||||
@@ -27,7 +33,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
{ where: '"deletedAt" IS NULL' },
|
||||
)
|
||||
export class PageLayoutTabEntity
|
||||
extends SyncableEntity
|
||||
extends OverridableEntity<PageLayoutTabOverrides>
|
||||
implements Required<PageLayoutTabEntity>
|
||||
{
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
|
||||
+14
-1
@@ -7,6 +7,7 @@ import {
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
Float,
|
||||
Mutation,
|
||||
Parent,
|
||||
Query,
|
||||
@@ -30,6 +31,7 @@ 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)
|
||||
@@ -48,15 +50,26 @@ 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: tab.title,
|
||||
title: resolvedTitle,
|
||||
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');
|
||||
}
|
||||
|
||||
@Query(() => [PageLayoutTabDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
async getPageLayoutTabs(
|
||||
|
||||
+4
@@ -233,6 +233,10 @@ export class PageLayoutTabService {
|
||||
fromUpdatePageLayoutTabInputToFlatPageLayoutTabToUpdateOrThrow({
|
||||
updatePageLayoutTabInput,
|
||||
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
|
||||
callerApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
|
||||
+4
@@ -1,3 +1,5 @@
|
||||
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';
|
||||
|
||||
@@ -14,6 +16,8 @@ export const fromFlatPageLayoutTabToPageLayoutTabDto = (
|
||||
|
||||
return {
|
||||
...rest,
|
||||
isOverridden:
|
||||
isDefined(rest.overrides) && Object.keys(rest.overrides).length > 0,
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
deletedAt: deletedAt ? new Date(deletedAt) : null,
|
||||
|
||||
+9
-1
@@ -11,7 +11,10 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { PageLayoutWidgetPosition } from 'twenty-shared/types';
|
||||
import {
|
||||
PageLayoutWidgetConditionalDisplay,
|
||||
PageLayoutWidgetPosition,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { GridPositionInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/grid-position.input';
|
||||
@@ -60,4 +63,9 @@ export class UpdatePageLayoutWidgetWithIdInput {
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
configuration: AllPageLayoutWidgetConfiguration;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null;
|
||||
}
|
||||
|
||||
+9
-1
@@ -10,7 +10,10 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { PageLayoutWidgetPosition } from 'twenty-shared/types';
|
||||
import {
|
||||
PageLayoutWidgetConditionalDisplay,
|
||||
PageLayoutWidgetPosition,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { GridPositionInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/grid-position.input';
|
||||
@@ -49,4 +52,9 @@ export class UpdatePageLayoutWidgetInput {
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
configuration?: AllPageLayoutWidgetConfiguration;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null;
|
||||
}
|
||||
|
||||
+13
-1
@@ -1,4 +1,9 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import {
|
||||
Field,
|
||||
HideField,
|
||||
ObjectType,
|
||||
registerEnumType,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
@@ -8,6 +13,7 @@ import {
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { type PageLayoutWidgetOverrides } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
import { PageLayoutWidgetPositionUnion } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-position.union';
|
||||
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
@@ -67,4 +73,10 @@ export class PageLayoutWidgetDTO {
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt?: Date;
|
||||
|
||||
@Field(() => Boolean, { nullable: false })
|
||||
isOverridden: boolean;
|
||||
|
||||
@HideField()
|
||||
overrides?: PageLayoutWidgetOverrides | null;
|
||||
}
|
||||
|
||||
+8
-2
@@ -23,9 +23,15 @@ import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { PageLayoutWidgetConfigurationTypeSettings } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configuration.type';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { OverridableEntity } from 'src/engine/workspace-manager/types/overridable-entity';
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
export type PageLayoutWidgetOverrides = {
|
||||
title?: string;
|
||||
position?: PageLayoutWidgetPosition | null;
|
||||
conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null;
|
||||
};
|
||||
|
||||
@Entity({ name: 'pageLayoutWidget', schema: 'core' })
|
||||
@ObjectType('PageLayoutWidget')
|
||||
@Index(
|
||||
@@ -38,7 +44,7 @@ export class PageLayoutWidgetEntity<
|
||||
TWidgetConfigurationType extends
|
||||
WidgetConfigurationType = WidgetConfigurationType,
|
||||
>
|
||||
extends SyncableEntity
|
||||
extends OverridableEntity<PageLayoutWidgetOverrides>
|
||||
implements Required<PageLayoutWidgetEntity>
|
||||
{
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
|
||||
+17
@@ -6,6 +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 { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
@@ -21,6 +22,7 @@ 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)
|
||||
@@ -95,6 +97,21 @@ 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;
|
||||
|
||||
+28
-16
@@ -327,24 +327,32 @@ export class PageLayoutWidgetService {
|
||||
existingFlatPageLayoutWidgetMaps,
|
||||
);
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatFrontComponentMaps: existingFlatFrontComponentMaps,
|
||||
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
const [
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatFrontComponentMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
'flatViewMaps',
|
||||
],
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatFrontComponentMaps: existingFlatFrontComponentMaps,
|
||||
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
},
|
||||
);
|
||||
{ workspaceCustomFlatApplication },
|
||||
] = await Promise.all([
|
||||
this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatFrontComponentMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
'flatViewMaps',
|
||||
],
|
||||
},
|
||||
),
|
||||
this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
),
|
||||
]);
|
||||
|
||||
const isConfigurationBeingUpdated = Object.prototype.hasOwnProperty.call(
|
||||
updateData,
|
||||
@@ -377,6 +385,10 @@ export class PageLayoutWidgetService {
|
||||
flatFrontComponentMaps: existingFlatFrontComponentMaps,
|
||||
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
callerApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
});
|
||||
|
||||
const shouldValidateChartFields =
|
||||
|
||||
+4
@@ -1,3 +1,5 @@
|
||||
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';
|
||||
|
||||
@@ -9,6 +11,8 @@ export const fromFlatPageLayoutWidgetToPageLayoutWidgetDto = (
|
||||
|
||||
return {
|
||||
...rest,
|
||||
isOverridden:
|
||||
isDefined(rest.overrides) && Object.keys(rest.overrides).length > 0,
|
||||
objectMetadataId: objectMetadataId ?? undefined,
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
|
||||
+2
@@ -295,6 +295,7 @@ export class PageLayoutUpdateService {
|
||||
widgetUniversalIdentifiers: [],
|
||||
icon: null,
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
overrides: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -498,6 +499,7 @@ export class PageLayoutUpdateService {
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
conditionalDisplay: null,
|
||||
overrides: null,
|
||||
universalConfiguration:
|
||||
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
|
||||
configuration: widgetInput.configuration,
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
|
||||
|
||||
const CUSTOM_APP_ID = 'custom-app-universal-id';
|
||||
const STANDARD_APP_ID = 'standard-app-universal-id';
|
||||
const OTHER_APP_ID = 'other-app-universal-id';
|
||||
|
||||
describe('isCallerOverridingEntity', () => {
|
||||
it('should return true when custom app updates a standard-app entity', () => {
|
||||
expect(
|
||||
isCallerOverridingEntity({
|
||||
callerApplicationUniversalIdentifier: CUSTOM_APP_ID,
|
||||
entityApplicationUniversalIdentifier: STANDARD_APP_ID,
|
||||
workspaceCustomApplicationUniversalIdentifier: CUSTOM_APP_ID,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when custom app updates its own entity', () => {
|
||||
expect(
|
||||
isCallerOverridingEntity({
|
||||
callerApplicationUniversalIdentifier: CUSTOM_APP_ID,
|
||||
entityApplicationUniversalIdentifier: CUSTOM_APP_ID,
|
||||
workspaceCustomApplicationUniversalIdentifier: CUSTOM_APP_ID,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when a non-custom app updates another app entity', () => {
|
||||
expect(
|
||||
isCallerOverridingEntity({
|
||||
callerApplicationUniversalIdentifier: OTHER_APP_ID,
|
||||
entityApplicationUniversalIdentifier: STANDARD_APP_ID,
|
||||
workspaceCustomApplicationUniversalIdentifier: CUSTOM_APP_ID,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { resolveOverridableEntityProperty } from 'src/engine/metadata-modules/utils/resolve-overridable-entity-property.util';
|
||||
|
||||
type TestEntity = {
|
||||
title: string;
|
||||
position: number;
|
||||
icon: string | null;
|
||||
overrides?: Partial<TestEntity> | null;
|
||||
};
|
||||
|
||||
describe('resolveOverridableEntityProperty', () => {
|
||||
it('should return override value when override exists for the property', () => {
|
||||
const entity: TestEntity = {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: null,
|
||||
overrides: { title: 'Overridden Title' },
|
||||
};
|
||||
|
||||
expect(resolveOverridableEntityProperty(entity, 'title')).toBe(
|
||||
'Overridden Title',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return base value when overrides is null', () => {
|
||||
const entity: TestEntity = {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: null,
|
||||
overrides: null,
|
||||
};
|
||||
|
||||
expect(resolveOverridableEntityProperty(entity, 'title')).toBe(
|
||||
'Base Title',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return base value when overrides exist but not for the requested property', () => {
|
||||
const entity: TestEntity = {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: null,
|
||||
overrides: { position: 5 },
|
||||
};
|
||||
|
||||
expect(resolveOverridableEntityProperty(entity, 'title')).toBe(
|
||||
'Base Title',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return base value when overrides is undefined', () => {
|
||||
const entity: TestEntity = {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: null,
|
||||
overrides: undefined,
|
||||
};
|
||||
|
||||
expect(resolveOverridableEntityProperty(entity, 'title')).toBe(
|
||||
'Base Title',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null when override explicitly sets a nullable property to null', () => {
|
||||
const entity: TestEntity = {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: 'IconStar',
|
||||
overrides: { icon: null },
|
||||
};
|
||||
|
||||
expect(resolveOverridableEntityProperty(entity, 'icon')).toBeNull();
|
||||
});
|
||||
});
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { sanitizeOverridableEntityInput } from 'src/engine/metadata-modules/utils/sanitize-overridable-entity-input.util';
|
||||
|
||||
describe('sanitizeOverridableEntityInput', () => {
|
||||
describe('when shouldOverride is false', () => {
|
||||
it('should pass through properties unchanged and preserve existing overrides', () => {
|
||||
const existingOverrides = { title: 'Previous Override' };
|
||||
const result = sanitizeOverridableEntityInput({
|
||||
metadataName: 'pageLayoutTab',
|
||||
existingFlatEntity: {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: null,
|
||||
overrides: existingOverrides,
|
||||
},
|
||||
updatedEditableProperties: { title: 'New Title' },
|
||||
shouldOverride: false,
|
||||
});
|
||||
|
||||
expect(result.updatedEditableProperties).toEqual({ title: 'New Title' });
|
||||
expect(result.overrides).toBe(existingOverrides);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when shouldOverride is true', () => {
|
||||
it('should move overridable property to overrides and remove it from editableProperties', () => {
|
||||
const result = sanitizeOverridableEntityInput({
|
||||
metadataName: 'pageLayoutTab',
|
||||
existingFlatEntity: {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: null,
|
||||
overrides: null,
|
||||
},
|
||||
updatedEditableProperties: { title: 'Overridden Title' },
|
||||
shouldOverride: true,
|
||||
});
|
||||
|
||||
expect(result.overrides).toEqual({ title: 'Overridden Title' });
|
||||
expect(result.updatedEditableProperties).not.toHaveProperty('title');
|
||||
});
|
||||
|
||||
it('should implicitly restore when new value matches base value by removing the key from overrides', () => {
|
||||
const result = sanitizeOverridableEntityInput({
|
||||
metadataName: 'pageLayoutTab',
|
||||
existingFlatEntity: {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: null,
|
||||
overrides: { title: 'Old Override', position: 5 },
|
||||
},
|
||||
updatedEditableProperties: { title: 'Base Title' },
|
||||
shouldOverride: true,
|
||||
});
|
||||
|
||||
expect(result.overrides).toEqual({ position: 5 });
|
||||
expect(result.updatedEditableProperties).not.toHaveProperty('title');
|
||||
});
|
||||
|
||||
it('should return null overrides when removing the last override key', () => {
|
||||
const result = sanitizeOverridableEntityInput({
|
||||
metadataName: 'pageLayoutTab',
|
||||
existingFlatEntity: {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: null,
|
||||
overrides: { title: 'Old Override' },
|
||||
},
|
||||
updatedEditableProperties: { title: 'Base Title' },
|
||||
shouldOverride: true,
|
||||
});
|
||||
|
||||
expect(result.overrides).toBeNull();
|
||||
expect(result.updatedEditableProperties).not.toHaveProperty('title');
|
||||
});
|
||||
|
||||
it('should not move non-overridable properties to overrides', () => {
|
||||
const result = sanitizeOverridableEntityInput({
|
||||
metadataName: 'pageLayoutTab',
|
||||
existingFlatEntity: {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: null,
|
||||
pageLayoutId: 'layout-1',
|
||||
overrides: null,
|
||||
},
|
||||
updatedEditableProperties: {
|
||||
title: 'Overridden Title',
|
||||
pageLayoutId: 'layout-2',
|
||||
},
|
||||
shouldOverride: true,
|
||||
});
|
||||
|
||||
expect(result.overrides).toEqual({ title: 'Overridden Title' });
|
||||
expect(result.updatedEditableProperties).toEqual({
|
||||
pageLayoutId: 'layout-2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve existing override keys when updating a different overridable property', () => {
|
||||
const result = sanitizeOverridableEntityInput({
|
||||
metadataName: 'pageLayoutTab',
|
||||
existingFlatEntity: {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: null,
|
||||
overrides: { title: 'Overridden Title' },
|
||||
},
|
||||
updatedEditableProperties: { position: 5 },
|
||||
shouldOverride: true,
|
||||
});
|
||||
|
||||
expect(result.overrides).toEqual({
|
||||
title: 'Overridden Title',
|
||||
position: 5,
|
||||
});
|
||||
expect(result.updatedEditableProperties).not.toHaveProperty('position');
|
||||
});
|
||||
|
||||
it('should create new overrides object when no existing overrides', () => {
|
||||
const result = sanitizeOverridableEntityInput({
|
||||
metadataName: 'pageLayoutTab',
|
||||
existingFlatEntity: {
|
||||
title: 'Base Title',
|
||||
position: 0,
|
||||
icon: null,
|
||||
overrides: null,
|
||||
},
|
||||
updatedEditableProperties: { icon: 'IconStar' },
|
||||
shouldOverride: true,
|
||||
});
|
||||
|
||||
expect(result.overrides).toEqual({ icon: 'IconStar' });
|
||||
expect(result.updatedEditableProperties).not.toHaveProperty('icon');
|
||||
});
|
||||
});
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
export const isCallerOverridingEntity = ({
|
||||
callerApplicationUniversalIdentifier,
|
||||
entityApplicationUniversalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
}: {
|
||||
callerApplicationUniversalIdentifier: string;
|
||||
entityApplicationUniversalIdentifier: string;
|
||||
workspaceCustomApplicationUniversalIdentifier: string;
|
||||
}): boolean => {
|
||||
return (
|
||||
callerApplicationUniversalIdentifier ===
|
||||
workspaceCustomApplicationUniversalIdentifier &&
|
||||
entityApplicationUniversalIdentifier !==
|
||||
workspaceCustomApplicationUniversalIdentifier
|
||||
);
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export const resolveOverridableEntityProperty = <
|
||||
TEntity extends { overrides?: Partial<TEntity> | null },
|
||||
K extends string & keyof TEntity,
|
||||
>(
|
||||
entity: TEntity,
|
||||
property: K,
|
||||
): TEntity[K] => {
|
||||
const overrideValue = entity.overrides?.[property];
|
||||
|
||||
return overrideValue !== undefined ? overrideValue : entity[property];
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ALL_OVERRIDABLE_PROPERTIES_BY_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-overridable-properties-by-metadata-name.constant';
|
||||
|
||||
type FlatEntityWithOverrides = {
|
||||
[key: string]: unknown;
|
||||
overrides: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export const sanitizeOverridableEntityInput = <
|
||||
T extends AllMetadataName,
|
||||
TProperties extends Record<string, unknown>,
|
||||
>({
|
||||
metadataName,
|
||||
existingFlatEntity,
|
||||
updatedEditableProperties,
|
||||
shouldOverride,
|
||||
}: {
|
||||
metadataName: T;
|
||||
existingFlatEntity: FlatEntityWithOverrides;
|
||||
updatedEditableProperties: TProperties;
|
||||
shouldOverride: boolean;
|
||||
}): {
|
||||
overrides: Record<string, unknown> | null;
|
||||
updatedEditableProperties: TProperties;
|
||||
} => {
|
||||
const existingOverrides = existingFlatEntity.overrides;
|
||||
|
||||
if (!shouldOverride) {
|
||||
return {
|
||||
overrides: existingOverrides,
|
||||
updatedEditableProperties,
|
||||
};
|
||||
}
|
||||
|
||||
const sanitizedEditableProperties = {
|
||||
...updatedEditableProperties,
|
||||
} as TProperties;
|
||||
|
||||
const overridableProperties = ALL_OVERRIDABLE_PROPERTIES_BY_METADATA_NAME[
|
||||
metadataName
|
||||
] as string[];
|
||||
|
||||
const overrides = overridableProperties.reduce<Record<
|
||||
string,
|
||||
unknown
|
||||
> | null>((acc, property) => {
|
||||
const isPropertyUpdated =
|
||||
sanitizedEditableProperties[property] !== undefined;
|
||||
|
||||
if (!isPropertyUpdated) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const propertyValue = sanitizedEditableProperties[property];
|
||||
|
||||
delete sanitizedEditableProperties[property];
|
||||
|
||||
if (isEqual(propertyValue, existingFlatEntity[property])) {
|
||||
if (
|
||||
isDefined(acc) &&
|
||||
Object.prototype.hasOwnProperty.call(acc, property)
|
||||
) {
|
||||
const { [property]: _, ...restOverrides } = acc;
|
||||
|
||||
return restOverrides;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[property]: propertyValue,
|
||||
};
|
||||
}, existingOverrides);
|
||||
|
||||
if (isDefined(overrides) && Object.keys(overrides).length === 0) {
|
||||
return {
|
||||
overrides: null,
|
||||
updatedEditableProperties: sanitizedEditableProperties,
|
||||
};
|
||||
}
|
||||
|
||||
return { overrides, updatedEditableProperties: sanitizedEditableProperties };
|
||||
};
|
||||
Reference in New Issue
Block a user