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';