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:
+3
-3
@@ -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);
|
||||
+147
-69
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+44
-17
@@ -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.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user