[DASHBOARDS] chat agent improvements + new validation layer (#17722)
https://github.com/user-attachments/assets/09550210-76c5-4a40-83b6-9ab785ca10c3 https://github.com/user-attachments/assets/352427fc-0a2a-4f1b-86e9-db99daea0018 --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
+2
@@ -3,6 +3,8 @@ export type BrowsingContextType =
|
||||
type: 'recordPage';
|
||||
objectNameSingular: string;
|
||||
recordId: string;
|
||||
pageLayoutId?: string;
|
||||
activeTabId?: string | null;
|
||||
}
|
||||
| {
|
||||
type: 'listView';
|
||||
|
||||
+16
-2
@@ -14,7 +14,7 @@ import {
|
||||
type UITools,
|
||||
} from 'ai';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
@@ -247,6 +247,8 @@ export class ChatExecutionService {
|
||||
workspace,
|
||||
browsingContext.objectNameSingular,
|
||||
browsingContext.recordId,
|
||||
browsingContext.pageLayoutId,
|
||||
browsingContext.activeTabId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -261,6 +263,8 @@ export class ChatExecutionService {
|
||||
workspace: WorkspaceEntity,
|
||||
objectNameSingular: string,
|
||||
recordId: string,
|
||||
pageLayoutId?: string,
|
||||
activeTabId?: string | null,
|
||||
): string {
|
||||
const resourceUrl = this.workspaceDomainsService.buildWorkspaceURL({
|
||||
workspace,
|
||||
@@ -270,7 +274,17 @@ export class ChatExecutionService {
|
||||
}),
|
||||
});
|
||||
|
||||
return `The user is viewing a ${objectNameSingular} record (ID: ${recordId}, URL: ${resourceUrl}). Use tools to fetch record details if needed.`;
|
||||
let context = `The user is viewing a ${objectNameSingular} record (ID: ${recordId}, URL: ${resourceUrl}). Use tools to fetch record details if needed.`;
|
||||
|
||||
if (isDefined(pageLayoutId)) {
|
||||
context += `\nPage layout ID: ${pageLayoutId}.`;
|
||||
}
|
||||
|
||||
if (isDefined(activeTabId)) {
|
||||
context += `\nActive tab ID: ${activeTabId}.`;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private buildListViewContext(browsingContext: {
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
|
||||
export const GRAPH_CONFIGURATION_TYPES = new Set<WidgetConfigurationType>([
|
||||
WidgetConfigurationType.AGGREGATE_CHART,
|
||||
WidgetConfigurationType.BAR_CHART,
|
||||
WidgetConfigurationType.GAUGE_CHART,
|
||||
WidgetConfigurationType.LINE_CHART,
|
||||
WidgetConfigurationType.PIE_CHART,
|
||||
]);
|
||||
+140
-4
@@ -3,9 +3,10 @@ import { Injectable } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util';
|
||||
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 { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util';
|
||||
@@ -17,13 +18,18 @@ import {
|
||||
import { CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
|
||||
import { UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input';
|
||||
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
|
||||
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,
|
||||
PageLayoutWidgetExceptionMessageKey,
|
||||
generatePageLayoutWidgetExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
|
||||
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
|
||||
import { isChartFieldsForValidation } from 'src/engine/metadata-modules/page-layout-widget/utils/is-chart-fields-for-validation.util';
|
||||
import { validateChartConfigurationFieldReferences } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-chart-configuration-field-references.util';
|
||||
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';
|
||||
@@ -92,6 +98,73 @@ export class PageLayoutWidgetService {
|
||||
}
|
||||
}
|
||||
|
||||
private async enrichRichTextConfigurationBody(
|
||||
configuration: AllPageLayoutWidgetConfiguration,
|
||||
): Promise<AllPageLayoutWidgetConfiguration> {
|
||||
if (
|
||||
configuration.configurationType !==
|
||||
WidgetConfigurationType.STANDALONE_RICH_TEXT
|
||||
) {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
if (!isDefined(configuration.body)) {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
...configuration,
|
||||
body: await transformRichTextV2Value(configuration.body),
|
||||
};
|
||||
} catch {
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
|
||||
private async validateChartFieldReferencesIfApplicable({
|
||||
configuration,
|
||||
objectMetadataId,
|
||||
widgetType,
|
||||
workspaceId,
|
||||
}: {
|
||||
configuration: AllPageLayoutWidgetConfiguration;
|
||||
objectMetadataId?: string | null;
|
||||
widgetType?: WidgetType | null;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
const needsChartValidation =
|
||||
isChartFieldsForValidation(configuration) ||
|
||||
widgetType === WidgetType.GRAPH;
|
||||
|
||||
if (!needsChartValidation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatFieldMetadataMaps', 'flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
validateChartConfigurationFieldReferences({
|
||||
configuration,
|
||||
objectMetadataId,
|
||||
widgetType,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new PageLayoutWidgetException(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async findByPageLayoutTabId({
|
||||
workspaceId,
|
||||
pageLayoutTabId,
|
||||
@@ -152,6 +225,15 @@ export class PageLayoutWidgetService {
|
||||
input: CreatePageLayoutWidgetInput;
|
||||
workspaceId: string;
|
||||
}): Promise<PageLayoutWidgetDTO> {
|
||||
const createInput = isDefined(input.configuration)
|
||||
? {
|
||||
...input,
|
||||
configuration: await this.enrichRichTextConfigurationBody(
|
||||
input.configuration,
|
||||
),
|
||||
}
|
||||
: input;
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
@@ -175,7 +257,7 @@ export class PageLayoutWidgetService {
|
||||
|
||||
const flatPageLayoutWidgetToCreate =
|
||||
fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate({
|
||||
createPageLayoutWidgetInput: input,
|
||||
createPageLayoutWidgetInput: createInput,
|
||||
workspaceId,
|
||||
flatApplication: workspaceCustomFlatApplication,
|
||||
flatPageLayoutTabMaps,
|
||||
@@ -183,6 +265,15 @@ export class PageLayoutWidgetService {
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (isDefined(createInput.configuration)) {
|
||||
await this.validateChartFieldReferencesIfApplicable({
|
||||
configuration: createInput.configuration,
|
||||
objectMetadataId: createInput.objectMetadataId ?? null,
|
||||
widgetType: createInput.type,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
await this.validateAndRunWidgetMigration({
|
||||
workspaceId,
|
||||
operations: {
|
||||
@@ -222,6 +313,11 @@ export class PageLayoutWidgetService {
|
||||
const existingFlatPageLayoutWidgetMaps =
|
||||
await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
const existingWidget = this.getExistingWidgetOrThrow(
|
||||
id,
|
||||
existingFlatPageLayoutWidgetMaps,
|
||||
);
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
@@ -233,12 +329,25 @@ export class PageLayoutWidgetService {
|
||||
},
|
||||
);
|
||||
|
||||
this.getExistingWidgetOrThrow(id, existingFlatPageLayoutWidgetMaps);
|
||||
const isConfigurationBeingUpdated = Object.prototype.hasOwnProperty.call(
|
||||
updateData,
|
||||
'configuration',
|
||||
);
|
||||
|
||||
const processedUpdateData =
|
||||
isConfigurationBeingUpdated && isDefined(updateData.configuration)
|
||||
? {
|
||||
...updateData,
|
||||
configuration: await this.enrichRichTextConfigurationBody(
|
||||
updateData.configuration,
|
||||
),
|
||||
}
|
||||
: updateData;
|
||||
|
||||
const updatePageLayoutWidgetInput: UpdatePageLayoutWidgetInputWithId = {
|
||||
id,
|
||||
update: {
|
||||
...updateData,
|
||||
...processedUpdateData,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -250,6 +359,33 @@ export class PageLayoutWidgetService {
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const shouldValidateChartFields =
|
||||
isConfigurationBeingUpdated ||
|
||||
Object.prototype.hasOwnProperty.call(updateData, 'objectMetadataId') ||
|
||||
Object.prototype.hasOwnProperty.call(updateData, 'type');
|
||||
|
||||
if (shouldValidateChartFields) {
|
||||
const isObjectMetadataIdBeingUpdated =
|
||||
Object.prototype.hasOwnProperty.call(updateData, 'objectMetadataId');
|
||||
const effectiveConfiguration = isConfigurationBeingUpdated
|
||||
? processedUpdateData.configuration
|
||||
: existingWidget.configuration;
|
||||
const effectiveObjectMetadataId = isObjectMetadataIdBeingUpdated
|
||||
? processedUpdateData.objectMetadataId
|
||||
: existingWidget.objectMetadataId;
|
||||
const effectiveWidgetType =
|
||||
processedUpdateData.type ?? existingWidget.type;
|
||||
|
||||
if (isDefined(effectiveConfiguration)) {
|
||||
await this.validateChartFieldReferencesIfApplicable({
|
||||
configuration: effectiveConfiguration,
|
||||
objectMetadataId: effectiveObjectMetadataId,
|
||||
widgetType: effectiveWidgetType,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.validateAndRunWidgetMigration({
|
||||
workspaceId,
|
||||
operations: {
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type AggregateChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto';
|
||||
import { type BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto';
|
||||
import { type GaugeChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto';
|
||||
import { type LineChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto';
|
||||
import { type PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
|
||||
|
||||
export type ChartFieldsForValidation =
|
||||
| AggregateChartConfigurationDTO
|
||||
| BarChartConfigurationDTO
|
||||
| GaugeChartConfigurationDTO
|
||||
| LineChartConfigurationDTO
|
||||
| PieChartConfigurationDTO;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
export const findActiveFlatFieldMetadataById = (
|
||||
fieldId: string | null | undefined,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
): FlatFieldMetadata | null => {
|
||||
if (!isDefined(fieldId)) return null;
|
||||
|
||||
const field = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: fieldId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(field) || !field.isActive) return null;
|
||||
|
||||
return field;
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES } from 'twenty-shared/constants';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
const compositeSubFieldMaps = COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES as Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
>;
|
||||
|
||||
export const getCompositeSubfieldNames = (fieldType: FieldMetadataType) =>
|
||||
Object.values(compositeSubFieldMaps[fieldType] ?? {});
|
||||
+8
@@ -0,0 +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';
|
||||
|
||||
export const isChartFieldsForValidation = (
|
||||
configuration: AllPageLayoutWidgetConfiguration,
|
||||
): configuration is ChartFieldsForValidation =>
|
||||
GRAPH_CONFIGURATION_TYPES.has(configuration.configurationType);
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
export const resolveMorphTargetObjectId = ({
|
||||
field,
|
||||
allFields,
|
||||
}: {
|
||||
field: FlatFieldMetadata;
|
||||
allFields: FlatFieldMetadata[];
|
||||
}): string | null => {
|
||||
if (!isDefined(field.morphId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetIds = new Set<string>();
|
||||
|
||||
allFields.forEach((flatField) => {
|
||||
if (
|
||||
flatField.morphId === field.morphId &&
|
||||
isDefined(flatField.relationTargetObjectMetadataId)
|
||||
) {
|
||||
targetIds.add(flatField.relationTargetObjectMetadataId);
|
||||
}
|
||||
});
|
||||
|
||||
if (targetIds.size !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [...targetIds][0] ?? null;
|
||||
};
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
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 { 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';
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (widgetType && widgetType !== WidgetType.GRAPH) {
|
||||
throw new Error(
|
||||
`Graph configuration is only valid for widgets of type GRAPH.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(objectMetadataId)) {
|
||||
throw new Error('objectMetadataId is required for graph widgets.');
|
||||
}
|
||||
|
||||
const objectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(objectMetadata) || !objectMetadata.isActive) {
|
||||
throw new Error(`objectMetadataId "${objectMetadataId}" not found.`);
|
||||
}
|
||||
|
||||
const allFields = Object.values(flatFieldMetadataMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
.filter((field) => field.isActive);
|
||||
|
||||
const fieldsByObjectId = new Map<string, FlatFieldMetadata[]>();
|
||||
|
||||
allFields.forEach((field) => {
|
||||
const existing = fieldsByObjectId.get(field.objectMetadataId) ?? [];
|
||||
|
||||
existing.push(field);
|
||||
fieldsByObjectId.set(field.objectMetadataId, existing);
|
||||
});
|
||||
|
||||
const aggregateField = findActiveFlatFieldMetadataById(
|
||||
configuration.aggregateFieldMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
if (!isDefined(aggregateField)) {
|
||||
throw new Error(
|
||||
`aggregateFieldMetadataId "${configuration.aggregateFieldMetadataId}" not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (aggregateField.objectMetadataId !== objectMetadataId) {
|
||||
throw new Error(
|
||||
`aggregateFieldMetadataId must belong to objectMetadataId "${objectMetadataId}".`,
|
||||
);
|
||||
}
|
||||
|
||||
switch (configuration.configurationType) {
|
||||
case WidgetConfigurationType.BAR_CHART:
|
||||
case WidgetConfigurationType.LINE_CHART: {
|
||||
validateGroupByField({
|
||||
fieldId: configuration.primaryAxisGroupByFieldMetadataId,
|
||||
subFieldName: configuration.primaryAxisGroupBySubFieldName,
|
||||
paramName: 'primaryAxisGroupByFieldMetadataId',
|
||||
objectMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
allFields,
|
||||
fieldsByObjectId,
|
||||
});
|
||||
|
||||
if (isDefined(configuration.secondaryAxisGroupBySubFieldName)) {
|
||||
if (!isDefined(configuration.secondaryAxisGroupByFieldMetadataId)) {
|
||||
throw new Error(
|
||||
'secondaryAxisGroupByFieldMetadataId is required when secondaryAxisGroupBySubFieldName is provided.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(configuration.secondaryAxisGroupByFieldMetadataId)) {
|
||||
validateGroupByField({
|
||||
fieldId: configuration.secondaryAxisGroupByFieldMetadataId,
|
||||
subFieldName: configuration.secondaryAxisGroupBySubFieldName,
|
||||
paramName: 'secondaryAxisGroupByFieldMetadataId',
|
||||
objectMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
allFields,
|
||||
fieldsByObjectId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WidgetConfigurationType.PIE_CHART: {
|
||||
validateGroupByField({
|
||||
fieldId: configuration.groupByFieldMetadataId,
|
||||
subFieldName: configuration.groupBySubFieldName,
|
||||
paramName: 'groupByFieldMetadataId',
|
||||
objectMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
allFields,
|
||||
fieldsByObjectId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case WidgetConfigurationType.AGGREGATE_CHART:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { getCompositeSubfieldNames } from 'src/engine/metadata-modules/page-layout-widget/utils/get-composite-subfield-names.util';
|
||||
|
||||
export const validateCompositeSubfield = ({
|
||||
field,
|
||||
subFieldName,
|
||||
paramName,
|
||||
}: {
|
||||
field: FlatFieldMetadata;
|
||||
subFieldName: string | null | undefined;
|
||||
paramName: string;
|
||||
}): void => {
|
||||
const allowedSubFields = getCompositeSubfieldNames(field.type);
|
||||
|
||||
if (!isDefined(subFieldName)) {
|
||||
throw new Error(
|
||||
`Composite field "${paramName}" requires a subfield. Allowed: ${allowedSubFields.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (subFieldName.includes('.')) {
|
||||
throw new Error(`Composite subfield "${subFieldName}" is invalid.`);
|
||||
}
|
||||
|
||||
if (!allowedSubFields.includes(subFieldName)) {
|
||||
throw new Error(
|
||||
`Invalid subfield "${subFieldName}" for "${paramName}". Allowed: ${allowedSubFields.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
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 { 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 = ({
|
||||
fieldId,
|
||||
subFieldName,
|
||||
paramName,
|
||||
objectMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
allFields,
|
||||
fieldsByObjectId,
|
||||
}: {
|
||||
fieldId?: string | null;
|
||||
subFieldName?: string | null;
|
||||
paramName: string;
|
||||
objectMetadataId: string;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
allFields: FlatFieldMetadata[];
|
||||
fieldsByObjectId: Map<string, FlatFieldMetadata[]>;
|
||||
}): void => {
|
||||
if (!isDefined(fieldId)) {
|
||||
throw new Error(`${paramName} is required.`);
|
||||
}
|
||||
|
||||
const field = findActiveFlatFieldMetadataById(fieldId, flatFieldMetadataMaps);
|
||||
|
||||
if (!isDefined(field)) {
|
||||
throw new Error(`${paramName} "${fieldId}" not found.`);
|
||||
}
|
||||
|
||||
if (field.objectMetadataId !== objectMetadataId) {
|
||||
throw new Error(
|
||||
`${paramName} must belong to objectMetadataId "${objectMetadataId}".`,
|
||||
);
|
||||
}
|
||||
|
||||
if (isCompositeFieldMetadataType(field.type)) {
|
||||
validateCompositeSubfield({
|
||||
field,
|
||||
subFieldName,
|
||||
paramName: field.name,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMorphOrRelationFlatFieldMetadata(field)) {
|
||||
validateRelationSubfield({
|
||||
field,
|
||||
subFieldName,
|
||||
paramName: field.name,
|
||||
allFields,
|
||||
fieldsByObjectId,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(subFieldName)) {
|
||||
throw new Error(`Field "${field.name}" does not support subfields.`);
|
||||
}
|
||||
};
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { getCompositeSubfieldNames } from 'src/engine/metadata-modules/page-layout-widget/utils/get-composite-subfield-names.util';
|
||||
import { resolveMorphTargetObjectId } from 'src/engine/metadata-modules/page-layout-widget/utils/resolve-morph-target-object-id.util';
|
||||
import { validateCompositeSubfield } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-composite-subfield.util';
|
||||
|
||||
export const validateRelationSubfield = ({
|
||||
field,
|
||||
subFieldName,
|
||||
paramName,
|
||||
allFields,
|
||||
fieldsByObjectId,
|
||||
}: {
|
||||
field: FlatFieldMetadata;
|
||||
subFieldName: string | null | undefined;
|
||||
paramName: string;
|
||||
allFields: FlatFieldMetadata[];
|
||||
fieldsByObjectId: Map<string, FlatFieldMetadata[]>;
|
||||
}): void => {
|
||||
if (!isDefined(subFieldName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dotIndex = subFieldName.indexOf('.');
|
||||
const nestedFieldName =
|
||||
dotIndex === -1 ? subFieldName : subFieldName.slice(0, dotIndex);
|
||||
const nestedSubFieldName =
|
||||
dotIndex === -1 ? undefined : subFieldName.slice(dotIndex + 1);
|
||||
|
||||
if (!nestedFieldName) {
|
||||
throw new Error(`Relation subfield "${subFieldName}" is invalid.`);
|
||||
}
|
||||
|
||||
if (isDefined(nestedSubFieldName) && nestedSubFieldName.includes('.')) {
|
||||
throw new Error(`Relation subfield "${subFieldName}" is invalid.`);
|
||||
}
|
||||
|
||||
let targetObjectId = field.relationTargetObjectMetadataId ?? null;
|
||||
|
||||
if (field.type === FieldMetadataType.MORPH_RELATION) {
|
||||
targetObjectId = resolveMorphTargetObjectId({ field, allFields });
|
||||
}
|
||||
|
||||
if (!isDefined(targetObjectId)) {
|
||||
throw new Error(
|
||||
`Relation field "${paramName}" does not have a resolvable target object.`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetFields = fieldsByObjectId.get(targetObjectId) ?? [];
|
||||
const nestedField = targetFields.find(
|
||||
(targetField) => targetField.name === nestedFieldName,
|
||||
);
|
||||
|
||||
if (!isDefined(nestedField)) {
|
||||
throw new Error(
|
||||
`Relation subfield "${nestedFieldName}" not found for "${paramName}".`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(nestedSubFieldName)) {
|
||||
if (isCompositeFieldMetadataType(nestedField.type)) {
|
||||
const allowed = getCompositeSubfieldNames(nestedField.type);
|
||||
|
||||
throw new Error(
|
||||
`Composite field "${nestedFieldName}" requires a subfield. Use "${nestedFieldName}.<subfield>" where subfield is one of: ${allowed.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCompositeFieldMetadataType(nestedField.type)) {
|
||||
throw new Error(`Field "${nestedFieldName}" is not composite.`);
|
||||
}
|
||||
|
||||
validateCompositeSubfield({
|
||||
field: nestedField,
|
||||
subFieldName: nestedSubFieldName,
|
||||
paramName: nestedFieldName,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user