fix: handle dashboard filters referencing deleted fields (#18512)

closes https://github.com/twentyhq/twenty/issues/18174

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
nitin
2026-03-26 15:25:28 +05:30
committed by GitHub
parent cfefe9273b
commit 3c5796bdb0
9 changed files with 463 additions and 129 deletions
@@ -1,8 +1,8 @@
import { GRAPH_CONFIGURATION_TYPES } from 'src/engine/metadata-modules/page-layout-widget/constants/graph-configuration-types.constant';
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
import { type ChartFieldsForValidation } from 'src/engine/metadata-modules/page-layout-widget/types/chart-fields-for-validation.type';
import { type ChartReferencingFieldInConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/chart-referencing-field-in-configuration.type';
export const isChartFieldsForValidation = (
export const isChartReferencingFieldInConfiguration = (
configuration: AllPageLayoutWidgetConfiguration,
): configuration is ChartFieldsForValidation =>
): configuration is ChartReferencingFieldInConfiguration =>
GRAPH_CONFIGURATION_TYPES.has(configuration.configurationType);
@@ -1,3 +1,4 @@
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
@@ -5,54 +6,85 @@ import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
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 {
PageLayoutWidgetException,
PageLayoutWidgetExceptionCode,
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
import { PageLayoutWidgetFieldValidationException } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget-field-validation.exception';
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
import { findActiveFlatFieldMetadataById } from 'src/engine/metadata-modules/page-layout-widget/utils/find-active-flat-field-metadata-by-id.util';
import { isChartFieldsForValidation } from 'src/engine/metadata-modules/page-layout-widget/utils/is-chart-fields-for-validation.util';
import { validateGroupByField } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-group-by-field.util';
import { isChartReferencingFieldInConfiguration } from 'src/engine/metadata-modules/page-layout-widget/utils/is-chart-referencing-field-in-configuration.util';
import { validateGroupByFieldOrThrow } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-group-by-field.util';
export const validateChartConfigurationFieldReferences = ({
configuration,
objectMetadataId,
widgetType,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
}: {
configuration?: AllPageLayoutWidgetConfiguration | null;
objectMetadataId?: string | null;
widgetType?: WidgetType | null;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
}): void => {
if (!isDefined(configuration)) return;
const buildChartFieldValidationException = (
message: string,
widgetTitle?: string | null,
): PageLayoutWidgetException => {
const prefix = isDefined(widgetTitle) ? `Chart "${widgetTitle}": ` : '';
const fullMessage = prefix + message;
if (!isChartFieldsForValidation(configuration)) {
if (widgetType === WidgetType.GRAPH) {
throw new Error(
'GRAPH widgets require configurationType AGGREGATE_CHART, BAR_CHART, GAUGE_CHART, LINE_CHART, or PIE_CHART.',
);
return new PageLayoutWidgetException(
fullMessage,
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
{
userFriendlyMessage: msg`${fullMessage}`,
},
);
};
const validateGroupByFieldAsChartFieldOrThrow = (
params: Parameters<typeof validateGroupByFieldOrThrow>[0],
widgetTitle?: string | null,
): void => {
try {
validateGroupByFieldOrThrow(params);
} catch (error) {
if (!(error instanceof PageLayoutWidgetFieldValidationException)) {
throw error;
}
throw buildChartFieldValidationException(error.message, widgetTitle);
}
};
export const validateChartConfigurationFieldReferencesOrThrow = ({
widgetConfiguration,
widgetObjectMetadataId,
widgetTitle,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
}: {
widgetConfiguration?: AllPageLayoutWidgetConfiguration | null;
widgetObjectMetadataId?: string | null;
widgetTitle?: string | null;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
}): void => {
if (!isDefined(widgetConfiguration)) {
return;
}
if (widgetType && widgetType !== WidgetType.GRAPH) {
throw new Error(
`Graph configuration is only valid for widgets of type GRAPH.`,
if (!isChartReferencingFieldInConfiguration(widgetConfiguration)) {
return;
}
if (!isDefined(widgetObjectMetadataId)) {
throw buildChartFieldValidationException(
'objectMetadataId is required for graph widgets.',
widgetTitle,
);
}
if (!isDefined(objectMetadataId)) {
throw new Error('objectMetadataId is required for graph widgets.');
}
const objectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: objectMetadataId,
flatEntityId: widgetObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(objectMetadata) || !objectMetadata.isActive) {
throw new Error(`objectMetadataId "${objectMetadataId}" not found.`);
throw buildChartFieldValidationException(
`objectMetadataId "${widgetObjectMetadataId}" not found.`,
widgetTitle,
);
}
const allFields = Object.values(flatFieldMetadataMaps.byUniversalIdentifier)
@@ -69,70 +101,116 @@ export const validateChartConfigurationFieldReferences = ({
});
const aggregateField = findActiveFlatFieldMetadataById(
configuration.aggregateFieldMetadataId,
widgetConfiguration.aggregateFieldMetadataId,
flatFieldMetadataMaps,
);
if (!isDefined(aggregateField)) {
throw new Error(
`aggregateFieldMetadataId "${configuration.aggregateFieldMetadataId}" not found.`,
throw buildChartFieldValidationException(
`aggregateFieldMetadataId "${widgetConfiguration.aggregateFieldMetadataId}" not found.`,
widgetTitle,
);
}
if (aggregateField.objectMetadataId !== objectMetadataId) {
throw new Error(
`aggregateFieldMetadataId must belong to objectMetadataId "${objectMetadataId}".`,
if (aggregateField.objectMetadataId !== widgetObjectMetadataId) {
throw buildChartFieldValidationException(
`aggregateFieldMetadataId must belong to objectMetadataId "${widgetObjectMetadataId}".`,
widgetTitle,
);
}
switch (configuration.configurationType) {
switch (widgetConfiguration.configurationType) {
case WidgetConfigurationType.BAR_CHART:
case WidgetConfigurationType.LINE_CHART: {
validateGroupByField({
fieldId: configuration.primaryAxisGroupByFieldMetadataId,
subFieldName: configuration.primaryAxisGroupBySubFieldName,
paramName: 'primaryAxisGroupByFieldMetadataId',
objectMetadataId,
flatFieldMetadataMaps,
allFields,
fieldsByObjectId,
});
validateGroupByFieldAsChartFieldOrThrow(
{
fieldId: widgetConfiguration.primaryAxisGroupByFieldMetadataId,
subFieldName: widgetConfiguration.primaryAxisGroupBySubFieldName,
paramName: 'primaryAxisGroupByFieldMetadataId',
objectMetadataId: widgetObjectMetadataId,
flatFieldMetadataMaps,
allFields,
fieldsByObjectId,
},
widgetTitle,
);
if (isDefined(configuration.secondaryAxisGroupBySubFieldName)) {
if (!isDefined(configuration.secondaryAxisGroupByFieldMetadataId)) {
throw new Error(
if (isDefined(widgetConfiguration.secondaryAxisGroupBySubFieldName)) {
if (
!isDefined(widgetConfiguration.secondaryAxisGroupByFieldMetadataId)
) {
throw buildChartFieldValidationException(
'secondaryAxisGroupByFieldMetadataId is required when secondaryAxisGroupBySubFieldName is provided.',
widgetTitle,
);
}
}
if (isDefined(configuration.secondaryAxisGroupByFieldMetadataId)) {
validateGroupByField({
fieldId: configuration.secondaryAxisGroupByFieldMetadataId,
subFieldName: configuration.secondaryAxisGroupBySubFieldName,
paramName: 'secondaryAxisGroupByFieldMetadataId',
objectMetadataId,
flatFieldMetadataMaps,
allFields,
fieldsByObjectId,
});
if (isDefined(widgetConfiguration.secondaryAxisGroupByFieldMetadataId)) {
validateGroupByFieldAsChartFieldOrThrow(
{
fieldId: widgetConfiguration.secondaryAxisGroupByFieldMetadataId,
subFieldName: widgetConfiguration.secondaryAxisGroupBySubFieldName,
paramName: 'secondaryAxisGroupByFieldMetadataId',
objectMetadataId: widgetObjectMetadataId,
flatFieldMetadataMaps,
allFields,
fieldsByObjectId,
},
widgetTitle,
);
}
break;
}
case WidgetConfigurationType.PIE_CHART: {
validateGroupByField({
fieldId: configuration.groupByFieldMetadataId,
subFieldName: configuration.groupBySubFieldName,
paramName: 'groupByFieldMetadataId',
objectMetadataId,
flatFieldMetadataMaps,
allFields,
fieldsByObjectId,
});
validateGroupByFieldAsChartFieldOrThrow(
{
fieldId: widgetConfiguration.groupByFieldMetadataId,
subFieldName: widgetConfiguration.groupBySubFieldName,
paramName: 'groupByFieldMetadataId',
objectMetadataId: widgetObjectMetadataId,
flatFieldMetadataMaps,
allFields,
fieldsByObjectId,
},
widgetTitle,
);
break;
}
case WidgetConfigurationType.AGGREGATE_CHART:
default:
break;
}
if (isDefined(widgetConfiguration.filter?.recordFilters)) {
for (const recordFilter of widgetConfiguration.filter.recordFilters) {
const filterField = findActiveFlatFieldMetadataById(
recordFilter.fieldMetadataId,
flatFieldMetadataMaps,
);
if (!isDefined(filterField)) {
const inactiveOrMissingField = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: recordFilter.fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
const fieldLabel = inactiveOrMissingField
? `"${inactiveOrMissingField.label}"`
: `field id "${recordFilter.fieldMetadataId}"`;
throw buildChartFieldValidationException(
`One of the chart filters uses ${fieldLabel}, but it was deleted. Please remove or replace this filter rule.`,
widgetTitle,
);
}
if (filterField.objectMetadataId !== widgetObjectMetadataId) {
throw buildChartFieldValidationException(
`Filter field "${recordFilter.fieldMetadataId}" must belong to objectMetadataId "${widgetObjectMetadataId}".`,
widgetTitle,
);
}
}
}
};
@@ -4,11 +4,24 @@ import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { PageLayoutWidgetFieldValidationException } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget-field-validation.exception';
import { findActiveFlatFieldMetadataById } from 'src/engine/metadata-modules/page-layout-widget/utils/find-active-flat-field-metadata-by-id.util';
import { validateCompositeSubfield } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-composite-subfield.util';
import { validateRelationSubfield } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-relation-subfield.util';
export const validateGroupByField = ({
const toGroupByFieldValidationException = (
error: unknown,
): PageLayoutWidgetFieldValidationException => {
if (error instanceof PageLayoutWidgetFieldValidationException) {
return error;
}
return new PageLayoutWidgetFieldValidationException(
error instanceof Error ? error.message : String(error),
);
};
export const validateGroupByFieldOrThrow = ({
fieldId,
subFieldName,
paramName,
@@ -26,44 +39,58 @@ export const validateGroupByField = ({
fieldsByObjectId: Map<string, FlatFieldMetadata[]>;
}): void => {
if (!isDefined(fieldId)) {
throw new Error(`${paramName} is required.`);
throw new PageLayoutWidgetFieldValidationException(
`${paramName} is required.`,
);
}
const field = findActiveFlatFieldMetadataById(fieldId, flatFieldMetadataMaps);
if (!isDefined(field)) {
throw new Error(`${paramName} "${fieldId}" not found.`);
throw new PageLayoutWidgetFieldValidationException(
`${paramName} "${fieldId}" not found.`,
);
}
if (field.objectMetadataId !== objectMetadataId) {
throw new Error(
throw new PageLayoutWidgetFieldValidationException(
`${paramName} must belong to objectMetadataId "${objectMetadataId}".`,
);
}
if (isCompositeFieldMetadataType(field.type)) {
validateCompositeSubfield({
field,
subFieldName,
paramName: field.name,
});
try {
validateCompositeSubfield({
field,
subFieldName,
paramName: field.name,
});
} catch (error) {
throw toGroupByFieldValidationException(error);
}
return;
}
if (isMorphOrRelationFlatFieldMetadata(field)) {
validateRelationSubfield({
field,
subFieldName,
paramName: field.name,
allFields,
fieldsByObjectId,
});
try {
validateRelationSubfield({
field,
subFieldName,
paramName: field.name,
allFields,
fieldsByObjectId,
});
} catch (error) {
throw toGroupByFieldValidationException(error);
}
return;
}
if (isDefined(subFieldName)) {
throw new Error(`Field "${field.name}" does not support subfields.`);
throw new PageLayoutWidgetFieldValidationException(
`Field "${field.name}" does not support subfields.`,
);
}
};