From 4faed25624d26e4da0bb54e16ec085a894531575 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Wed, 7 Jan 2026 16:00:13 +0100
Subject: [PATCH] [PAGE LAYOUTS] Add widgets validation (#16635)
- Add widget validation
- Remove 'None' option for primary axis group by
- Fix error message parsing by passing the operation type in
`useMetadataErrorHandler`
---
.../SaveDashboardSingleRecordAction.tsx | 9 +-
...oupByFieldSelectionDropdownContentBase.tsx | 30 +-
.../hooks/useMetadataErrorHandler.ts | 165 +++---
.../hooks/useCreateOneFieldMetadataItem.ts | 2 +
.../hooks/useCreateOneObjectMetadataItem.ts | 2 +
.../hooks/useDeleteOneFieldMetadataItem.ts | 2 +
.../hooks/useDeleteOneObjectMetadataItem.ts | 2 +
.../hooks/useUpdateOneFieldMetadataItem.ts | 2 +
.../hooks/useUpdateOneObjectMetadataItem.ts | 2 +
.../page-layout/hooks/useSavePageLayout.ts | 39 +-
.../useUpdatePageLayoutWithTabsAndWidgets.ts | 61 ++
.../hooks/usePersistServerlessFunction.ts | 4 +
.../views/hooks/internal/usePersistView.ts | 4 +
.../hooks/internal/usePersistViewField.ts | 5 +
.../hooks/internal/usePersistViewFilter.ts | 5 +
.../hooks/internal/usePersistViewGroup.ts | 2 +
.../flat-page-layout-widget.module.ts | 11 +-
...ge-layout-widget-type-validator.service.ts | 229 ++++++++
...-page-layout-widget-type-validator.type.ts | 25 +
...age-layout-widget-validation-error.type.ts | 10 +
...-flat-page-layout-widget-to-create.util.ts | 8 +-
...e-layout-widget-to-update-or-throw.util.ts | 12 +
...alid-graph-configuration-types.constant.ts | 9 +
.../types/base-graph-configuration.type.ts | 6 +
.../types/graph-configuration.type.ts | 12 +
.../types/iframe-configuration.type.ts | 6 +
.../utils/reject-widget-type.util.ts | 22 +
.../validate-bar-chart-configuration.util.ts | 31 +
.../utils/validate-base-graph-fields.util.ts | 31 +
...lidate-graph-configuration-by-type.util.ts | 27 +
.../validate-graph-configuration-type.util.ts | 44 ++
...at-page-layout-widget-for-creation.util.ts | 54 ++
...flat-page-layout-widget-for-update.util.ts | 46 ++
...validate-iframe-configuration-type.util.ts | 37 ++
...at-page-layout-widget-for-creation.util.ts | 42 ++
...flat-page-layout-widget-for-update.util.ts | 34 ++
.../utils/validate-iframe-url.util.ts | 24 +
.../validate-line-chart-configuration.util.ts | 23 +
.../validate-pie-chart-configuration.util.ts | 23 +
...validate-standalone-rich-text-body.util.ts | 37 ++
...alone-rich-text-configuration-type.util.ts | 43 ++
...at-page-layout-widget-for-creation.util.ts | 46 ++
...flat-page-layout-widget-for-update.util.ts | 38 ++
.../inputs/create-page-layout-widget.input.ts | 1 -
.../services/page-layout-widget.service.ts | 145 +----
...ransform-widget-configuration.util.spec.ts | 378 ------------
...te-widget-configuration-input.util.spec.ts | 269 +++++++++
...validate-widget-grid-position.util.spec.ts | 297 ++++++----
...and-transform-widget-configuration.util.ts | 227 -------
...lidate-widget-configuration-by-dto.util.ts | 19 +
...alidate-widget-configuration-input.util.ts | 194 ++++++
.../validate-widget-grid-position.util.ts | 46 +-
.../utils/seed-page-layout-widgets.util.ts | 37 +-
...ration-v2-index-actions-builder.service.ts | 3 +
...e-layout-widget-actions-builder.service.ts | 24 +-
...ace-entity-migration-builder-v2.service.ts | 4 +
.../types/flat-entity-validation-args.type.ts | 2 +
.../flat-field-metadata-validator.service.ts | 4 +
...at-page-layout-widget-validator.service.ts | 180 +++++-
...ace-migration-builder-validators.module.ts | 2 +
...idget-configuration-test-data.constants.ts | 136 ++---
...t-widget-creation.integration-spec.ts.snap | 130 +++-
...out-widget-update.integration-spec.ts.snap | 121 ++++
...t-widget-creation.integration-spec.ts.snap | 556 +++++++++++++++++-
...out-widget-update.integration-spec.ts.snap | 308 +++++++++-
...layout-widget-creation.integration-spec.ts | 299 ++++++++--
...e-layout-widget-update.integration-spec.ts | 250 ++++++++
...layout-widget-creation.integration-spec.ts | 233 +++++++-
...e-layout-widget-update.integration-spec.ts | 85 +++
.../src/types/CrudOperationType.ts | 7 +
packages/twenty-shared/src/types/index.ts | 1 +
71 files changed, 4004 insertions(+), 1220 deletions(-)
create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-type-validator.type.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/constants/valid-graph-configuration-types.constant.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/base-graph-configuration.type.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/reject-widget-type.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-bar-chart-configuration.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-base-graph-fields.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-by-type.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-type.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-creation.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-update.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-configuration-type.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-creation.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-update.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-url.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-line-chart-configuration.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-pie-chart-configuration.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-body.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-configuration-type.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-creation.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-update.util.ts
delete mode 100644 packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-widget-configuration-input.util.spec.ts
delete mode 100644 packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-and-transform-widget-configuration.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-by-dto.util.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util.ts
create mode 100644 packages/twenty-shared/src/types/CrudOperationType.ts
diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/SaveDashboardSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/SaveDashboardSingleRecordAction.tsx
index ba5bbc5a91..67fa3d70e9 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/SaveDashboardSingleRecordAction.tsx
+++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/SaveDashboardSingleRecordAction.tsx
@@ -21,9 +21,12 @@ export const SaveDashboardSingleRecordAction = () => {
const { closeCommandMenu } = useCommandMenu();
const handleClick = async () => {
- await savePageLayout();
- closeCommandMenu();
- setIsPageLayoutInEditMode(false);
+ const result = await savePageLayout();
+
+ if (result.status === 'successful') {
+ closeCommandMenu();
+ setIsPageLayoutInEditMode(false);
+ }
};
return ;
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx
index 77325e7805..c15001734c 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx
@@ -101,6 +101,13 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
const { getIcon } = useIcons();
+ const isSecondaryAxisGroupBy =
+ fieldMetadataIdKey === 'secondaryAxisGroupByFieldMetadataId';
+
+ const selectableItemIdArray = isSecondaryAxisGroupBy
+ ? ['none', ...availableFieldMetadataItems.map((item) => item.id)]
+ : availableFieldMetadataItems.map((item) => item.id);
+
if (!isDefined(sourceObjectMetadataItem)) {
return null;
}
@@ -227,19 +234,18 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
item.id),
- ]}
+ selectableItemIdArray={selectableItemIdArray}
>
-
-
-
+ {isSecondaryAxisGroupBy && (
+
+
+
+ )}
{availableFieldMetadataItems.map((fieldMetadataItem) => (
{
const { enqueueErrorSnackBar } = useSnackBar();
+
+ const TRANSLATED_OPERATION_TYPE = {
+ [CrudOperationType.CREATE]: t`create`,
+ [CrudOperationType.UPDATE]: t`update`,
+ [CrudOperationType.DELETE]: t`delete`,
+ [CrudOperationType.RESTORE]: t`restore`,
+ [CrudOperationType.DESTROY]: t`destroy`,
+ } as const satisfies Record;
+
const TRANSLATED_METADATA_NAME = {
objectMetadata: t`object`,
fieldMetadata: t`field`,
@@ -35,85 +44,83 @@ export const useMetadataErrorHandler = () => {
viewFilterGroup: t`view filter group`,
} as const satisfies Record;
- const handleMetadataError = useCallback(
- (
- error: ApolloError,
- options: {
- primaryMetadataName: AllMetadataName;
- },
- ) => {
- const classification = classifyMetadataError({
- error,
- primaryMetadataName: options.primaryMetadataName,
- });
-
- const translatedMetadataName =
- TRANSLATED_METADATA_NAME[options.primaryMetadataName];
-
- switch (classification.type) {
- case 'v1':
- enqueueErrorSnackBar({ apolloError: classification.error });
- break;
-
- case 'v2-validation': {
- const {
- extensions,
- primaryMetadataName,
- relatedFailingMetadataNames,
- } = classification;
-
- const targetErrors = extensions.errors[primaryMetadataName] ?? [];
- if (targetErrors.length > 0) {
- targetErrors.forEach((entityError) => {
- entityError.errors.forEach((validationError) =>
- enqueueErrorSnackBar({
- message:
- validationError.userFriendlyMessage ??
- validationError.message,
- }),
- );
- });
- }
-
- if (
- targetErrors.length === 0 &&
- relatedFailingMetadataNames.length > 0
- ) {
- const relatedEntityNames = relatedFailingMetadataNames
- .map((metadataName) => TRANSLATED_METADATA_NAME[metadataName])
- .join(', ');
-
- enqueueErrorSnackBar({
- message: t`Failed to create ${translatedMetadataName}. Related ${relatedEntityNames} validation failed. Please check your configuration and try again.`,
- });
- }
-
- if (
- targetErrors.length === 0 &&
- relatedFailingMetadataNames.length === 0
- ) {
- enqueueErrorSnackBar({
- message: t`Failed to create ${translatedMetadataName}. Please try again.`,
- });
- }
- break;
- }
-
- case 'v2-internal': {
- const { code } = classification;
- const errorMessage =
- code ===
- WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR
- ? t`An internal error occurred while validating your changes. Please contact support.`
- : t`An internal error occurred while applying your changes. Please contact support and try again later.`;
-
- enqueueErrorSnackBar({ message: errorMessage });
- break;
- }
- }
+ const handleMetadataError = (
+ error: ApolloError,
+ options: {
+ primaryMetadataName: AllMetadataName;
+ operationType: CrudOperationType;
},
- [enqueueErrorSnackBar, TRANSLATED_METADATA_NAME],
- );
+ ) => {
+ const classification = classifyMetadataError({
+ error,
+ primaryMetadataName: options.primaryMetadataName,
+ });
+
+ const translatedMetadataName =
+ TRANSLATED_METADATA_NAME[options.primaryMetadataName];
+
+ switch (classification.type) {
+ case 'v1':
+ enqueueErrorSnackBar({ apolloError: classification.error });
+ break;
+
+ case 'v2-validation': {
+ const { extensions, primaryMetadataName, relatedFailingMetadataNames } =
+ classification;
+
+ const targetErrors = extensions.errors[primaryMetadataName] ?? [];
+ if (targetErrors.length > 0) {
+ targetErrors.forEach((entityError) => {
+ entityError.errors.forEach((validationError) =>
+ enqueueErrorSnackBar({
+ message:
+ validationError.userFriendlyMessage ??
+ validationError.message,
+ }),
+ );
+ });
+ }
+
+ const translatedOperationType =
+ TRANSLATED_OPERATION_TYPE[options.operationType];
+
+ if (
+ targetErrors.length === 0 &&
+ relatedFailingMetadataNames.length > 0
+ ) {
+ const relatedEntityNames = relatedFailingMetadataNames
+ .map((metadataName) => TRANSLATED_METADATA_NAME[metadataName])
+ .join(', ');
+
+ enqueueErrorSnackBar({
+ message: t`Failed to ${translatedOperationType} ${translatedMetadataName}. Related ${relatedEntityNames} validation failed. Please check your configuration and try again.`,
+ });
+ }
+
+ if (
+ targetErrors.length === 0 &&
+ relatedFailingMetadataNames.length === 0
+ ) {
+ enqueueErrorSnackBar({
+ message: t`Failed to ${translatedOperationType} ${translatedMetadataName}. Please try again.`,
+ });
+ }
+ break;
+ }
+
+ case 'v2-internal': {
+ const { code } = classification;
+ const errorMessage =
+ code ===
+ WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR
+ ? t`An internal error occurred while validating your changes. Please contact support.`
+ : t`An internal error occurred while applying your changes. Please contact support and try again later.`;
+
+ enqueueErrorSnackBar({ message: errorMessage });
+ break;
+ }
+ }
+ };
return {
handleMetadataError,
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts
index 118ee2e1d3..af809b2129 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts
@@ -10,6 +10,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
export const useCreateOneFieldMetadataItem = () => {
const { refreshObjectMetadataItems } =
@@ -52,6 +53,7 @@ export const useCreateOneFieldMetadataItem = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'fieldMetadata',
+ operationType: CrudOperationType.CREATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts
index 7293ab40e1..06f7533b74 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts
@@ -10,6 +10,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const useCreateOneObjectMetadataItem = () => {
@@ -54,6 +55,7 @@ export const useCreateOneObjectMetadataItem = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'objectMetadata',
+ operationType: CrudOperationType.CREATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts
index 77f72ce902..2b35b0128e 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts
@@ -13,6 +13,7 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
export const useDeleteOneFieldMetadataItem = () => {
const [deleteOneFieldMetadataItemMutation] =
@@ -80,6 +81,7 @@ export const useDeleteOneFieldMetadataItem = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'fieldMetadata',
+ operationType: CrudOperationType.DELETE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts
index 217ec66636..03c5a38489 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts
@@ -7,6 +7,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRefreshAllCoreViews } from '@/views/hooks/useRefreshAllCoreViews';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
export const useDeleteOneObjectMetadataItem = () => {
const [deleteOneObjectMetadataItemMutation] =
@@ -45,6 +46,7 @@ export const useDeleteOneObjectMetadataItem = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'objectMetadata',
+ operationType: CrudOperationType.DELETE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts
index f6996ad36e..a8b88a7875 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts
@@ -10,6 +10,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
export const useUpdateOneFieldMetadataItem = () => {
const { refreshObjectMetadataItems } =
@@ -67,6 +68,7 @@ export const useUpdateOneFieldMetadataItem = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'fieldMetadata',
+ operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts
index ef4681a1a2..c9c767ea14 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts
@@ -10,6 +10,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
// TODO: Slice the Apollo store synchronously in the update function instead of subscribing, so we can use update after read in the same function call
export const useUpdateOneObjectMetadataItem = () => {
@@ -55,6 +56,7 @@ export const useUpdateOneObjectMetadataItem = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'objectMetadata',
+ operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts b/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts
index 03822dc5cd..57e19ec342 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts
@@ -1,4 +1,5 @@
import { usePageLayoutDraftState } from '@/page-layout/hooks/usePageLayoutDraftState';
+import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
@@ -10,7 +11,6 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
-import { useUpdatePageLayoutWithTabsAndWidgetsMutation } from '~/generated/graphql';
export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -30,8 +30,8 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
const { pageLayoutDraft } = usePageLayoutDraftState(pageLayoutId);
- const [updatePageLayoutWithTabsAndWidgets] =
- useUpdatePageLayoutWithTabsAndWidgetsMutation();
+ const { updatePageLayoutWithTabsAndWidgets } =
+ useUpdatePageLayoutWithTabsAndWidgets();
const savePageLayout = useRecoilCallback(
({ set }) =>
@@ -39,25 +39,28 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
const updateInput =
convertPageLayoutDraftToUpdateInput(pageLayoutDraft);
- const { data } = await updatePageLayoutWithTabsAndWidgets({
- variables: {
- id: pageLayoutId,
- input: updateInput,
- },
- });
+ const result = await updatePageLayoutWithTabsAndWidgets(
+ pageLayoutId,
+ updateInput,
+ );
- const updatedPageLayout = data?.updatePageLayoutWithTabsAndWidgets;
+ if (result.status === 'successful') {
+ const updatedPageLayout =
+ result.response.data?.updatePageLayoutWithTabsAndWidgets;
- if (isDefined(updatedPageLayout)) {
- const pageLayoutToPersist: PageLayout =
- transformPageLayout(updatedPageLayout);
+ if (isDefined(updatedPageLayout)) {
+ const pageLayoutToPersist: PageLayout =
+ transformPageLayout(updatedPageLayout);
- set(pageLayoutPersistedCallbackState, pageLayoutToPersist);
- set(
- pageLayoutCurrentLayoutsCallbackState,
- convertPageLayoutToTabLayouts(pageLayoutToPersist),
- );
+ set(pageLayoutPersistedCallbackState, pageLayoutToPersist);
+ set(
+ pageLayoutCurrentLayoutsCallbackState,
+ convertPageLayoutToTabLayouts(pageLayoutToPersist),
+ );
+ }
}
+
+ return result;
},
[
pageLayoutCurrentLayoutsCallbackState,
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets.ts b/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets.ts
new file mode 100644
index 0000000000..47ec755052
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets.ts
@@ -0,0 +1,61 @@
+import {
+ type UpdatePageLayoutWithTabsInput,
+ useUpdatePageLayoutWithTabsAndWidgetsMutation,
+} from '~/generated/graphql';
+
+import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
+import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { ApolloError } from '@apollo/client';
+import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
+
+export const useUpdatePageLayoutWithTabsAndWidgets = () => {
+ const [updatePageLayoutWithTabsAndWidgetsMutation] =
+ useUpdatePageLayoutWithTabsAndWidgetsMutation();
+
+ const { handleMetadataError } = useMetadataErrorHandler();
+ const { enqueueErrorSnackBar } = useSnackBar();
+
+ const updatePageLayoutWithTabsAndWidgets = async (
+ id: string,
+ input: UpdatePageLayoutWithTabsInput,
+ ): Promise<
+ MetadataRequestResult<
+ Awaited>
+ >
+ > => {
+ try {
+ const updatedPageLayout =
+ await updatePageLayoutWithTabsAndWidgetsMutation({
+ variables: {
+ id,
+ input,
+ },
+ });
+
+ return {
+ status: 'successful',
+ response: updatedPageLayout,
+ };
+ } catch (error) {
+ if (error instanceof ApolloError) {
+ handleMetadataError(error, {
+ primaryMetadataName: 'pageLayout',
+ operationType: CrudOperationType.UPDATE,
+ });
+ } else {
+ enqueueErrorSnackBar({ message: t`An error occurred.` });
+ }
+
+ return {
+ status: 'failed',
+ error,
+ };
+ }
+ };
+
+ return {
+ updatePageLayoutWithTabsAndWidgets,
+ };
+};
diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePersistServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePersistServerlessFunction.ts
index 370b4a76a7..854860db71 100644
--- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePersistServerlessFunction.ts
+++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePersistServerlessFunction.ts
@@ -12,6 +12,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
import {
type CreateOneServerlessFunctionItemMutation,
type CreateOneServerlessFunctionItemMutationVariables,
@@ -72,6 +73,7 @@ export const usePersistServerlessFunction = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'serverlessFunction',
+ operationType: CrudOperationType.CREATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
@@ -114,6 +116,7 @@ export const usePersistServerlessFunction = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'serverlessFunction',
+ operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
@@ -157,6 +160,7 @@ export const usePersistServerlessFunction = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'serverlessFunction',
+ operationType: CrudOperationType.DELETE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts
index e981650a85..700c6b6726 100644
--- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts
+++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts
@@ -6,6 +6,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useViewsSideEffectsOnViewGroups } from '@/views/hooks/useViewsSideEffectsOnViewGroups';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import {
@@ -72,6 +73,7 @@ export const usePersistView = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'view',
+ operationType: CrudOperationType.CREATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
@@ -110,6 +112,7 @@ export const usePersistView = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'view',
+ operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
@@ -143,6 +146,7 @@ export const usePersistView = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'view',
+ operationType: CrudOperationType.DELETE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewField.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewField.ts
index 8baa63d21e..f8614fcb1f 100644
--- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewField.ts
+++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewField.ts
@@ -6,6 +6,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTriggerViewFieldOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFieldOptimisticEffect';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
type CreateManyCoreViewFieldsMutationVariables,
@@ -71,6 +72,7 @@ export const usePersistViewField = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewField',
+ operationType: CrudOperationType.CREATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
@@ -132,6 +134,7 @@ export const usePersistViewField = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewField',
+ operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
@@ -193,6 +196,7 @@ export const usePersistViewField = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewField',
+ operationType: CrudOperationType.DELETE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
@@ -244,6 +248,7 @@ export const usePersistViewField = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewField',
+ operationType: CrudOperationType.DESTROY,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilter.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilter.ts
index f236dab241..ec61b7b3ee 100644
--- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilter.ts
+++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilter.ts
@@ -6,6 +6,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTriggerViewFilterOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFilterOptimisticEffect';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
type CreateCoreViewFilterMutationVariables,
@@ -71,6 +72,7 @@ export const usePersistViewFilterRecords = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewFilter',
+ operationType: CrudOperationType.CREATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
@@ -132,6 +134,7 @@ export const usePersistViewFilterRecords = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewFilter',
+ operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
@@ -193,6 +196,7 @@ export const usePersistViewFilterRecords = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewFilter',
+ operationType: CrudOperationType.DELETE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
@@ -244,6 +248,7 @@ export const usePersistViewFilterRecords = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewFilter',
+ operationType: CrudOperationType.DESTROY,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts
index eca0fff275..d7dd66f4ba 100644
--- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts
+++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts
@@ -6,6 +6,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTriggerViewGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
+import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
type UpdateCoreViewGroupMutationVariables,
@@ -63,6 +64,7 @@ export const usePersistViewGroupRecords = () => {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewGroup',
+ operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts
index 70efd38288..d8b380b3d3 100644
--- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts
@@ -1,12 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
+import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
@Module({
imports: [TypeOrmModule.forFeature([PageLayoutWidgetEntity])],
- providers: [WorkspaceFlatPageLayoutWidgetMapCacheService],
- exports: [WorkspaceFlatPageLayoutWidgetMapCacheService],
+ providers: [
+ WorkspaceFlatPageLayoutWidgetMapCacheService,
+ FlatPageLayoutWidgetTypeValidatorService,
+ ],
+ exports: [
+ WorkspaceFlatPageLayoutWidgetMapCacheService,
+ FlatPageLayoutWidgetTypeValidatorService,
+ ],
})
export class FlatPageLayoutWidgetModule {}
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service.ts
new file mode 100644
index 0000000000..e7af3da97b
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service.ts
@@ -0,0 +1,229 @@
+import { Injectable } from '@nestjs/common';
+
+import { msg } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
+import {
+ type FlatPageLayoutWidgetTypeValidatorForCreation,
+ type FlatPageLayoutWidgetTypeValidatorForUpdate,
+} from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-type-validator.type';
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { rejectWidgetType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/reject-widget-type.util';
+import { validateGraphFlatPageLayoutWidgetForCreation } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-creation.util';
+import { validateGraphFlatPageLayoutWidgetForUpdate } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-update.util';
+import { validateIframeFlatPageLayoutWidgetForCreation } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-creation.util';
+import { validateIframeFlatPageLayoutWidgetForUpdate } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-update.util';
+import { validateStandaloneRichTextFlatPageLayoutWidgetForCreation } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-creation.util';
+import { validateStandaloneRichTextFlatPageLayoutWidgetForUpdate } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-update.util';
+import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
+
+export type GenericValidateFlatPageLayoutWidgetTypeSpecificitiesArgs =
+ FlatEntityValidationArgs<'pageLayoutWidget'> & {
+ updates?: FlatEntityPropertiesUpdates<'pageLayoutWidget'>;
+ };
+
+export type ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs =
+ FlatEntityValidationArgs<'pageLayoutWidget'>;
+
+export type ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs =
+ FlatEntityValidationArgs<'pageLayoutWidget'> & {
+ updates: FlatEntityPropertiesUpdates<'pageLayoutWidget'>;
+ };
+
+@Injectable()
+export class FlatPageLayoutWidgetTypeValidatorService {
+ constructor() {}
+
+ private readonly PAGE_LAYOUT_WIDGET_TYPE_VALIDATOR_FOR_CREATION_HASHMAP: FlatPageLayoutWidgetTypeValidatorForCreation =
+ {
+ VIEW: rejectWidgetType(
+ WidgetType.VIEW,
+ 'Widget type VIEW is not supported yet.',
+ msg`Widget type VIEW is not supported yet.`,
+ ),
+ IFRAME: validateIframeFlatPageLayoutWidgetForCreation,
+ FIELD: rejectWidgetType(
+ WidgetType.FIELD,
+ 'Widget type FIELD is not supported yet.',
+ msg`Widget type FIELD is not supported yet.`,
+ ),
+ FIELDS: rejectWidgetType(
+ WidgetType.FIELDS,
+ 'Widget type FIELDS is not supported yet.',
+ msg`Widget type FIELDS is not supported yet.`,
+ ),
+ GRAPH: validateGraphFlatPageLayoutWidgetForCreation,
+ STANDALONE_RICH_TEXT:
+ validateStandaloneRichTextFlatPageLayoutWidgetForCreation,
+ TIMELINE: rejectWidgetType(
+ WidgetType.TIMELINE,
+ 'Widget type TIMELINE is not supported yet.',
+ msg`Widget type TIMELINE is not supported yet.`,
+ ),
+ TASKS: rejectWidgetType(
+ WidgetType.TASKS,
+ 'Widget type TASKS is not supported yet.',
+ msg`Widget type TASKS is not supported yet.`,
+ ),
+ NOTES: rejectWidgetType(
+ WidgetType.NOTES,
+ 'Widget type NOTES is not supported yet.',
+ msg`Widget type NOTES is not supported yet.`,
+ ),
+ FILES: rejectWidgetType(
+ WidgetType.FILES,
+ 'Widget type FILES is not supported yet.',
+ msg`Widget type FILES is not supported yet.`,
+ ),
+ EMAILS: rejectWidgetType(
+ WidgetType.EMAILS,
+ 'Widget type EMAILS is not supported yet.',
+ msg`Widget type EMAILS is not supported yet.`,
+ ),
+ CALENDAR: rejectWidgetType(
+ WidgetType.CALENDAR,
+ 'Widget type CALENDAR is not supported yet.',
+ msg`Widget type CALENDAR is not supported yet.`,
+ ),
+ FIELD_RICH_TEXT: rejectWidgetType(
+ WidgetType.FIELD_RICH_TEXT,
+ 'Widget type FIELD_RICH_TEXT is not supported yet.',
+ msg`Widget type FIELD_RICH_TEXT is not supported yet.`,
+ ),
+ WORKFLOW: rejectWidgetType(
+ WidgetType.WORKFLOW,
+ 'Widget type WORKFLOW is not supported yet.',
+ msg`Widget type WORKFLOW is not supported yet.`,
+ ),
+ WORKFLOW_VERSION: rejectWidgetType(
+ WidgetType.WORKFLOW_VERSION,
+ 'Widget type WORKFLOW_VERSION is not supported yet.',
+ msg`Widget type WORKFLOW_VERSION is not supported yet.`,
+ ),
+ WORKFLOW_RUN: rejectWidgetType(
+ WidgetType.WORKFLOW_RUN,
+ 'Widget type WORKFLOW_RUN is not supported yet.',
+ msg`Widget type WORKFLOW_RUN is not supported yet.`,
+ ),
+ };
+
+ private readonly PAGE_LAYOUT_WIDGET_TYPE_VALIDATOR_FOR_UPDATE_HASHMAP: FlatPageLayoutWidgetTypeValidatorForUpdate =
+ {
+ VIEW: rejectWidgetType(
+ WidgetType.VIEW,
+ 'Widget type VIEW is not supported yet.',
+ msg`Widget type VIEW is not supported yet.`,
+ ),
+ IFRAME: validateIframeFlatPageLayoutWidgetForUpdate,
+ FIELD: rejectWidgetType(
+ WidgetType.FIELD,
+ 'Widget type FIELD is not supported yet.',
+ msg`Widget type FIELD is not supported yet.`,
+ ),
+ FIELDS: rejectWidgetType(
+ WidgetType.FIELDS,
+ 'Widget type FIELDS is not supported yet.',
+ msg`Widget type FIELDS is not supported yet.`,
+ ),
+ GRAPH: validateGraphFlatPageLayoutWidgetForUpdate,
+ STANDALONE_RICH_TEXT:
+ validateStandaloneRichTextFlatPageLayoutWidgetForUpdate,
+ TIMELINE: rejectWidgetType(
+ WidgetType.TIMELINE,
+ 'Widget type TIMELINE is not supported yet.',
+ msg`Widget type TIMELINE is not supported yet.`,
+ ),
+ TASKS: rejectWidgetType(
+ WidgetType.TASKS,
+ 'Widget type TASKS is not supported yet.',
+ msg`Widget type TASKS is not supported yet.`,
+ ),
+ NOTES: rejectWidgetType(
+ WidgetType.NOTES,
+ 'Widget type NOTES is not supported yet.',
+ msg`Widget type NOTES is not supported yet.`,
+ ),
+ FILES: rejectWidgetType(
+ WidgetType.FILES,
+ 'Widget type FILES is not supported yet.',
+ msg`Widget type FILES is not supported yet.`,
+ ),
+ EMAILS: rejectWidgetType(
+ WidgetType.EMAILS,
+ 'Widget type EMAILS is not supported yet.',
+ msg`Widget type EMAILS is not supported yet.`,
+ ),
+ CALENDAR: rejectWidgetType(
+ WidgetType.CALENDAR,
+ 'Widget type CALENDAR is not supported yet.',
+ msg`Widget type CALENDAR is not supported yet.`,
+ ),
+ FIELD_RICH_TEXT: rejectWidgetType(
+ WidgetType.FIELD_RICH_TEXT,
+ 'Widget type FIELD_RICH_TEXT is not supported yet.',
+ msg`Widget type FIELD_RICH_TEXT is not supported yet.`,
+ ),
+ WORKFLOW: rejectWidgetType(
+ WidgetType.WORKFLOW,
+ 'Widget type WORKFLOW is not supported yet.',
+ msg`Widget type WORKFLOW is not supported yet.`,
+ ),
+ WORKFLOW_VERSION: rejectWidgetType(
+ WidgetType.WORKFLOW_VERSION,
+ 'Widget type WORKFLOW_VERSION is not supported yet.',
+ msg`Widget type WORKFLOW_VERSION is not supported yet.`,
+ ),
+ WORKFLOW_RUN: rejectWidgetType(
+ WidgetType.WORKFLOW_RUN,
+ 'Widget type WORKFLOW_RUN is not supported yet.',
+ msg`Widget type WORKFLOW_RUN is not supported yet.`,
+ ),
+ };
+
+ public validateFlatPageLayoutWidgetTypeSpecificitiesForCreation(
+ args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
+ ): FlatPageLayoutWidgetValidationError[] {
+ const { flatEntityToValidate } = args;
+ const widgetType = flatEntityToValidate.type;
+ const pageLayoutWidgetTypeValidator =
+ this.PAGE_LAYOUT_WIDGET_TYPE_VALIDATOR_FOR_CREATION_HASHMAP[widgetType];
+
+ if (!isDefined(pageLayoutWidgetTypeValidator)) {
+ return [
+ {
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: `Unsupported page layout widget type ${widgetType}`,
+ value: widgetType,
+ userFriendlyMessage: msg`Unsupported page layout widget type ${widgetType}`,
+ },
+ ];
+ }
+
+ return pageLayoutWidgetTypeValidator(args);
+ }
+
+ public validateFlatPageLayoutWidgetTypeSpecificitiesForUpdate(
+ args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
+ ): FlatPageLayoutWidgetValidationError[] {
+ const { flatEntityToValidate } = args;
+ const widgetType = flatEntityToValidate.type;
+ const pageLayoutWidgetTypeValidator =
+ this.PAGE_LAYOUT_WIDGET_TYPE_VALIDATOR_FOR_UPDATE_HASHMAP[widgetType];
+
+ if (!isDefined(pageLayoutWidgetTypeValidator)) {
+ return [
+ {
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: `Unsupported page layout widget type ${widgetType}`,
+ value: widgetType,
+ userFriendlyMessage: msg`Unsupported page layout widget type ${widgetType}`,
+ },
+ ];
+ }
+
+ return pageLayoutWidgetTypeValidator(args);
+ }
+}
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-type-validator.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-type-validator.type.ts
new file mode 100644
index 0000000000..68cbe34140
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-type-validator.type.ts
@@ -0,0 +1,25 @@
+import {
+ type GenericValidateFlatPageLayoutWidgetTypeSpecificitiesArgs,
+ type ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
+ type ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
+} from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
+
+export type FlatPageLayoutWidgetTypeValidator = {
+ [T in WidgetType]: (
+ args: GenericValidateFlatPageLayoutWidgetTypeSpecificitiesArgs,
+ ) => FlatPageLayoutWidgetValidationError[];
+};
+
+export type FlatPageLayoutWidgetTypeValidatorForCreation = {
+ [T in WidgetType]: (
+ args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
+ ) => FlatPageLayoutWidgetValidationError[];
+};
+
+export type FlatPageLayoutWidgetTypeValidatorForUpdate = {
+ [T in WidgetType]: (
+ args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
+ ) => FlatPageLayoutWidgetValidationError[];
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type.ts
new file mode 100644
index 0000000000..8253a5668e
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type.ts
@@ -0,0 +1,10 @@
+import { type MessageDescriptor } from '@lingui/core';
+
+import { type PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export type FlatPageLayoutWidgetValidationError = {
+ code: PageLayoutWidgetExceptionCode;
+ message: string;
+ userFriendlyMessage?: MessageDescriptor;
+ value?: unknown;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts
index 3ea3d51d2a..406402d594 100644
--- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts
@@ -3,7 +3,7 @@ import { v4 } from 'uuid';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
-import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
+import { validateWidgetConfigurationInput } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util';
export type FromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreateArgs =
{
@@ -23,6 +23,10 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({
['pageLayoutTabId'],
);
+ validateWidgetConfigurationInput({
+ configuration: createPageLayoutWidgetInput.configuration,
+ });
+
const createdAt = new Date().toISOString();
const pageLayoutWidgetId = v4();
@@ -35,7 +39,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({
deletedAt: null,
universalIdentifier: pageLayoutWidgetId,
title: createPageLayoutWidgetInput.title,
- type: createPageLayoutWidgetInput.type ?? WidgetType.VIEW,
+ type: createPageLayoutWidgetInput.type,
objectMetadataId: createPageLayoutWidgetInput.objectMetadataId ?? null,
gridPosition: createPageLayoutWidgetInput.gridPosition,
configuration: createPageLayoutWidgetInput.configuration,
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts
index 14b284f035..d76bc73399 100644
--- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts
@@ -12,6 +12,7 @@ import {
PageLayoutWidgetException,
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 { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export type UpdatePageLayoutWidgetInputWithId = {
@@ -47,6 +48,17 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro
FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES,
);
+ if (
+ Object.prototype.hasOwnProperty.call(
+ updatedEditableFieldProperties,
+ 'configuration',
+ )
+ ) {
+ validateWidgetConfigurationInput({
+ configuration: updatedEditableFieldProperties.configuration,
+ });
+ }
+
return mergeUpdateInExistingRecord({
existing: existingFlatPageLayoutWidgetToUpdate,
properties: FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES,
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/constants/valid-graph-configuration-types.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/constants/valid-graph-configuration-types.constant.ts
new file mode 100644
index 0000000000..7c91a389b1
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/constants/valid-graph-configuration-types.constant.ts
@@ -0,0 +1,9 @@
+import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
+
+export const VALID_GRAPH_CONFIGURATION_TYPES = [
+ WidgetConfigurationType.AGGREGATE_CHART,
+ WidgetConfigurationType.BAR_CHART,
+ WidgetConfigurationType.LINE_CHART,
+ WidgetConfigurationType.PIE_CHART,
+ WidgetConfigurationType.GAUGE_CHART,
+] as const;
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/base-graph-configuration.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/base-graph-configuration.type.ts
new file mode 100644
index 0000000000..8675c0b6ec
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/base-graph-configuration.type.ts
@@ -0,0 +1,6 @@
+import { type GraphConfiguration } from './graph-configuration.type';
+
+export type BaseGraphConfiguration = Pick<
+ GraphConfiguration,
+ 'configurationType' | 'aggregateFieldMetadataId' | 'aggregateOperation'
+>;
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type.ts
new file mode 100644
index 0000000000..cd7baf194c
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type.ts
@@ -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 GraphConfiguration =
+ | BarChartConfigurationDTO
+ | LineChartConfigurationDTO
+ | PieChartConfigurationDTO
+ | AggregateChartConfigurationDTO
+ | GaugeChartConfigurationDTO;
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type.ts
new file mode 100644
index 0000000000..8e34a48ee6
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type.ts
@@ -0,0 +1,6 @@
+import { type WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
+
+export type IframeConfiguration = {
+ configurationType?: WidgetConfigurationType;
+ url?: string;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/reject-widget-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/reject-widget-type.util.ts
new file mode 100644
index 0000000000..85e085faf6
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/reject-widget-type.util.ts
@@ -0,0 +1,22 @@
+import { type msg } from '@lingui/core/macro';
+
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const rejectWidgetType = (
+ widgetType: WidgetType,
+ message: string,
+ userFriendlyMessage: ReturnType,
+) => {
+ return (): FlatPageLayoutWidgetValidationError[] => {
+ return [
+ {
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message,
+ value: widgetType,
+ userFriendlyMessage,
+ },
+ ];
+ };
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-bar-chart-configuration.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-bar-chart-configuration.util.ts
new file mode 100644
index 0000000000..ad327e68af
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-bar-chart-configuration.util.ts
@@ -0,0 +1,31 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validateBarChartConfiguration = (
+ configuration: BarChartConfigurationDTO,
+ widgetTitle: string,
+): FlatPageLayoutWidgetValidationError[] => {
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration.primaryAxisGroupByFieldMetadataId)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Primary axis group by field is required for bar chart widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`Primary axis group by field is required for bar chart`,
+ });
+ }
+
+ if (!isDefined(configuration.layout)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Layout is required for bar chart widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`Layout is required for bar chart`,
+ });
+ }
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-base-graph-fields.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-base-graph-fields.util.ts
new file mode 100644
index 0000000000..95ad403ba6
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-base-graph-fields.util.ts
@@ -0,0 +1,31 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type BaseGraphConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/base-graph-configuration.type';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validateBaseGraphFields = (
+ configuration: BaseGraphConfiguration,
+ widgetTitle: string,
+): FlatPageLayoutWidgetValidationError[] => {
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration.aggregateFieldMetadataId)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Aggregate field metadata ID is required for graph widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`Aggregate field is required for graph widget`,
+ });
+ }
+
+ if (!isDefined(configuration.aggregateOperation)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Aggregate operation is required for graph widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`Aggregate operation is required for graph widget`,
+ });
+ }
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-by-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-by-type.util.ts
new file mode 100644
index 0000000000..7d1e0000ab
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-by-type.util.ts
@@ -0,0 +1,27 @@
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type GraphConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type';
+import { validateBarChartConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-bar-chart-configuration.util';
+import { validateLineChartConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-line-chart-configuration.util';
+import { validatePieChartConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-pie-chart-configuration.util';
+import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
+
+export const validateGraphConfigurationByType = (
+ configuration: GraphConfiguration,
+ widgetTitle: string,
+): FlatPageLayoutWidgetValidationError[] => {
+ const configurationType = configuration.configurationType;
+
+ switch (configurationType) {
+ case WidgetConfigurationType.BAR_CHART:
+ return validateBarChartConfiguration(configuration, widgetTitle);
+ case WidgetConfigurationType.LINE_CHART:
+ return validateLineChartConfiguration(configuration, widgetTitle);
+ case WidgetConfigurationType.PIE_CHART:
+ return validatePieChartConfiguration(configuration, widgetTitle);
+ case WidgetConfigurationType.AGGREGATE_CHART:
+ case WidgetConfigurationType.GAUGE_CHART:
+ return [];
+ default:
+ return [];
+ }
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-type.util.ts
new file mode 100644
index 0000000000..81499b6702
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-type.util.ts
@@ -0,0 +1,44 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { VALID_GRAPH_CONFIGURATION_TYPES } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/constants/valid-graph-configuration-types.constant';
+import { type GraphConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validateGraphConfigurationType = (
+ configuration: GraphConfiguration,
+ widgetTitle: string,
+): FlatPageLayoutWidgetValidationError[] => {
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration.configurationType)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Configuration type is required for widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`Configuration type is required`,
+ });
+
+ return errors;
+ }
+
+ const isValidGraphConfigurationType =
+ VALID_GRAPH_CONFIGURATION_TYPES.includes(configuration.configurationType);
+
+ if (!isValidGraphConfigurationType) {
+ const expectedConfigurationTypes = VALID_GRAPH_CONFIGURATION_TYPES.map(
+ (type) => type.toString(),
+ ).join(', ');
+
+ const configurationTypeString = configuration.configurationType.toString();
+
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Invalid configuration type for graph widget "${widgetTitle}". Expected one of ${expectedConfigurationTypes}, got ${configurationTypeString}`,
+ userFriendlyMessage: msg`Invalid configuration type for graph widget`,
+ value: configuration.configurationType,
+ });
+ }
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-creation.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-creation.util.ts
new file mode 100644
index 0000000000..5251194adf
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-creation.util.ts
@@ -0,0 +1,54 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type GraphConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type';
+import { validateBaseGraphFields } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-base-graph-fields.util';
+import { validateGraphConfigurationByType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-by-type.util';
+import { validateGraphConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-type.util';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validateGraphFlatPageLayoutWidgetForCreation = (
+ args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
+): FlatPageLayoutWidgetValidationError[] => {
+ const { flatEntityToValidate } = args;
+ const { configuration, title } = flatEntityToValidate;
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Configuration is required for graph widget "${title}"`,
+ userFriendlyMessage: msg`Configuration is required for graph widget`,
+ });
+
+ return errors;
+ }
+
+ const graphConfiguration = configuration as GraphConfiguration;
+
+ const configurationTypeErrors = validateGraphConfigurationType(
+ graphConfiguration,
+ title,
+ );
+
+ errors.push(...configurationTypeErrors);
+
+ if (configurationTypeErrors.length > 0) {
+ return errors;
+ }
+
+ const baseFieldErrors = validateBaseGraphFields(graphConfiguration, title);
+
+ errors.push(...baseFieldErrors);
+
+ const typeSpecificErrors = validateGraphConfigurationByType(
+ graphConfiguration,
+ title,
+ );
+
+ errors.push(...typeSpecificErrors);
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-update.util.ts
new file mode 100644
index 0000000000..ae6057ded6
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-update.util.ts
@@ -0,0 +1,46 @@
+import { isDefined } from 'twenty-shared/utils';
+
+import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type GraphConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type';
+import { validateBaseGraphFields } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-base-graph-fields.util';
+import { validateGraphConfigurationByType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-by-type.util';
+import { validateGraphConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-type.util';
+
+export const validateGraphFlatPageLayoutWidgetForUpdate = (
+ args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
+): FlatPageLayoutWidgetValidationError[] => {
+ const { flatEntityToValidate } = args;
+ const { configuration, title } = flatEntityToValidate;
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration)) {
+ return [];
+ }
+
+ const graphConfiguration = configuration as GraphConfiguration;
+
+ const configurationTypeErrors = validateGraphConfigurationType(
+ graphConfiguration,
+ title,
+ );
+
+ errors.push(...configurationTypeErrors);
+
+ if (configurationTypeErrors.length > 0) {
+ return errors;
+ }
+
+ const baseFieldErrors = validateBaseGraphFields(graphConfiguration, title);
+
+ errors.push(...baseFieldErrors);
+
+ const typeSpecificErrors = validateGraphConfigurationByType(
+ graphConfiguration,
+ title,
+ );
+
+ errors.push(...typeSpecificErrors);
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-configuration-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-configuration-type.util.ts
new file mode 100644
index 0000000000..eb43e9a49a
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-configuration-type.util.ts
@@ -0,0 +1,37 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type IframeConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type';
+import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validateIframeConfigurationType = (
+ configuration: IframeConfiguration,
+ widgetTitle: string,
+): FlatPageLayoutWidgetValidationError[] => {
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration.configurationType)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Configuration type is required for widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`Configuration type is required`,
+ });
+
+ return errors;
+ }
+
+ if (configuration.configurationType !== WidgetConfigurationType.IFRAME) {
+ const configurationTypeString = configuration.configurationType.toString();
+
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Invalid configuration type for iframe widget "${widgetTitle}". Expected IFRAME, got ${configurationTypeString}`,
+ userFriendlyMessage: msg`Invalid configuration type for iframe widget`,
+ value: configuration.configurationType,
+ });
+ }
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-creation.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-creation.util.ts
new file mode 100644
index 0000000000..ed884dcbb8
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-creation.util.ts
@@ -0,0 +1,42 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type IframeConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type';
+import { validateIframeConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-configuration-type.util';
+import { validateIframeUrl } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-url.util';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validateIframeFlatPageLayoutWidgetForCreation = (
+ args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
+): FlatPageLayoutWidgetValidationError[] => {
+ const { flatEntityToValidate } = args;
+ const { configuration, title } = flatEntityToValidate;
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Configuration is required for iframe widget "${title}"`,
+ userFriendlyMessage: msg`Configuration is required for iframe widget`,
+ });
+
+ return errors;
+ }
+
+ const iframeConfiguration = configuration as IframeConfiguration;
+
+ const configurationTypeErrors = validateIframeConfigurationType(
+ iframeConfiguration,
+ title,
+ );
+
+ errors.push(...configurationTypeErrors);
+
+ const urlErrors = validateIframeUrl(iframeConfiguration, title);
+
+ errors.push(...urlErrors);
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-update.util.ts
new file mode 100644
index 0000000000..755b0d54d6
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-update.util.ts
@@ -0,0 +1,34 @@
+import { isDefined } from 'twenty-shared/utils';
+
+import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type IframeConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type';
+import { validateIframeConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-configuration-type.util';
+import { validateIframeUrl } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-url.util';
+
+export const validateIframeFlatPageLayoutWidgetForUpdate = (
+ args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
+): FlatPageLayoutWidgetValidationError[] => {
+ const { flatEntityToValidate } = args;
+ const { configuration, title } = flatEntityToValidate;
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration)) {
+ return [];
+ }
+
+ const iframeConfiguration = configuration as IframeConfiguration;
+
+ const configurationTypeErrors = validateIframeConfigurationType(
+ iframeConfiguration,
+ title,
+ );
+
+ errors.push(...configurationTypeErrors);
+
+ const urlErrors = validateIframeUrl(iframeConfiguration, title);
+
+ errors.push(...urlErrors);
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-url.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-url.util.ts
new file mode 100644
index 0000000000..08160dd532
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-url.util.ts
@@ -0,0 +1,24 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type IframeConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validateIframeUrl = (
+ configuration: IframeConfiguration,
+ widgetTitle: string,
+): FlatPageLayoutWidgetValidationError[] => {
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (isDefined(configuration.url) && typeof configuration.url !== 'string') {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`URL must be a string for widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`URL must be a string`,
+ value: configuration.url,
+ });
+ }
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-line-chart-configuration.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-line-chart-configuration.util.ts
new file mode 100644
index 0000000000..35334aa289
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-line-chart-configuration.util.ts
@@ -0,0 +1,23 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type LineChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validateLineChartConfiguration = (
+ configuration: LineChartConfigurationDTO,
+ widgetTitle: string,
+): FlatPageLayoutWidgetValidationError[] => {
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration.primaryAxisGroupByFieldMetadataId)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Primary axis group by field is required for line chart widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`Primary axis group by field is required for line chart`,
+ });
+ }
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-pie-chart-configuration.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-pie-chart-configuration.util.ts
new file mode 100644
index 0000000000..6d27dae982
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-pie-chart-configuration.util.ts
@@ -0,0 +1,23 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validatePieChartConfiguration = (
+ configuration: PieChartConfigurationDTO,
+ widgetTitle: string,
+): FlatPageLayoutWidgetValidationError[] => {
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration.groupByFieldMetadataId)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Group by field is required for pie chart widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`Group by field is required for pie chart`,
+ });
+ }
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-body.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-body.util.ts
new file mode 100644
index 0000000000..321f500e5a
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-body.util.ts
@@ -0,0 +1,37 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validateStandaloneRichTextBody = (
+ configuration: StandaloneRichTextConfigurationDTO,
+ widgetTitle: string,
+): FlatPageLayoutWidgetValidationError[] => {
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration.body)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Body is required for standalone rich text widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`Body is required for standalone rich text widget`,
+ });
+
+ return errors;
+ }
+
+ if (
+ typeof configuration.body !== 'object' ||
+ Array.isArray(configuration.body)
+ ) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Body must be an object for widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`Body must be an object`,
+ value: configuration.body,
+ });
+ }
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-configuration-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-configuration-type.util.ts
new file mode 100644
index 0000000000..721d490f16
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-configuration-type.util.ts
@@ -0,0 +1,43 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { type StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
+import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validateStandaloneRichTextConfigurationType = (
+ configuration: StandaloneRichTextConfigurationDTO,
+ widgetTitle: string,
+): FlatPageLayoutWidgetValidationError[] => {
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration.configurationType)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Configuration type is required for widget "${widgetTitle}"`,
+ userFriendlyMessage: msg`Configuration type is required`,
+ });
+
+ return errors;
+ }
+
+ if (
+ configuration.configurationType !==
+ WidgetConfigurationType.STANDALONE_RICH_TEXT
+ ) {
+ const expectedConfigurationType =
+ WidgetConfigurationType.STANDALONE_RICH_TEXT;
+
+ const configurationType = configuration.configurationType;
+
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Invalid configuration type for standalone rich text widget "${widgetTitle}". Expected ${expectedConfigurationType}, got ${configurationType}`,
+ userFriendlyMessage: msg`Invalid configuration type for standalone rich text widget`,
+ value: configuration.configurationType,
+ });
+ }
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-creation.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-creation.util.ts
new file mode 100644
index 0000000000..13819c71b6
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-creation.util.ts
@@ -0,0 +1,46 @@
+import { msg, t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { validateStandaloneRichTextBody } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-body.util';
+import { validateStandaloneRichTextConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-configuration-type.util';
+import { type StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+
+export const validateStandaloneRichTextFlatPageLayoutWidgetForCreation = (
+ args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
+): FlatPageLayoutWidgetValidationError[] => {
+ const { flatEntityToValidate } = args;
+ const { configuration, title } = flatEntityToValidate;
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration)) {
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Configuration is required for standalone rich text widget "${title}"`,
+ userFriendlyMessage: msg`Configuration is required for standalone rich text widget`,
+ });
+
+ return errors;
+ }
+
+ const standaloneRichTextConfiguration =
+ configuration as StandaloneRichTextConfigurationDTO;
+
+ const configurationTypeErrors = validateStandaloneRichTextConfigurationType(
+ standaloneRichTextConfiguration,
+ title,
+ );
+
+ errors.push(...configurationTypeErrors);
+
+ const bodyErrors = validateStandaloneRichTextBody(
+ standaloneRichTextConfiguration,
+ title,
+ );
+
+ errors.push(...bodyErrors);
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-update.util.ts
new file mode 100644
index 0000000000..3e4f9821c0
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-update.util.ts
@@ -0,0 +1,38 @@
+import { isDefined } from 'twenty-shared/utils';
+
+import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
+import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
+import { validateStandaloneRichTextBody } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-body.util';
+import { validateStandaloneRichTextConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-configuration-type.util';
+import { type StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
+
+export const validateStandaloneRichTextFlatPageLayoutWidgetForUpdate = (
+ args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
+): FlatPageLayoutWidgetValidationError[] => {
+ const { flatEntityToValidate } = args;
+ const { configuration, title } = flatEntityToValidate;
+ const errors: FlatPageLayoutWidgetValidationError[] = [];
+
+ if (!isDefined(configuration)) {
+ return [];
+ }
+
+ const standaloneRichTextConfiguration =
+ configuration as StandaloneRichTextConfigurationDTO;
+
+ const configurationTypeErrors = validateStandaloneRichTextConfigurationType(
+ standaloneRichTextConfiguration,
+ title,
+ );
+
+ errors.push(...configurationTypeErrors);
+
+ const bodyErrors = validateStandaloneRichTextBody(
+ standaloneRichTextConfiguration,
+ title,
+ );
+
+ errors.push(...bodyErrors);
+
+ return errors;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input.ts
index aa0dd7a6da..9a7fc18b69 100644
--- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input.ts
@@ -31,7 +31,6 @@ export class CreatePageLayoutWidgetInput {
@Field(() => WidgetType, { nullable: false })
@IsEnum(WidgetType)
- @IsOptional()
type: WidgetType;
@Field(() => UUIDScalarType, { nullable: true })
diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts
index 65a45f47b8..4184ed1e44 100644
--- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts
@@ -3,8 +3,6 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
-import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
-import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
@@ -20,16 +18,13 @@ 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 { 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 { 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 { validateAndTransformWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-and-transform-widget-configuration.util';
import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@@ -43,7 +38,6 @@ type WidgetMigrationOperations = {
@Injectable()
export class PageLayoutWidgetService {
constructor(
- private readonly featureFlagService: FeatureFlagService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
@@ -63,56 +57,6 @@ export class PageLayoutWidgetService {
return flatPageLayoutWidgetMaps;
}
- private async validateWidgetConfigurationOrThrow({
- type,
- configuration,
- workspaceId,
- titleForError,
- }: {
- type: WidgetType;
- configuration: AllPageLayoutWidgetConfiguration;
- workspaceId: string;
- titleForError: string;
- }): Promise {
- const isDashboardV2Enabled = await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IS_DASHBOARD_V2_ENABLED,
- workspaceId,
- );
-
- let validatedConfig: AllPageLayoutWidgetConfiguration | null = null;
-
- try {
- validatedConfig = await validateAndTransformWidgetConfiguration({
- type,
- configuration,
- isDashboardV2Enabled,
- });
- } catch (error) {
- throw new PageLayoutWidgetException(
- generatePageLayoutWidgetExceptionMessage(
- PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
- titleForError,
- type,
- error instanceof Error ? error.message : String(error),
- ),
- PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
- );
- }
-
- if (!isDefined(validatedConfig)) {
- throw new PageLayoutWidgetException(
- generatePageLayoutWidgetExceptionMessage(
- PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
- titleForError,
- type,
- ),
- PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
- );
- }
-
- return validatedConfig;
- }
-
private async validateAndRunWidgetMigration({
workspaceId,
operations,
@@ -189,18 +133,6 @@ export class PageLayoutWidgetService {
createPageLayoutWidgetInput: CreatePageLayoutWidgetInput,
workspaceId: string,
): Promise {
- this.validateCreateInput(createPageLayoutWidgetInput);
-
- validateWidgetGridPosition(
- createPageLayoutWidgetInput.gridPosition,
- createPageLayoutWidgetInput.title,
- );
-
- const validatedConfig = await this.getValidatedConfigurationForCreate(
- createPageLayoutWidgetInput,
- workspaceId,
- );
-
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
@@ -208,10 +140,7 @@ export class PageLayoutWidgetService {
const flatPageLayoutWidgetToCreate =
fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate({
- createPageLayoutWidgetInput: {
- ...createPageLayoutWidgetInput,
- configuration: validatedConfig,
- },
+ createPageLayoutWidgetInput,
workspaceId,
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
});
@@ -237,47 +166,6 @@ export class PageLayoutWidgetService {
);
}
- private validateCreateInput(input: CreatePageLayoutWidgetInput): void {
- if (!isDefined(input.title)) {
- throw new PageLayoutWidgetException(
- generatePageLayoutWidgetExceptionMessage(
- PageLayoutWidgetExceptionMessageKey.TITLE_REQUIRED,
- ),
- PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
- );
- }
-
- if (!isDefined(input.pageLayoutTabId)) {
- throw new PageLayoutWidgetException(
- generatePageLayoutWidgetExceptionMessage(
- PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_ID_REQUIRED,
- ),
- PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
- );
- }
-
- if (!isDefined(input.gridPosition)) {
- throw new PageLayoutWidgetException(
- generatePageLayoutWidgetExceptionMessage(
- PageLayoutWidgetExceptionMessageKey.GRID_POSITION_REQUIRED,
- ),
- PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
- );
- }
- }
-
- private async getValidatedConfigurationForCreate(
- input: CreatePageLayoutWidgetInput,
- workspaceId: string,
- ): Promise {
- return await this.validateWidgetConfigurationOrThrow({
- type: input.type,
- configuration: input.configuration,
- workspaceId,
- titleForError: input.title,
- });
- }
-
async update(
id: string,
workspaceId: string,
@@ -297,21 +185,10 @@ export class PageLayoutWidgetService {
validateWidgetGridPosition(updateData.gridPosition, titleForValidation);
}
- const validatedConfig = await this.getValidatedConfigurationForUpdate(
- updateData,
- existingWidget,
- workspaceId,
- );
-
const updatePageLayoutWidgetInput: UpdatePageLayoutWidgetInputWithId = {
id,
update: {
...updateData,
- ...(isDefined(validatedConfig)
- ? {
- configuration: validatedConfig,
- }
- : {}),
},
};
@@ -361,26 +238,6 @@ export class PageLayoutWidgetService {
return existingWidget;
}
- private async getValidatedConfigurationForUpdate(
- updateData: UpdatePageLayoutWidgetInput,
- existingWidget: FlatPageLayoutWidget,
- workspaceId: string,
- ): Promise {
- if (!isDefined(updateData.configuration)) {
- return undefined;
- }
-
- const typeForValidation = updateData.type ?? existingWidget.type;
- const titleForError = updateData.title ?? existingWidget.title;
-
- return await this.validateWidgetConfigurationOrThrow({
- type: typeForValidation,
- configuration: updateData.configuration,
- workspaceId,
- titleForError,
- });
- }
-
async delete(id: string, workspaceId: string): Promise {
const existingFlatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts
deleted file mode 100644
index acace9ca95..0000000000
--- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts
+++ /dev/null
@@ -1,378 +0,0 @@
-import {
- INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
- INVALID_IFRAME_CONFIG_BAD_URL,
- INVALID_IFRAME_CONFIG_EMPTY_URL,
- INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
- INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
- INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
- INVALID_STANDALONE_RICH_TEXT_CONFIG_INVALID_SUBFIELDS,
- INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
- INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
- TEST_GAUGE_CHART_CONFIG,
- TEST_HORIZONTAL_BAR_CHART_CONFIG,
- TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
- TEST_IFRAME_CONFIG,
- TEST_LINE_CHART_CONFIG,
- TEST_NUMBER_CHART_CONFIG,
- TEST_NUMBER_CHART_CONFIG_MINIMAL,
- TEST_PIE_CHART_CONFIG,
- TEST_STANDALONE_RICH_TEXT_CONFIG,
- TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL,
- TEST_VERTICAL_BAR_CHART_CONFIG,
- TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
-} from 'test/integration/constants/widget-configuration-test-data.constants';
-
-import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
-import { validateAndTransformWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-and-transform-widget-configuration.util';
-
-jest.mock(
- 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util',
- () => ({
- transformRichTextV2Value: jest.fn((value) =>
- Promise.resolve({
- blocknote: value.blocknote ?? null,
- markdown: value.markdown ?? null,
- }),
- ),
- }),
-);
-
-describe('validateAndTransformWidgetConfiguration', () => {
- describe('IFRAME widget', () => {
- it('should validate and transform valid iframe configuration', async () => {
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.IFRAME,
- configuration: TEST_IFRAME_CONFIG,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toMatchObject(TEST_IFRAME_CONFIG);
- });
-
- it('should throw error for invalid URL', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.IFRAME,
- configuration: INVALID_IFRAME_CONFIG_BAD_URL,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow(/url must be a URL address/);
- });
-
- it('should throw error for empty URL', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.IFRAME,
- configuration: INVALID_IFRAME_CONFIG_EMPTY_URL,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow(/url must be a URL address/);
- });
- });
-
- describe('STANDALONE_RICH_TEXT widget', () => {
- it('should validate and transform valid standalone rich text configuration', async () => {
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.STANDALONE_RICH_TEXT,
- configuration: TEST_STANDALONE_RICH_TEXT_CONFIG,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toMatchObject(TEST_STANDALONE_RICH_TEXT_CONFIG);
- });
-
- it('should validate minimal standalone rich text configuration', async () => {
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.STANDALONE_RICH_TEXT,
- configuration: TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toMatchObject(TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL);
- });
-
- it('should throw error for missing body', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.STANDALONE_RICH_TEXT,
- configuration: INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow(/body/);
- });
-
- it('should throw error when body is wrong type', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.STANDALONE_RICH_TEXT,
- configuration: INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow();
- });
-
- it('should strip invalid subfields from body', async () => {
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.STANDALONE_RICH_TEXT,
- configuration: INVALID_STANDALONE_RICH_TEXT_CONFIG_INVALID_SUBFIELDS,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toBeDefined();
- expect((result as any).body.blocknote).toBeDefined();
- expect((result as any).body.markdown).toBe('valid');
- expect((result as any).body.invalidField).toBeUndefined();
- });
- });
-
- describe('GRAPH widget', () => {
- describe('NUMBER graph', () => {
- it('should validate full number graph configuration', async () => {
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_NUMBER_CHART_CONFIG,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG);
- });
-
- it('should validate minimal number graph configuration', async () => {
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG_MINIMAL);
- });
-
- it('should throw error for partial number graph configuration with missing required fields', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow(/aggregateFieldMetadataId.*aggregateOperation/);
- });
-
- it('should throw error for invalid UUID', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow(/aggregateFieldMetadataId must be a UUID/);
- });
- });
-
- describe('VERTICAL_BAR graph', () => {
- it('should validate full vertical bar graph configuration', async () => {
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_VERTICAL_BAR_CHART_CONFIG,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG);
- });
-
- it('should validate minimal vertical bar graph configuration', async () => {
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL);
- });
-
- it('should throw error for partial vertical bar graph configuration with missing required fields', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow(/primaryAxisGroupByFieldMetadataId/);
- });
- });
-
- describe('HORIZONTAL_BAR graph', () => {
- it('should validate full horizontal bar graph configuration', async () => {
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG);
- });
-
- it('should validate minimal horizontal bar graph configuration', async () => {
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL);
- });
-
- it('should throw error for partial horizontal bar graph configuration with missing required fields', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow(/primaryAxisGroupByFieldMetadataId/);
- });
- });
-
- it('should return null for unsupported graph type', async () => {
- const configuration = {
- graphType: 'UNSUPPORTED',
- viewId: '550e8400-e29b-41d4-a716-446655440000',
- };
-
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: configuration,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toBeNull();
- });
-
- it('should return null for missing graph type', async () => {
- const configuration = {
- viewId: '550e8400-e29b-41d4-a716-446655440000',
- };
-
- const result = await validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: configuration,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toBeNull();
- });
- });
-
- describe('Edge cases', () => {
- it('should throw error for null configuration', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.IFRAME,
- configuration: null,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow('Invalid configuration: not an object');
- });
-
- it('should throw error for undefined configuration', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.IFRAME,
- configuration: undefined,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow('Invalid configuration: not an object');
- });
-
- it('should throw error for non-object configuration', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.IFRAME,
- configuration: 'string',
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow('Invalid configuration: not an object');
- });
-
- it('should return null for unsupported widget type', async () => {
- const configuration = { someField: 'value' };
-
- const result = await validateAndTransformWidgetConfiguration({
- type: 'UNSUPPORTED' as WidgetType,
- configuration: configuration,
- isDashboardV2Enabled: false,
- });
-
- expect(result).toBeNull();
- });
- });
-
- describe('Error messages', () => {
- it('should include validation details in error message', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow(/aggregateFieldMetadataId must be a UUID/);
- });
- });
-
- describe('Feature flags', () => {
- it('should throw error for GAUGE chart type when IS_DASHBOARD_V2_ENABLED is false', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_GAUGE_CHART_CONFIG,
- isDashboardV2Enabled: false,
- }),
- ).rejects.toThrow(/IS_DASHBOARD_V2_ENABLED feature flag/);
- });
-
- it('should not throw error for GAUGE chart type when IS_DASHBOARD_V2_ENABLED is true', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_GAUGE_CHART_CONFIG,
- isDashboardV2Enabled: true,
- }),
- ).resolves.not.toThrow();
- });
-
- it('should not throw error for PIE chart type regardless of IS_DASHBOARD_V2_ENABLED', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_PIE_CHART_CONFIG,
- isDashboardV2Enabled: false,
- }),
- ).resolves.not.toThrow();
-
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_PIE_CHART_CONFIG,
- isDashboardV2Enabled: true,
- }),
- ).resolves.not.toThrow();
- });
-
- it('should not throw error for LINE chart type regardless of IS_DASHBOARD_V2_ENABLED', async () => {
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_LINE_CHART_CONFIG,
- isDashboardV2Enabled: false,
- }),
- ).resolves.not.toThrow();
-
- await expect(
- validateAndTransformWidgetConfiguration({
- type: WidgetType.GRAPH,
- configuration: TEST_LINE_CHART_CONFIG,
- isDashboardV2Enabled: true,
- }),
- ).resolves.not.toThrow();
- });
- });
-});
diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-widget-configuration-input.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-widget-configuration-input.util.spec.ts
new file mode 100644
index 0000000000..7d308fa333
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-widget-configuration-input.util.spec.ts
@@ -0,0 +1,269 @@
+import {
+ INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
+ INVALID_IFRAME_CONFIG_BAD_URL,
+ INVALID_IFRAME_CONFIG_EMPTY_URL,
+ INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
+ INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
+ INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
+ INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
+ INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
+ TEST_GAUGE_CHART_CONFIG,
+ TEST_HORIZONTAL_BAR_CHART_CONFIG,
+ TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
+ TEST_IFRAME_CONFIG,
+ TEST_LINE_CHART_CONFIG,
+ TEST_NUMBER_CHART_CONFIG,
+ TEST_NUMBER_CHART_CONFIG_MINIMAL,
+ TEST_PIE_CHART_CONFIG,
+ TEST_STANDALONE_RICH_TEXT_CONFIG,
+ TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL,
+ TEST_VERTICAL_BAR_CHART_CONFIG,
+ TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
+} from 'test/integration/constants/widget-configuration-test-data.constants';
+
+import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
+import { validateWidgetConfigurationInput } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util';
+
+describe('validateWidgetConfigurationInput', () => {
+ describe('IFRAME widget', () => {
+ it('should not throw for valid iframe configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_IFRAME_CONFIG,
+ }),
+ ).not.toThrow();
+ });
+
+ it('should throw error for invalid URL', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: {
+ ...INVALID_IFRAME_CONFIG_BAD_URL,
+ configurationType: WidgetConfigurationType.IFRAME,
+ },
+ }),
+ ).toThrow(/url must be a URL address/);
+ });
+
+ it('should throw error for empty URL', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: {
+ ...INVALID_IFRAME_CONFIG_EMPTY_URL,
+ configurationType: WidgetConfigurationType.IFRAME,
+ },
+ }),
+ ).toThrow(/url must be a URL address/);
+ });
+ });
+
+ describe('STANDALONE_RICH_TEXT widget', () => {
+ it('should not throw for valid standalone rich text configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_STANDALONE_RICH_TEXT_CONFIG,
+ }),
+ ).not.toThrow();
+ });
+
+ it('should not throw for minimal standalone rich text configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL,
+ }),
+ ).not.toThrow();
+ });
+
+ it('should throw error for missing body', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: {
+ ...INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
+ configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
+ },
+ }),
+ ).toThrow(/body/);
+ });
+
+ it('should throw error when body is wrong type', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: {
+ ...INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
+ configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
+ },
+ }),
+ ).toThrow();
+ });
+ });
+
+ describe('GRAPH widget', () => {
+ describe('AGGREGATE_CHART graph', () => {
+ it('should not throw for full aggregate chart configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_NUMBER_CHART_CONFIG,
+ }),
+ ).not.toThrow();
+ });
+
+ it('should not throw for minimal aggregate chart configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL,
+ }),
+ ).not.toThrow();
+ });
+
+ it('should throw error for partial aggregate chart configuration with missing required fields', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
+ }),
+ ).toThrow(/aggregateFieldMetadataId.*aggregateOperation/);
+ });
+
+ it('should throw error for invalid UUID', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
+ }),
+ ).toThrow(/aggregateFieldMetadataId must be a UUID/);
+ });
+ });
+
+ describe('BAR_CHART graph', () => {
+ describe('VERTICAL layout', () => {
+ it('should not throw for full vertical bar chart configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_VERTICAL_BAR_CHART_CONFIG,
+ }),
+ ).not.toThrow();
+ });
+
+ it('should not throw for minimal vertical bar chart configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
+ }),
+ ).not.toThrow();
+ });
+
+ it('should throw error for partial vertical bar chart configuration with missing required fields', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
+ }),
+ ).toThrow(/primaryAxisGroupByFieldMetadataId/);
+ });
+ });
+
+ describe('HORIZONTAL layout', () => {
+ it('should not throw for full horizontal bar chart configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG,
+ }),
+ ).not.toThrow();
+ });
+
+ it('should not throw for minimal horizontal bar chart configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
+ }),
+ ).not.toThrow();
+ });
+
+ it('should throw error for partial horizontal bar chart configuration with missing required fields', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration:
+ INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
+ }),
+ ).toThrow(/primaryAxisGroupByFieldMetadataId/);
+ });
+ });
+ });
+
+ describe('PIE_CHART graph', () => {
+ it('should not throw for pie chart configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_PIE_CHART_CONFIG,
+ }),
+ ).not.toThrow();
+ });
+ });
+
+ describe('LINE_CHART graph', () => {
+ it('should not throw for line chart configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_LINE_CHART_CONFIG,
+ }),
+ ).not.toThrow();
+ });
+ });
+
+ describe('GAUGE_CHART graph', () => {
+ it('should not throw for gauge chart configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: TEST_GAUGE_CHART_CONFIG,
+ }),
+ ).not.toThrow();
+ });
+ });
+ });
+
+ describe('Edge cases', () => {
+ it('should throw error for null configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: null,
+ }),
+ ).toThrow('Invalid configuration: not an object');
+ });
+
+ it('should throw error for undefined configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: undefined,
+ }),
+ ).toThrow('Invalid configuration: not an object');
+ });
+
+ it('should throw error for non-object configuration', () => {
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: 'string',
+ }),
+ ).toThrow('Invalid configuration: not an object');
+ });
+
+ it('should throw error for missing configurationType', () => {
+ const configuration = { someField: 'value' };
+
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: configuration,
+ }),
+ ).toThrow('Invalid configuration: missing configuration type');
+ });
+
+ it('should throw error for unsupported configurationType', () => {
+ const configuration = {
+ configurationType: 'UNSUPPORTED_TYPE',
+ someField: 'value',
+ };
+
+ expect(() =>
+ validateWidgetConfigurationInput({
+ configuration: configuration,
+ }),
+ ).toThrow(/Invalid configuration type: UNSUPPORTED_TYPE/);
+ });
+ });
+});
diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-widget-grid-position.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-widget-grid-position.util.spec.ts
index d2357af9b3..d715d195c3 100644
--- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-widget-grid-position.util.spec.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/__tests__/validate-widget-grid-position.util.spec.ts
@@ -1,6 +1,6 @@
import { WIDGET_GRID_MAX_COLUMNS } from 'src/engine/metadata-modules/page-layout-widget/constants/widget-grid-max-columns.constant';
import { WIDGET_GRID_MAX_ROWS } from 'src/engine/metadata-modules/page-layout-widget/constants/widget-grid-max-rows.constant';
-import { PageLayoutWidgetException } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util';
describe('validateWidgetGridPosition', () => {
@@ -12,176 +12,213 @@ describe('validateWidgetGridPosition', () => {
};
describe('Valid grid positions', () => {
- it('should not throw for valid grid position', () => {
- expect(() =>
- validateWidgetGridPosition(validGridPosition, 'Test Widget'),
- ).not.toThrow();
+ it('should return empty array for valid grid position', () => {
+ const errors = validateWidgetGridPosition(
+ validGridPosition,
+ 'Test Widget',
+ );
+
+ expect(errors).toEqual([]);
});
- it('should not throw for widget at max column boundary', () => {
- expect(() =>
- validateWidgetGridPosition(
- {
- row: 0,
- column: WIDGET_GRID_MAX_COLUMNS - 1,
- rowSpan: 1,
- columnSpan: 1,
- },
- 'Test Widget',
- ),
- ).not.toThrow();
+ it('should return empty array for widget at max column boundary', () => {
+ const errors = validateWidgetGridPosition(
+ {
+ row: 0,
+ column: WIDGET_GRID_MAX_COLUMNS - 1,
+ rowSpan: 1,
+ columnSpan: 1,
+ },
+ 'Test Widget',
+ );
+
+ expect(errors).toEqual([]);
});
- it('should not throw for widget at max row boundary', () => {
- expect(() =>
- validateWidgetGridPosition(
- {
- row: WIDGET_GRID_MAX_ROWS - 1,
- column: 0,
- rowSpan: 1,
- columnSpan: 1,
- },
- 'Test Widget',
- ),
- ).not.toThrow();
+ it('should return empty array for widget at max row boundary', () => {
+ const errors = validateWidgetGridPosition(
+ {
+ row: WIDGET_GRID_MAX_ROWS - 1,
+ column: 0,
+ rowSpan: 1,
+ columnSpan: 1,
+ },
+ 'Test Widget',
+ );
+
+ expect(errors).toEqual([]);
});
- it('should not throw for widget spanning to column grid edge', () => {
- expect(() =>
- validateWidgetGridPosition(
- {
- row: 0,
- column: 8,
- rowSpan: 1,
- columnSpan: 4,
- },
- 'Test Widget',
- ),
- ).not.toThrow();
+ it('should return empty array for widget spanning to column grid edge', () => {
+ const errors = validateWidgetGridPosition(
+ {
+ row: 0,
+ column: 8,
+ rowSpan: 1,
+ columnSpan: 4,
+ },
+ 'Test Widget',
+ );
+
+ expect(errors).toEqual([]);
});
- it('should not throw for widget spanning to row grid edge', () => {
- expect(() =>
- validateWidgetGridPosition(
- {
- row: WIDGET_GRID_MAX_ROWS - 5,
- column: 0,
- rowSpan: 5,
- columnSpan: 6,
- },
- 'Test Widget',
- ),
- ).not.toThrow();
+ it('should return empty array for widget spanning to row grid edge', () => {
+ const errors = validateWidgetGridPosition(
+ {
+ row: WIDGET_GRID_MAX_ROWS - 5,
+ column: 0,
+ rowSpan: 5,
+ columnSpan: 6,
+ },
+ 'Test Widget',
+ );
+
+ expect(errors).toEqual([]);
});
});
describe('Invalid row positions', () => {
- it('should throw for row exceeding max rows', () => {
- expect(() =>
- validateWidgetGridPosition(
- { ...validGridPosition, row: WIDGET_GRID_MAX_ROWS },
- 'Test Widget',
- ),
- ).toThrow(PageLayoutWidgetException);
+ it('should return error for row exceeding max rows', () => {
+ const errors = validateWidgetGridPosition(
+ { ...validGridPosition, row: WIDGET_GRID_MAX_ROWS },
+ 'Test Widget',
+ );
+
+ expect(errors.length).toBeGreaterThan(0);
+ expect(errors[0].code).toBe(
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
});
- it('should throw when widget extends beyond grid height', () => {
- expect(() =>
- validateWidgetGridPosition(
- {
- row: WIDGET_GRID_MAX_ROWS - 2,
- column: 0,
- rowSpan: 5,
- columnSpan: 6,
- },
- 'Test Widget',
+ it('should return error when widget extends beyond grid height', () => {
+ const errors = validateWidgetGridPosition(
+ {
+ row: WIDGET_GRID_MAX_ROWS - 2,
+ column: 0,
+ rowSpan: 5,
+ columnSpan: 6,
+ },
+ 'Test Widget',
+ );
+
+ expect(errors.length).toBeGreaterThan(0);
+ expect(
+ errors.some((error) =>
+ error.message.includes('extends beyond grid height'),
),
- ).toThrow(/extends beyond grid height/);
+ ).toBe(true);
});
});
describe('Invalid column positions', () => {
- it('should throw for column exceeding max columns', () => {
- expect(() =>
- validateWidgetGridPosition(
- { ...validGridPosition, column: WIDGET_GRID_MAX_COLUMNS },
- 'Test Widget',
- ),
- ).toThrow(PageLayoutWidgetException);
+ it('should return error for column exceeding max columns', () => {
+ const errors = validateWidgetGridPosition(
+ { ...validGridPosition, column: WIDGET_GRID_MAX_COLUMNS },
+ 'Test Widget',
+ );
+
+ expect(errors.length).toBeGreaterThan(0);
+ expect(errors[0].code).toBe(
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
});
});
describe('Widget extending beyond grid', () => {
- it('should throw when widget extends beyond grid width', () => {
- expect(() =>
- validateWidgetGridPosition(
- {
- row: 0,
- column: 10,
- rowSpan: 1,
- columnSpan: 3,
- },
- 'Test Widget',
+ it('should return error when widget extends beyond grid width', () => {
+ const errors = validateWidgetGridPosition(
+ {
+ row: 0,
+ column: 10,
+ rowSpan: 1,
+ columnSpan: 3,
+ },
+ 'Test Widget',
+ );
+
+ expect(errors.length).toBeGreaterThan(0);
+ expect(
+ errors.some((error) =>
+ error.message.includes('extends beyond grid width'),
),
- ).toThrow(/extends beyond grid width/);
+ ).toBe(true);
});
});
describe('Error messages', () => {
it('should include max columns value in error', () => {
- expect(() =>
- validateWidgetGridPosition(
- {
- row: 0,
- column: 10,
- rowSpan: 1,
- columnSpan: 5,
- },
- 'Test Widget',
+ const errors = validateWidgetGridPosition(
+ {
+ row: 0,
+ column: 10,
+ rowSpan: 1,
+ columnSpan: 5,
+ },
+ 'Test Widget',
+ );
+
+ expect(errors.length).toBeGreaterThan(0);
+ expect(
+ errors.some((error) =>
+ error.message.includes(WIDGET_GRID_MAX_COLUMNS.toString()),
),
- ).toThrow(new RegExp(WIDGET_GRID_MAX_COLUMNS.toString()));
+ ).toBe(true);
});
it('should include max rows value in error for row start', () => {
- expect(() =>
- validateWidgetGridPosition(
- {
- row: WIDGET_GRID_MAX_ROWS + 10,
- column: 0,
- rowSpan: 1,
- columnSpan: 1,
- },
- 'Test Widget',
+ const errors = validateWidgetGridPosition(
+ {
+ row: WIDGET_GRID_MAX_ROWS + 10,
+ column: 0,
+ rowSpan: 1,
+ columnSpan: 1,
+ },
+ 'Test Widget',
+ );
+
+ expect(errors.length).toBeGreaterThan(0);
+ expect(
+ errors.some((error) =>
+ error.message.includes(WIDGET_GRID_MAX_ROWS.toString()),
),
- ).toThrow(new RegExp(WIDGET_GRID_MAX_ROWS.toString()));
+ ).toBe(true);
});
it('should include max rows value in error for row extension', () => {
- expect(() =>
- validateWidgetGridPosition(
- {
- row: 95,
- column: 0,
- rowSpan: 10,
- columnSpan: 6,
- },
- 'Test Widget',
+ const errors = validateWidgetGridPosition(
+ {
+ row: 95,
+ column: 0,
+ rowSpan: 10,
+ columnSpan: 6,
+ },
+ 'Test Widget',
+ );
+
+ expect(errors.length).toBeGreaterThan(0);
+ expect(
+ errors.some((error) =>
+ error.message.includes(WIDGET_GRID_MAX_ROWS.toString()),
),
- ).toThrow(new RegExp(WIDGET_GRID_MAX_ROWS.toString()));
+ ).toBe(true);
});
it('should include widget title in error message', () => {
- expect(() =>
- validateWidgetGridPosition(
- {
- row: WIDGET_GRID_MAX_ROWS,
- column: 0,
- rowSpan: 1,
- columnSpan: 1,
- },
- 'My Custom Widget',
- ),
- ).toThrow(/My Custom Widget/);
+ const errors = validateWidgetGridPosition(
+ {
+ row: WIDGET_GRID_MAX_ROWS,
+ column: 0,
+ rowSpan: 1,
+ columnSpan: 1,
+ },
+ 'My Custom Widget',
+ );
+
+ expect(errors.length).toBeGreaterThan(0);
+ expect(
+ errors.some((error) => error.message.includes('My Custom Widget')),
+ ).toBe(true);
});
});
});
diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-and-transform-widget-configuration.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-and-transform-widget-configuration.util.ts
deleted file mode 100644
index 206fd3407f..0000000000
--- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-and-transform-widget-configuration.util.ts
+++ /dev/null
@@ -1,227 +0,0 @@
-import { plainToInstance } from 'class-transformer';
-import { validateSync, type ValidationError } from 'class-validator';
-import { isDefined } from 'twenty-shared/utils';
-
-import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util';
-import { AggregateChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto';
-import { BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto';
-import { GaugeChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto';
-import { IframeConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/iframe-configuration.dto';
-import { LineChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto';
-import { PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
-import { StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
-import { BarChartGroupMode } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-group-mode.enum';
-import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
-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';
-
-const formatValidationErrors = (errors: ValidationError[]): string => {
- return errors
- .map((err) => {
- const constraints = err.constraints
- ? Object.values(err.constraints).join(', ')
- : 'Unknown error';
-
- return `${err.property}: ${constraints}`;
- })
- .join('; ');
-};
-
-const validateGraphConfiguration = ({
- configuration,
- isDashboardV2Enabled,
-}: {
- configuration: Record;
- isDashboardV2Enabled: boolean;
-}): AllPageLayoutWidgetConfiguration | null => {
- const configurationType = configuration.configurationType as GraphType;
-
- if (
- !configurationType ||
- !Object.values(GraphType).includes(configurationType)
- ) {
- return null;
- }
-
- if (configurationType === GraphType.GAUGE_CHART && !isDashboardV2Enabled) {
- throw new Error(
- `Chart type ${configurationType} requires IS_DASHBOARD_V2_ENABLED feature flag`,
- );
- }
-
- switch (configurationType) {
- case GraphType.BAR_CHART: {
- const instance = plainToInstance(BarChartConfigurationDTO, configuration);
-
- const errors = validateSync(instance, {
- whitelist: true,
- forbidUnknownValues: true,
- });
-
- if (errors.length > 0) {
- throw errors;
- }
-
- if (
- isDefined(instance.secondaryAxisGroupByFieldMetadataId) &&
- !isDefined(instance.groupMode)
- ) {
- instance.groupMode = BarChartGroupMode.STACKED;
- }
-
- return instance;
- }
- case GraphType.LINE_CHART: {
- const instance = plainToInstance(
- LineChartConfigurationDTO,
- configuration,
- );
-
- const errors = validateSync(instance, {
- whitelist: true,
- forbidUnknownValues: true,
- });
-
- if (errors.length > 0) {
- throw errors;
- }
-
- if (
- isDefined(instance.secondaryAxisGroupByFieldMetadataId) &&
- !isDefined(instance.isStacked)
- ) {
- instance.isStacked = true;
- }
-
- return instance;
- }
- case GraphType.PIE_CHART: {
- const instance = plainToInstance(PieChartConfigurationDTO, configuration);
-
- const errors = validateSync(instance, {
- whitelist: true,
- forbidUnknownValues: true,
- });
-
- if (errors.length > 0) {
- throw errors;
- }
-
- return instance;
- }
- case GraphType.AGGREGATE_CHART: {
- const instance = plainToInstance(
- AggregateChartConfigurationDTO,
- configuration,
- );
-
- const errors = validateSync(instance, {
- whitelist: true,
- forbidUnknownValues: true,
- });
-
- if (errors.length > 0) {
- throw errors;
- }
-
- return instance;
- }
- case GraphType.GAUGE_CHART: {
- const instance = plainToInstance(
- GaugeChartConfigurationDTO,
- configuration,
- );
-
- const errors = validateSync(instance, {
- whitelist: true,
- forbidUnknownValues: true,
- });
-
- if (errors.length > 0) {
- throw errors;
- }
-
- return instance;
- }
- default:
- return null;
- }
-};
-
-const validateIframeConfiguration = (
- configuration: unknown,
-): AllPageLayoutWidgetConfiguration | null => {
- const instance = plainToInstance(IframeConfigurationDTO, configuration);
-
- const errors = validateSync(instance, {
- whitelist: true,
- forbidUnknownValues: true,
- });
-
- if (errors.length > 0) {
- throw errors;
- }
-
- return instance;
-};
-
-const validateStandaloneRichTextConfiguration = async (
- configuration: unknown,
-): Promise => {
- const instance = plainToInstance(
- StandaloneRichTextConfigurationDTO,
- configuration,
- );
-
- const errors = validateSync(instance, {
- whitelist: true,
- forbidUnknownValues: true,
- });
-
- if (errors.length > 0) {
- throw errors;
- }
-
- if (instance.body) {
- instance.body = await transformRichTextV2Value(instance.body);
- }
-
- return instance;
-};
-
-export const validateAndTransformWidgetConfiguration = async ({
- type,
- configuration,
- isDashboardV2Enabled,
-}: {
- type: WidgetType;
- configuration: unknown;
- isDashboardV2Enabled: boolean;
-}): Promise => {
- if (!configuration || typeof configuration !== 'object') {
- throw new Error('Invalid configuration: not an object');
- }
-
- try {
- switch (type) {
- case WidgetType.GRAPH:
- return validateGraphConfiguration({
- configuration: configuration as Record,
- isDashboardV2Enabled,
- });
- case WidgetType.IFRAME:
- return validateIframeConfiguration(configuration);
- case WidgetType.STANDALONE_RICH_TEXT:
- return await validateStandaloneRichTextConfiguration(configuration);
- default:
- return null;
- }
- } catch (error) {
- if (Array.isArray(error)) {
- const errorMessage = formatValidationErrors(error);
-
- throw new Error(errorMessage);
- }
- throw error;
- }
-};
diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-by-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-by-dto.util.ts
new file mode 100644
index 0000000000..c9e3660d19
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-by-dto.util.ts
@@ -0,0 +1,19 @@
+import { plainToInstance } from 'class-transformer';
+import { type ClassConstructor } from 'class-transformer/types/interfaces';
+import { validateSync, type ValidationError } from 'class-validator';
+
+import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
+
+export const validateWidgetConfigurationByDto = <
+ T extends AllPageLayoutWidgetConfiguration,
+>(
+ DtoClass: ClassConstructor,
+ configuration: unknown,
+): ValidationError[] => {
+ const instance = plainToInstance(DtoClass, configuration);
+
+ return validateSync(instance, {
+ whitelist: true,
+ forbidUnknownValues: true,
+ });
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util.ts
new file mode 100644
index 0000000000..f93cd26127
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util.ts
@@ -0,0 +1,194 @@
+import { isNotEmptyObject, type ValidationError } from 'class-validator';
+
+import { AggregateChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto';
+import { BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto';
+import { GaugeChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto';
+import { IframeConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/iframe-configuration.dto';
+import { LineChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto';
+import { PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
+import { StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
+import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
+import {
+ PageLayoutWidgetException,
+ PageLayoutWidgetExceptionCode,
+} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+import { validateWidgetConfigurationByDto } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-by-dto.util';
+
+const formatValidationErrors = (
+ errors: ValidationError[],
+ parentProperty?: string,
+): string => {
+ return errors
+ .map((err) => {
+ const propertyPath = parentProperty
+ ? `${parentProperty}.${err.property}`
+ : err.property;
+
+ if (err.constraints) {
+ const constraints = Object.values(err.constraints).join(', ');
+
+ return `${propertyPath}: ${constraints}`;
+ }
+
+ if (err.children && err.children.length > 0) {
+ return formatValidationErrors(err.children, propertyPath);
+ }
+
+ return `${propertyPath}: Unknown error`;
+ })
+ .join('; ');
+};
+
+export const validateWidgetConfigurationInput = ({
+ configuration,
+}: {
+ configuration: unknown;
+}): void => {
+ if (!isNotEmptyObject(configuration)) {
+ throw new PageLayoutWidgetException(
+ 'Invalid configuration: not an object',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ }
+
+ const configurationRecord = configuration as Record;
+
+ if (
+ !Object.prototype.hasOwnProperty.call(
+ configurationRecord,
+ 'configurationType',
+ )
+ ) {
+ throw new PageLayoutWidgetException(
+ 'Invalid configuration: missing configuration type',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ }
+
+ const configurationType =
+ configurationRecord.configurationType as WidgetConfigurationType;
+
+ let errors: ValidationError[] = [];
+
+ switch (configurationType) {
+ case WidgetConfigurationType.BAR_CHART:
+ errors = validateWidgetConfigurationByDto(
+ BarChartConfigurationDTO,
+ configuration,
+ );
+ break;
+ case WidgetConfigurationType.LINE_CHART:
+ errors = validateWidgetConfigurationByDto(
+ LineChartConfigurationDTO,
+ configuration,
+ );
+ break;
+ case WidgetConfigurationType.PIE_CHART:
+ errors = validateWidgetConfigurationByDto(
+ PieChartConfigurationDTO,
+ configuration,
+ );
+ break;
+ case WidgetConfigurationType.AGGREGATE_CHART:
+ errors = validateWidgetConfigurationByDto(
+ AggregateChartConfigurationDTO,
+ configuration,
+ );
+ break;
+ case WidgetConfigurationType.GAUGE_CHART:
+ errors = validateWidgetConfigurationByDto(
+ GaugeChartConfigurationDTO,
+ configuration,
+ );
+ break;
+ case WidgetConfigurationType.IFRAME:
+ errors = validateWidgetConfigurationByDto(
+ IframeConfigurationDTO,
+ configuration,
+ );
+ break;
+ case WidgetConfigurationType.STANDALONE_RICH_TEXT:
+ errors = validateWidgetConfigurationByDto(
+ StandaloneRichTextConfigurationDTO,
+ configuration,
+ );
+ break;
+ case WidgetConfigurationType.VIEW:
+ throw new PageLayoutWidgetException(
+ 'View configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.FIELD:
+ throw new PageLayoutWidgetException(
+ 'Field configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.FIELDS:
+ throw new PageLayoutWidgetException(
+ 'Fields configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.TIMELINE:
+ throw new PageLayoutWidgetException(
+ 'Timeline configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.TASKS:
+ throw new PageLayoutWidgetException(
+ 'Tasks configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.NOTES:
+ throw new PageLayoutWidgetException(
+ 'Notes configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.FILES:
+ throw new PageLayoutWidgetException(
+ 'Files configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.EMAILS:
+ throw new PageLayoutWidgetException(
+ 'Emails configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.CALENDAR:
+ throw new PageLayoutWidgetException(
+ 'Calendar configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.FIELD_RICH_TEXT:
+ throw new PageLayoutWidgetException(
+ 'Field rich text configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.WORKFLOW:
+ throw new PageLayoutWidgetException(
+ 'Workflow configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.WORKFLOW_VERSION:
+ throw new PageLayoutWidgetException(
+ 'Workflow version configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ case WidgetConfigurationType.WORKFLOW_RUN:
+ throw new PageLayoutWidgetException(
+ 'Workflow run configuration is not supported yet',
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ default:
+ throw new PageLayoutWidgetException(
+ `Invalid configuration type: ${configurationType}`,
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ }
+
+ if (errors.length > 0) {
+ throw new PageLayoutWidgetException(
+ formatValidationErrors(errors),
+ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ );
+ }
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util.ts
index f05974e28e..c223ef739b 100644
--- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util.ts
@@ -1,11 +1,13 @@
+import { msg } from '@lingui/core/macro';
+
import { WIDGET_GRID_MAX_COLUMNS } from 'src/engine/metadata-modules/page-layout-widget/constants/widget-grid-max-columns.constant';
import { WIDGET_GRID_MAX_ROWS } from 'src/engine/metadata-modules/page-layout-widget/constants/widget-grid-max-rows.constant';
import {
- PageLayoutWidgetException,
PageLayoutWidgetExceptionCode,
PageLayoutWidgetExceptionMessageKey,
generatePageLayoutWidgetExceptionMessage,
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
+import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
type GridPosition = {
row: number;
@@ -17,54 +19,62 @@ type GridPosition = {
export const validateWidgetGridPosition = (
gridPosition: GridPosition,
widgetTitle: string,
-): void => {
+): FlatEntityValidationError[] => {
+ const errors: FlatEntityValidationError[] = [];
+
const { row, column, rowSpan, columnSpan } = gridPosition;
if (column >= WIDGET_GRID_MAX_COLUMNS) {
- throw new PageLayoutWidgetException(
- generatePageLayoutWidgetExceptionMessage(
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: generatePageLayoutWidgetExceptionMessage(
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
widgetTitle,
undefined,
`column ${column} exceeds grid width (max column is ${WIDGET_GRID_MAX_COLUMNS - 1})`,
),
- PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
- );
+ userFriendlyMessage: msg`Widget extends beyond grid width`,
+ });
}
if (column + columnSpan > WIDGET_GRID_MAX_COLUMNS) {
- throw new PageLayoutWidgetException(
- generatePageLayoutWidgetExceptionMessage(
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: generatePageLayoutWidgetExceptionMessage(
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
widgetTitle,
undefined,
`widget extends beyond grid width (column ${column} + columnSpan ${columnSpan} > ${WIDGET_GRID_MAX_COLUMNS})`,
),
- PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
- );
+ userFriendlyMessage: msg`Widget extends beyond grid width`,
+ });
}
if (row >= WIDGET_GRID_MAX_ROWS) {
- throw new PageLayoutWidgetException(
- generatePageLayoutWidgetExceptionMessage(
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: generatePageLayoutWidgetExceptionMessage(
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
widgetTitle,
undefined,
`row ${row} exceeds maximum allowed rows (${WIDGET_GRID_MAX_ROWS})`,
),
- PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
- );
+ userFriendlyMessage: msg`Widget row exceeds grid height`,
+ });
}
if (row + rowSpan > WIDGET_GRID_MAX_ROWS) {
- throw new PageLayoutWidgetException(
- generatePageLayoutWidgetExceptionMessage(
+ errors.push({
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: generatePageLayoutWidgetExceptionMessage(
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
widgetTitle,
undefined,
`widget extends beyond grid height (row ${row} + rowSpan ${rowSpan} > ${WIDGET_GRID_MAX_ROWS})`,
),
- PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
- );
+ userFriendlyMessage: msg`Widget extends beyond grid height`,
+ });
}
+
+ return errors;
};
diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util.ts
index 348924c326..9cea232e0b 100644
--- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util.ts
@@ -1,8 +1,9 @@
+import { isDefined } from 'class-validator';
import { type DataSource } from 'typeorm';
import { v4 } from 'uuid';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
-import { validateAndTransformWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-and-transform-widget-configuration.util';
+import { validateWidgetConfigurationInput } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util';
import { getPageLayoutWidgetDataSeeds } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util';
export const seedPageLayoutWidgets = async ({
@@ -26,26 +27,22 @@ export const seedPageLayoutWidgets = async ({
isDashboardV2Enabled,
);
- const pageLayoutWidgets = await Promise.all(
- widgetSeeds.map(async (widget) => {
- const validatedConfiguration = widget.configuration
- ? await validateAndTransformWidgetConfiguration({
- type: widget.type,
- configuration: widget.configuration,
- isDashboardV2Enabled,
- })
- : null;
+ const pageLayoutWidgets = widgetSeeds.map((widget) => {
+ if (isDefined(widget.configuration)) {
+ validateWidgetConfigurationInput({
+ configuration: widget.configuration,
+ });
+ }
- return {
- ...widget,
- workspaceId,
- gridPosition: widget.gridPosition,
- configuration: validatedConfiguration,
- universalIdentifier: v4(),
- applicationId: workspaceCustomApplicationId,
- };
- }),
- );
+ return {
+ ...widget,
+ workspaceId,
+ gridPosition: widget.gridPosition,
+ configuration: widget.configuration,
+ universalIdentifier: v4(),
+ applicationId: workspaceCustomApplicationId,
+ };
+ });
if (pageLayoutWidgets.length > 0) {
await dataSource
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service.ts
index 3f5fa8302b..019d0a91d1 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service.ts
@@ -81,6 +81,7 @@ export class WorkspaceMigrationV2IndexActionsBuilderService extends WorkspaceEnt
flatEntityUpdates,
buildOptions,
workspaceId,
+ additionalCacheDataMaps,
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.index
>): FlatEntityValidationReturnType {
@@ -113,6 +114,7 @@ export class WorkspaceMigrationV2IndexActionsBuilderService extends WorkspaceEnt
workspaceId,
flatEntityToValidate: flatEntity,
remainingFlatEntityMapsToValidate: createEmptyFlatEntityMaps(),
+ additionalCacheDataMaps,
});
if (deletionValidationResult.errors.length > 0) {
@@ -152,6 +154,7 @@ export class WorkspaceMigrationV2IndexActionsBuilderService extends WorkspaceEnt
flatIndexMaps: tempOptimisticFlatIndexMaps,
},
remainingFlatEntityMapsToValidate: createEmptyFlatEntityMaps(),
+ additionalCacheDataMaps,
});
if (creationValidationResult.errors.length > 0) {
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/page-layout-widget/workspace-migration-v2-page-layout-widget-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/page-layout-widget/workspace-migration-v2-page-layout-widget-actions-builder.service.ts
index 46146e7ac0..ee5fee9be0 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/page-layout-widget/workspace-migration-v2-page-layout-widget-actions-builder.service.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/page-layout-widget/workspace-migration-v2-page-layout-widget-actions-builder.service.ts
@@ -19,14 +19,16 @@ export class WorkspaceMigrationV2PageLayoutWidgetActionsBuilderService extends W
super(ALL_METADATA_NAME.pageLayoutWidget);
}
- protected validateFlatEntityCreation(
+ protected async validateFlatEntityCreation(
args: FlatEntityValidationArgs,
- ): FlatEntityValidationReturnType<
- typeof ALL_METADATA_NAME.pageLayoutWidget,
- 'create'
+ ): Promise<
+ FlatEntityValidationReturnType<
+ typeof ALL_METADATA_NAME.pageLayoutWidget,
+ 'create'
+ >
> {
const validationResult =
- this.flatPageLayoutWidgetValidatorService.validateFlatPageLayoutWidgetCreation(
+ await this.flatPageLayoutWidgetValidatorService.validateFlatPageLayoutWidgetCreation(
args,
);
@@ -77,16 +79,18 @@ export class WorkspaceMigrationV2PageLayoutWidgetActionsBuilderService extends W
};
}
- protected validateFlatEntityUpdate(
+ protected async validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.pageLayoutWidget
>,
- ): FlatEntityValidationReturnType<
- typeof ALL_METADATA_NAME.pageLayoutWidget,
- 'update'
+ ): Promise<
+ FlatEntityValidationReturnType<
+ typeof ALL_METADATA_NAME.pageLayoutWidget,
+ 'update'
+ >
> {
const validationResult =
- this.flatPageLayoutWidgetValidatorService.validateFlatPageLayoutWidgetUpdate(
+ await this.flatPageLayoutWidgetValidatorService.validateFlatPageLayoutWidgetUpdate(
args,
);
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts
index 9c1dc1f46b..2c46f191b6 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts
@@ -57,6 +57,7 @@ export abstract class WorkspaceEntityMigrationBuilderV2Service<
dependencyOptimisticFlatEntityMaps: inputDependencyOptimisticFlatEntityMaps,
from: fromFlatEntityMaps,
to: toFlatEntityMaps,
+ additionalCacheDataMaps,
workspaceId,
}: ValidateAndBuildArgs): ValidateAndBuildReturnType {
this.logger.time(`EntityBuilder ${this.metadataName}`, 'validateAndBuild');
@@ -125,6 +126,7 @@ export abstract class WorkspaceEntityMigrationBuilderV2Service<
});
const validationResult = await this.validateFlatEntityCreation({
+ additionalCacheDataMaps,
flatEntityToValidate: flatEntityToCreate,
workspaceId,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
@@ -193,6 +195,7 @@ export abstract class WorkspaceEntityMigrationBuilderV2Service<
remainingFlatEntityMapsToValidate: remainingFlatEntityMapsToDelete,
buildOptions,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
+ additionalCacheDataMaps,
});
if (validationResult.status === 'fail') {
@@ -237,6 +240,7 @@ export abstract class WorkspaceEntityMigrationBuilderV2Service<
optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
workspaceId,
buildOptions,
+ additionalCacheDataMaps,
});
if (validationResult.status === 'fail') {
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type.ts
index 1bb79c12b0..c38f2d0ab1 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type.ts
@@ -3,9 +3,11 @@ import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataFlatEntityAndRelatedFlatEntityMapsForValidation } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-and-related-flat-entity-maps-for-validation.type';
import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
+import { type WorkspaceMigrationBuilderAdditionalCacheDataMaps } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-builder-additional-cache-data-maps.type';
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type';
export type FlatEntityValidationArgs = {
+ additionalCacheDataMaps: WorkspaceMigrationBuilderAdditionalCacheDataMaps;
flatEntityToValidate: MetadataFlatEntity;
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: MetadataFlatEntityAndRelatedFlatEntityMapsForValidation;
workspaceId: string;
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts
index 27cfb4955c..8e05d87680 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts
@@ -36,6 +36,7 @@ export class FlatFieldMetadataValidatorService {
},
workspaceId,
buildOptions,
+ additionalCacheDataMaps,
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.fieldMetadata
>): FailedFlatEntityValidation<'fieldMetadata', 'update'> {
@@ -184,6 +185,7 @@ export class FlatFieldMetadataValidatorService {
buildOptions,
remainingFlatEntityMapsToValidate: createEmptyFlatEntityMaps(),
workspaceId,
+ additionalCacheDataMaps,
},
);
@@ -285,6 +287,7 @@ export class FlatFieldMetadataValidatorService {
workspaceId,
buildOptions,
remainingFlatEntityMapsToValidate,
+ additionalCacheDataMaps,
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.fieldMetadata
>): FailedFlatEntityValidation<'fieldMetadata', 'create'> {
@@ -372,6 +375,7 @@ export class FlatFieldMetadataValidatorService {
buildOptions,
workspaceId,
remainingFlatEntityMapsToValidate,
+ additionalCacheDataMaps,
},
),
);
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-widget-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-widget-validator.service.ts
index 058341dfd1..77fee92667 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-widget-validator.service.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-widget-validator.service.ts
@@ -4,10 +4,20 @@ import { msg, t } from '@lingui/core/macro';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
+import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
+import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { PageLayoutTabExceptionCode } from 'src/engine/metadata-modules/page-layout-tab/exceptions/page-layout-tab.exception';
+import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
+import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
-import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
+import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
+import { GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type';
+import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util';
+import {
+ FailedFlatEntityValidation,
+ FlatEntityValidationError,
+} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/utils/get-flat-entity-validation-error.util';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
@@ -15,19 +25,26 @@ import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/w
@Injectable()
export class FlatPageLayoutWidgetValidatorService {
- constructor() {}
+ constructor(
+ private readonly flatPageLayoutWidgetTypeValidatorService: FlatPageLayoutWidgetTypeValidatorService,
+ ) {}
- public validateFlatPageLayoutWidgetUpdate({
+ public async validateFlatPageLayoutWidgetUpdate({
flatEntityId,
flatEntityUpdates,
- optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
- flatPageLayoutWidgetMaps: optimisticFlatPageLayoutWidgetMaps,
- },
+ optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
+ additionalCacheDataMaps: { featureFlagsMap },
+ workspaceId,
+ buildOptions,
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.pageLayoutWidget
- >): FailedFlatEntityValidation<'pageLayoutWidget', 'update'> {
+ >): Promise> {
+ const isDashboardV2Enabled =
+ featureFlagsMap[FeatureFlagKey.IS_DASHBOARD_V2_ENABLED] ?? false;
+
const existingFlatPageLayoutWidget =
- optimisticFlatPageLayoutWidgetMaps.byId[flatEntityId];
+ optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutWidgetMaps
+ .byId[flatEntityId];
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
@@ -61,6 +78,38 @@ export class FlatPageLayoutWidgetValidatorService {
pageLayoutTabId: updatedFlatPageLayoutWidget.pageLayoutTabId,
};
+ const gridPositionErrors = this.validateGridPosition({
+ gridPosition: updatedFlatPageLayoutWidget.gridPosition,
+ widgetTitle: updatedFlatPageLayoutWidget.title,
+ });
+
+ validationResult.errors.push(...gridPositionErrors);
+
+ const featureFlagErrors = this.validateFeatureFlags({
+ type: updatedFlatPageLayoutWidget.type,
+ configuration: updatedFlatPageLayoutWidget.configuration,
+ widgetTitle: updatedFlatPageLayoutWidget.title,
+ isDashboardV2Enabled,
+ });
+
+ validationResult.errors.push(...featureFlagErrors);
+
+ const typeSpecificityErrors =
+ this.flatPageLayoutWidgetTypeValidatorService.validateFlatPageLayoutWidgetTypeSpecificitiesForUpdate(
+ {
+ flatEntityToValidate: updatedFlatPageLayoutWidget,
+ optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
+ updates: flatEntityUpdates,
+ additionalCacheDataMaps: { featureFlagsMap },
+ workspaceId,
+ buildOptions,
+ remainingFlatEntityMapsToValidate:
+ optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutWidgetMaps,
+ },
+ );
+
+ validationResult.errors.push(...typeSpecificityErrors);
+
return validationResult;
}
@@ -100,15 +149,19 @@ export class FlatPageLayoutWidgetValidatorService {
return validationResult;
}
- public validateFlatPageLayoutWidgetCreation({
+ public async validateFlatPageLayoutWidgetCreation({
flatEntityToValidate: flatPageLayoutWidgetToValidate,
- optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
- flatPageLayoutTabMaps,
- flatPageLayoutWidgetMaps: optimisticFlatPageLayoutWidgetMaps,
- },
+ additionalCacheDataMaps: { featureFlagsMap },
+ optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
+ workspaceId,
+ buildOptions,
+ remainingFlatEntityMapsToValidate,
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.pageLayoutWidget
- >): FailedFlatEntityValidation<'pageLayoutWidget', 'create'> {
+ >): Promise> {
+ const isDashboardV2Enabled =
+ featureFlagsMap[FeatureFlagKey.IS_DASHBOARD_V2_ENABLED] ?? false;
+
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
id: flatPageLayoutWidgetToValidate.id,
@@ -120,9 +173,8 @@ export class FlatPageLayoutWidgetValidatorService {
});
const existingFlatPageLayoutWidget =
- optimisticFlatPageLayoutWidgetMaps.byId[
- flatPageLayoutWidgetToValidate.id
- ];
+ optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutWidgetMaps
+ .byId[flatPageLayoutWidgetToValidate.id];
if (isDefined(existingFlatPageLayoutWidget)) {
const flatPageLayoutWidgetId = flatPageLayoutWidgetToValidate.id;
@@ -136,7 +188,8 @@ export class FlatPageLayoutWidgetValidatorService {
const referencedPageLayoutTab = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatPageLayoutWidgetToValidate.pageLayoutTabId,
- flatEntityMaps: flatPageLayoutTabMaps,
+ flatEntityMaps:
+ optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutTabMaps,
});
if (!isDefined(referencedPageLayoutTab)) {
@@ -147,6 +200,97 @@ export class FlatPageLayoutWidgetValidatorService {
});
}
+ const gridPositionErrors = this.validateGridPosition({
+ gridPosition: flatPageLayoutWidgetToValidate.gridPosition,
+ widgetTitle: flatPageLayoutWidgetToValidate.title,
+ });
+
+ validationResult.errors.push(...gridPositionErrors);
+
+ const featureFlagErrors = this.validateFeatureFlags({
+ type: flatPageLayoutWidgetToValidate.type,
+ configuration: flatPageLayoutWidgetToValidate.configuration,
+ widgetTitle: flatPageLayoutWidgetToValidate.title,
+ isDashboardV2Enabled,
+ });
+
+ validationResult.errors.push(...featureFlagErrors);
+
+ const typeSpecificityErrors =
+ this.flatPageLayoutWidgetTypeValidatorService.validateFlatPageLayoutWidgetTypeSpecificitiesForCreation(
+ {
+ flatEntityToValidate: flatPageLayoutWidgetToValidate,
+ optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
+ additionalCacheDataMaps: { featureFlagsMap },
+ workspaceId,
+ buildOptions,
+ remainingFlatEntityMapsToValidate,
+ },
+ );
+
+ validationResult.errors.push(...typeSpecificityErrors);
+
return validationResult;
}
+
+ private validateGridPosition({
+ gridPosition,
+ widgetTitle,
+ }: {
+ gridPosition: GridPosition | undefined;
+ widgetTitle: string;
+ }): FlatEntityValidationError[] {
+ if (!isDefined(gridPosition)) {
+ return [
+ {
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Grid position is required`,
+ userFriendlyMessage: msg`Grid position is required`,
+ },
+ ];
+ }
+
+ return validateWidgetGridPosition(gridPosition, widgetTitle);
+ }
+
+ private validateFeatureFlags({
+ type,
+ configuration,
+ widgetTitle,
+ isDashboardV2Enabled,
+ }: {
+ type: WidgetType | undefined;
+ configuration: AllPageLayoutWidgetConfiguration | null | undefined;
+ widgetTitle: string;
+ isDashboardV2Enabled: boolean;
+ }): FlatEntityValidationError[] {
+ if (!isDefined(type) || !isDefined(configuration)) {
+ return [];
+ }
+
+ if (type !== WidgetType.GRAPH) {
+ return [];
+ }
+
+ const graphConfiguration = configuration as unknown as {
+ configurationType?: GraphType;
+ };
+
+ if (
+ graphConfiguration.configurationType === GraphType.GAUGE_CHART &&
+ !isDashboardV2Enabled
+ ) {
+ const chartType = graphConfiguration.configurationType;
+
+ return [
+ {
+ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
+ message: t`Invalid configuration for widget "${widgetTitle}": Chart type ${chartType} requires IS_DASHBOARD_V2_ENABLED feature flag`,
+ userFriendlyMessage: msg`This chart type requires the Dashboard V2 feature to be enabled`,
+ },
+ ];
+ }
+
+ return [];
+ }
}
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts
index 5a71a30e57..bf9cb5701a 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
+import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { FlatAgentValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-agent-validator.service';
import { FlatCronTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-cron-trigger-validator.service';
import { FlatDatabaseEventTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-database-event-trigger-validator.service';
@@ -39,6 +40,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatDatabaseEventTriggerValidatorService,
FlatCronTriggerValidatorService,
FlatFieldMetadataTypeValidatorService,
+ FlatPageLayoutWidgetTypeValidatorService,
FlatRouteTriggerValidatorService,
FlatRoleValidatorService,
FlatRoleTargetValidatorService,
diff --git a/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts b/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts
index 824716bd0a..6b024fa090 100644
--- a/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts
+++ b/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts
@@ -1,4 +1,11 @@
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
+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 IframeConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/iframe-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';
+import { type StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
import { AxisNameDisplay } from 'src/engine/metadata-modules/page-layout-widget/enums/axis-name-display.enum';
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
@@ -9,26 +16,28 @@ export const TEST_FIELD_METADATA_ID_2 = '20202020-2222-4222-a222-222222222222';
export const TEST_FIELD_METADATA_ID_3 = '20202020-3333-4333-a333-333333333333';
export const TEST_FIELD_METADATA_ID_4 = '20202020-4444-4444-a444-444444444444';
-export const TEST_IFRAME_CONFIG = {
+export const TEST_IFRAME_CONFIG: IframeConfigurationDTO = {
configurationType: WidgetConfigurationType.IFRAME,
url: 'https://example.com/dashboard',
-} as const;
+};
-export const TEST_STANDALONE_RICH_TEXT_CONFIG = {
- configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
- body: {
- blocknote:
- '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello world"}]}]}',
- markdown: '# Hello world',
- },
-} as const;
+export const TEST_STANDALONE_RICH_TEXT_CONFIG: StandaloneRichTextConfigurationDTO =
+ {
+ configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
+ body: {
+ blocknote:
+ '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello world"}]}]}',
+ markdown: '# Hello world',
+ },
+ };
-export const TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL = {
- configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
- body: {
- markdown: 'Simple text',
- },
-} as const;
+export const TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL: StandaloneRichTextConfigurationDTO =
+ {
+ configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
+ body: {
+ markdown: 'Simple text',
+ },
+ };
export const INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY = {};
@@ -45,12 +54,12 @@ export const INVALID_STANDALONE_RICH_TEXT_CONFIG_INVALID_SUBFIELDS = {
},
};
-export const TEST_IFRAME_CONFIG_ALTERNATIVE = {
+export const TEST_IFRAME_CONFIG_ALTERNATIVE: IframeConfigurationDTO = {
configurationType: WidgetConfigurationType.IFRAME,
url: 'https://app.twenty.com/analytics',
-} as const;
+};
-export const TEST_NUMBER_CHART_CONFIG = {
+export const TEST_NUMBER_CHART_CONFIG: AggregateChartConfigurationDTO = {
configurationType: WidgetConfigurationType.AGGREGATE_CHART,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.COUNT,
@@ -60,14 +69,15 @@ export const TEST_NUMBER_CHART_CONFIG = {
displayDataLabel: true,
};
-export const TEST_NUMBER_CHART_CONFIG_MINIMAL = {
- configurationType: WidgetConfigurationType.AGGREGATE_CHART,
- aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
- aggregateOperation: AggregateOperations.SUM,
- displayDataLabel: false,
-};
+export const TEST_NUMBER_CHART_CONFIG_MINIMAL: AggregateChartConfigurationDTO =
+ {
+ configurationType: WidgetConfigurationType.AGGREGATE_CHART,
+ aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
+ aggregateOperation: AggregateOperations.SUM,
+ displayDataLabel: false,
+ };
-export const TEST_VERTICAL_BAR_CHART_CONFIG = {
+export const TEST_VERTICAL_BAR_CHART_CONFIG: BarChartConfigurationDTO = {
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.VERTICAL,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
@@ -83,18 +93,19 @@ export const TEST_VERTICAL_BAR_CHART_CONFIG = {
rangeMax: 100000,
};
-export const TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL = {
- configurationType: WidgetConfigurationType.BAR_CHART,
- layout: BarChartLayout.VERTICAL,
- aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
- aggregateOperation: AggregateOperations.COUNT,
- primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
- primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
- displayDataLabel: false,
- axisNameDisplay: AxisNameDisplay.NONE,
-};
+export const TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL: BarChartConfigurationDTO =
+ {
+ configurationType: WidgetConfigurationType.BAR_CHART,
+ layout: BarChartLayout.VERTICAL,
+ aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
+ aggregateOperation: AggregateOperations.COUNT,
+ primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
+ primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
+ displayDataLabel: false,
+ axisNameDisplay: AxisNameDisplay.NONE,
+ };
-export const TEST_HORIZONTAL_BAR_CHART_CONFIG = {
+export const TEST_HORIZONTAL_BAR_CHART_CONFIG: BarChartConfigurationDTO = {
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.HORIZONTAL,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
@@ -110,18 +121,19 @@ export const TEST_HORIZONTAL_BAR_CHART_CONFIG = {
rangeMax: 100000,
};
-export const TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL = {
- configurationType: WidgetConfigurationType.BAR_CHART,
- layout: BarChartLayout.HORIZONTAL,
- aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
- aggregateOperation: AggregateOperations.COUNT,
- primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
- primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
- displayDataLabel: false,
- axisNameDisplay: AxisNameDisplay.NONE,
-};
+export const TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL: BarChartConfigurationDTO =
+ {
+ configurationType: WidgetConfigurationType.BAR_CHART,
+ layout: BarChartLayout.HORIZONTAL,
+ aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
+ aggregateOperation: AggregateOperations.COUNT,
+ primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
+ primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
+ displayDataLabel: false,
+ axisNameDisplay: AxisNameDisplay.NONE,
+ };
-export const TEST_LINE_CHART_CONFIG = {
+export const TEST_LINE_CHART_CONFIG: LineChartConfigurationDTO = {
configurationType: WidgetConfigurationType.LINE_CHART,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.AVG,
@@ -138,7 +150,7 @@ export const TEST_LINE_CHART_CONFIG = {
rangeMax: 100,
};
-export const TEST_LINE_CHART_CONFIG_MINIMAL = {
+export const TEST_LINE_CHART_CONFIG_MINIMAL: LineChartConfigurationDTO = {
configurationType: WidgetConfigurationType.LINE_CHART,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.MAX,
@@ -148,7 +160,7 @@ export const TEST_LINE_CHART_CONFIG_MINIMAL = {
axisNameDisplay: AxisNameDisplay.NONE,
};
-export const TEST_PIE_CHART_CONFIG = {
+export const TEST_PIE_CHART_CONFIG: PieChartConfigurationDTO = {
configurationType: WidgetConfigurationType.PIE_CHART,
groupByFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_2,
@@ -159,18 +171,9 @@ export const TEST_PIE_CHART_CONFIG = {
showCenterMetric: true,
color: 'yellow',
description: 'Distribution by category',
- filter: {
- and: [
- {
- field: 'status',
- operator: 'eq',
- value: 'active',
- },
- ],
- },
};
-export const TEST_PIE_CHART_CONFIG_MINIMAL = {
+export const TEST_PIE_CHART_CONFIG_MINIMAL: PieChartConfigurationDTO = {
configurationType: WidgetConfigurationType.PIE_CHART,
groupByFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_2,
@@ -179,24 +182,15 @@ export const TEST_PIE_CHART_CONFIG_MINIMAL = {
displayDataLabel: false,
};
-export const TEST_GAUGE_CHART_CONFIG = {
+export const TEST_GAUGE_CHART_CONFIG: GaugeChartConfigurationDTO = {
configurationType: WidgetConfigurationType.GAUGE_CHART,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.SUM,
description: 'Completion percentage',
displayDataLabel: true,
- filter: {
- or: [
- {
- field: 'completed',
- operator: 'eq',
- value: true,
- },
- ],
- },
};
-export const TEST_GAUGE_CHART_CONFIG_MINIMAL = {
+export const TEST_GAUGE_CHART_CONFIG_MINIMAL: GaugeChartConfigurationDTO = {
configurationType: WidgetConfigurationType.GAUGE_CHART,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.COUNT_TRUE,
diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-creation.integration-spec.ts.snap
index 38f6c705ba..a1a3fd7901 100644
--- a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-creation.integration-spec.ts.snap
+++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-creation.integration-spec.ts.snap
@@ -1,6 +1,86 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Page layout widget creation should fail when gridPosition has invalid values 1`] = `
+exports[`Page layout widget creation should fail AGGREGATE_CHART widget configuration validation failures when AGGREGATE_CHART configuration has invalid UUID 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "aggregateFieldMetadataId: aggregateFieldMetadataId must be a UUID",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget creation should fail AGGREGATE_CHART widget configuration validation failures when AGGREGATE_CHART configuration has missing required fields 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "aggregateFieldMetadataId: aggregateFieldMetadataId should not be empty, aggregateFieldMetadataId must be a UUID; aggregateOperation: aggregateOperation should not be empty, aggregateOperation must be one of the following values: MIN, MAX, AVG, SUM, COUNT, COUNT_UNIQUE_VALUES, COUNT_EMPTY, COUNT_NOT_EMPTY, COUNT_TRUE, COUNT_FALSE, PERCENTAGE_EMPTY, PERCENTAGE_NOT_EMPTY",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget creation should fail BAR_CHART widget configuration validation failures when HORIZONTAL BAR_CHART configuration is missing group by field 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "primaryAxisGroupByFieldMetadataId: primaryAxisGroupByFieldMetadataId should not be empty, primaryAxisGroupByFieldMetadataId must be a UUID; layout: layout should not be empty, layout must be one of the following values: VERTICAL, HORIZONTAL",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget creation should fail BAR_CHART widget configuration validation failures when VERTICAL BAR_CHART configuration is missing group by field 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "primaryAxisGroupByFieldMetadataId: primaryAxisGroupByFieldMetadataId should not be empty, primaryAxisGroupByFieldMetadataId must be a UUID; layout: layout should not be empty, layout must be one of the following values: VERTICAL, HORIZONTAL",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget creation should fail Edge case configuration validation failures when configuration has missing configurationType 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "Invalid configuration: missing configuration type",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget creation should fail Edge case configuration validation failures when configuration has unsupported configurationType 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "Invalid configuration type: UNSUPPORTED_TYPE",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget creation should fail Edge case configuration validation failures when configuration is null 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "http": {
+ "status": 400,
+ },
+ "userFriendlyMessage": "An error occurred.",
+ },
+ "message": "Expected non-nullable type "JSON!" not to be null.",
+ "name": "GraphQLError",
+}
+`;
+
+exports[`Page layout widget creation should fail General validation failures when gridPosition has invalid values 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
@@ -11,7 +91,7 @@ exports[`Page layout widget creation should fail when gridPosition has invalid v
}
`;
-exports[`Page layout widget creation should fail when pageLayoutTabId references non-existent tab 1`] = `
+exports[`Page layout widget creation should fail General validation failures when pageLayoutTabId references non-existent tab 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
@@ -48,7 +128,7 @@ exports[`Page layout widget creation should fail when pageLayoutTabId references
}
`;
-exports[`Page layout widget creation should fail when title is missing 1`] = `
+exports[`Page layout widget creation should fail General validation failures when title is missing 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
@@ -61,3 +141,47 @@ exports[`Page layout widget creation should fail when title is missing 1`] = `
"name": "GraphQLError",
}
`;
+
+exports[`Page layout widget creation should fail IFRAME widget configuration validation failures when IFRAME configuration has empty URL 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "url: url must be a URL address",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget creation should fail IFRAME widget configuration validation failures when IFRAME configuration has invalid URL 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "url: url must be a URL address",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget creation should fail STANDALONE_RICH_TEXT widget configuration validation failures when STANDALONE_RICH_TEXT configuration has missing body 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "body: body should not be empty",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget creation should fail STANDALONE_RICH_TEXT widget configuration validation failures when STANDALONE_RICH_TEXT configuration has wrong body type 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "body: nested property body must be either object or array",
+ "name": "UserInputError",
+}
+`;
diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-update.integration-spec.ts.snap
index 5367dabf0d..5d4c745e24 100644
--- a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-update.integration-spec.ts.snap
+++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-update.integration-spec.ts.snap
@@ -1,5 +1,126 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`Page layout widget update should fail Widget configuration validation failures on update AGGREGATE_CHART widget configuration validation failures when updating to AGGREGATE_CHART with invalid UUID 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "aggregateFieldMetadataId: aggregateFieldMetadataId must be a UUID",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget update should fail Widget configuration validation failures on update AGGREGATE_CHART widget configuration validation failures when updating to AGGREGATE_CHART with missing required fields 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "aggregateFieldMetadataId: aggregateFieldMetadataId should not be empty, aggregateFieldMetadataId must be a UUID; aggregateOperation: aggregateOperation should not be empty, aggregateOperation must be one of the following values: MIN, MAX, AVG, SUM, COUNT, COUNT_UNIQUE_VALUES, COUNT_EMPTY, COUNT_NOT_EMPTY, COUNT_TRUE, COUNT_FALSE, PERCENTAGE_EMPTY, PERCENTAGE_NOT_EMPTY",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget update should fail Widget configuration validation failures on update BAR_CHART widget configuration validation failures when updating to BAR_CHART with missing group by field (horizontal) 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "primaryAxisGroupByFieldMetadataId: primaryAxisGroupByFieldMetadataId should not be empty, primaryAxisGroupByFieldMetadataId must be a UUID; layout: layout should not be empty, layout must be one of the following values: VERTICAL, HORIZONTAL",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget update should fail Widget configuration validation failures on update BAR_CHART widget configuration validation failures when updating to BAR_CHART with missing group by field (vertical) 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "primaryAxisGroupByFieldMetadataId: primaryAxisGroupByFieldMetadataId should not be empty, primaryAxisGroupByFieldMetadataId must be a UUID; layout: layout should not be empty, layout must be one of the following values: VERTICAL, HORIZONTAL",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget update should fail Widget configuration validation failures on update Edge case configuration validation failures when updating configuration to null 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "Invalid configuration: not an object",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget update should fail Widget configuration validation failures on update Edge case configuration validation failures when updating configuration with missing configurationType 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "Invalid configuration: missing configuration type",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget update should fail Widget configuration validation failures on update Edge case configuration validation failures when updating configuration with unsupported configurationType 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "Invalid configuration type: UNSUPPORTED_TYPE",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget update should fail Widget configuration validation failures on update IFRAME widget configuration validation failures when updating IFRAME configuration with empty URL 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "url: url must be a URL address",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget update should fail Widget configuration validation failures on update IFRAME widget configuration validation failures when updating IFRAME configuration with invalid URL 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "url: url must be a URL address",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget update should fail Widget configuration validation failures on update STANDALONE_RICH_TEXT widget configuration validation failures when updating to STANDALONE_RICH_TEXT with missing body 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "body: body should not be empty",
+ "name": "UserInputError",
+}
+`;
+
+exports[`Page layout widget update should fail Widget configuration validation failures on update STANDALONE_RICH_TEXT widget configuration validation failures when updating to STANDALONE_RICH_TEXT with wrong body type 1`] = `
+{
+ "extensions": {
+ "code": "BAD_USER_INPUT",
+ "userFriendlyMessage": "Invalid page layout widget data.",
+ },
+ "message": "body: nested property body must be either object or array",
+ "name": "UserInputError",
+}
+`;
+
exports[`Page layout widget update should fail when updating a non-existent page layout widget 1`] = `
{
"extensions": {
diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-creation.integration-spec.ts.snap
index 422d2cade3..1b39d61942 100644
--- a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-creation.integration-spec.ts.snap
+++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-creation.integration-spec.ts.snap
@@ -1,6 +1,251 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Page layout widget creation should succeed should create a page layout widget 1`] = `
+exports[`Page layout widget creation should succeed should create a page layout widget with AGGREGATE_CHART full configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "COUNT",
+ "configurationType": "AGGREGATE_CHART",
+ "description": "Count of all records",
+ "displayDataLabel": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "format": "0,0",
+ "label": "Total Records",
+ "prefix": null,
+ "suffix": null,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Number Chart Widget",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with AGGREGATE_CHART minimal configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "SUM",
+ "configurationType": "AGGREGATE_CHART",
+ "description": null,
+ "displayDataLabel": false,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "format": null,
+ "label": null,
+ "prefix": null,
+ "suffix": null,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Number Chart Widget Minimal",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with GAUGE_CHART full configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "SUM",
+ "color": null,
+ "configurationType": "GAUGE_CHART",
+ "description": "Completion percentage",
+ "displayDataLabel": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Gauge Chart Widget",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with GAUGE_CHART minimal configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "COUNT_TRUE",
+ "color": null,
+ "configurationType": "GAUGE_CHART",
+ "description": null,
+ "displayDataLabel": false,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Gauge Chart Widget Minimal",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with HORIZONTAL BAR_CHART full configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "SUM",
+ "axisNameDisplay": "NONE",
+ "color": "blue",
+ "configurationType": "BAR_CHART",
+ "description": "Horizontal revenue breakdown",
+ "displayDataLabel": true,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "groupMode": null,
+ "isCumulative": null,
+ "layout": "HORIZONTAL",
+ "omitNullValues": true,
+ "primaryAxisDateGranularity": "DAY",
+ "primaryAxisGroupByFieldMetadataId": Any,
+ "primaryAxisGroupBySubFieldName": null,
+ "primaryAxisManualSortOrder": null,
+ "primaryAxisOrderBy": "FIELD_ASC",
+ "rangeMax": 100000,
+ "rangeMin": 0,
+ "secondaryAxisGroupByDateGranularity": "DAY",
+ "secondaryAxisGroupByFieldMetadataId": null,
+ "secondaryAxisGroupBySubFieldName": null,
+ "secondaryAxisManualSortOrder": null,
+ "secondaryAxisOrderBy": null,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Horizontal Bar Chart Widget",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with HORIZONTAL BAR_CHART minimal configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "COUNT",
+ "axisNameDisplay": "NONE",
+ "color": null,
+ "configurationType": "BAR_CHART",
+ "description": null,
+ "displayDataLabel": false,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "groupMode": null,
+ "isCumulative": null,
+ "layout": "HORIZONTAL",
+ "omitNullValues": null,
+ "primaryAxisDateGranularity": "DAY",
+ "primaryAxisGroupByFieldMetadataId": Any,
+ "primaryAxisGroupBySubFieldName": null,
+ "primaryAxisManualSortOrder": null,
+ "primaryAxisOrderBy": "VALUE_DESC",
+ "rangeMax": null,
+ "rangeMin": null,
+ "secondaryAxisGroupByDateGranularity": "DAY",
+ "secondaryAxisGroupByFieldMetadataId": null,
+ "secondaryAxisGroupBySubFieldName": null,
+ "secondaryAxisManualSortOrder": null,
+ "secondaryAxisOrderBy": null,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Horizontal Bar Chart Widget Minimal",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with IFRAME configuration 1`] = `
+{
+ "configuration": {
+ "configurationType": "IFRAME",
+ "url": "https://example.com/dashboard",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Iframe Widget",
+ "type": "IFRAME",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with IFRAME minimal configuration 1`] = `
{
"configuration": {
"configurationType": "IFRAME",
@@ -17,31 +262,324 @@ exports[`Page layout widget creation should succeed should create a page layout
"id": Any,
"objectMetadataId": null,
"pageLayoutTabId": Any,
- "title": "Test Widget",
+ "title": "Iframe Widget Minimal",
"type": "IFRAME",
"updatedAt": Any,
}
`;
-exports[`Page layout widget creation should succeed should create a page layout widget with specific type 1`] = `
+exports[`Page layout widget creation should succeed should create a page layout widget with LINE_CHART full configuration 1`] = `
{
"configuration": {
- "configurationType": "IFRAME",
- "url": "https://example.com",
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "AVG",
+ "axisNameDisplay": "NONE",
+ "color": "cyan",
+ "configurationType": "LINE_CHART",
+ "description": "Trend over time",
+ "displayDataLabel": true,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "isCumulative": null,
+ "isStacked": null,
+ "omitNullValues": false,
+ "primaryAxisDateGranularity": "DAY",
+ "primaryAxisGroupByFieldMetadataId": Any,
+ "primaryAxisGroupBySubFieldName": null,
+ "primaryAxisManualSortOrder": null,
+ "primaryAxisOrderBy": "FIELD_ASC",
+ "rangeMax": 100,
+ "rangeMin": -100,
+ "secondaryAxisGroupByDateGranularity": "DAY",
+ "secondaryAxisGroupByFieldMetadataId": Any,
+ "secondaryAxisGroupBySubFieldName": null,
+ "secondaryAxisManualSortOrder": null,
+ "secondaryAxisOrderBy": "FIELD_DESC",
+ "timezone": "UTC",
},
"createdAt": Any,
"deletedAt": null,
"gridPosition": {
"column": 0,
- "columnSpan": 2,
+ "columnSpan": 1,
"row": 0,
- "rowSpan": 2,
+ "rowSpan": 1,
},
"id": Any,
"objectMetadataId": null,
"pageLayoutTabId": Any,
- "title": "Iframe Widget",
- "type": "IFRAME",
+ "title": "Line Chart Widget",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with LINE_CHART minimal configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "MAX",
+ "axisNameDisplay": "NONE",
+ "color": null,
+ "configurationType": "LINE_CHART",
+ "description": null,
+ "displayDataLabel": false,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "isCumulative": null,
+ "isStacked": null,
+ "omitNullValues": null,
+ "primaryAxisDateGranularity": "DAY",
+ "primaryAxisGroupByFieldMetadataId": Any,
+ "primaryAxisGroupBySubFieldName": null,
+ "primaryAxisManualSortOrder": null,
+ "primaryAxisOrderBy": "VALUE_ASC",
+ "rangeMax": null,
+ "rangeMin": null,
+ "secondaryAxisGroupByDateGranularity": "DAY",
+ "secondaryAxisGroupByFieldMetadataId": null,
+ "secondaryAxisGroupBySubFieldName": null,
+ "secondaryAxisManualSortOrder": null,
+ "secondaryAxisOrderBy": null,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Line Chart Widget Minimal",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with PIE_CHART full configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "SUM",
+ "color": "yellow",
+ "configurationType": "PIE_CHART",
+ "dateGranularity": "DAY",
+ "description": "Distribution by category",
+ "displayDataLabel": true,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "groupByFieldMetadataId": Any,
+ "groupBySubFieldName": null,
+ "manualSortOrder": null,
+ "orderBy": "VALUE_DESC",
+ "showCenterMetric": true,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Pie Chart Widget",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with PIE_CHART minimal configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "COUNT",
+ "color": null,
+ "configurationType": "PIE_CHART",
+ "dateGranularity": "DAY",
+ "description": null,
+ "displayDataLabel": false,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "groupByFieldMetadataId": Any,
+ "groupBySubFieldName": null,
+ "manualSortOrder": null,
+ "orderBy": "FIELD_ASC",
+ "showCenterMetric": true,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Pie Chart Widget Minimal",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with STANDALONE_RICH_TEXT configuration 1`] = `
+{
+ "configuration": {
+ "body": {
+ "blocknote": "{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello world"}]}]}",
+ "markdown": "# Hello world",
+ },
+ "configurationType": "STANDALONE_RICH_TEXT",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Rich Text Widget",
+ "type": "STANDALONE_RICH_TEXT",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with STANDALONE_RICH_TEXT minimal configuration 1`] = `
+{
+ "configuration": {
+ "body": {
+ "blocknote": null,
+ "markdown": "Simple text",
+ },
+ "configurationType": "STANDALONE_RICH_TEXT",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Rich Text Widget Minimal",
+ "type": "STANDALONE_RICH_TEXT",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with VERTICAL BAR_CHART full configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "SUM",
+ "axisNameDisplay": "NONE",
+ "color": "red",
+ "configurationType": "BAR_CHART",
+ "description": "Monthly revenue breakdown",
+ "displayDataLabel": true,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "groupMode": null,
+ "isCumulative": null,
+ "layout": "VERTICAL",
+ "omitNullValues": true,
+ "primaryAxisDateGranularity": "DAY",
+ "primaryAxisGroupByFieldMetadataId": Any,
+ "primaryAxisGroupBySubFieldName": null,
+ "primaryAxisManualSortOrder": null,
+ "primaryAxisOrderBy": "FIELD_ASC",
+ "rangeMax": 100000,
+ "rangeMin": 0,
+ "secondaryAxisGroupByDateGranularity": "DAY",
+ "secondaryAxisGroupByFieldMetadataId": null,
+ "secondaryAxisGroupBySubFieldName": null,
+ "secondaryAxisManualSortOrder": null,
+ "secondaryAxisOrderBy": null,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Vertical Bar Chart Widget",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget creation should succeed should create a page layout widget with VERTICAL BAR_CHART minimal configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "COUNT",
+ "axisNameDisplay": "NONE",
+ "color": null,
+ "configurationType": "BAR_CHART",
+ "description": null,
+ "displayDataLabel": false,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "groupMode": null,
+ "isCumulative": null,
+ "layout": "VERTICAL",
+ "omitNullValues": null,
+ "primaryAxisDateGranularity": "DAY",
+ "primaryAxisGroupByFieldMetadataId": Any,
+ "primaryAxisGroupBySubFieldName": null,
+ "primaryAxisManualSortOrder": null,
+ "primaryAxisOrderBy": "VALUE_DESC",
+ "rangeMax": null,
+ "rangeMin": null,
+ "secondaryAxisGroupByDateGranularity": "DAY",
+ "secondaryAxisGroupByFieldMetadataId": null,
+ "secondaryAxisGroupBySubFieldName": null,
+ "secondaryAxisManualSortOrder": null,
+ "secondaryAxisOrderBy": null,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Vertical Bar Chart Widget Minimal",
+ "type": "GRAPH",
"updatedAt": Any,
}
`;
diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-update.integration-spec.ts.snap
index 0e49c0d12c..002f875e39 100644
--- a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-update.integration-spec.ts.snap
+++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-update.integration-spec.ts.snap
@@ -46,11 +46,313 @@ exports[`Page layout widget update should succeed should update page layout widg
}
`;
-exports[`Page layout widget update should succeed should update page layout widget type 1`] = `
+exports[`Page layout widget update should succeed should update page layout widget to AGGREGATE_CHART configuration 1`] = `
{
"configuration": {
- "configurationType": "IFRAME",
- "url": null,
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "COUNT",
+ "configurationType": "AGGREGATE_CHART",
+ "description": "Count of all records",
+ "displayDataLabel": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "format": "0,0",
+ "label": "Total Records",
+ "prefix": null,
+ "suffix": null,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Original Widget Title",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget update should succeed should update page layout widget to GAUGE_CHART configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "SUM",
+ "color": null,
+ "configurationType": "GAUGE_CHART",
+ "description": "Completion percentage",
+ "displayDataLabel": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Original Widget Title",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget update should succeed should update page layout widget to HORIZONTAL BAR_CHART configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "SUM",
+ "axisNameDisplay": "NONE",
+ "color": "blue",
+ "configurationType": "BAR_CHART",
+ "description": "Horizontal revenue breakdown",
+ "displayDataLabel": true,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "groupMode": null,
+ "isCumulative": null,
+ "layout": "HORIZONTAL",
+ "omitNullValues": true,
+ "primaryAxisDateGranularity": "DAY",
+ "primaryAxisGroupByFieldMetadataId": Any,
+ "primaryAxisGroupBySubFieldName": null,
+ "primaryAxisManualSortOrder": null,
+ "primaryAxisOrderBy": "FIELD_ASC",
+ "rangeMax": 100000,
+ "rangeMin": 0,
+ "secondaryAxisGroupByDateGranularity": "DAY",
+ "secondaryAxisGroupByFieldMetadataId": null,
+ "secondaryAxisGroupBySubFieldName": null,
+ "secondaryAxisManualSortOrder": null,
+ "secondaryAxisOrderBy": null,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Original Widget Title",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget update should succeed should update page layout widget to IFRAME configuration 1`] = `
+{
+ "configuration": {
+ "configurationType": "IFRAME",
+ "url": "https://app.twenty.com/analytics",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Original Widget Title",
+ "type": "IFRAME",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget update should succeed should update page layout widget to LINE_CHART configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "AVG",
+ "axisNameDisplay": "NONE",
+ "color": "cyan",
+ "configurationType": "LINE_CHART",
+ "description": "Trend over time",
+ "displayDataLabel": true,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "isCumulative": null,
+ "isStacked": null,
+ "omitNullValues": false,
+ "primaryAxisDateGranularity": "DAY",
+ "primaryAxisGroupByFieldMetadataId": Any,
+ "primaryAxisGroupBySubFieldName": null,
+ "primaryAxisManualSortOrder": null,
+ "primaryAxisOrderBy": "FIELD_ASC",
+ "rangeMax": 100,
+ "rangeMin": -100,
+ "secondaryAxisGroupByDateGranularity": "DAY",
+ "secondaryAxisGroupByFieldMetadataId": Any,
+ "secondaryAxisGroupBySubFieldName": null,
+ "secondaryAxisManualSortOrder": null,
+ "secondaryAxisOrderBy": "FIELD_DESC",
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Original Widget Title",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget update should succeed should update page layout widget to PIE_CHART configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "SUM",
+ "color": "yellow",
+ "configurationType": "PIE_CHART",
+ "dateGranularity": "DAY",
+ "description": "Distribution by category",
+ "displayDataLabel": true,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "groupByFieldMetadataId": Any,
+ "groupBySubFieldName": null,
+ "manualSortOrder": null,
+ "orderBy": "VALUE_DESC",
+ "showCenterMetric": true,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Original Widget Title",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget update should succeed should update page layout widget to STANDALONE_RICH_TEXT configuration 1`] = `
+{
+ "configuration": {
+ "body": {
+ "blocknote": "{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello world"}]}]}",
+ "markdown": "# Hello world",
+ },
+ "configurationType": "STANDALONE_RICH_TEXT",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Original Widget Title",
+ "type": "STANDALONE_RICH_TEXT",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget update should succeed should update page layout widget to VERTICAL BAR_CHART configuration 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "SUM",
+ "axisNameDisplay": "NONE",
+ "color": "red",
+ "configurationType": "BAR_CHART",
+ "description": "Monthly revenue breakdown",
+ "displayDataLabel": true,
+ "displayLegend": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "groupMode": null,
+ "isCumulative": null,
+ "layout": "VERTICAL",
+ "omitNullValues": true,
+ "primaryAxisDateGranularity": "DAY",
+ "primaryAxisGroupByFieldMetadataId": Any,
+ "primaryAxisGroupBySubFieldName": null,
+ "primaryAxisManualSortOrder": null,
+ "primaryAxisOrderBy": "FIELD_ASC",
+ "rangeMax": 100000,
+ "rangeMin": 0,
+ "secondaryAxisGroupByDateGranularity": "DAY",
+ "secondaryAxisGroupByFieldMetadataId": null,
+ "secondaryAxisGroupBySubFieldName": null,
+ "secondaryAxisManualSortOrder": null,
+ "secondaryAxisOrderBy": null,
+ "timezone": "UTC",
+ },
+ "createdAt": Any,
+ "deletedAt": null,
+ "gridPosition": {
+ "column": 0,
+ "columnSpan": 1,
+ "row": 0,
+ "rowSpan": 1,
+ },
+ "id": Any,
+ "objectMetadataId": null,
+ "pageLayoutTabId": Any,
+ "title": "Original Widget Title",
+ "type": "GRAPH",
+ "updatedAt": Any,
+}
+`;
+
+exports[`Page layout widget update should succeed should update page layout widget type 1`] = `
+{
+ "configuration": {
+ "aggregateFieldMetadataId": Any,
+ "aggregateOperation": "COUNT",
+ "configurationType": "AGGREGATE_CHART",
+ "description": "Count of all records",
+ "displayDataLabel": true,
+ "filter": null,
+ "firstDayOfTheWeek": 1,
+ "format": "0,0",
+ "label": "Total Records",
+ "prefix": null,
+ "suffix": null,
+ "timezone": "UTC",
},
"createdAt": Any,
"deletedAt": null,
diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-creation.integration-spec.ts
index 865b9fb69e..4d8d71ef85 100644
--- a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-creation.integration-spec.ts
+++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-creation.integration-spec.ts
@@ -1,5 +1,15 @@
import { faker } from '@faker-js/faker';
-import { TEST_IFRAME_CONFIG } from 'test/integration/constants/widget-configuration-test-data.constants';
+import {
+ INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
+ INVALID_IFRAME_CONFIG_BAD_URL,
+ INVALID_IFRAME_CONFIG_EMPTY_URL,
+ INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
+ INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
+ INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
+ INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
+ INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
+ TEST_IFRAME_CONFIG,
+} from 'test/integration/constants/widget-configuration-test-data.constants';
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util';
import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util';
@@ -8,8 +18,16 @@ import { createOnePageLayout } from 'test/integration/metadata/suites/page-layou
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
import { type CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
+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';
+const DEFAULT_GRID_POSITION = {
+ row: 0,
+ column: 0,
+ rowSpan: 1,
+ columnSpan: 1,
+};
+
describe('Page layout widget creation should fail', () => {
let testPageLayoutId: string;
let testPageLayoutTabId: string;
@@ -44,62 +62,251 @@ describe('Page layout widget creation should fail', () => {
});
});
- it('when title is missing', async () => {
- const { errors } = await createOnePageLayoutWidget({
- expectToFail: true,
- input: {
- pageLayoutTabId: testPageLayoutTabId,
- type: WidgetType.IFRAME,
- gridPosition: {
- row: 0,
- column: 0,
- rowSpan: 1,
- columnSpan: 1,
- },
- configuration: TEST_IFRAME_CONFIG,
- } as CreatePageLayoutWidgetInput,
+ describe('General validation failures', () => {
+ it('when title is missing', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.IFRAME,
+ gridPosition: DEFAULT_GRID_POSITION,
+ configuration: TEST_IFRAME_CONFIG,
+ } as CreatePageLayoutWidgetInput,
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
});
- expectOneNotInternalServerErrorSnapshot({ errors });
+ it('when pageLayoutTabId references non-existent tab', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Widget With Non-Existent Tab',
+ pageLayoutTabId: faker.string.uuid(),
+ type: WidgetType.IFRAME,
+ configuration: TEST_IFRAME_CONFIG,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+
+ it('when gridPosition has invalid values', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Widget With Invalid Grid Position',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.IFRAME,
+ configuration: TEST_IFRAME_CONFIG,
+ gridPosition: {
+ row: -1,
+ column: 0,
+ rowSpan: 1,
+ columnSpan: 1,
+ },
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
});
- it('when pageLayoutTabId references non-existent tab', async () => {
- const { errors } = await createOnePageLayoutWidget({
- expectToFail: true,
- input: {
- title: 'Widget With Non-Existent Tab',
- pageLayoutTabId: faker.string.uuid(),
- type: WidgetType.IFRAME,
- configuration: TEST_IFRAME_CONFIG,
- gridPosition: {
- row: 0,
- column: 0,
- rowSpan: 1,
- columnSpan: 1,
+ describe('IFRAME widget configuration validation failures', () => {
+ it('when IFRAME configuration has invalid URL', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Widget With Invalid URL',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.IFRAME,
+ configuration: {
+ ...INVALID_IFRAME_CONFIG_BAD_URL,
+ configurationType: WidgetConfigurationType.IFRAME,
+ },
+ gridPosition: DEFAULT_GRID_POSITION,
},
- },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
});
- expectOneNotInternalServerErrorSnapshot({ errors });
+ it('when IFRAME configuration has empty URL', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Widget With Empty URL',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.IFRAME,
+ configuration: {
+ ...INVALID_IFRAME_CONFIG_EMPTY_URL,
+ configurationType: WidgetConfigurationType.IFRAME,
+ },
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
});
- it('when gridPosition has invalid values', async () => {
- const { errors } = await createOnePageLayoutWidget({
- expectToFail: true,
- input: {
- title: 'Widget With Invalid Grid Position',
- pageLayoutTabId: testPageLayoutTabId,
- type: WidgetType.IFRAME,
- configuration: TEST_IFRAME_CONFIG,
- gridPosition: {
- row: -1,
- column: 0,
- rowSpan: 1,
- columnSpan: 1,
+ describe('STANDALONE_RICH_TEXT widget configuration validation failures', () => {
+ it('when STANDALONE_RICH_TEXT configuration has missing body', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Widget With Missing Body',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.STANDALONE_RICH_TEXT,
+ configuration: {
+ ...INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
+ configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
+ } as unknown as CreatePageLayoutWidgetInput['configuration'],
+ gridPosition: DEFAULT_GRID_POSITION,
},
- },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
});
- expectOneNotInternalServerErrorSnapshot({ errors });
+ it('when STANDALONE_RICH_TEXT configuration has wrong body type', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Widget With Wrong Body Type',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.STANDALONE_RICH_TEXT,
+ configuration: {
+ ...INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
+ configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
+ } as unknown as CreatePageLayoutWidgetInput['configuration'],
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+ });
+
+ describe('AGGREGATE_CHART widget configuration validation failures', () => {
+ it('when AGGREGATE_CHART configuration has missing required fields', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Widget With Missing Fields',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.GRAPH,
+ configuration:
+ INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS as unknown as CreatePageLayoutWidgetInput['configuration'],
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+
+ it('when AGGREGATE_CHART configuration has invalid UUID', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Widget With Bad UUID',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.GRAPH,
+ configuration:
+ INVALID_NUMBER_CHART_CONFIG_BAD_UUID as unknown as CreatePageLayoutWidgetInput['configuration'],
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+ });
+
+ describe('BAR_CHART widget configuration validation failures', () => {
+ it('when VERTICAL BAR_CHART configuration is missing group by field', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Vertical Bar Chart Missing Group By',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.GRAPH,
+ configuration:
+ INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY as unknown as CreatePageLayoutWidgetInput['configuration'],
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+
+ it('when HORIZONTAL BAR_CHART configuration is missing group by field', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Horizontal Bar Chart Missing Group By',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.GRAPH,
+ configuration:
+ INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY as unknown as CreatePageLayoutWidgetInput['configuration'],
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+ });
+
+ describe('Edge case configuration validation failures', () => {
+ it('when configuration is null', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Widget With Null Config',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.IFRAME,
+ configuration:
+ null as unknown as CreatePageLayoutWidgetInput['configuration'],
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+
+ it('when configuration has missing configurationType', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Widget With Missing Config Type',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.IFRAME,
+ configuration: {
+ someField: 'value',
+ } as unknown as CreatePageLayoutWidgetInput['configuration'],
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+
+ it('when configuration has unsupported configurationType', async () => {
+ const { errors } = await createOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ title: 'Widget With Unsupported Config Type',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.IFRAME,
+ configuration: {
+ configurationType: 'UNSUPPORTED_TYPE',
+ someField: 'value',
+ } as unknown as CreatePageLayoutWidgetInput['configuration'],
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
});
});
diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-update.integration-spec.ts
index 3e8510def9..62eca9469d 100644
--- a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-update.integration-spec.ts
+++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-update.integration-spec.ts
@@ -1,5 +1,33 @@
+import {
+ INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
+ INVALID_IFRAME_CONFIG_BAD_URL,
+ INVALID_IFRAME_CONFIG_EMPTY_URL,
+ INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
+ INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
+ INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
+ INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
+ INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
+ TEST_IFRAME_CONFIG,
+} from 'test/integration/constants/widget-configuration-test-data.constants';
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
+import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util';
+import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util';
+import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util';
+import { destroyOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget.util';
import { updateOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget.util';
+import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util';
+import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
+
+import { type UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input';
+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';
+
+const DEFAULT_GRID_POSITION = {
+ row: 0,
+ column: 0,
+ rowSpan: 1,
+ columnSpan: 1,
+};
describe('Page layout widget update should fail', () => {
it('when updating a non-existent page layout widget', async () => {
@@ -13,4 +41,226 @@ describe('Page layout widget update should fail', () => {
expectOneNotInternalServerErrorSnapshot({ errors });
});
+
+ describe('Widget configuration validation failures on update', () => {
+ let testPageLayoutId: string;
+ let testPageLayoutTabId: string;
+ let testPageLayoutWidgetId: string;
+
+ beforeAll(async () => {
+ const { data: layoutData } = await createOnePageLayout({
+ expectToFail: false,
+ input: { name: 'Test Page Layout For Widget Update Failures' },
+ });
+
+ testPageLayoutId = layoutData.createPageLayout.id;
+
+ const { data: tabData } = await createOnePageLayoutTab({
+ expectToFail: false,
+ input: {
+ title: 'Test Tab For Widget Update Failures',
+ pageLayoutId: testPageLayoutId,
+ },
+ });
+
+ testPageLayoutTabId = tabData.createPageLayoutTab.id;
+ });
+
+ afterAll(async () => {
+ await destroyOnePageLayoutTab({
+ expectToFail: false,
+ input: { id: testPageLayoutTabId },
+ });
+ await destroyOnePageLayout({
+ expectToFail: false,
+ input: { id: testPageLayoutId },
+ });
+ });
+
+ beforeEach(async () => {
+ const { data } = await createOnePageLayoutWidget({
+ expectToFail: false,
+ input: {
+ title: 'Original Widget',
+ pageLayoutTabId: testPageLayoutTabId,
+ type: WidgetType.IFRAME,
+ configuration: TEST_IFRAME_CONFIG,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ });
+
+ testPageLayoutWidgetId = data.createPageLayoutWidget.id;
+ });
+
+ afterEach(async () => {
+ await destroyOnePageLayoutWidget({
+ expectToFail: false,
+ input: { id: testPageLayoutWidgetId },
+ });
+ });
+
+ describe('IFRAME widget configuration validation failures', () => {
+ it('when updating IFRAME configuration with invalid URL', async () => {
+ const { errors } = await updateOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ id: testPageLayoutWidgetId,
+ configuration: {
+ ...INVALID_IFRAME_CONFIG_BAD_URL,
+ configurationType: WidgetConfigurationType.IFRAME,
+ },
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+
+ it('when updating IFRAME configuration with empty URL', async () => {
+ const { errors } = await updateOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ id: testPageLayoutWidgetId,
+ configuration: {
+ ...INVALID_IFRAME_CONFIG_EMPTY_URL,
+ configurationType: WidgetConfigurationType.IFRAME,
+ },
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+ });
+
+ describe('STANDALONE_RICH_TEXT widget configuration validation failures', () => {
+ it('when updating to STANDALONE_RICH_TEXT with missing body', async () => {
+ const { errors } = await updateOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ id: testPageLayoutWidgetId,
+ configuration: {
+ ...INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
+ configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
+ } as unknown as UpdatePageLayoutWidgetInput['configuration'],
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+
+ it('when updating to STANDALONE_RICH_TEXT with wrong body type', async () => {
+ const { errors } = await updateOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ id: testPageLayoutWidgetId,
+ configuration: {
+ ...INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
+ configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
+ } as unknown as UpdatePageLayoutWidgetInput['configuration'],
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+ });
+
+ describe('AGGREGATE_CHART widget configuration validation failures', () => {
+ it('when updating to AGGREGATE_CHART with missing required fields', async () => {
+ const { errors } = await updateOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ id: testPageLayoutWidgetId,
+ configuration:
+ INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS as unknown as UpdatePageLayoutWidgetInput['configuration'],
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+
+ it('when updating to AGGREGATE_CHART with invalid UUID', async () => {
+ const { errors } = await updateOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ id: testPageLayoutWidgetId,
+ configuration:
+ INVALID_NUMBER_CHART_CONFIG_BAD_UUID as unknown as UpdatePageLayoutWidgetInput['configuration'],
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+ });
+
+ describe('BAR_CHART widget configuration validation failures', () => {
+ it('when updating to BAR_CHART with missing group by field (vertical)', async () => {
+ const { errors } = await updateOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ id: testPageLayoutWidgetId,
+ configuration:
+ INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY as unknown as UpdatePageLayoutWidgetInput['configuration'],
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+
+ it('when updating to BAR_CHART with missing group by field (horizontal)', async () => {
+ const { errors } = await updateOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ id: testPageLayoutWidgetId,
+ configuration:
+ INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY as unknown as UpdatePageLayoutWidgetInput['configuration'],
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+ });
+
+ describe('Edge case configuration validation failures', () => {
+ it('when updating configuration to null', async () => {
+ const { errors } = await updateOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ id: testPageLayoutWidgetId,
+ configuration:
+ null as unknown as UpdatePageLayoutWidgetInput['configuration'],
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+
+ it('when updating configuration with missing configurationType', async () => {
+ const { errors } = await updateOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ id: testPageLayoutWidgetId,
+ configuration: {
+ someField: 'value',
+ } as unknown as UpdatePageLayoutWidgetInput['configuration'],
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+
+ it('when updating configuration with unsupported configurationType', async () => {
+ const { errors } = await updateOnePageLayoutWidget({
+ expectToFail: true,
+ input: {
+ id: testPageLayoutWidgetId,
+ configuration: {
+ configurationType: 'UNSUPPORTED_TYPE',
+ someField: 'value',
+ } as unknown as UpdatePageLayoutWidgetInput['configuration'],
+ },
+ });
+
+ expectOneNotInternalServerErrorSnapshot({ errors });
+ });
+ });
+ });
});
diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-creation.integration-spec.ts
index 8a65c32695..e490123526 100644
--- a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-creation.integration-spec.ts
+++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-creation.integration-spec.ts
@@ -1,3 +1,20 @@
+import {
+ TEST_GAUGE_CHART_CONFIG,
+ TEST_GAUGE_CHART_CONFIG_MINIMAL,
+ TEST_HORIZONTAL_BAR_CHART_CONFIG,
+ TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
+ TEST_IFRAME_CONFIG,
+ TEST_LINE_CHART_CONFIG,
+ TEST_LINE_CHART_CONFIG_MINIMAL,
+ TEST_NUMBER_CHART_CONFIG,
+ TEST_NUMBER_CHART_CONFIG_MINIMAL,
+ TEST_PIE_CHART_CONFIG,
+ TEST_PIE_CHART_CONFIG_MINIMAL,
+ TEST_STANDALONE_RICH_TEXT_CONFIG,
+ TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL,
+ TEST_VERTICAL_BAR_CHART_CONFIG,
+ TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
+} from 'test/integration/constants/widget-configuration-test-data.constants';
import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util';
import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util';
import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util';
@@ -28,41 +45,205 @@ type TestContext = {
};
};
+const DEFAULT_GRID_POSITION = {
+ row: 0,
+ column: 0,
+ rowSpan: 1,
+ columnSpan: 1,
+};
+
const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [
+ // IFRAME widget tests
{
- title: 'create a page layout widget',
- context: {
- input: {
- title: 'Test Widget',
- type: WidgetType.IFRAME,
- configuration: {
- configurationType: WidgetConfigurationType.IFRAME,
- },
- gridPosition: {
- row: 0,
- column: 0,
- rowSpan: 1,
- columnSpan: 1,
- },
- },
- },
- },
- {
- title: 'create a page layout widget with specific type',
+ title: 'create a page layout widget with IFRAME configuration',
context: {
input: {
title: 'Iframe Widget',
type: WidgetType.IFRAME,
+ configuration: TEST_IFRAME_CONFIG,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ {
+ title: 'create a page layout widget with IFRAME minimal configuration',
+ context: {
+ input: {
+ title: 'Iframe Widget Minimal',
+ type: WidgetType.IFRAME,
configuration: {
configurationType: WidgetConfigurationType.IFRAME,
- url: 'https://example.com',
- },
- gridPosition: {
- row: 0,
- column: 0,
- rowSpan: 2,
- columnSpan: 2,
},
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ // STANDALONE_RICH_TEXT widget tests
+ {
+ title:
+ 'create a page layout widget with STANDALONE_RICH_TEXT configuration',
+ context: {
+ input: {
+ title: 'Rich Text Widget',
+ type: WidgetType.STANDALONE_RICH_TEXT,
+ configuration: TEST_STANDALONE_RICH_TEXT_CONFIG,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ {
+ title:
+ 'create a page layout widget with STANDALONE_RICH_TEXT minimal configuration',
+ context: {
+ input: {
+ title: 'Rich Text Widget Minimal',
+ type: WidgetType.STANDALONE_RICH_TEXT,
+ configuration: TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ // AGGREGATE_CHART (number chart) widget tests
+ {
+ title:
+ 'create a page layout widget with AGGREGATE_CHART full configuration',
+ context: {
+ input: {
+ title: 'Number Chart Widget',
+ type: WidgetType.GRAPH,
+ configuration: TEST_NUMBER_CHART_CONFIG,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ {
+ title:
+ 'create a page layout widget with AGGREGATE_CHART minimal configuration',
+ context: {
+ input: {
+ title: 'Number Chart Widget Minimal',
+ type: WidgetType.GRAPH,
+ configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ // BAR_CHART vertical widget tests
+ {
+ title:
+ 'create a page layout widget with VERTICAL BAR_CHART full configuration',
+ context: {
+ input: {
+ title: 'Vertical Bar Chart Widget',
+ type: WidgetType.GRAPH,
+ configuration: TEST_VERTICAL_BAR_CHART_CONFIG,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ {
+ title:
+ 'create a page layout widget with VERTICAL BAR_CHART minimal configuration',
+ context: {
+ input: {
+ title: 'Vertical Bar Chart Widget Minimal',
+ type: WidgetType.GRAPH,
+ configuration: TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ // BAR_CHART horizontal widget tests
+ {
+ title:
+ 'create a page layout widget with HORIZONTAL BAR_CHART full configuration',
+ context: {
+ input: {
+ title: 'Horizontal Bar Chart Widget',
+ type: WidgetType.GRAPH,
+ configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ {
+ title:
+ 'create a page layout widget with HORIZONTAL BAR_CHART minimal configuration',
+ context: {
+ input: {
+ title: 'Horizontal Bar Chart Widget Minimal',
+ type: WidgetType.GRAPH,
+ configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ // PIE_CHART widget tests
+ {
+ title: 'create a page layout widget with PIE_CHART full configuration',
+ context: {
+ input: {
+ title: 'Pie Chart Widget',
+ type: WidgetType.GRAPH,
+ configuration: TEST_PIE_CHART_CONFIG,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ {
+ title: 'create a page layout widget with PIE_CHART minimal configuration',
+ context: {
+ input: {
+ title: 'Pie Chart Widget Minimal',
+ type: WidgetType.GRAPH,
+ configuration: TEST_PIE_CHART_CONFIG_MINIMAL,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ // LINE_CHART widget tests
+ {
+ title: 'create a page layout widget with LINE_CHART full configuration',
+ context: {
+ input: {
+ title: 'Line Chart Widget',
+ type: WidgetType.GRAPH,
+ configuration: TEST_LINE_CHART_CONFIG,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ {
+ title: 'create a page layout widget with LINE_CHART minimal configuration',
+ context: {
+ input: {
+ title: 'Line Chart Widget Minimal',
+ type: WidgetType.GRAPH,
+ configuration: TEST_LINE_CHART_CONFIG_MINIMAL,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ // GAUGE_CHART widget tests
+ {
+ title: 'create a page layout widget with GAUGE_CHART full configuration',
+ context: {
+ input: {
+ title: 'Gauge Chart Widget',
+ type: WidgetType.GRAPH,
+ configuration: TEST_GAUGE_CHART_CONFIG,
+ gridPosition: DEFAULT_GRID_POSITION,
+ },
+ },
+ },
+ {
+ title: 'create a page layout widget with GAUGE_CHART minimal configuration',
+ context: {
+ input: {
+ title: 'Gauge Chart Widget Minimal',
+ type: WidgetType.GRAPH,
+ configuration: TEST_GAUGE_CHART_CONFIG_MINIMAL,
+ gridPosition: DEFAULT_GRID_POSITION,
},
},
},
diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-update.integration-spec.ts
index 7623248f83..f85a2e02c6 100644
--- a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-update.integration-spec.ts
+++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-update.integration-spec.ts
@@ -1,3 +1,13 @@
+import {
+ TEST_GAUGE_CHART_CONFIG,
+ TEST_HORIZONTAL_BAR_CHART_CONFIG,
+ TEST_IFRAME_CONFIG_ALTERNATIVE,
+ TEST_LINE_CHART_CONFIG,
+ TEST_NUMBER_CHART_CONFIG,
+ TEST_PIE_CHART_CONFIG,
+ TEST_STANDALONE_RICH_TEXT_CONFIG,
+ TEST_VERTICAL_BAR_CHART_CONFIG,
+} from 'test/integration/constants/widget-configuration-test-data.constants';
import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util';
import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util';
import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util';
@@ -13,11 +23,13 @@ import {
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';
type TestContext = {
input: {
title?: string;
type?: WidgetType;
+ configuration?: AllPageLayoutWidgetConfiguration;
gridPosition?: {
row: number;
column: number;
@@ -41,6 +53,7 @@ const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [
context: {
input: {
type: WidgetType.GRAPH,
+ configuration: TEST_NUMBER_CHART_CONFIG,
},
},
},
@@ -57,6 +70,78 @@ const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [
},
},
},
+ // Configuration update tests
+ {
+ title: 'update page layout widget to IFRAME configuration',
+ context: {
+ input: {
+ configuration: TEST_IFRAME_CONFIG_ALTERNATIVE,
+ },
+ },
+ },
+ {
+ title: 'update page layout widget to STANDALONE_RICH_TEXT configuration',
+ context: {
+ input: {
+ type: WidgetType.STANDALONE_RICH_TEXT,
+ configuration: TEST_STANDALONE_RICH_TEXT_CONFIG,
+ },
+ },
+ },
+ {
+ title: 'update page layout widget to AGGREGATE_CHART configuration',
+ context: {
+ input: {
+ type: WidgetType.GRAPH,
+ configuration: TEST_NUMBER_CHART_CONFIG,
+ },
+ },
+ },
+ {
+ title: 'update page layout widget to VERTICAL BAR_CHART configuration',
+ context: {
+ input: {
+ type: WidgetType.GRAPH,
+ configuration: TEST_VERTICAL_BAR_CHART_CONFIG,
+ },
+ },
+ },
+ {
+ title: 'update page layout widget to HORIZONTAL BAR_CHART configuration',
+ context: {
+ input: {
+ type: WidgetType.GRAPH,
+ configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG,
+ },
+ },
+ },
+ {
+ title: 'update page layout widget to PIE_CHART configuration',
+ context: {
+ input: {
+ type: WidgetType.GRAPH,
+ configuration: TEST_PIE_CHART_CONFIG,
+ },
+ },
+ },
+ {
+ title: 'update page layout widget to LINE_CHART configuration',
+ context: {
+ input: {
+ type: WidgetType.GRAPH,
+ configuration: TEST_LINE_CHART_CONFIG,
+ },
+ },
+ },
+ {
+ title: 'update page layout widget to GAUGE_CHART configuration',
+ context: {
+ input: {
+ type: WidgetType.GRAPH,
+ configuration: TEST_GAUGE_CHART_CONFIG,
+ },
+ },
+ },
];
describe('Page layout widget update should succeed', () => {
diff --git a/packages/twenty-shared/src/types/CrudOperationType.ts b/packages/twenty-shared/src/types/CrudOperationType.ts
new file mode 100644
index 0000000000..d6e1c4a5a8
--- /dev/null
+++ b/packages/twenty-shared/src/types/CrudOperationType.ts
@@ -0,0 +1,7 @@
+export enum CrudOperationType {
+ CREATE = 'CREATE',
+ UPDATE = 'UPDATE',
+ DELETE = 'DELETE',
+ RESTORE = 'RESTORE',
+ DESTROY = 'DESTROY',
+}
diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts
index 1e38d93f21..059911e523 100644
--- a/packages/twenty-shared/src/types/index.ts
+++ b/packages/twenty-shared/src/types/index.ts
@@ -50,6 +50,7 @@ export {
export type { CompositeFieldSubFieldName } from './CompositeFieldSubFieldNameType';
export type { ConfigVariableValue } from './ConfigVariableValue';
export { ConnectedAccountProvider } from './ConnectedAccountProvider';
+export { CrudOperationType } from './CrudOperationType';
export type { EnumFieldMetadataType } from './EnumFieldMetadataType';
export type { ExcludeFunctions } from './ExcludeFunctions';
export type { ExtractPropertiesThatEndsWithId } from './ExtractPropertiesThatEndsWithId';