diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 86065a4b19..317aadef7d 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -269,6 +269,25 @@ export type AvailableWorkspacesAndAccessTokensOutput = { tokens: AuthTokenPair; }; +export type BarChartConfiguration = { + __typename?: 'BarChartConfiguration'; + aggregateFieldMetadataId: Scalars['UUID']; + aggregateOperation: AggregateOperations; + color?: Maybe; + description?: Maybe; + filter?: Maybe; + graphType: GraphType; + groupByFieldMetadataIdX: Scalars['UUID']; + groupByFieldMetadataIdY?: Maybe; + omitNullValues?: Maybe; + orderByX: GraphOrderBy; + orderByY?: Maybe; + rangeMax?: Maybe; + rangeMin?: Maybe; + xAxisName?: Maybe; + yAxisName?: Maybe; +}; + export type Billing = { __typename?: 'Billing'; billingUrl?: Maybe; @@ -1270,6 +1289,17 @@ export type FullName = { lastName: Scalars['String']; }; +export type GaugeChartConfiguration = { + __typename?: 'GaugeChartConfiguration'; + aggregateFieldMetadataId: Scalars['UUID']; + aggregateFieldMetadataIdTotal: Scalars['UUID']; + aggregateOperation: AggregateOperations; + aggregateOperationTotal: AggregateOperations; + description?: Maybe; + filter?: Maybe; + graphType: GraphType; +}; + export type GetApiKeyDto = { id: Scalars['UUID']; }; @@ -1303,6 +1333,23 @@ export type GetWebhookDto = { id: Scalars['UUID']; }; +/** Order by options for graph widgets */ +export enum GraphOrderBy { + FIELD_ASC = 'FIELD_ASC', + FIELD_DESC = 'FIELD_DESC', + VALUE_ASC = 'VALUE_ASC', + VALUE_DESC = 'VALUE_DESC' +} + +/** Type of graph widget */ +export enum GraphType { + BAR = 'BAR', + GAUGE = 'GAUGE', + LINE = 'LINE', + NUMBER = 'NUMBER', + PIE = 'PIE' +} + export type GridPosition = { __typename?: 'GridPosition'; column: Scalars['Float']; @@ -1331,6 +1378,11 @@ export enum IdentityProviderType { SAML = 'SAML' } +export type IframeConfiguration = { + __typename?: 'IframeConfiguration'; + url: Scalars['String']; +}; + export type ImapSmtpCaldavConnectionParameters = { __typename?: 'ImapSmtpCaldavConnectionParameters'; CALDAV?: Maybe; @@ -1456,6 +1508,25 @@ export type InvalidatePassword = { success: Scalars['Boolean']; }; +export type LineChartConfiguration = { + __typename?: 'LineChartConfiguration'; + aggregateFieldMetadataId: Scalars['UUID']; + aggregateOperation: AggregateOperations; + color?: Maybe; + description?: Maybe; + filter?: Maybe; + graphType: GraphType; + groupByFieldMetadataIdX: Scalars['UUID']; + groupByFieldMetadataIdY?: Maybe; + omitNullValues?: Maybe; + orderByX: GraphOrderBy; + orderByY?: Maybe; + rangeMax?: Maybe; + rangeMin?: Maybe; + xAxisName?: Maybe; + yAxisName?: Maybe; +}; + export type LinkMetadata = { __typename?: 'LinkMetadata'; label: Scalars['String']; @@ -2476,6 +2547,18 @@ export type MutationVerifyTwoFactorAuthenticationMethodForAuthenticatedUserArgs otp: Scalars['String']; }; +export type NumberChartConfiguration = { + __typename?: 'NumberChartConfiguration'; + aggregateFieldMetadataId: Scalars['UUID']; + aggregateOperation: AggregateOperations; + color?: Maybe; + description?: Maybe; + filter?: Maybe; + format?: Maybe; + graphType: GraphType; + label?: Maybe; +}; + export type Object = { __typename?: 'Object'; createdAt: Scalars['DateTime']; @@ -2674,7 +2757,7 @@ export enum PageLayoutType { export type PageLayoutWidget = { __typename?: 'PageLayoutWidget'; - configuration?: Maybe; + configuration?: Maybe; createdAt: Scalars['DateTime']; deletedAt?: Maybe; gridPosition: GridPosition; @@ -2708,6 +2791,18 @@ export enum PermissionFlagType { WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS' } +export type PieChartConfiguration = { + __typename?: 'PieChartConfiguration'; + aggregateFieldMetadataId: Scalars['UUID']; + aggregateOperation: AggregateOperations; + color?: Maybe; + description?: Maybe; + filter?: Maybe; + graphType: GraphType; + groupByFieldMetadataId: Scalars['UUID']; + orderBy: GraphOrderBy; +}; + export type PlaceDetailsResultDto = { __typename?: 'PlaceDetailsResultDto'; city?: Maybe; @@ -3692,7 +3787,7 @@ export type UpdatePageLayoutWidgetInput = { }; export type UpdatePageLayoutWidgetWithIdInput = { - configuration: Scalars['JSON']; + configuration?: InputMaybe; gridPosition: GridPositionInput; id: Scalars['UUID']; objectMetadataId?: InputMaybe; @@ -4047,6 +4142,8 @@ export type Webhook = { workspaceId: Scalars['UUID']; }; +export type WidgetConfiguration = BarChartConfiguration | GaugeChartConfiguration | IframeConfiguration | LineChartConfiguration | NumberChartConfiguration | PieChartConfiguration; + export enum WidgetType { FIELDS = 'FIELDS', GRAPH = 'GRAPH', diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 8175b1f006..e1b57d8d2a 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -269,6 +269,25 @@ export type AvailableWorkspacesAndAccessTokensOutput = { tokens: AuthTokenPair; }; +export type BarChartConfiguration = { + __typename?: 'BarChartConfiguration'; + aggregateFieldMetadataId: Scalars['UUID']; + aggregateOperation: AggregateOperations; + color?: Maybe; + description?: Maybe; + filter?: Maybe; + graphType: GraphType; + groupByFieldMetadataIdX: Scalars['UUID']; + groupByFieldMetadataIdY?: Maybe; + omitNullValues?: Maybe; + orderByX: GraphOrderBy; + orderByY?: Maybe; + rangeMax?: Maybe; + rangeMin?: Maybe; + xAxisName?: Maybe; + yAxisName?: Maybe; +}; + export type Billing = { __typename?: 'Billing'; billingUrl?: Maybe; @@ -1227,6 +1246,17 @@ export type FullName = { lastName: Scalars['String']; }; +export type GaugeChartConfiguration = { + __typename?: 'GaugeChartConfiguration'; + aggregateFieldMetadataId: Scalars['UUID']; + aggregateFieldMetadataIdTotal: Scalars['UUID']; + aggregateOperation: AggregateOperations; + aggregateOperationTotal: AggregateOperations; + description?: Maybe; + filter?: Maybe; + graphType: GraphType; +}; + export type GetApiKeyDto = { id: Scalars['UUID']; }; @@ -1260,6 +1290,23 @@ export type GetWebhookDto = { id: Scalars['UUID']; }; +/** Order by options for graph widgets */ +export enum GraphOrderBy { + FIELD_ASC = 'FIELD_ASC', + FIELD_DESC = 'FIELD_DESC', + VALUE_ASC = 'VALUE_ASC', + VALUE_DESC = 'VALUE_DESC' +} + +/** Type of graph widget */ +export enum GraphType { + BAR = 'BAR', + GAUGE = 'GAUGE', + LINE = 'LINE', + NUMBER = 'NUMBER', + PIE = 'PIE' +} + export type GridPosition = { __typename?: 'GridPosition'; column: Scalars['Float']; @@ -1288,6 +1335,11 @@ export enum IdentityProviderType { SAML = 'SAML' } +export type IframeConfiguration = { + __typename?: 'IframeConfiguration'; + url: Scalars['String']; +}; + export type ImapSmtpCaldavConnectionParameters = { __typename?: 'ImapSmtpCaldavConnectionParameters'; CALDAV?: Maybe; @@ -1413,6 +1465,25 @@ export type InvalidatePassword = { success: Scalars['Boolean']; }; +export type LineChartConfiguration = { + __typename?: 'LineChartConfiguration'; + aggregateFieldMetadataId: Scalars['UUID']; + aggregateOperation: AggregateOperations; + color?: Maybe; + description?: Maybe; + filter?: Maybe; + graphType: GraphType; + groupByFieldMetadataIdX: Scalars['UUID']; + groupByFieldMetadataIdY?: Maybe; + omitNullValues?: Maybe; + orderByX: GraphOrderBy; + orderByY?: Maybe; + rangeMax?: Maybe; + rangeMin?: Maybe; + xAxisName?: Maybe; + yAxisName?: Maybe; +}; + export type LinkMetadata = { __typename?: 'LinkMetadata'; label: Scalars['String']; @@ -2387,6 +2458,18 @@ export type MutationVerifyTwoFactorAuthenticationMethodForAuthenticatedUserArgs otp: Scalars['String']; }; +export type NumberChartConfiguration = { + __typename?: 'NumberChartConfiguration'; + aggregateFieldMetadataId: Scalars['UUID']; + aggregateOperation: AggregateOperations; + color?: Maybe; + description?: Maybe; + filter?: Maybe; + format?: Maybe; + graphType: GraphType; + label?: Maybe; +}; + export type Object = { __typename?: 'Object'; createdAt: Scalars['DateTime']; @@ -2585,7 +2668,7 @@ export enum PageLayoutType { export type PageLayoutWidget = { __typename?: 'PageLayoutWidget'; - configuration?: Maybe; + configuration?: Maybe; createdAt: Scalars['DateTime']; deletedAt?: Maybe; gridPosition: GridPosition; @@ -2619,6 +2702,18 @@ export enum PermissionFlagType { WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS' } +export type PieChartConfiguration = { + __typename?: 'PieChartConfiguration'; + aggregateFieldMetadataId: Scalars['UUID']; + aggregateOperation: AggregateOperations; + color?: Maybe; + description?: Maybe; + filter?: Maybe; + graphType: GraphType; + groupByFieldMetadataId: Scalars['UUID']; + orderBy: GraphOrderBy; +}; + export type PlaceDetailsResultDto = { __typename?: 'PlaceDetailsResultDto'; city?: Maybe; @@ -3538,7 +3633,7 @@ export type UpdatePageLayoutWidgetInput = { }; export type UpdatePageLayoutWidgetWithIdInput = { - configuration: Scalars['JSON']; + configuration?: InputMaybe; gridPosition: GridPositionInput; id: Scalars['UUID']; objectMetadataId?: InputMaybe; @@ -3875,6 +3970,8 @@ export type Webhook = { workspaceId: Scalars['UUID']; }; +export type WidgetConfiguration = BarChartConfiguration | GaugeChartConfiguration | IframeConfiguration | LineChartConfiguration | NumberChartConfiguration | PieChartConfiguration; + export enum WidgetType { FIELDS = 'FIELDS', GRAPH = 'GRAPH', @@ -4088,13 +4185,15 @@ export type SearchQueryVariables = Exact<{ export type SearchQuery = { __typename?: 'Query', search: { __typename?: 'SearchResultConnection', edges: Array<{ __typename?: 'SearchResultEdge', cursor: string, node: { __typename?: 'SearchRecord', recordId: any, objectNameSingular: string, label: string, imageUrl?: string | null, tsRankCD: number, tsRank: number } }>, pageInfo: { __typename?: 'SearchResultPageInfo', hasNextPage: boolean, endCursor?: string | null } } }; +export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, xAxisName?: string | null, yAxisName?: string | null, rangeMin?: number | null, rangeMax?: number | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, aggregateOperationTotal: AggregateOperations, aggregateFieldMetadataIdTotal: any, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, orderBy: GraphOrderBy, filter?: any | null } | null }; + export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{ id: Scalars['String']; input: UpdatePageLayoutWithTabsInput; }>; -export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, pageLayoutTabId: any, objectMetadataId?: any | null, configuration?: any | null, createdAt: string, updatedAt: string, gridPosition: { __typename?: 'GridPosition', row: number, column: number, rowSpan: number, columnSpan: number } }> | null }> | null } }; +export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, xAxisName?: string | null, yAxisName?: string | null, rangeMin?: number | null, rangeMax?: number | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, aggregateOperationTotal: AggregateOperations, aggregateFieldMetadataIdTotal: any, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, orderBy: GraphOrderBy, filter?: any | null } | null }> | null }> | null } }; export type OnDbEventSubscriptionVariables = Exact<{ input: OnDbEventInput; @@ -4372,6 +4471,78 @@ export type FindOneCoreViewSortQueryVariables = Exact<{ export type FindOneCoreViewSortQuery = { __typename?: 'Query', getCoreViewSort?: { __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any } | null }; +export const PageLayoutWidgetFragmentFragmentDoc = gql` + fragment PageLayoutWidgetFragment on PageLayoutWidget { + id + title + type + objectMetadataId + createdAt + updatedAt + deletedAt + gridPosition { + column + columnSpan + row + rowSpan + } + configuration { + ... on BarChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + groupByFieldMetadataIdX + orderByX + groupByFieldMetadataIdY + orderByY + omitNullValues + xAxisName + yAxisName + rangeMin + rangeMax + filter + } + ... on LineChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + groupByFieldMetadataIdX + orderByX + groupByFieldMetadataIdY + orderByY + filter + } + ... on PieChartConfiguration { + graphType + groupByFieldMetadataId + aggregateFieldMetadataId + aggregateOperation + orderBy + filter + } + ... on NumberChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + description + filter + } + ... on GaugeChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + aggregateOperationTotal + aggregateFieldMetadataIdTotal + description + filter + } + ... on IframeConfiguration { + url + } + } + pageLayoutTabId +} + `; export const ViewFieldFragmentFragmentDoc = gql` fragment ViewFieldFragment on CoreViewField { id @@ -4539,27 +4710,14 @@ export const UpdatePageLayoutWithTabsAndWidgetsDocument = gql` position pageLayoutId widgets { - id - title - type - pageLayoutTabId - objectMetadataId - gridPosition { - row - column - rowSpan - columnSpan - } - configuration - createdAt - updatedAt + ...PageLayoutWidgetFragment } createdAt updatedAt } } } - `; + ${PageLayoutWidgetFragmentFragmentDoc}`; export type UpdatePageLayoutWithTabsAndWidgetsMutationFn = Apollo.MutationFunction; /** diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx index 921a25701a..914b6a0e34 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx @@ -1,7 +1,7 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget'; -import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets'; +import { GraphType, WidgetType } from '~/generated-metadata/graphql'; import styled from '@emotion/styled'; import { diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx index 3eb1d31933..18b61b2f2a 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx @@ -62,7 +62,10 @@ export const CommandMenuPageLayoutIframeConfig = () => { const [title, setTitle] = useState(editingWidget?.title || ''); - const configUrl = editingWidget?.configuration?.url; + const configUrl = + editingWidget?.configuration && 'url' in editingWidget.configuration + ? editingWidget.configuration.url + : undefined; const [url, setUrl] = useState(isString(configUrl) ? configUrl : ''); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx index 8aec131da2..8116cb677a 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx @@ -1,12 +1,12 @@ import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu'; import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; -import { WidgetType } from '@/page-layout/mocks/mockWidgets'; import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import styled from '@emotion/styled'; import { IconChartPie, IconFrame, IconList } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; +import { WidgetType } from '~/generated-metadata/graphql'; const StyledContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/dashboards/components/DashboardContentRenderer.tsx b/packages/twenty-front/src/modules/dashboards/components/DashboardContentRenderer.tsx index ba70264b1a..4c337acb15 100644 --- a/packages/twenty-front/src/modules/dashboards/components/DashboardContentRenderer.tsx +++ b/packages/twenty-front/src/modules/dashboards/components/DashboardContentRenderer.tsx @@ -1,7 +1,7 @@ import { type Dashboard } from '@/dashboards/components/types/Dashboard'; import { useSetIsDashboardInEditMode } from '@/dashboards/hooks/useSetDashboardInEditMode'; import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer'; -import { type PageLayout } from '@/page-layout/types/pageLayoutTypes'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; import { isPageLayoutEmpty } from '@/page-layout/utils/isPageLayoutEmpty'; type DashboardContentRendererProps = { diff --git a/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutWidgetFragment.ts b/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutWidgetFragment.ts index 5a0a7c976d..deb139d212 100644 --- a/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutWidgetFragment.ts +++ b/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutWidgetFragment.ts @@ -1,18 +1 @@ -import { gql } from '@apollo/client'; - -export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` - fragment PageLayoutWidgetFragment on PageLayoutWidget { - id - title - type - objectMetadataId - gridPosition { - column - columnSpan - row - rowSpan - } - configuration - pageLayoutTabId - } -`; +export { PAGE_LAYOUT_WIDGET_FRAGMENT } from '@/page-layout/graphql/fragments/pageLayoutWidgetFragment'; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx index 4787ede016..ccc98aefa9 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx @@ -14,7 +14,6 @@ import { pageLayoutCurrentBreakpointComponentState } from '@/page-layout/states/ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceholder'; import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer'; -import { type Widget } from '@/page-layout/widgets/types/Widget'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; @@ -84,7 +83,7 @@ export const PageLayoutGridLayout = () => { return null; } - const activeTabWidgets = currentPageLayout?.tabs.find( + const activeTabWidgets = currentPageLayout.tabs.find( (tab) => tab.id === activeTabId, )?.widgets; @@ -141,7 +140,7 @@ export const PageLayoutGridLayout = () => { ) : ( activeTabWidgets?.map((widget) => (
- +
)) )} diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx index 20fa70141e..be76b74be9 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx @@ -2,8 +2,9 @@ import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLa import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; -import { type PageLayout } from '@/page-layout/types/pageLayoutTypes'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts'; +import { transformPageLayout } from '@/page-layout/utils/transformPageLayout'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useQuery } from '@apollo/client'; @@ -29,7 +30,9 @@ export const PageLayoutInitializationQueryEffect = ({ }, }); - const pageLayout: PageLayout | undefined = data?.getPageLayout; + const pageLayout: PageLayout | undefined = data?.getPageLayout + ? transformPageLayout(data.getPageLayout) + : undefined; const pageLayoutPersistedComponentCallbackState = useRecoilComponentCallbackState(pageLayoutPersistedComponentState); diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx index 55b5753c2e..aa2c1d7c3d 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx @@ -2,7 +2,7 @@ import { PageLayoutInitializationQueryEffect } from '@/page-layout/components/Pa import { PageLayoutRendererContent } from '@/page-layout/components/PageLayoutRendererContent'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; -import { type PageLayout } from '@/page-layout/types/pageLayoutTypes'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId'; import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext'; import 'react-grid-layout/css/styles.css'; diff --git a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx index 9aaae30b99..3993b83b8c 100644 --- a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx @@ -4,8 +4,13 @@ import { expect, within } from '@storybook/test'; import { MemoryRouter } from 'react-router-dom'; import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLayout'; +import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer'; -import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets'; +import { + GraphOrderBy, + GraphType, + WidgetType, +} from '~/generated-metadata/graphql'; import { RecoilRoot } from 'recoil'; import { PageLayoutType, type PageLayoutWidget } from '~/generated/graphql'; @@ -53,7 +58,10 @@ const mixedGraphsPageLayoutMocks = { columnSpan: 3, }, configuration: { + __typename: 'NumberChartConfiguration', graphType: GraphType.NUMBER, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', }, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', @@ -74,7 +82,12 @@ const mixedGraphsPageLayoutMocks = { columnSpan: 3, }, configuration: { + __typename: 'GaugeChartConfiguration', graphType: GraphType.GAUGE, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', + aggregateOperationTotal: AggregateOperations.COUNT, + aggregateFieldMetadataIdTotal: 'id', }, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', @@ -95,7 +108,12 @@ const mixedGraphsPageLayoutMocks = { columnSpan: 3, }, configuration: { + __typename: 'PieChartConfiguration', graphType: GraphType.PIE, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', + groupByFieldMetadataId: 'createdAt', + orderBy: GraphOrderBy.VALUE_DESC, }, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', @@ -116,7 +134,12 @@ const mixedGraphsPageLayoutMocks = { columnSpan: 6, }, configuration: { + __typename: 'BarChartConfiguration', graphType: GraphType.BAR, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', + groupByFieldMetadataIdX: 'createdAt', + orderByX: GraphOrderBy.FIELD_ASC, }, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', diff --git a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts new file mode 100644 index 0000000000..a87258a9ad --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts @@ -0,0 +1,74 @@ +import { gql } from '@apollo/client'; + +export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` + fragment PageLayoutWidgetFragment on PageLayoutWidget { + id + title + type + objectMetadataId + createdAt + updatedAt + deletedAt + gridPosition { + column + columnSpan + row + rowSpan + } + configuration { + ... on BarChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + groupByFieldMetadataIdX + orderByX + groupByFieldMetadataIdY + orderByY + omitNullValues + xAxisName + yAxisName + rangeMin + rangeMax + filter + } + ... on LineChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + groupByFieldMetadataIdX + orderByX + groupByFieldMetadataIdY + orderByY + filter + } + ... on PieChartConfiguration { + graphType + groupByFieldMetadataId + aggregateFieldMetadataId + aggregateOperation + orderBy + filter + } + ... on NumberChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + description + filter + } + ... on GaugeChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + aggregateOperationTotal + aggregateFieldMetadataIdTotal + description + filter + } + ... on IframeConfiguration { + url + } + } + pageLayoutTabId + } +`; diff --git a/packages/twenty-front/src/modules/page-layout/graphql/mutations/updatePageLayoutWithTabsAndWidgets.ts b/packages/twenty-front/src/modules/page-layout/graphql/mutations/updatePageLayoutWithTabsAndWidgets.ts index d9203b9410..e69ad74375 100644 --- a/packages/twenty-front/src/modules/page-layout/graphql/mutations/updatePageLayoutWithTabsAndWidgets.ts +++ b/packages/twenty-front/src/modules/page-layout/graphql/mutations/updatePageLayoutWithTabsAndWidgets.ts @@ -1,6 +1,9 @@ import { gql } from '@apollo/client'; +import { PAGE_LAYOUT_WIDGET_FRAGMENT } from '../fragments/pageLayoutWidgetFragment'; + export const UPDATE_PAGE_LAYOUT_WITH_TABS_AND_WIDGETS = gql` + ${PAGE_LAYOUT_WIDGET_FRAGMENT} mutation UpdatePageLayoutWithTabsAndWidgets( $id: String! $input: UpdatePageLayoutWithTabsInput! @@ -19,20 +22,7 @@ export const UPDATE_PAGE_LAYOUT_WITH_TABS_AND_WIDGETS = gql` position pageLayoutId widgets { - id - title - type - pageLayoutTabId - objectMetadataId - gridPosition { - row - column - rowSpan - columnSpan - } - configuration - createdAt - updatedAt + ...PageLayoutWidgetFragment } createdAt updatedAt diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx index 14a65baea4..c89e4d149a 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx @@ -1,7 +1,7 @@ import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget'; -import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { GraphType, WidgetType } from '~/generated-metadata/graphql'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; @@ -180,7 +180,11 @@ describe('useCreatePageLayoutGraphWidget', () => { const widget = result.current.allWidgets[index]; expect(widget.type).toBe(WidgetType.GRAPH); expect(widget.pageLayoutTabId).toBe('tab-1'); - expect(widget.configuration.graphType).toBe(graphType); + expect( + widget.configuration && 'graphType' in widget.configuration + ? widget.configuration.graphType + : null, + ).toBe(graphType); expect(widget.id).toBe('mock-uuid'); }); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx index 47ab4094f1..9209e36007 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx @@ -1,4 +1,9 @@ -import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets'; +import { + GraphOrderBy, + GraphType, + WidgetType, +} from '~/generated-metadata/graphql'; +import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { act, renderHook } from '@testing-library/react'; import { PageLayoutType } from '~/generated/graphql'; import { usePageLayoutDraftState } from '../usePageLayoutDraftState'; @@ -95,7 +100,13 @@ describe('usePageLayoutDraftState', () => { title: 'New Widget', type: WidgetType.GRAPH, gridPosition: { row: 2, column: 2, rowSpan: 2, columnSpan: 2 }, - configuration: { graphType: GraphType.BAR }, + configuration: { + graphType: GraphType.BAR, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', + groupByFieldMetadataIdX: 'createdAt', + orderByX: GraphOrderBy.FIELD_ASC, + }, objectMetadataId: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts index f4a345febb..fe727c733f 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts @@ -1,9 +1,9 @@ -import { type GraphType } from '@/page-layout/mocks/mockWidgets'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState'; import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; +import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget'; import { getWidgetSize, getWidgetTitle, @@ -11,12 +11,13 @@ import { import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition'; import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId'; import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts'; +import { type GraphType } from '~/generated-metadata/graphql'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; -import { type PageLayoutWidget, type WidgetType } from '~/generated/graphql'; +import { type WidgetType } from '~/generated/graphql'; export const useCreatePageLayoutGraphWidget = ( pageLayoutIdFromProps?: string, @@ -72,7 +73,10 @@ export const useCreatePageLayoutGraphWidget = ( const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets); const existingWidgetCount = allWidgets.filter( (w) => - w.type === widgetType && w.configuration.graphType === graphType, + w.type === widgetType && + w.configuration && + 'graphType' in w.configuration && + w.configuration.graphType === graphType, ).length; const title = getWidgetTitle(graphType, existingWidgetCount); const widgetId = uuidv4(); @@ -83,25 +87,18 @@ export const useCreatePageLayoutGraphWidget = ( defaultSize, ); - const newWidget: PageLayoutWidget = { - id: widgetId, - pageLayoutTabId: activeTabId, + const newWidget = createDefaultGraphWidget( + widgetId, + activeTabId, title, - type: widgetType, - gridPosition: { + graphType, + { row: position.y, column: position.x, rowSpan: position.h, columnSpan: position.w, }, - configuration: { - graphType, - }, - objectMetadataId: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - deletedAt: null, - }; + ); const newLayout = { i: widgetId, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts index 3b1e1f6c3a..777bdfd5c5 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts @@ -3,6 +3,7 @@ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pag import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState'; import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; +import { createDefaultIframeWidget } from '@/page-layout/utils/createDefaultIframeWidget'; import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition'; import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId'; import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts'; @@ -12,7 +13,6 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component- import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useRecoilCallback } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; -import { type PageLayoutWidget, WidgetType } from '~/generated/graphql'; export const useCreatePageLayoutIframeWidget = ( pageLayoutIdFromProps?: string, @@ -64,25 +64,18 @@ export const useCreatePageLayoutIframeWidget = ( defaultSize, ); - const newWidget: PageLayoutWidget = { - id: widgetId, - pageLayoutTabId: activeTabId, + const newWidget = createDefaultIframeWidget( + widgetId, + activeTabId, title, - type: WidgetType.IFRAME, - gridPosition: { + url, + { row: position.y, column: position.x, rowSpan: position.h, columnSpan: position.w, }, - configuration: { - url, - }, - objectMetadataId: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - deletedAt: null, - }; + ); const newLayout = { i: widgetId, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutTab.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutTab.ts index 6b9784ebb5..c9e20452f5 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutTab.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutTab.ts @@ -5,7 +5,7 @@ import { useRecoilCallback } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; import { pageLayoutCurrentLayoutsComponentState } from '../states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '../states/pageLayoutDraftComponentState'; -import { type PageLayoutTab } from '../types/pageLayoutTypes'; +import { type PageLayoutTab } from '../types/PageLayoutTab'; import { getEmptyTabLayout } from '../utils/getEmptyTabLayout'; export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => { diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutHandleLayoutChange.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutHandleLayoutChange.ts index 15655a8ce1..13ccec298d 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutHandleLayoutChange.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutHandleLayoutChange.ts @@ -71,22 +71,9 @@ export const usePageLayoutHandleLayoutChange = ( ...prev, tabs: prev.tabs.map((tab) => { if (tab.id === activeTabId) { - const tabWidgets = updatedWidgets - .filter((w) => w.pageLayoutTabId === activeTabId) - .map((widget) => ({ - id: widget.id, - pageLayoutTabId: widget.pageLayoutTabId || activeTabId, - title: widget.title, - type: widget.type, - objectMetadataId: null, - gridPosition: widget.gridPosition, - configuration: widget.configuration || undefined, - createdAt: - tab.widgets.find((w) => w.id === widget.id)?.createdAt || - new Date().toISOString(), - updatedAt: new Date().toISOString(), - deletedAt: null, - })); + const tabWidgets = updatedWidgets.filter( + (w) => w.pageLayoutTabId === activeTabId, + ); return { ...tab, widgets: tabWidgets, 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 79a1a1afb1..03822dc5cd 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts @@ -2,9 +2,10 @@ import { usePageLayoutDraftState } from '@/page-layout/hooks/usePageLayoutDraftS import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; -import { type PageLayout } from '@/page-layout/types/pageLayoutTypes'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput'; import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts'; +import { transformPageLayout } from '@/page-layout/utils/transformPageLayout'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; @@ -48,35 +49,8 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => { const updatedPageLayout = data?.updatePageLayoutWithTabsAndWidgets; if (isDefined(updatedPageLayout)) { - const pageLayoutToPersist: PageLayout = { - id: updatedPageLayout.id, - name: updatedPageLayout.name, - type: updatedPageLayout.type, - objectMetadataId: updatedPageLayout.objectMetadataId, - tabs: - updatedPageLayout.tabs?.map((tab) => ({ - id: tab.id, - title: tab.title, - position: tab.position, - pageLayoutId: tab.pageLayoutId, - createdAt: tab.createdAt, - updatedAt: tab.updatedAt, - widgets: - tab.widgets?.map((widget) => ({ - id: widget.id, - title: widget.title, - type: widget.type, - pageLayoutTabId: widget.pageLayoutTabId, - objectMetadataId: widget.objectMetadataId, - configuration: widget.configuration, - gridPosition: widget.gridPosition, - createdAt: widget.createdAt, - updatedAt: widget.updatedAt, - })) ?? [], - })) ?? [], - createdAt: updatedPageLayout.createdAt, - updatedAt: updatedPageLayout.updatedAt, - }; + const pageLayoutToPersist: PageLayout = + transformPageLayout(updatedPageLayout); set(pageLayoutPersistedCallbackState, pageLayoutToPersist); set( diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts index c863350c67..4f9750bcfc 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts @@ -1,9 +1,9 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { type PageLayoutWidget } from '~/generated/graphql'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; -import { type PageLayoutWidget } from '~/generated/graphql'; export const useUpdatePageLayoutWidget = (pageLayoutIdFromProps?: string) => { const pageLayoutId = useAvailableComponentInstanceIdOrThrow( diff --git a/packages/twenty-front/src/modules/page-layout/mocks/mockWidgets.ts b/packages/twenty-front/src/modules/page-layout/mocks/mockWidgets.ts deleted file mode 100644 index 05d0cd3af3..0000000000 --- a/packages/twenty-front/src/modules/page-layout/mocks/mockWidgets.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { type Layouts } from 'react-grid-layout'; -import { type PageLayoutWidget } from '~/generated/graphql'; - -export enum WidgetType { - VIEW = 'VIEW', - IFRAME = 'IFRAME', - FIELDS = 'FIELDS', - GRAPH = 'GRAPH', -} - -export enum GraphType { - NUMBER = 'NUMBER', - GAUGE = 'GAUGE', - PIE = 'PIE', - BAR = 'BAR', - LINE = 'LINE', -} - -export const mockPageLayoutWidgets: PageLayoutWidget[] = [ - { - id: 'widget-1', - pageLayoutTabId: 'tab-overview', - type: WidgetType.GRAPH, - title: 'Sales Pipeline', - objectMetadataId: null, - gridPosition: { - row: 0, - column: 0, - rowSpan: 2, - columnSpan: 3, - }, - configuration: { - graphType: GraphType.NUMBER, - }, - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - deletedAt: null, - }, - { - id: 'widget-2', - pageLayoutTabId: 'tab-overview', - type: WidgetType.GRAPH, - title: 'Conversion Rate', - objectMetadataId: null, - gridPosition: { - row: 0, - column: 6, - rowSpan: 5, - columnSpan: 3, - }, - configuration: { - graphType: GraphType.GAUGE, - }, - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - deletedAt: null, - }, - { - id: 'widget-3', - pageLayoutTabId: 'tab-analytics', - type: WidgetType.GRAPH, - title: 'Lead Distribution', - objectMetadataId: null, - gridPosition: { - row: 2, - column: 0, - rowSpan: 5, - columnSpan: 6, - }, - configuration: { - graphType: GraphType.PIE, - }, - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - deletedAt: null, - }, - { - id: 'widget-4', - pageLayoutTabId: 'tab-reports', - type: WidgetType.GRAPH, - title: 'Monthly Performance', - objectMetadataId: null, - gridPosition: { - row: 0, - column: 9, - rowSpan: 8, - columnSpan: 4, - }, - configuration: { - graphType: GraphType.BAR, - }, - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - deletedAt: null, - }, -]; - -export const mockLayouts: Layouts = { - desktop: [ - { - i: 'widget-1', - x: 0, - y: 0, - w: 3, - h: 2, - }, - { - i: 'widget-2', - x: 6, - y: 0, - w: 3, - h: 5, - }, - { - i: 'widget-3', - x: 0, - y: 2, - w: 6, - h: 5, - }, - { - i: 'widget-4', - x: 9, - y: 0, - w: 4, - h: 8, - }, - ], - mobile: [ - { - i: 'widget-1', - x: 0, - y: 0, - w: 1, - h: 2, - }, - { - i: 'widget-2', - x: 0, - y: 2, - w: 1, - h: 5, - }, - { - i: 'widget-3', - x: 0, - y: 7, - w: 1, - h: 5, - }, - { - i: 'widget-4', - x: 0, - y: 12, - w: 1, - h: 5, - }, - ], -}; diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutPersistedComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutPersistedComponentState.ts index 9ade31fcef..408683f43a 100644 --- a/packages/twenty-front/src/modules/page-layout/states/pageLayoutPersistedComponentState.ts +++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutPersistedComponentState.ts @@ -1,6 +1,6 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -import { type PageLayout } from '../types/pageLayoutTypes'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; export const pageLayoutPersistedComponentState = createComponentState< diff --git a/packages/twenty-front/src/modules/page-layout/states/savedPageLayoutsComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/savedPageLayoutsComponentState.ts index 6215bda47f..1f9a566d1f 100644 --- a/packages/twenty-front/src/modules/page-layout/states/savedPageLayoutsComponentState.ts +++ b/packages/twenty-front/src/modules/page-layout/states/savedPageLayoutsComponentState.ts @@ -1,5 +1,5 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -import { type PageLayout } from '../types/pageLayoutTypes'; +import { type PageLayout } from '../types/PageLayout'; import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; diff --git a/packages/twenty-front/src/modules/page-layout/types/PageLayout.ts b/packages/twenty-front/src/modules/page-layout/types/PageLayout.ts new file mode 100644 index 0000000000..017aacf957 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/types/PageLayout.ts @@ -0,0 +1,6 @@ +import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; +import { type PageLayout as PageLayoutGenerated } from '~/generated/graphql'; + +export type PageLayout = Omit & { + tabs: PageLayoutTab[]; +}; diff --git a/packages/twenty-front/src/modules/page-layout/types/pageLayoutTypes.ts b/packages/twenty-front/src/modules/page-layout/types/PageLayoutTab.ts similarity index 62% rename from packages/twenty-front/src/modules/page-layout/types/pageLayoutTypes.ts rename to packages/twenty-front/src/modules/page-layout/types/PageLayoutTab.ts index 1c0466e07c..d279c70d87 100644 --- a/packages/twenty-front/src/modules/page-layout/types/pageLayoutTypes.ts +++ b/packages/twenty-front/src/modules/page-layout/types/PageLayoutTab.ts @@ -1,5 +1,4 @@ import { - type PageLayout as PageLayoutGenerated, type PageLayoutTab as PageLayoutTabGenerated, type PageLayoutWidget, } from '~/generated/graphql'; @@ -7,7 +6,3 @@ import { export type PageLayoutTab = Omit & { widgets: PageLayoutWidget[]; }; - -export type PageLayout = Omit & { - tabs: PageLayoutTab[]; -}; diff --git a/packages/twenty-front/src/modules/page-layout/types/draft-page-layout.ts b/packages/twenty-front/src/modules/page-layout/types/draft-page-layout.ts index 6750fa48d8..d957f6855a 100644 --- a/packages/twenty-front/src/modules/page-layout/types/draft-page-layout.ts +++ b/packages/twenty-front/src/modules/page-layout/types/draft-page-layout.ts @@ -1,4 +1,4 @@ -import { type PageLayout } from './pageLayoutTypes'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; export type DraftPageLayout = Omit< PageLayout, diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/addWidgetToTab.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/addWidgetToTab.test.ts index c59c3f9f06..232a521667 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/addWidgetToTab.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/addWidgetToTab.test.ts @@ -1,14 +1,21 @@ +import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; +import { GraphType, WidgetType } from '~/generated-metadata/graphql'; +import { type PageLayoutTab } from '../../types/PageLayoutTab'; import { type PageLayoutWidget } from '~/generated/graphql'; -import { WidgetType } from '../../mocks/mockWidgets'; -import { type PageLayoutTab } from '../../types/pageLayoutTypes'; import { addWidgetToTab } from '../addWidgetToTab'; describe('addWidgetToTab', () => { const mockWidget: PageLayoutWidget = { + __typename: 'PageLayoutWidget', id: 'widget-1', pageLayoutTabId: 'tab-1', title: 'Test Widget', type: WidgetType.GRAPH, + configuration: { + graphType: GraphType.NUMBER, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', + }, gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 }, objectMetadataId: null, createdAt: '2024-01-01T00:00:00Z', diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts index 6cdcc23d68..bb5be3e45e 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts @@ -1,5 +1,10 @@ -import { type PageLayoutWidget } from '~/generated/graphql'; -import { GraphType, WidgetType } from '../../mocks/mockWidgets'; +import { + GraphOrderBy, + GraphType, + WidgetType, + type PageLayoutWidget, +} from '~/generated-metadata/graphql'; +import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { convertLayoutsToWidgets } from '../convertLayoutsToWidgets'; describe('convertLayoutsToWidgets', () => { @@ -18,6 +23,8 @@ describe('convertLayoutsToWidgets', () => { }, configuration: { graphType: GraphType.NUMBER, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', }, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', @@ -37,6 +44,10 @@ describe('convertLayoutsToWidgets', () => { }, configuration: { graphType: GraphType.PIE, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', + groupByFieldMetadataId: 'status', + orderBy: GraphOrderBy.VALUE_DESC, }, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts index beb22ff48f..b2c5673e1b 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts @@ -1,6 +1,12 @@ -import { type PageLayout } from '@/page-layout/types/pageLayoutTypes'; +import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts'; -import { PageLayoutType, WidgetType } from '~/generated/graphql'; +import { + GraphOrderBy, + GraphType, + PageLayoutType, + WidgetType, +} from '~/generated-metadata/graphql'; describe('convertPageLayoutToTabLayouts', () => { it('should convert page layout to tab layouts', () => { @@ -17,10 +23,16 @@ describe('convertPageLayoutToTabLayouts', () => { pageLayoutId: 'page-layout-1', widgets: [ { + __typename: 'PageLayoutWidget', id: 'widget-1', pageLayoutTabId: 'tab-1', title: 'Widget 1', type: WidgetType.GRAPH, + configuration: { + graphType: GraphType.NUMBER, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', + }, gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 }, objectMetadataId: 'object-metadata-1', createdAt: '2025-01-01T00:00:00.000Z', @@ -28,10 +40,18 @@ describe('convertPageLayoutToTabLayouts', () => { deletedAt: null, }, { + __typename: 'PageLayoutWidget', id: 'widget-2', pageLayoutTabId: 'tab-1', title: 'Widget 2', type: WidgetType.GRAPH, + configuration: { + graphType: GraphType.PIE, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', + groupByFieldMetadataId: 'status', + orderBy: GraphOrderBy.VALUE_DESC, + }, gridPosition: { row: 2, column: 0, rowSpan: 2, columnSpan: 2 }, objectMetadataId: 'object-metadata-1', createdAt: '2025-01-01T00:00:00.000Z', diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetFromTab.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetFromTab.test.ts index 5e3a9f6462..20669c9240 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetFromTab.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetFromTab.test.ts @@ -1,5 +1,10 @@ -import { WidgetType } from '../../mocks/mockWidgets'; -import { type PageLayoutTab } from '../../types/pageLayoutTypes'; +import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; +import { + GraphOrderBy, + GraphType, + WidgetType, +} from '~/generated-metadata/graphql'; +import { type PageLayoutTab } from '../../types/PageLayoutTab'; import { removeWidgetFromTab } from '../removeWidgetFromTab'; describe('removeWidgetFromTab', () => { @@ -11,10 +16,16 @@ describe('removeWidgetFromTab', () => { pageLayoutId: 'layout-1', widgets: [ { + __typename: 'PageLayoutWidget' as const, id: 'widget-1', pageLayoutTabId: 'tab-1', title: 'Widget 1', type: WidgetType.GRAPH, + configuration: { + graphType: GraphType.NUMBER, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', + }, gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 }, objectMetadataId: null, createdAt: '2024-01-01T00:00:00Z', @@ -22,10 +33,18 @@ describe('removeWidgetFromTab', () => { deletedAt: null, }, { + __typename: 'PageLayoutWidget' as const, id: 'widget-2', pageLayoutTabId: 'tab-1', title: 'Widget 2', type: WidgetType.GRAPH, + configuration: { + graphType: GraphType.PIE, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: 'id', + groupByFieldMetadataId: 'status', + orderBy: GraphOrderBy.VALUE_DESC, + }, gridPosition: { row: 2, column: 0, rowSpan: 2, columnSpan: 2 }, objectMetadataId: null, createdAt: '2024-01-01T00:00:00Z', @@ -44,10 +63,14 @@ describe('removeWidgetFromTab', () => { pageLayoutId: 'layout-1', widgets: [ { + __typename: 'PageLayoutWidget' as const, id: 'widget-3', pageLayoutTabId: 'tab-2', title: 'Widget 3', type: WidgetType.IFRAME, + configuration: { + url: 'https://example.com', + }, gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 }, objectMetadataId: null, createdAt: '2024-01-01T00:00:00Z', diff --git a/packages/twenty-front/src/modules/page-layout/utils/addWidgetToTab.ts b/packages/twenty-front/src/modules/page-layout/utils/addWidgetToTab.ts index 415319751f..f2236527bb 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/addWidgetToTab.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/addWidgetToTab.ts @@ -1,4 +1,4 @@ -import { type PageLayoutTab } from '@/page-layout/types/pageLayoutTypes'; +import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; import { type PageLayoutWidget } from '~/generated/graphql'; export const addWidgetToTab = ( diff --git a/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts b/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts index b63c727962..ddd053b04b 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts @@ -1,5 +1,5 @@ -import { type Layouts } from 'react-grid-layout'; import { type PageLayoutWidget } from '~/generated/graphql'; +import { type Layouts } from 'react-grid-layout'; export const convertLayoutsToWidgets = ( widgets: PageLayoutWidget[], diff --git a/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts b/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts index af59d7e5fd..41c14469af 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts @@ -1,4 +1,4 @@ -import { type PageLayout } from '@/page-layout/types/pageLayoutTypes'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; import { type TabLayouts } from '@/page-layout/types/tab-layouts'; export const convertPageLayoutToTabLayouts = ( diff --git a/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts new file mode 100644 index 0000000000..a12ee541b9 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts @@ -0,0 +1,91 @@ +import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; +import { v4 as uuidv4 } from 'uuid'; +import { GraphOrderBy, GraphType } from '~/generated-metadata/graphql'; +import { + type GridPosition, + type PageLayoutWidget, + type WidgetConfiguration, + WidgetType, +} from '~/generated/graphql'; + +const createDefaultGraphConfiguration = ( + graphType: GraphType, +): WidgetConfiguration => { + const placeholderFieldId1 = uuidv4(); + const placeholderFieldId2 = uuidv4(); + + switch (graphType) { + case GraphType.NUMBER: + return { + graphType: GraphType.NUMBER, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: placeholderFieldId1, + }; + + case GraphType.PIE: + return { + graphType: GraphType.PIE, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: placeholderFieldId1, + groupByFieldMetadataId: placeholderFieldId2, + orderBy: GraphOrderBy.VALUE_DESC, + }; + + case GraphType.BAR: + return { + graphType: GraphType.BAR, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: placeholderFieldId1, + groupByFieldMetadataIdX: placeholderFieldId2, + orderByX: GraphOrderBy.FIELD_ASC, + }; + + case GraphType.LINE: + return { + graphType: GraphType.LINE, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: placeholderFieldId1, + groupByFieldMetadataIdX: placeholderFieldId2, + orderByX: GraphOrderBy.FIELD_ASC, + }; + + case GraphType.GAUGE: + return { + graphType: GraphType.GAUGE, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: placeholderFieldId1, + aggregateOperationTotal: AggregateOperations.COUNT, + aggregateFieldMetadataIdTotal: placeholderFieldId2, + }; + + default: + return { + graphType: GraphType.NUMBER, + aggregateOperation: AggregateOperations.COUNT, + aggregateFieldMetadataId: placeholderFieldId1, + }; + } +}; + +export const createDefaultGraphWidget = ( + id: string, + pageLayoutTabId: string, + title: string, + graphType: GraphType, + gridPosition: GridPosition, + objectMetadataId?: string | null, +): PageLayoutWidget => { + return { + __typename: 'PageLayoutWidget', + id, + pageLayoutTabId, + title, + type: WidgetType.GRAPH, + configuration: createDefaultGraphConfiguration(graphType), + gridPosition, + objectMetadataId: objectMetadataId ?? null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + deletedAt: null, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/createDefaultIframeWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/createDefaultIframeWidget.ts new file mode 100644 index 0000000000..6831b628e8 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/createDefaultIframeWidget.ts @@ -0,0 +1,30 @@ +import { + type GridPosition, + type PageLayoutWidget, + WidgetType, +} from '~/generated/graphql'; + +export const createDefaultIframeWidget = ( + id: string, + pageLayoutTabId: string, + title: string, + url: string, + gridPosition: GridPosition, + objectMetadataId?: string | null, +): PageLayoutWidget => { + return { + __typename: 'PageLayoutWidget', + id, + pageLayoutTabId, + title, + type: WidgetType.IFRAME, + configuration: { + url, + }, + gridPosition, + objectMetadataId: objectMetadataId ?? null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + deletedAt: null, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts b/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts index 95c795c45b..203a3d37e9 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts @@ -1,4 +1,4 @@ -import { GraphType } from '../mocks/mockWidgets'; +import { GraphType } from '~/generated-metadata/graphql'; export const getDefaultWidgetData = (graphType: GraphType) => { switch (graphType) { diff --git a/packages/twenty-front/src/modules/page-layout/utils/isPageLayoutEmpty.ts b/packages/twenty-front/src/modules/page-layout/utils/isPageLayoutEmpty.ts index f8108532a0..69ec106d84 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/isPageLayoutEmpty.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/isPageLayoutEmpty.ts @@ -1,4 +1,4 @@ -import { type PageLayout } from '~/modules/page-layout/types/pageLayoutTypes'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; export const isPageLayoutEmpty = (pageLayout: PageLayout): boolean => { return ( diff --git a/packages/twenty-front/src/modules/page-layout/utils/removeWidgetFromTab.ts b/packages/twenty-front/src/modules/page-layout/utils/removeWidgetFromTab.ts index 2efad46735..f3401944b8 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/removeWidgetFromTab.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/removeWidgetFromTab.ts @@ -1,4 +1,4 @@ -import { type PageLayoutTab } from '../types/pageLayoutTypes'; +import { type PageLayoutTab } from '../types/PageLayoutTab'; export const removeWidgetFromTab = ( tabs: PageLayoutTab[], diff --git a/packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts b/packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts new file mode 100644 index 0000000000..9e2a70eee6 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts @@ -0,0 +1,17 @@ +import { type PageLayout } from '@/page-layout/types/PageLayout'; +import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; +import { type PageLayout as PageLayoutGenerated } from '~/generated/graphql'; + +export const transformPageLayout = ( + pageLayout: PageLayoutGenerated, +): PageLayout => { + return { + ...pageLayout, + tabs: (pageLayout.tabs ?? []).map( + (tab): PageLayoutTab => ({ + ...tab, + widgets: tab.widgets ?? [], + }), + ), + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContainer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContainer.tsx index 546b8703d3..5d7385b615 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContainer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContainer.tsx @@ -1,8 +1,13 @@ +import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import styled from '@emotion/styled'; import { type ReactNode } from 'react'; import { isDefined } from 'twenty-shared/utils'; -const StyledContainer = styled.div<{ onClick?: () => void }>` +const StyledContainer = styled.div<{ + onClick?: () => void; + showHover?: boolean; +}>` background: ${({ theme }) => theme.background.secondary}; border: 1px solid ${({ theme }) => theme.border.color.medium}; border-radius: ${({ theme }) => theme.border.radius.md}; @@ -14,21 +19,36 @@ const StyledContainer = styled.div<{ onClick?: () => void }>` position: relative; padding: ${({ theme }) => theme.spacing(4)}; - &:hover { - cursor: ${({ onClick }) => (isDefined(onClick) ? 'pointer' : 'default')}; - border: 1px solid ${({ theme }) => theme.color.blue}; - background: ${({ theme }) => theme.background.primary}; - } + ${({ showHover, onClick, theme }) => + showHover && + ` + &:hover { + cursor: ${isDefined(onClick) ? 'pointer' : 'default'}; + border: 1px solid ${theme.color.blue}; + background: ${theme.background.primary}; + } + `} `; type WidgetContainerProps = { children?: ReactNode; onClick?: () => void; + isRestricted?: boolean; }; export const WidgetContainer = ({ children, onClick, + isRestricted = false, }: WidgetContainerProps) => { - return {children}; + const isPageLayoutInEditModeComponent = useRecoilComponentValue( + isPageLayoutInEditModeComponentState, + ); + const showHover = !isRestricted || isPageLayoutInEditModeComponent; + + return ( + + {children} + + ); }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx index 4e48fe5304..86ae9aa875 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx @@ -1,11 +1,9 @@ import { GraphWidgetRenderer } from '@/page-layout/widgets/graph/components/GraphWidgetRenderer'; import { IframeWidget } from '@/page-layout/widgets/iframe/components/IframeWidget'; -import { type Widget } from '@/page-layout/widgets/types/Widget'; -import { isString } from '@sniptt/guards'; -import { WidgetType } from '~/generated/graphql'; +import { type PageLayoutWidget, WidgetType } from '~/generated/graphql'; type WidgetContentRendererProps = { - widget: Widget; + widget: PageLayoutWidget; }; export const WidgetContentRenderer = ({ @@ -15,12 +13,9 @@ export const WidgetContentRenderer = ({ case WidgetType.GRAPH: return ; - case WidgetType.IFRAME: { - const url = widget.configuration?.url; - return ( - - ); - } + case WidgetType.IFRAME: + return ; + default: return null; } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx index 07db62e635..b5a5c359f5 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx @@ -1,15 +1,17 @@ +import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay'; import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget'; import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWidget'; import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; import { WidgetContainer } from '@/page-layout/widgets/components/WidgetContainer'; import { WidgetContentRenderer } from '@/page-layout/widgets/components/WidgetContentRenderer'; import { WidgetHeader } from '@/page-layout/widgets/components/WidgetHeader'; -import { type Widget as WidgetType } from '@/page-layout/widgets/types/Widget'; +import { useWidgetObjectPermissions } from '@/page-layout/widgets/hooks/useWidgetObjectPermissions'; +import { type PageLayoutWidget } from '~/generated/graphql'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import styled from '@emotion/styled'; type WidgetRendererProps = { - widget: WidgetType; + widget: PageLayoutWidget; }; const StyledContent = styled.div` @@ -23,13 +25,14 @@ const StyledContent = styled.div` export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { const { deletePageLayoutWidget } = useDeletePageLayoutWidget(); const { handleEditWidget } = useEditPageLayoutWidget(); + const { haveAccessToWidgetsObject } = useWidgetObjectPermissions(widget); const isPageLayoutInEditMode = useRecoilComponentValue( isPageLayoutInEditModeComponentState, ); return ( - + { onRemove={() => deletePageLayoutWidget(widget.id)} /> - + {!haveAccessToWidgetsObject ? ( + + ) : ( + + )} ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetPlaceholder.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetPlaceholder.stories.tsx index afb863b4c6..7ca3242803 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetPlaceholder.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetPlaceholder.stories.tsx @@ -1,3 +1,4 @@ +import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper'; import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceholder'; import { type Meta, type StoryObj } from '@storybook/react'; import { ComponentDecorator } from 'twenty-ui/testing'; @@ -5,7 +6,14 @@ import { ComponentDecorator } from 'twenty-ui/testing'; const meta: Meta = { title: 'Modules/PageLayout/Widgets/WidgetPlaceholder', component: WidgetPlaceholder, - decorators: [ComponentDecorator], + decorators: [ + (Story) => ( + + + + ), + ComponentDecorator, + ], parameters: { layout: 'centered', docs: { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx index 15c9b01c88..c1ee47153c 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx @@ -1,9 +1,9 @@ import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper'; -import { GraphType } from '@/page-layout/mocks/mockWidgets'; +import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget'; import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer'; +import { GraphType } from '~/generated-metadata/graphql'; import { type Meta, type StoryObj } from '@storybook/react'; import { ComponentDecorator } from 'twenty-ui/testing'; -import { WidgetType } from '~/generated/graphql'; const meta: Meta = { title: 'Modules/PageLayout/Widgets/WidgetRenderer', @@ -30,23 +30,18 @@ type Story = StoryObj; export const WithNumberChart: Story = { args: { - widget: { - title: 'Sales Pipeline', - type: WidgetType.GRAPH, - configuration: { - graphType: GraphType.NUMBER, - }, - gridPosition: { + widget: createDefaultGraphWidget( + 'widget-1', + 'tab-overview', + 'Sales Pipeline', + GraphType.NUMBER, + { row: 0, column: 0, rowSpan: 2, columnSpan: 3, }, - id: 'widget-1', - pageLayoutTabId: 'tab-overview', - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - }, + ), }, render: (args) => (
@@ -57,23 +52,18 @@ export const WithNumberChart: Story = { export const WithGaugeChart: Story = { args: { - widget: { - title: 'Conversion Rate', - type: WidgetType.GRAPH, - configuration: { - graphType: GraphType.GAUGE, - }, - createdAt: '2024-01-01T00:00:00Z', - gridPosition: { + widget: createDefaultGraphWidget( + 'widget-1', + 'tab-overview', + 'Conversion Rate', + GraphType.GAUGE, + { row: 0, column: 0, rowSpan: 5, columnSpan: 3, }, - id: 'widget-1', - pageLayoutTabId: 'tab-overview', - updatedAt: '2024-01-01T00:00:00Z', - }, + ), }, render: (args) => (
@@ -84,23 +74,18 @@ export const WithGaugeChart: Story = { export const WithPieChart: Story = { args: { - widget: { - title: 'Lead Distribution', - type: WidgetType.GRAPH, - configuration: { - graphType: GraphType.PIE, - }, - createdAt: '2024-01-01T00:00:00Z', - gridPosition: { + widget: createDefaultGraphWidget( + 'widget-1', + 'tab-overview', + 'Lead Distribution', + GraphType.PIE, + { row: 0, column: 0, rowSpan: 5, columnSpan: 3, }, - id: 'widget-1', - pageLayoutTabId: 'tab-overview', - updatedAt: '2024-01-01T00:00:00Z', - }, + ), }, render: (args) => (
@@ -111,23 +96,18 @@ export const WithPieChart: Story = { export const SmallWidget: Story = { args: { - widget: { - title: 'Small Widget (2x2 grid)', - type: WidgetType.GRAPH, - configuration: { - graphType: GraphType.NUMBER, - }, - createdAt: '2024-01-01T00:00:00Z', - gridPosition: { + widget: createDefaultGraphWidget( + 'widget-1', + 'tab-overview', + 'Small Widget (2x2 grid)', + GraphType.NUMBER, + { row: 0, column: 0, rowSpan: 2, columnSpan: 2, }, - id: 'widget-1', - pageLayoutTabId: 'tab-overview', - updatedAt: '2024-01-01T00:00:00Z', - }, + ), }, parameters: { docs: { @@ -145,23 +125,18 @@ export const SmallWidget: Story = { export const MediumWidget: Story = { args: { - widget: { - title: 'Medium Widget (4x3 grid)', - type: WidgetType.GRAPH, - configuration: { - graphType: GraphType.GAUGE, - }, - createdAt: '2024-01-01T00:00:00Z', - gridPosition: { + widget: createDefaultGraphWidget( + 'widget-1', + 'tab-overview', + 'Medium Widget (4x3 grid)', + GraphType.GAUGE, + { row: 0, column: 0, rowSpan: 3, columnSpan: 4, }, - id: 'widget-1', - pageLayoutTabId: 'tab-overview', - updatedAt: '2024-01-01T00:00:00Z', - }, + ), }, parameters: { docs: { @@ -179,23 +154,18 @@ export const MediumWidget: Story = { export const LargeWidget: Story = { args: { - widget: { - title: 'Large Widget (6x4 grid)', - type: WidgetType.GRAPH, - configuration: { - graphType: GraphType.PIE, - }, - createdAt: '2024-01-01T00:00:00Z', - gridPosition: { + widget: createDefaultGraphWidget( + 'widget-1', + 'tab-overview', + 'Large Widget (6x4 grid)', + GraphType.PIE, + { row: 0, column: 0, rowSpan: 4, columnSpan: 6, }, - id: 'widget-1', - pageLayoutTabId: 'tab-overview', - updatedAt: '2024-01-01T00:00:00Z', - }, + ), }, parameters: { docs: { @@ -213,23 +183,18 @@ export const LargeWidget: Story = { export const WideWidget: Story = { args: { - widget: { - title: 'Wide Widget (8x2 grid)', - type: WidgetType.GRAPH, - configuration: { - graphType: GraphType.NUMBER, - }, - createdAt: '2024-01-01T00:00:00Z', - gridPosition: { + widget: createDefaultGraphWidget( + 'widget-1', + 'tab-overview', + 'Wide Widget (8x2 grid)', + GraphType.NUMBER, + { row: 0, column: 0, rowSpan: 2, columnSpan: 8, }, - id: 'widget-1', - pageLayoutTabId: 'tab-overview', - updatedAt: '2024-01-01T00:00:00Z', - }, + ), }, parameters: { docs: { @@ -247,23 +212,18 @@ export const WideWidget: Story = { export const TallWidget: Story = { args: { - widget: { - title: 'Tall Widget (3x6 grid)', - type: WidgetType.GRAPH, - configuration: { - graphType: GraphType.GAUGE, - }, - createdAt: '2024-01-01T00:00:00Z', - gridPosition: { + widget: createDefaultGraphWidget( + 'widget-1', + 'tab-overview', + 'Tall Widget (3x6 grid)', + GraphType.GAUGE, + { row: 0, column: 0, rowSpan: 6, columnSpan: 3, }, - id: 'widget-1', - pageLayoutTabId: 'tab-overview', - updatedAt: '2024-01-01T00:00:00Z', - }, + ), }, parameters: { docs: { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx index 844192ad39..ef2cedd22e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx @@ -1,9 +1,8 @@ -import { GraphType } from '@/page-layout/mocks/mockWidgets'; import { getDefaultWidgetData } from '@/page-layout/utils/getDefaultWidgetData'; import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; import { GraphWidgetNumberChart } from '@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart'; -import { type GraphWidget } from '@/page-layout/widgets/graph/types/GraphWidget'; import { lazy, Suspense } from 'react'; +import { GraphType, type PageLayoutWidget } from '~/generated-metadata/graphql'; const GraphWidgetBarChart = lazy(() => import( @@ -38,14 +37,22 @@ const GraphWidgetGaugeChart = lazy(() => ); type GraphWidgetRendererProps = { - widget: GraphWidget; + widget: PageLayoutWidget; }; export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => { - const graphType = widget.configuration?.graphType; + if (!widget.configuration || !('graphType' in widget.configuration)) { + throw new Error( + `Invalid configuration for widget ${widget.id}: missing graphType`, + ); + } + + const graphType = widget.configuration.graphType; if (!Object.values(GraphType).includes(graphType)) { - return null; + throw new Error( + `Unsupported graph type ${graphType} for widget ${widget.id}`, + ); } const data: any = getDefaultWidgetData(graphType); @@ -54,7 +61,7 @@ export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => { return null; } - switch (graphType as GraphType) { + switch (graphType) { case GraphType.NUMBER: return ( { + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + + if (!isDefined(widget.objectMetadataId)) { + return { + haveAccessToWidgetsObject: true, + }; + } + + const objectPermissions = getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + widget.objectMetadataId, + ); + + return { + haveAccessToWidgetsObject: objectPermissions.canReadObjectRecords, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx index 90d8c40e9c..86064424fc 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx @@ -1,4 +1,5 @@ import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; +import { type PageLayoutWidget } from '~/generated-metadata/graphql'; import styled from '@emotion/styled'; import { useState } from 'react'; @@ -57,14 +58,21 @@ const StyledErrorUrl = styled.div` `; export type IframeWidgetProps = { - url: string; - title?: string; + widget: PageLayoutWidget; }; -export const IframeWidget = ({ - url, - title = 'Embedded Content', -}: IframeWidgetProps) => { +export const IframeWidget = ({ widget }: IframeWidgetProps) => { + const configuration = widget.configuration; + + if (!configuration || !('url' in configuration)) { + throw new Error( + `Invalid configuration for widget ${widget.id}: missing url`, + ); + } + + const url = configuration.url; + const title = widget.title; + const [isLoading, setIsLoading] = useState(true); const [hasError, setHasError] = useState(false); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/iframe/types/IframeWidget.ts b/packages/twenty-front/src/modules/page-layout/widgets/iframe/types/IframeWidget.ts deleted file mode 100644 index 6e81316617..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/iframe/types/IframeWidget.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type PageLayoutWidget, type WidgetType } from '~/generated/graphql'; - -export type IframeWidget = PageLayoutWidget & { - type: WidgetType.IFRAME; - configuration: { - url: string; - }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/types/Widget.ts b/packages/twenty-front/src/modules/page-layout/widgets/types/Widget.ts deleted file mode 100644 index 87af872dcb..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/types/Widget.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { type GraphWidget } from '@/page-layout/widgets/graph/types/GraphWidget'; -import { type IframeWidget } from '@/page-layout/widgets/iframe/types/IframeWidget'; - -export type Widget = GraphWidget | IframeWidget; diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts new file mode 100644 index 0000000000..76b519edbb --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts @@ -0,0 +1,98 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsNumber, + IsObject, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { GraphQLJSON } from 'graphql-type-json'; + +import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; +import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('BarChartConfiguration') +export class BarChartConfigurationDTO { + @Field(() => GraphType) + @IsEnum(GraphType) + @IsNotEmpty() + graphType: GraphType.BAR; + + @Field(() => UUIDScalarType) + @IsUUID() + @IsNotEmpty() + aggregateFieldMetadataId: string; + + @Field(() => AggregateOperations) + @IsEnum(AggregateOperations) + @IsNotEmpty() + aggregateOperation: AggregateOperations; + + @Field(() => UUIDScalarType) + @IsUUID() + @IsNotEmpty() + groupByFieldMetadataIdX: string; + + @Field(() => GraphOrderBy) + @IsEnum(GraphOrderBy) + @IsNotEmpty() + orderByX: GraphOrderBy; + + @Field(() => UUIDScalarType, { nullable: true }) + @IsUUID() + @IsOptional() + groupByFieldMetadataIdY?: string; + + @Field(() => GraphOrderBy, { nullable: true }) + @IsEnum(GraphOrderBy) + @IsOptional() + orderByY?: GraphOrderBy; + + @Field(() => Boolean, { nullable: true }) + @IsBoolean() + @IsOptional() + omitNullValues?: boolean; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + xAxisName?: string; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + yAxisName?: string; + + @Field(() => Number, { nullable: true }) + @IsNumber() + @IsOptional() + rangeMin?: number; + + @Field(() => Number, { nullable: true }) + @IsNumber() + @IsOptional() + rangeMax?: number; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + description?: string; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + color?: string; + + @Field(() => GraphQLJSON, { nullable: true }) + @IsObject() + @IsOptional() + filter?: ObjectRecordFilter; +} diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts new file mode 100644 index 0000000000..022d93a5cd --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts @@ -0,0 +1,55 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { + IsEnum, + IsNotEmpty, + IsObject, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { GraphQLJSON } from 'graphql-type-json'; + +import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('GaugeChartConfiguration') +export class GaugeChartConfigurationDTO { + @Field(() => GraphType) + @IsEnum(GraphType) + @IsNotEmpty() + graphType: GraphType.GAUGE; + + @Field(() => UUIDScalarType) + @IsUUID() + @IsNotEmpty() + aggregateFieldMetadataId: string; + + @Field(() => AggregateOperations) + @IsEnum(AggregateOperations) + @IsNotEmpty() + aggregateOperation: AggregateOperations; + + @Field(() => AggregateOperations) + @IsEnum(AggregateOperations) + @IsNotEmpty() + aggregateOperationTotal: AggregateOperations; + + @Field(() => UUIDScalarType) + @IsUUID() + @IsNotEmpty() + aggregateFieldMetadataIdTotal: string; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + description?: string; + + @Field(() => GraphQLJSON, { nullable: true }) + @IsObject() + @IsOptional() + filter?: ObjectRecordFilter; +} diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/iframe-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/iframe-configuration.dto.ts new file mode 100644 index 0000000000..40d01cebb3 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/iframe-configuration.dto.ts @@ -0,0 +1,12 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { IsNotEmpty, IsString, IsUrl } from 'class-validator'; + +@ObjectType('IframeConfiguration') +export class IframeConfigurationDTO { + @Field(() => String) + @IsString() + @IsNotEmpty() + @IsUrl() + url: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget-with-id.input.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget-with-id.input.ts index acb93dfaa0..cc69426dfa 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget-with-id.input.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget-with-id.input.ts @@ -49,7 +49,7 @@ export class UpdatePageLayoutWidgetWithIdInput { @IsNotEmpty() gridPosition: GridPositionInput; - @Field(() => GraphQLJSON) + @Field(() => GraphQLJSON, { nullable: true }) @IsObject() @IsOptional() configuration: Record | null; diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts new file mode 100644 index 0000000000..516df0eee4 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts @@ -0,0 +1,98 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsNumber, + IsObject, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { GraphQLJSON } from 'graphql-type-json'; + +import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum'; +import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; + +@ObjectType('LineChartConfiguration') +export class LineChartConfigurationDTO { + @Field(() => GraphType) + @IsEnum(GraphType) + @IsNotEmpty() + graphType: GraphType.LINE; + + @Field(() => UUIDScalarType) + @IsUUID() + @IsNotEmpty() + aggregateFieldMetadataId: string; + + @Field(() => AggregateOperations) + @IsEnum(AggregateOperations) + @IsNotEmpty() + aggregateOperation: AggregateOperations; + + @Field(() => UUIDScalarType) + @IsUUID() + @IsNotEmpty() + groupByFieldMetadataIdX: string; + + @Field(() => GraphOrderBy) + @IsEnum(GraphOrderBy) + @IsNotEmpty() + orderByX: GraphOrderBy; + + @Field(() => UUIDScalarType, { nullable: true }) + @IsUUID() + @IsOptional() + groupByFieldMetadataIdY?: string; + + @Field(() => GraphOrderBy, { nullable: true }) + @IsEnum(GraphOrderBy) + @IsOptional() + orderByY?: GraphOrderBy; + + @Field(() => Boolean, { nullable: true }) + @IsBoolean() + @IsOptional() + omitNullValues?: boolean; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + xAxisName?: string; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + yAxisName?: string; + + @Field(() => Number, { nullable: true }) + @IsNumber() + @IsOptional() + rangeMin?: number; + + @Field(() => Number, { nullable: true }) + @IsNumber() + @IsOptional() + rangeMax?: number; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + description?: string; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + color?: string; + + @Field(() => GraphQLJSON, { nullable: true }) + @IsObject() + @IsOptional() + filter?: ObjectRecordFilter; +} diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto.ts new file mode 100644 index 0000000000..5458e611c7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto.ts @@ -0,0 +1,60 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { + IsEnum, + IsNotEmpty, + IsObject, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { GraphQLJSON } from 'graphql-type-json'; + +import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('NumberChartConfiguration') +export class NumberChartConfigurationDTO { + @Field(() => GraphType) + @IsEnum(GraphType) + @IsNotEmpty() + graphType: GraphType.NUMBER; + + @Field(() => UUIDScalarType) + @IsUUID() + @IsNotEmpty() + aggregateFieldMetadataId: string; + + @Field(() => AggregateOperations) + @IsEnum(AggregateOperations) + @IsNotEmpty() + aggregateOperation: AggregateOperations; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + label?: string; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + format?: string; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + description?: string; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + color?: string; + + @Field(() => GraphQLJSON, { nullable: true }) + @IsObject() + @IsOptional() + filter?: ObjectRecordFilter; +} diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/page-layout-widget.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/page-layout-widget.dto.ts index 217cdee409..87e4c5f97a 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/page-layout-widget.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/page-layout-widget.dto.ts @@ -1,9 +1,12 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; -import { GraphQLJSON } from 'graphql-type-json'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { + WidgetConfiguration, + WidgetConfigurationInterface, +} from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface'; import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; registerEnumType(WidgetType, { name: 'WidgetType' }); @@ -43,8 +46,8 @@ export class PageLayoutWidgetDTO { @Field(() => GridPositionDTO, { nullable: false }) gridPosition: GridPositionDTO; - @Field(() => GraphQLJSON, { nullable: true }) - configuration: Record | null; + @Field(() => WidgetConfiguration, { nullable: true }) + configuration: WidgetConfigurationInterface | null; @Field() createdAt: Date; diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts new file mode 100644 index 0000000000..f699efa545 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts @@ -0,0 +1,61 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { + IsEnum, + IsNotEmpty, + IsObject, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { GraphQLJSON } from 'graphql-type-json'; + +import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; +import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('PieChartConfiguration') +export class PieChartConfigurationDTO { + @Field(() => GraphType) + @IsEnum(GraphType) + @IsNotEmpty() + graphType: GraphType.PIE; + + @Field(() => UUIDScalarType) + @IsUUID() + @IsNotEmpty() + aggregateFieldMetadataId: string; + + @Field(() => AggregateOperations) + @IsEnum(AggregateOperations) + @IsNotEmpty() + aggregateOperation: AggregateOperations; + + @Field(() => UUIDScalarType) + @IsUUID() + @IsNotEmpty() + groupByFieldMetadataId: string; + + @Field(() => GraphOrderBy) + @IsEnum(GraphOrderBy) + @IsNotEmpty() + orderBy: GraphOrderBy; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + description?: string; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + color?: string; + + @Field(() => GraphQLJSON, { nullable: true }) + @IsObject() + @IsOptional() + filter?: ObjectRecordFilter; +} diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/widget-configuration.interface.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/widget-configuration.interface.ts new file mode 100644 index 0000000000..056a189853 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/widget-configuration.interface.ts @@ -0,0 +1,55 @@ +import { createUnionType } from '@nestjs/graphql'; + +import { BarChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto'; +import { GaugeChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto'; +import { IframeConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/iframe-configuration.dto'; +import { LineChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto'; +import { NumberChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto'; +import { PieChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto'; +import { WidgetConfigurationType } from 'src/engine/core-modules/page-layout/enums/widget-configuration-type.enum'; + +export const WidgetConfiguration = createUnionType({ + name: 'WidgetConfiguration', + types: () => [ + BarChartConfigurationDTO, + LineChartConfigurationDTO, + PieChartConfigurationDTO, + NumberChartConfigurationDTO, + GaugeChartConfigurationDTO, + IframeConfigurationDTO, + ], + resolveType(configuration: Record) { + if (!('configurationType' in configuration)) { + throw new Error( + 'Widget configuration missing configurationType discriminator. This indicates a validation bug or data corruption.', + ); + } + + switch (configuration.configurationType) { + case WidgetConfigurationType.IFRAME_CONFIG: + return IframeConfigurationDTO; + case WidgetConfigurationType.BAR_CHART_CONFIG: + return BarChartConfigurationDTO; + case WidgetConfigurationType.LINE_CHART_CONFIG: + return LineChartConfigurationDTO; + case WidgetConfigurationType.PIE_CHART_CONFIG: + return PieChartConfigurationDTO; + case WidgetConfigurationType.NUMBER_CHART_CONFIG: + return NumberChartConfigurationDTO; + case WidgetConfigurationType.GAUGE_CHART_CONFIG: + return GaugeChartConfigurationDTO; + default: + throw new Error( + `Unknown widget configuration type: ${configuration.configurationType}`, + ); + } + }, +}); + +export type WidgetConfigurationInterface = + | BarChartConfigurationDTO + | LineChartConfigurationDTO + | PieChartConfigurationDTO + | NumberChartConfigurationDTO + | GaugeChartConfigurationDTO + | IframeConfigurationDTO; diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/entities/page-layout-widget.entity.ts b/packages/twenty-server/src/engine/core-modules/page-layout/entities/page-layout-widget.entity.ts index c7280fbbdb..382c3192b4 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/entities/page-layout-widget.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/entities/page-layout-widget.entity.ts @@ -11,6 +11,7 @@ import { UpdateDateColumn, } from 'typeorm'; +import { WidgetConfigurationInterface } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface'; import { PageLayoutTabEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-tab.entity'; import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; import { GridPosition } from 'src/engine/core-modules/page-layout/types/grid-position.type'; @@ -72,7 +73,7 @@ export class PageLayoutWidgetEntity gridPosition: GridPosition; @Column({ type: 'jsonb', nullable: true }) - configuration: Record | null; + configuration: WidgetConfigurationInterface | null; @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/enums/graph-order-by.enum.ts b/packages/twenty-server/src/engine/core-modules/page-layout/enums/graph-order-by.enum.ts new file mode 100644 index 0000000000..0a04f44033 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/enums/graph-order-by.enum.ts @@ -0,0 +1,13 @@ +import { registerEnumType } from '@nestjs/graphql'; + +export enum GraphOrderBy { + FIELD_ASC = 'FIELD_ASC', + FIELD_DESC = 'FIELD_DESC', + VALUE_ASC = 'VALUE_ASC', + VALUE_DESC = 'VALUE_DESC', +} + +registerEnumType(GraphOrderBy, { + name: 'GraphOrderBy', + description: 'Order by options for graph widgets', +}); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/enums/graph-type.enum.ts b/packages/twenty-server/src/engine/core-modules/page-layout/enums/graph-type.enum.ts new file mode 100644 index 0000000000..413fec9106 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/enums/graph-type.enum.ts @@ -0,0 +1,14 @@ +import { registerEnumType } from '@nestjs/graphql'; + +export enum GraphType { + NUMBER = 'NUMBER', + GAUGE = 'GAUGE', + PIE = 'PIE', + BAR = 'BAR', + LINE = 'LINE', +} + +registerEnumType(GraphType, { + name: 'GraphType', + description: 'Type of graph widget', +}); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-configuration-type.enum.ts b/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-configuration-type.enum.ts new file mode 100644 index 0000000000..49bdcbe460 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-configuration-type.enum.ts @@ -0,0 +1,8 @@ +export enum WidgetConfigurationType { + BAR_CHART_CONFIG = 'BAR_CHART_CONFIG', + LINE_CHART_CONFIG = 'LINE_CHART_CONFIG', + PIE_CHART_CONFIG = 'PIE_CHART_CONFIG', + NUMBER_CHART_CONFIG = 'NUMBER_CHART_CONFIG', + GAUGE_CHART_CONFIG = 'GAUGE_CHART_CONFIG', + IFRAME_CONFIG = 'IFRAME_CONFIG', +} diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/exceptions/page-layout-widget.exception.ts b/packages/twenty-server/src/engine/core-modules/page-layout/exceptions/page-layout-widget.exception.ts index fc9f2292b2..771640c480 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/exceptions/page-layout-widget.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/exceptions/page-layout-widget.exception.ts @@ -14,17 +14,20 @@ export enum PageLayoutWidgetExceptionMessageKey { PAGE_LAYOUT_TAB_NOT_FOUND = 'PAGE_LAYOUT_TAB_NOT_FOUND', PAGE_LAYOUT_WIDGET_NOT_DELETED = 'PAGE_LAYOUT_WIDGET_NOT_DELETED', GRID_POSITION_REQUIRED = 'GRID_POSITION_REQUIRED', + INVALID_WIDGET_CONFIGURATION = 'INVALID_WIDGET_CONFIGURATION', } export class PageLayoutWidgetException extends CustomException {} export const generatePageLayoutWidgetExceptionMessage = ( key: PageLayoutWidgetExceptionMessageKey, - value?: string, + widgetTitle?: string, + widgetType?: string, + detailedError?: string, ): string => { switch (key) { case PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND: - return `Page layout widget with ID "${value}" not found`; + return `Page layout widget with ID "${widgetTitle}" not found`; case PageLayoutWidgetExceptionMessageKey.TITLE_REQUIRED: return 'Page layout widget title is required'; case PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_ID_REQUIRED: @@ -35,6 +38,18 @@ export const generatePageLayoutWidgetExceptionMessage = ( return 'Page layout widget is not deleted and cannot be restored'; case PageLayoutWidgetExceptionMessageKey.GRID_POSITION_REQUIRED: return 'Grid position is required'; + case PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION: + if (widgetTitle && widgetType && detailedError) { + return `Invalid configuration for widget "${widgetTitle}" of type ${widgetType}: ${detailedError}`; + } + if (widgetTitle && widgetType) { + return `Invalid configuration for widget "${widgetTitle}" of type ${widgetType}`; + } + if (widgetType) { + return `Invalid configuration for widget type ${widgetType}`; + } + + return 'Invalid widget configuration'; default: assertUnreachable(key); } diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/services/__tests__/page-layout-update.service.spec.ts b/packages/twenty-server/src/engine/core-modules/page-layout/services/__tests__/page-layout-update.service.spec.ts index 7504c59545..4b97e34b5c 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/services/__tests__/page-layout-update.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/services/__tests__/page-layout-update.service.spec.ts @@ -147,7 +147,7 @@ describe('PageLayoutUpdateService', () => { gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 }, pageLayoutTabId: 'tab-1', objectMetadataId: null, - configuration: {}, + configuration: null, }, ], }, @@ -210,8 +210,10 @@ describe('PageLayoutUpdateService', () => { const id = 'non-existent-id'; const workspaceId = 'workspace-id'; const input = { - ...mockPageLayout, name: 'Updated Page Layout', + type: PageLayoutType.DASHBOARD, + objectMetadataId: 'object-metadata-id', + tabs: [], }; jest @@ -435,7 +437,7 @@ describe('PageLayoutUpdateService', () => { id: 'new-widget-id', pageLayoutTabId: tabId, objectMetadataId: null, - configuration: {}, + configuration: null, title: 'New Widget', type: WidgetType.VIEW, gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 }, @@ -451,7 +453,7 @@ describe('PageLayoutUpdateService', () => { title: 'New Widget', pageLayoutTabId: tabId, objectMetadataId: null, - configuration: {}, + configuration: null, gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 }, }); @@ -467,7 +469,7 @@ describe('PageLayoutUpdateService', () => { id: 'new-widget-id', pageLayoutTabId: tabId, objectMetadataId: null, - configuration: {}, + configuration: null, title: 'New Widget', type: WidgetType.VIEW, gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 }, @@ -484,7 +486,7 @@ describe('PageLayoutUpdateService', () => { { pageLayoutTabId: tabId, objectMetadataId: null, - configuration: {}, + configuration: null, id: 'widget-1', title: 'Updated Widget', type: WidgetType.VIEW, @@ -517,7 +519,7 @@ describe('PageLayoutUpdateService', () => { type: WidgetType.VIEW, pageLayoutTabId: tabId, objectMetadataId: null, - configuration: {}, + configuration: null, gridPosition: { row: 1, column: 1, rowSpan: 2, columnSpan: 2 }, }, mockTransactionManager, @@ -559,7 +561,7 @@ describe('PageLayoutUpdateService', () => { id: 'widget-1', pageLayoutTabId: tabId, objectMetadataId: null, - configuration: {}, + configuration: null, title: 'Updated Widget', type: WidgetType.VIEW, gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 }, @@ -568,7 +570,7 @@ describe('PageLayoutUpdateService', () => { id: 'new-widget-id', pageLayoutTabId: tabId, objectMetadataId: null, - configuration: {}, + configuration: null, title: 'New Widget', type: WidgetType.FIELDS, gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 2 }, @@ -624,7 +626,7 @@ describe('PageLayoutUpdateService', () => { type: WidgetType.VIEW, pageLayoutTabId: tabId, objectMetadataId: null, - configuration: {}, + configuration: null, gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 }, }, mockTransactionManager, @@ -636,7 +638,7 @@ describe('PageLayoutUpdateService', () => { type: WidgetType.FIELDS, pageLayoutTabId: tabId, objectMetadataId: null, - configuration: {}, + configuration: null, gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 2 }, }, workspaceId, @@ -666,7 +668,7 @@ describe('PageLayoutUpdateService', () => { gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 }, pageLayoutTabId: 'tab-1', objectMetadataId: null, - configuration: {}, + configuration: null, }, { id: 'widget-2', @@ -675,7 +677,7 @@ describe('PageLayoutUpdateService', () => { gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 2 }, pageLayoutTabId: 'tab-1', objectMetadataId: null, - configuration: {}, + configuration: null, }, ], }, diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-update.service.ts b/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-update.service.ts index e79dda4d57..618914a7d7 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-update.service.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-update.service.ts @@ -3,8 +3,10 @@ import { Injectable } from '@nestjs/common'; import { computeDiffBetweenObjects, isDefined } from 'twenty-shared/utils'; import { DataSource, EntityManager } from 'typeorm'; +import { CreatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/create-page-layout-widget.input'; import { UpdatePageLayoutTabWithWidgetsInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-tab-with-widgets.input'; import { UpdatePageLayoutWidgetWithIdInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget-with-id.input'; +import { UpdatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget.input'; import { UpdatePageLayoutWithTabsInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input'; import { PageLayoutTabEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-widget.entity'; @@ -229,14 +231,14 @@ export class PageLayoutUpdateService { await this.pageLayoutWidgetService.update( widgetUpdate.id, workspaceId, - widgetUpdate, + widgetUpdate as UpdatePageLayoutWidgetInput, transactionManager, ); } for (const widgetToCreate of entitiesToCreate) { await this.pageLayoutWidgetService.create( - widgetToCreate, + widgetToCreate as CreatePageLayoutWidgetInput, workspaceId, transactionManager, ); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts b/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts index e91bec091b..126fb0d120 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts @@ -7,6 +7,7 @@ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity import { CreatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/create-page-layout-widget.input'; import { UpdatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget.input'; +import { WidgetConfigurationInterface } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface'; import { PageLayoutWidgetEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-widget.entity'; import { PageLayoutTabException, @@ -19,6 +20,7 @@ import { generatePageLayoutWidgetExceptionMessage, } from 'src/engine/core-modules/page-layout/exceptions/page-layout-widget.exception'; import { PageLayoutTabService } from 'src/engine/core-modules/page-layout/services/page-layout-tab.service'; +import { validateAndTransformWidgetConfiguration } from 'src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util'; @Injectable() export class PageLayoutWidgetService { @@ -120,11 +122,44 @@ export class PageLayoutWidgetService { transactionManager, ); + let validatedConfig: WidgetConfigurationInterface | null = null; + + if (pageLayoutWidgetData.configuration && pageLayoutWidgetData.type) { + try { + validatedConfig = validateAndTransformWidgetConfiguration( + pageLayoutWidgetData.type, + pageLayoutWidgetData.configuration, + ); + } catch (error) { + throw new PageLayoutWidgetException( + generatePageLayoutWidgetExceptionMessage( + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION, + pageLayoutWidgetData.title, + pageLayoutWidgetData.type, + error instanceof Error ? error.message : String(error), + ), + PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + ); + } + + if (!validatedConfig) { + throw new PageLayoutWidgetException( + generatePageLayoutWidgetExceptionMessage( + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION, + pageLayoutWidgetData.title, + pageLayoutWidgetData.type, + ), + PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + ); + } + } + const repository = this.getPageLayoutWidgetRepository(transactionManager); const insertResult = await repository.insert({ ...pageLayoutWidgetData, workspaceId, + ...(validatedConfig && { configuration: validatedConfig }), } as QueryDeepPartialEntity); return this.findByIdOrThrow( @@ -174,10 +209,47 @@ export class PageLayoutWidgetService { ); } - await repository.update( - { id }, - updateData as QueryDeepPartialEntity, - ); + let validatedConfig: WidgetConfigurationInterface | null = null; + + if (updateData.configuration) { + const typeForValidation = updateData.type ?? existingWidget.type; + const titleForError = updateData.title ?? existingWidget.title; + + if (typeForValidation) { + try { + validatedConfig = validateAndTransformWidgetConfiguration( + typeForValidation, + updateData.configuration, + ); + } catch (error) { + throw new PageLayoutWidgetException( + generatePageLayoutWidgetExceptionMessage( + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION, + titleForError, + typeForValidation, + error instanceof Error ? error.message : String(error), + ), + PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + ); + } + + if (!validatedConfig) { + throw new PageLayoutWidgetException( + generatePageLayoutWidgetExceptionMessage( + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION, + titleForError, + typeForValidation, + ), + PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + ); + } + } + } + + await repository.update({ id }, { + ...updateData, + ...(validatedConfig && { configuration: validatedConfig }), + } as QueryDeepPartialEntity); return this.findByIdOrThrow(id, workspaceId, transactionManager); } diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/is-widget-configuration-valid.util.spec.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/is-widget-configuration-valid.util.spec.ts new file mode 100644 index 0000000000..6b4e4876b0 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/is-widget-configuration-valid.util.spec.ts @@ -0,0 +1,43 @@ +import { + INVALID_IFRAME_CONFIG_BAD_URL, + TEST_IFRAME_CONFIG, + TEST_NUMBER_CHART_CONFIG, +} from 'test/integration/constants/widget-configuration-test-data.constants'; + +import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; +import { isWidgetConfigurationValid } from 'src/engine/core-modules/page-layout/utils/is-widget-configuration-valid.util'; + +describe('isWidgetConfigurationValid', () => { + it('should return true for valid configuration', () => { + const result = isWidgetConfigurationValid( + WidgetType.IFRAME, + TEST_IFRAME_CONFIG, + ); + + expect(result).toBe(true); + }); + + it('should return false for invalid configuration', () => { + const result = isWidgetConfigurationValid( + WidgetType.IFRAME, + INVALID_IFRAME_CONFIG_BAD_URL, + ); + + expect(result).toBe(false); + }); + + it('should return false for null configuration', () => { + const result = isWidgetConfigurationValid(WidgetType.IFRAME, null); + + expect(result).toBe(false); + }); + + it('should return false for unsupported widget type', () => { + const result = isWidgetConfigurationValid( + 'UNSUPPORTED' as WidgetType, + TEST_NUMBER_CHART_CONFIG, + ); + + expect(result).toBe(false); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts new file mode 100644 index 0000000000..46b537f827 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts @@ -0,0 +1,294 @@ +import { + INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY, + INVALID_IFRAME_CONFIG_BAD_URL, + INVALID_IFRAME_CONFIG_EMPTY_URL, + INVALID_IFRAME_CONFIG_MISSING_URL, + INVALID_NUMBER_CHART_CONFIG_BAD_UUID, + INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS, + TEST_BAR_CHART_CONFIG, + TEST_BAR_CHART_CONFIG_MINIMAL, + TEST_GAUGE_CHART_CONFIG, + TEST_GAUGE_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, +} from 'test/integration/constants/widget-configuration-test-data.constants'; + +import { WidgetConfigurationType } from 'src/engine/core-modules/page-layout/enums/widget-configuration-type.enum'; +import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; +import { validateAndTransformWidgetConfiguration } from 'src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util'; + +describe('validateAndTransformWidgetConfiguration', () => { + describe('IFRAME widget', () => { + it('should validate and transform valid iframe configuration', () => { + const result = validateAndTransformWidgetConfiguration( + WidgetType.IFRAME, + TEST_IFRAME_CONFIG, + ); + + expect(result).toMatchObject(TEST_IFRAME_CONFIG); + expect((result as any)?.configurationType).toBe( + WidgetConfigurationType.IFRAME_CONFIG, + ); + }); + + it('should throw error for invalid URL', () => { + expect(() => + validateAndTransformWidgetConfiguration( + WidgetType.IFRAME, + INVALID_IFRAME_CONFIG_BAD_URL, + ), + ).toThrow(/url must be a URL address/); + }); + + it('should throw error for missing URL', () => { + expect(() => + validateAndTransformWidgetConfiguration( + WidgetType.IFRAME, + INVALID_IFRAME_CONFIG_MISSING_URL, + ), + ).toThrow(/url must be a URL address/); + }); + + it('should throw error for empty URL', () => { + expect(() => + validateAndTransformWidgetConfiguration( + WidgetType.IFRAME, + INVALID_IFRAME_CONFIG_EMPTY_URL, + ), + ).toThrow(/url must be a URL address/); + }); + }); + + describe('GRAPH widget', () => { + describe('NUMBER graph', () => { + it('should validate full number graph configuration', () => { + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + TEST_NUMBER_CHART_CONFIG, + ); + + expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG); + expect((result as any)?.configurationType).toBe( + WidgetConfigurationType.NUMBER_CHART_CONFIG, + ); + }); + + it('should validate minimal number graph configuration', () => { + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + TEST_NUMBER_CHART_CONFIG_MINIMAL, + ); + + expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG_MINIMAL); + expect((result as any)?.configurationType).toBe( + WidgetConfigurationType.NUMBER_CHART_CONFIG, + ); + }); + + it('should throw error for missing required fields', () => { + expect(() => + validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS, + ), + ).toThrow(/aggregateFieldMetadataId should not be empty/); + }); + + it('should throw error for invalid UUID', () => { + expect(() => + validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + INVALID_NUMBER_CHART_CONFIG_BAD_UUID, + ), + ).toThrow(/aggregateFieldMetadataId must be a UUID/); + }); + }); + + describe('BAR graph', () => { + it('should validate full bar graph configuration', () => { + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + TEST_BAR_CHART_CONFIG, + ); + + expect(result).toMatchObject(TEST_BAR_CHART_CONFIG); + expect((result as any)?.configurationType).toBe( + WidgetConfigurationType.BAR_CHART_CONFIG, + ); + }); + + it('should validate minimal bar graph configuration', () => { + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + TEST_BAR_CHART_CONFIG_MINIMAL, + ); + + expect(result).toMatchObject(TEST_BAR_CHART_CONFIG_MINIMAL); + expect((result as any)?.configurationType).toBe( + WidgetConfigurationType.BAR_CHART_CONFIG, + ); + }); + + it('should throw error for missing group by field', () => { + expect(() => + validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY, + ), + ).toThrow(/groupByFieldMetadataIdX should not be empty/); + }); + }); + + describe('LINE graph', () => { + it('should validate full line graph configuration', () => { + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + TEST_LINE_CHART_CONFIG, + ); + + expect(result).toMatchObject(TEST_LINE_CHART_CONFIG); + expect((result as any)?.configurationType).toBe( + WidgetConfigurationType.LINE_CHART_CONFIG, + ); + }); + + it('should validate minimal line graph configuration', () => { + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + TEST_LINE_CHART_CONFIG_MINIMAL, + ); + + expect(result).toMatchObject(TEST_LINE_CHART_CONFIG_MINIMAL); + expect((result as any)?.configurationType).toBe( + WidgetConfigurationType.LINE_CHART_CONFIG, + ); + }); + }); + + describe('PIE graph', () => { + it('should validate full pie graph configuration', () => { + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + TEST_PIE_CHART_CONFIG, + ); + + expect(result).toMatchObject(TEST_PIE_CHART_CONFIG); + expect((result as any)?.configurationType).toBe( + WidgetConfigurationType.PIE_CHART_CONFIG, + ); + }); + + it('should validate minimal pie graph configuration', () => { + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + TEST_PIE_CHART_CONFIG_MINIMAL, + ); + + expect(result).toMatchObject(TEST_PIE_CHART_CONFIG_MINIMAL); + expect((result as any)?.configurationType).toBe( + WidgetConfigurationType.PIE_CHART_CONFIG, + ); + }); + }); + + describe('GAUGE graph', () => { + it('should validate full gauge graph configuration', () => { + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + TEST_GAUGE_CHART_CONFIG, + ); + + expect(result).toMatchObject(TEST_GAUGE_CHART_CONFIG); + expect((result as any)?.configurationType).toBe( + WidgetConfigurationType.GAUGE_CHART_CONFIG, + ); + }); + + it('should validate minimal gauge graph configuration', () => { + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + TEST_GAUGE_CHART_CONFIG_MINIMAL, + ); + + expect(result).toMatchObject(TEST_GAUGE_CHART_CONFIG_MINIMAL); + expect((result as any)?.configurationType).toBe( + WidgetConfigurationType.GAUGE_CHART_CONFIG, + ); + }); + }); + + it('should return null for unsupported graph type', () => { + const configuration = { + graphType: 'UNSUPPORTED', + viewId: '550e8400-e29b-41d4-a716-446655440000', + }; + + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + configuration, + ); + + expect(result).toBeNull(); + }); + + it('should return null for missing graph type', () => { + const configuration = { + viewId: '550e8400-e29b-41d4-a716-446655440000', + }; + + const result = validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + configuration, + ); + + expect(result).toBeNull(); + }); + }); + + describe('Edge cases', () => { + it('should throw error for null configuration', () => { + expect(() => + validateAndTransformWidgetConfiguration(WidgetType.IFRAME, null), + ).toThrow('Invalid configuration: not an object'); + }); + + it('should throw error for undefined configuration', () => { + expect(() => + validateAndTransformWidgetConfiguration(WidgetType.IFRAME, undefined), + ).toThrow('Invalid configuration: not an object'); + }); + + it('should throw error for non-object configuration', () => { + expect(() => + validateAndTransformWidgetConfiguration(WidgetType.IFRAME, 'string'), + ).toThrow('Invalid configuration: not an object'); + }); + + it('should return null for unsupported widget type', () => { + const configuration = { someField: 'value' }; + + const result = validateAndTransformWidgetConfiguration( + 'UNSUPPORTED' as WidgetType, + configuration, + ); + + expect(result).toBeNull(); + }); + }); + + describe('Error messages', () => { + it('should include validation details in error message', () => { + expect(() => + validateAndTransformWidgetConfiguration( + WidgetType.GRAPH, + INVALID_NUMBER_CHART_CONFIG_BAD_UUID, + ), + ).toThrow(/aggregateFieldMetadataId must be a UUID/); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/is-widget-configuration-valid.util.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/is-widget-configuration-valid.util.ts new file mode 100644 index 0000000000..cdcb7ba687 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/is-widget-configuration-valid.util.ts @@ -0,0 +1,18 @@ +import { type WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; +import { validateAndTransformWidgetConfiguration } from 'src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util'; + +export const isWidgetConfigurationValid = ( + type: WidgetType, + configuration: unknown, +): boolean => { + try { + const validatedConfig = validateAndTransformWidgetConfiguration( + type, + configuration, + ); + + return validatedConfig !== null; + } catch { + return false; + } +}; diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts new file mode 100644 index 0000000000..6f1dc9ec61 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts @@ -0,0 +1,165 @@ +import { plainToInstance } from 'class-transformer'; +import { validateSync, type ValidationError } from 'class-validator'; + +import { BarChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto'; +import { GaugeChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto'; +import { IframeConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/iframe-configuration.dto'; +import { LineChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto'; +import { NumberChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto'; +import { PieChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto'; +import { type WidgetConfigurationInterface } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface'; +import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; +import { WidgetConfigurationType } from 'src/engine/core-modules/page-layout/enums/widget-configuration-type.enum'; +import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; + +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: Record, +): WidgetConfigurationInterface | null => { + const graphType = configuration.graphType as GraphType; + + if (!graphType || !Object.values(GraphType).includes(graphType)) { + return null; + } + + switch (graphType) { + case GraphType.BAR: { + const instance = plainToInstance(BarChartConfigurationDTO, configuration); + + const errors = validateSync(instance); + + if (errors.length > 0) { + throw errors; + } + + return { + ...instance, + configurationType: WidgetConfigurationType.BAR_CHART_CONFIG, + } as WidgetConfigurationInterface; + } + case GraphType.LINE: { + const instance = plainToInstance( + LineChartConfigurationDTO, + configuration, + ); + + const errors = validateSync(instance); + + if (errors.length > 0) { + throw errors; + } + + return { + ...instance, + configurationType: WidgetConfigurationType.LINE_CHART_CONFIG, + } as WidgetConfigurationInterface; + } + case GraphType.PIE: { + const instance = plainToInstance(PieChartConfigurationDTO, configuration); + + const errors = validateSync(instance); + + if (errors.length > 0) { + throw errors; + } + + return { + ...instance, + configurationType: WidgetConfigurationType.PIE_CHART_CONFIG, + } as WidgetConfigurationInterface; + } + case GraphType.NUMBER: { + const instance = plainToInstance( + NumberChartConfigurationDTO, + configuration, + ); + + const errors = validateSync(instance); + + if (errors.length > 0) { + throw errors; + } + + return { + ...instance, + configurationType: WidgetConfigurationType.NUMBER_CHART_CONFIG, + } as WidgetConfigurationInterface; + } + case GraphType.GAUGE: { + const instance = plainToInstance( + GaugeChartConfigurationDTO, + configuration, + ); + + const errors = validateSync(instance); + + if (errors.length > 0) { + throw errors; + } + + return { + ...instance, + configurationType: WidgetConfigurationType.GAUGE_CHART_CONFIG, + } as WidgetConfigurationInterface; + } + default: + return null; + } +}; + +const validateIframeConfiguration = ( + configuration: unknown, +): WidgetConfigurationInterface | null => { + const instance = plainToInstance(IframeConfigurationDTO, configuration); + + const errors = validateSync(instance); + + if (errors.length > 0) { + throw errors; + } + + return { + ...instance, + configurationType: WidgetConfigurationType.IFRAME_CONFIG, + } as WidgetConfigurationInterface; +}; + +export const validateAndTransformWidgetConfiguration = ( + type: WidgetType, + configuration: unknown, +): WidgetConfigurationInterface | null => { + if (!configuration || typeof configuration !== 'object') { + throw new Error('Invalid configuration: not an object'); + } + + try { + switch (type) { + case WidgetType.GRAPH: + return validateGraphConfiguration( + configuration as Record, + ); + case WidgetType.IFRAME: + return validateIframeConfiguration(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/workspace-manager/dev-seeder/core/constants/page-layout-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/constants/page-layout-seeds.constant.ts index af1c3cdaac..5682f309b8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/constants/page-layout-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/constants/page-layout-seeds.constant.ts @@ -2,11 +2,4 @@ export const PAGE_LAYOUT_SEEDS = { SALES_DASHBOARD: 'SALES_DASHBOARD', CUSTOMER_DASHBOARD: 'CUSTOMER_DASHBOARD', TEAM_DASHBOARD: 'TEAM_DASHBOARD', - REVENUE_ANALYTICS: 'REVENUE_ANALYTICS', - MARKETING_METRICS: 'MARKETING_METRICS', - SUPPORT_DASHBOARD: 'SUPPORT_DASHBOARD', - PRODUCT_USAGE: 'PRODUCT_USAGE', - OPERATIONS_KPI: 'OPERATIONS_KPI', - FINANCE_OVERVIEW: 'FINANCE_OVERVIEW', - EXECUTIVE_SUMMARY: 'EXECUTIVE_SUMMARY', }; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant.ts index 8c6d3ed7d2..c5f9f0475f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant.ts @@ -5,11 +5,4 @@ export const PAGE_LAYOUT_TAB_SEEDS = { CUSTOMER_ANALYTICS: 'CUSTOMER_ANALYTICS_TAB', TEAM_OVERVIEW: 'TEAM_OVERVIEW_TAB', TEAM_METRICS: 'TEAM_METRICS_TAB', - REVENUE_MAIN: 'REVENUE_MAIN_TAB', - MARKETING_MAIN: 'MARKETING_MAIN_TAB', - SUPPORT_MAIN: 'SUPPORT_MAIN_TAB', - PRODUCT_MAIN: 'PRODUCT_MAIN_TAB', - OPERATIONS_MAIN: 'OPERATIONS_MAIN_TAB', - FINANCE_MAIN: 'FINANCE_MAIN_TAB', - EXECUTIVE_MAIN: 'EXECUTIVE_MAIN_TAB', }; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/constants/page-layout-widget-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/constants/page-layout-widget-seeds.constant.ts index 16b46eee2d..1dd5ab7ad9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/constants/page-layout-widget-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/constants/page-layout-widget-seeds.constant.ts @@ -1,55 +1,20 @@ export const PAGE_LAYOUT_WIDGET_SEEDS = { - SALES_PIPELINE_NUMBER: 'SALES_PIPELINE_NUMBER_WIDGET', - SALES_CONVERSION_GAUGE: 'SALES_CONVERSION_GAUGE_WIDGET', - SALES_MONTHLY_REVENUE: 'SALES_MONTHLY_REVENUE_WIDGET', + SALES_PIPELINE_VALUE: 'SALES_PIPELINE_VALUE_WIDGET', + SALES_AVERAGE_DEAL_SIZE: 'SALES_AVERAGE_DEAL_SIZE_WIDGET', + SALES_REVENUE_FORECAST: 'SALES_REVENUE_FORECAST_WIDGET', SALES_DEALS_BY_STAGE: 'SALES_DEALS_BY_STAGE_WIDGET', - SALES_TOP_PERFORMERS: 'SALES_TOP_PERFORMERS_WIDGET', - SALES_FORECAST_LINE: 'SALES_FORECAST_LINE_WIDGET', + SALES_DEAL_DISTRIBUTION: 'SALES_DEAL_DISTRIBUTION_WIDGET', + SALES_OPPORTUNITY_COUNT: 'SALES_OPPORTUNITY_COUNT_WIDGET', CUSTOMER_TOTAL_COUNT: 'CUSTOMER_TOTAL_COUNT_WIDGET', - CUSTOMER_ACQUISITION_TREND: 'CUSTOMER_ACQUISITION_TREND_WIDGET', - CUSTOMER_SEGMENTS_PIE: 'CUSTOMER_SEGMENTS_PIE_WIDGET', - CUSTOMER_SATISFACTION_GAUGE: 'CUSTOMER_SATISFACTION_GAUGE_WIDGET', - CUSTOMER_RETENTION_RATE: 'CUSTOMER_RETENTION_RATE_WIDGET', - CUSTOMER_LIFETIME_VALUE: 'CUSTOMER_LIFETIME_VALUE_WIDGET', + CUSTOMER_NEW_OVER_TIME: 'CUSTOMER_NEW_OVER_TIME_WIDGET', + CUSTOMER_COMPANIES_BY_SIZE: 'CUSTOMER_COMPANIES_BY_SIZE_WIDGET', + CUSTOMER_ANNUAL_RECURRING_REVENUE: 'CUSTOMER_ANNUAL_RECURRING_REVENUE_WIDGET', + CUSTOMER_REVENUE_DISTRIBUTION: 'CUSTOMER_REVENUE_DISTRIBUTION_WIDGET', + CUSTOMER_AVERAGE_ARR: 'CUSTOMER_AVERAGE_ARR_WIDGET', - TEAM_ACTIVITY_OVERVIEW: 'TEAM_ACTIVITY_OVERVIEW_WIDGET', - TEAM_PRODUCTIVITY_METRICS: 'TEAM_PRODUCTIVITY_METRICS_WIDGET', - TEAM_GOAL_PROGRESS: 'TEAM_GOAL_PROGRESS_WIDGET', - TEAM_MEMBER_LEADERBOARD: 'TEAM_MEMBER_LEADERBOARD_WIDGET', - - REVENUE_TOTAL_NUMBER: 'REVENUE_TOTAL_NUMBER_WIDGET', - REVENUE_GROWTH_TREND: 'REVENUE_GROWTH_TREND_WIDGET', - REVENUE_BY_PRODUCT: 'REVENUE_BY_PRODUCT_WIDGET', - REVENUE_BY_REGION: 'REVENUE_BY_REGION_WIDGET', - - MARKETING_LEADS_GENERATED: 'MARKETING_LEADS_GENERATED_WIDGET', - MARKETING_CAMPAIGN_ROI: 'MARKETING_CAMPAIGN_ROI_WIDGET', - MARKETING_CHANNEL_PERFORMANCE: 'MARKETING_CHANNEL_PERFORMANCE_WIDGET', - MARKETING_CONVERSION_FUNNEL: 'MARKETING_CONVERSION_FUNNEL_WIDGET', - - SUPPORT_TICKET_COUNT: 'SUPPORT_TICKET_COUNT_WIDGET', - SUPPORT_RESOLUTION_TIME: 'SUPPORT_RESOLUTION_TIME_WIDGET', - SUPPORT_SATISFACTION_SCORE: 'SUPPORT_SATISFACTION_SCORE_WIDGET', - SUPPORT_TICKET_BY_PRIORITY: 'SUPPORT_TICKET_BY_PRIORITY_WIDGET', - - PRODUCT_ACTIVE_USERS: 'PRODUCT_ACTIVE_USERS_WIDGET', - PRODUCT_FEATURE_ADOPTION: 'PRODUCT_FEATURE_ADOPTION_WIDGET', - PRODUCT_USAGE_HEATMAP: 'PRODUCT_USAGE_HEATMAP_WIDGET', - PRODUCT_USER_ENGAGEMENT: 'PRODUCT_USER_ENGAGEMENT_WIDGET', - - OPERATIONS_EFFICIENCY_GAUGE: 'OPERATIONS_EFFICIENCY_GAUGE_WIDGET', - OPERATIONS_COST_BREAKDOWN: 'OPERATIONS_COST_BREAKDOWN_WIDGET', - OPERATIONS_PROCESS_TIME: 'OPERATIONS_PROCESS_TIME_WIDGET', - OPERATIONS_ERROR_RATE: 'OPERATIONS_ERROR_RATE_WIDGET', - - FINANCE_CASH_FLOW: 'FINANCE_CASH_FLOW_WIDGET', - FINANCE_EXPENSES_PIE: 'FINANCE_EXPENSES_PIE_WIDGET', - FINANCE_PROFIT_MARGIN: 'FINANCE_PROFIT_MARGIN_WIDGET', - FINANCE_BUDGET_VARIANCE: 'FINANCE_BUDGET_VARIANCE_WIDGET', - - EXECUTIVE_KEY_METRICS: 'EXECUTIVE_KEY_METRICS_WIDGET', - EXECUTIVE_COMPANY_HEALTH: 'EXECUTIVE_COMPANY_HEALTH_WIDGET', - EXECUTIVE_QUARTERLY_REVIEW: 'EXECUTIVE_QUARTERLY_REVIEW_WIDGET', - EXECUTIVE_STRATEGIC_GOALS: 'EXECUTIVE_STRATEGIC_GOALS_WIDGET', + TEAM_SIZE: 'TEAM_SIZE_WIDGET', + TEAM_GEOGRAPHIC_DISTRIBUTION: 'TEAM_GEOGRAPHIC_DISTRIBUTION_WIDGET', + TEAM_CONTACT_ROLES: 'TEAM_CONTACT_ROLES_WIDGET', + TEAM_OPEN_TASKS: 'TEAM_OPEN_TASKS_WIDGET', }; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-data-seeds.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-data-seeds.util.ts index c1be05a4a3..caa26789d9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-data-seeds.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-data-seeds.util.ts @@ -34,53 +34,4 @@ export const getPageLayoutDataSeeds = ( objectMetadataId: null, workspaceId, }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.REVENUE_ANALYTICS), - name: 'Revenue Analytics Layout', - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.MARKETING_METRICS), - name: 'Marketing Metrics Layout', - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.SUPPORT_DASHBOARD), - name: 'Support Dashboard Layout', - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.PRODUCT_USAGE), - name: 'Product Usage Layout', - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.OPERATIONS_KPI), - name: 'Operations KPI Layout', - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.FINANCE_OVERVIEW), - name: 'Finance Overview Layout', - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.EXECUTIVE_SUMMARY), - name: 'Executive Summary Layout', - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - workspaceId, - }, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-tab-data-seeds.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-tab-data-seeds.util.ts index cc7474a947..31af03c1b3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-tab-data-seeds.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-tab-data-seeds.util.ts @@ -55,80 +55,16 @@ export const getPageLayoutTabDataSeeds = ( }, { id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.TEAM_OVERVIEW), - title: 'Overview', + title: 'Team & People', position: 0, pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD), workspaceId, }, { id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS), - title: 'Metrics', + title: 'Tasks & Activity', position: 1, pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD), workspaceId, }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.REVENUE_MAIN), - title: 'Revenue', - position: 0, - pageLayoutId: generateSeedId( - workspaceId, - PAGE_LAYOUT_SEEDS.REVENUE_ANALYTICS, - ), - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.MARKETING_MAIN), - title: 'Marketing', - position: 0, - pageLayoutId: generateSeedId( - workspaceId, - PAGE_LAYOUT_SEEDS.MARKETING_METRICS, - ), - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.SUPPORT_MAIN), - title: 'Support', - position: 0, - pageLayoutId: generateSeedId( - workspaceId, - PAGE_LAYOUT_SEEDS.SUPPORT_DASHBOARD, - ), - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.PRODUCT_MAIN), - title: 'Product', - position: 0, - pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.PRODUCT_USAGE), - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.OPERATIONS_MAIN), - title: 'Operations', - position: 0, - pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.OPERATIONS_KPI), - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.FINANCE_MAIN), - title: 'Finance', - position: 0, - pageLayoutId: generateSeedId( - workspaceId, - PAGE_LAYOUT_SEEDS.FINANCE_OVERVIEW, - ), - workspaceId, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.EXECUTIVE_MAIN), - title: 'Executive', - position: 0, - pageLayoutId: generateSeedId( - workspaceId, - PAGE_LAYOUT_SEEDS.EXECUTIVE_SUMMARY, - ), - workspaceId, - }, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts index cbe2e830eb..a1100edc5e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts @@ -1,7 +1,16 @@ +import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; +import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PAGE_LAYOUT_TAB_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant'; import { PAGE_LAYOUT_WIDGET_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-widget-seeds.constant'; import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util'; +import { + BASE_OBJECT_STANDARD_FIELD_IDS, + COMPANY_STANDARD_FIELD_IDS, + OPPORTUNITY_STANDARD_FIELD_IDS, + PERSON_STANDARD_FIELD_IDS, +} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; type PageLayoutWidgetDataSeed = { id: string; @@ -20,684 +29,357 @@ type PageLayoutWidgetDataSeed = { export const getPageLayoutWidgetDataSeeds = ( workspaceId: string, -): PageLayoutWidgetDataSeed[] => [ - // Sales Overview Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.SALES_PIPELINE_NUMBER, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW, - ), - title: 'Pipeline Value', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 }, - configuration: { graphType: 'NUMBER' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.SALES_CONVERSION_GAUGE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW, - ), - title: 'Conversion Rate', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 3, rowSpan: 4, columnSpan: 4 }, - configuration: { graphType: 'GAUGE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.SALES_MONTHLY_REVENUE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW, - ), - title: 'Monthly Revenue', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 7, rowSpan: 8, columnSpan: 5 }, - configuration: { graphType: 'LINE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.SALES_DEALS_BY_STAGE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW, - ), - title: 'Deals by Stage', - type: WidgetType.GRAPH, - gridPosition: { row: 4, column: 0, rowSpan: 4, columnSpan: 6 }, - configuration: { graphType: 'BAR' }, - objectMetadataId: null, - }, + objectMetadataItems: ObjectMetadataEntity[], +): PageLayoutWidgetDataSeed[] => { + const opportunityObject = objectMetadataItems.find( + (obj) => obj.standardId === STANDARD_OBJECT_IDS.opportunity, + ); + const companyObject = objectMetadataItems.find( + (obj) => obj.standardId === STANDARD_OBJECT_IDS.company, + ); + const personObject = objectMetadataItems.find( + (obj) => obj.standardId === STANDARD_OBJECT_IDS.person, + ); + const taskObject = objectMetadataItems.find( + (obj) => obj.standardId === STANDARD_OBJECT_IDS.task, + ); + const rocketObject = objectMetadataItems.find( + (obj) => obj.nameSingular === 'rocket', + ); - // Sales Details Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.SALES_TOP_PERFORMERS, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.SALES_DETAILS, - ), - title: 'Top Sales Performers', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 5, columnSpan: 5 }, - configuration: { graphType: 'BAR' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.SALES_FORECAST_LINE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.SALES_DETAILS, - ), - title: 'Sales Forecast', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 5, rowSpan: 5, columnSpan: 7 }, - configuration: { graphType: 'LINE' }, - objectMetadataId: null, - }, + return [ + // Sales Overview Tab Widgets + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.SALES_PIPELINE_VALUE, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW, + ), + title: 'Total Pipeline Value', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 }, + configuration: { + graphType: 'NUMBER', + aggregateFieldMetadataId: OPPORTUNITY_STANDARD_FIELD_IDS.amount, + aggregateOperation: AggregateOperations.SUM, + }, + objectMetadataId: opportunityObject?.id ?? null, + }, + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.SALES_AVERAGE_DEAL_SIZE, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW, + ), + title: 'Average Deal Size', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 3, rowSpan: 4, columnSpan: 4 }, + configuration: { + graphType: 'GAUGE', + aggregateFieldMetadataId: OPPORTUNITY_STANDARD_FIELD_IDS.amount, + aggregateOperation: AggregateOperations.AVG, + aggregateFieldMetadataIdTotal: OPPORTUNITY_STANDARD_FIELD_IDS.amount, + aggregateOperationTotal: AggregateOperations.MAX, + }, + objectMetadataId: rocketObject?.id ?? null, + }, + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.SALES_REVENUE_FORECAST, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW, + ), + title: 'Revenue Forecast', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 7, rowSpan: 8, columnSpan: 5 }, + configuration: { + graphType: 'LINE', + aggregateFieldMetadataId: OPPORTUNITY_STANDARD_FIELD_IDS.amount, + aggregateOperation: AggregateOperations.SUM, + groupByFieldMetadataIdX: OPPORTUNITY_STANDARD_FIELD_IDS.closeDate, + orderByX: 'FIELD_ASC', + }, + objectMetadataId: opportunityObject?.id ?? null, + }, + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.SALES_DEALS_BY_STAGE, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW, + ), + title: 'Deals by Stage', + type: WidgetType.GRAPH, + gridPosition: { row: 4, column: 0, rowSpan: 4, columnSpan: 6 }, + configuration: { + graphType: 'BAR', + aggregateFieldMetadataId: OPPORTUNITY_STANDARD_FIELD_IDS.amount, + aggregateOperation: AggregateOperations.SUM, + groupByFieldMetadataIdX: OPPORTUNITY_STANDARD_FIELD_IDS.stage, + orderByX: 'FIELD_DESC', + }, + objectMetadataId: opportunityObject?.id ?? null, + }, - // Customer Overview Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_TOTAL_COUNT, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW, - ), - title: 'Total Customers', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 }, - configuration: { graphType: 'NUMBER' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_ACQUISITION_TREND, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW, - ), - title: 'Customer Acquisition', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 3, rowSpan: 6, columnSpan: 5 }, - configuration: { graphType: 'LINE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_SEGMENTS_PIE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW, - ), - title: 'Customer Segments', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 8, rowSpan: 6, columnSpan: 4 }, - configuration: { graphType: 'PIE' }, - objectMetadataId: null, - }, + // Sales Details Tab Widgets + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.SALES_DEAL_DISTRIBUTION, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.SALES_DETAILS, + ), + title: 'Deal Distribution', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 0, rowSpan: 5, columnSpan: 5 }, + configuration: { + graphType: 'PIE', + aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id, + aggregateOperation: AggregateOperations.COUNT, + groupByFieldMetadataId: OPPORTUNITY_STANDARD_FIELD_IDS.stage, + orderBy: 'VALUE_DESC', + }, + objectMetadataId: rocketObject?.id ?? null, + }, + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.SALES_OPPORTUNITY_COUNT, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.SALES_DETAILS, + ), + title: 'Opportunity Count', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 5, rowSpan: 5, columnSpan: 7 }, + configuration: { + graphType: 'NUMBER', + aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id, + aggregateOperation: AggregateOperations.COUNT, + }, + objectMetadataId: opportunityObject?.id ?? null, + }, - // Customer Analytics Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_SATISFACTION_GAUGE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS, - ), - title: 'Customer Satisfaction', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 }, - configuration: { graphType: 'GAUGE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_RETENTION_RATE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS, - ), - title: 'Retention Rate', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 3 }, - configuration: { graphType: 'NUMBER' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_LIFETIME_VALUE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS, - ), - title: 'Lifetime Value', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 7, rowSpan: 6, columnSpan: 5 }, - configuration: { graphType: 'LINE' }, - objectMetadataId: null, - }, + // Customer Overview Tab Widgets + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_TOTAL_COUNT, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW, + ), + title: 'Total Customers', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 }, + configuration: { + graphType: 'NUMBER', + aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id, + aggregateOperation: AggregateOperations.COUNT, + }, + objectMetadataId: companyObject?.id ?? null, + }, + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_NEW_OVER_TIME, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW, + ), + title: 'New Customers Over Time', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 3, rowSpan: 6, columnSpan: 5 }, + configuration: { + graphType: 'LINE', + aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id, + aggregateOperation: AggregateOperations.COUNT, + groupByFieldMetadataIdX: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt, + orderByX: 'FIELD_ASC', + }, + objectMetadataId: companyObject?.id ?? null, + }, + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_COMPANIES_BY_SIZE, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW, + ), + title: 'Companies by Size', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 8, rowSpan: 6, columnSpan: 4 }, + configuration: { + graphType: 'BAR', + aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id, + aggregateOperation: AggregateOperations.COUNT, + groupByFieldMetadataIdX: COMPANY_STANDARD_FIELD_IDS.employees, + orderByX: 'FIELD_ASC', + }, + objectMetadataId: companyObject?.id ?? null, + }, - // Team Overview Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.TEAM_ACTIVITY_OVERVIEW, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.TEAM_OVERVIEW, - ), - title: 'Team Activity', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 5, columnSpan: 6 }, - configuration: { graphType: 'BAR' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.TEAM_PRODUCTIVITY_METRICS, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.TEAM_OVERVIEW, - ), - title: 'Productivity Metrics', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 6, rowSpan: 5, columnSpan: 6 }, - configuration: { graphType: 'LINE' }, - objectMetadataId: null, - }, + // Customer Analytics Tab Widgets + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_ANNUAL_RECURRING_REVENUE, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS, + ), + title: 'Annual Recurring Revenue', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 }, + configuration: { + graphType: 'NUMBER', + aggregateFieldMetadataId: + COMPANY_STANDARD_FIELD_IDS.annualRecurringRevenue, + aggregateOperation: AggregateOperations.SUM, + }, + objectMetadataId: companyObject?.id ?? null, + }, + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_REVENUE_DISTRIBUTION, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS, + ), + title: 'Revenue Distribution', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 3 }, + configuration: { + graphType: 'PIE', + aggregateFieldMetadataId: + COMPANY_STANDARD_FIELD_IDS.annualRecurringRevenue, + aggregateOperation: AggregateOperations.SUM, + groupByFieldMetadataId: COMPANY_STANDARD_FIELD_IDS.name, + orderBy: 'VALUE_DESC', + }, + objectMetadataId: companyObject?.id ?? null, + }, + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_AVERAGE_ARR, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS, + ), + title: 'Average ARR', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 7, rowSpan: 6, columnSpan: 5 }, + configuration: { + graphType: 'GAUGE', + aggregateFieldMetadataId: + COMPANY_STANDARD_FIELD_IDS.annualRecurringRevenue, + aggregateOperation: AggregateOperations.AVG, + aggregateFieldMetadataIdTotal: + COMPANY_STANDARD_FIELD_IDS.annualRecurringRevenue, + aggregateOperationTotal: AggregateOperations.MAX, + }, + objectMetadataId: companyObject?.id ?? null, + }, - // Team Metrics Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.TEAM_GOAL_PROGRESS, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS, - ), - title: 'Goal Progress', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 6 }, - configuration: { graphType: 'GAUGE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.TEAM_MEMBER_LEADERBOARD, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS, - ), - title: 'Team Leaderboard', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 6, rowSpan: 6, columnSpan: 6 }, - configuration: { graphType: 'BAR' }, - objectMetadataId: null, - }, + // Team Overview Tab Widgets + { + id: generateSeedId(workspaceId, PAGE_LAYOUT_WIDGET_SEEDS.TEAM_SIZE), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.TEAM_OVERVIEW, + ), + title: 'Team Size', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 0, rowSpan: 5, columnSpan: 6 }, + configuration: { + graphType: 'NUMBER', + aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id, + aggregateOperation: AggregateOperations.COUNT, + }, + objectMetadataId: personObject?.id ?? null, + }, + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.TEAM_GEOGRAPHIC_DISTRIBUTION, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.TEAM_OVERVIEW, + ), + title: 'Geographic Distribution', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 6, rowSpan: 5, columnSpan: 6 }, + configuration: { + graphType: 'BAR', + aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id, + aggregateOperation: AggregateOperations.COUNT, + groupByFieldMetadataIdX: PERSON_STANDARD_FIELD_IDS.city, + orderByX: 'FIELD_DESC', + }, + objectMetadataId: personObject?.id ?? null, + }, - // Revenue Main Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.REVENUE_TOTAL_NUMBER, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.REVENUE_MAIN, - ), - title: 'Total Revenue', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 }, - configuration: { graphType: 'NUMBER' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.REVENUE_GROWTH_TREND, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.REVENUE_MAIN, - ), - title: 'Revenue Growth', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 3, rowSpan: 6, columnSpan: 5 }, - configuration: { graphType: 'LINE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.REVENUE_BY_PRODUCT, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.REVENUE_MAIN, - ), - title: 'Revenue by Product', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 8, rowSpan: 6, columnSpan: 4 }, - configuration: { graphType: 'PIE' }, - objectMetadataId: null, - }, - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_WIDGET_SEEDS.REVENUE_BY_REGION), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.REVENUE_MAIN, - ), - title: 'Revenue by Region', - type: WidgetType.GRAPH, - gridPosition: { row: 6, column: 0, rowSpan: 4, columnSpan: 12 }, - configuration: { graphType: 'BAR' }, - objectMetadataId: null, - }, - - // Marketing Main Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.MARKETING_LEADS_GENERATED, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.MARKETING_MAIN, - ), - title: 'Leads Generated', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 }, - configuration: { graphType: 'NUMBER' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.MARKETING_CAMPAIGN_ROI, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.MARKETING_MAIN, - ), - title: 'Campaign ROI', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 3, rowSpan: 4, columnSpan: 4 }, - configuration: { graphType: 'GAUGE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.MARKETING_CHANNEL_PERFORMANCE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.MARKETING_MAIN, - ), - title: 'Channel Performance', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 7, rowSpan: 6, columnSpan: 5 }, - configuration: { graphType: 'BAR' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.MARKETING_CONVERSION_FUNNEL, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.MARKETING_MAIN, - ), - title: 'Conversion Funnel', - type: WidgetType.GRAPH, - gridPosition: { row: 4, column: 0, rowSpan: 4, columnSpan: 7 }, - configuration: { graphType: 'FUNNEL' }, - objectMetadataId: null, - }, - - // Support Main Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.SUPPORT_TICKET_COUNT, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.SUPPORT_MAIN, - ), - title: 'Open Tickets', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 }, - configuration: { graphType: 'NUMBER' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.SUPPORT_RESOLUTION_TIME, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.SUPPORT_MAIN, - ), - title: 'Avg Resolution Time', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 3, rowSpan: 2, columnSpan: 3 }, - configuration: { graphType: 'NUMBER' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.SUPPORT_SATISFACTION_SCORE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.SUPPORT_MAIN, - ), - title: 'Satisfaction Score', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 6, rowSpan: 4, columnSpan: 4 }, - configuration: { graphType: 'GAUGE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.SUPPORT_TICKET_BY_PRIORITY, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.SUPPORT_MAIN, - ), - title: 'Tickets by Priority', - type: WidgetType.GRAPH, - gridPosition: { row: 2, column: 0, rowSpan: 4, columnSpan: 6 }, - configuration: { graphType: 'PIE' }, - objectMetadataId: null, - }, - - // Product Main Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.PRODUCT_ACTIVE_USERS, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.PRODUCT_MAIN, - ), - title: 'Active Users', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 }, - configuration: { graphType: 'NUMBER' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.PRODUCT_FEATURE_ADOPTION, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.PRODUCT_MAIN, - ), - title: 'Feature Adoption', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 3, rowSpan: 6, columnSpan: 5 }, - configuration: { graphType: 'BAR' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.PRODUCT_USAGE_HEATMAP, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.PRODUCT_MAIN, - ), - title: 'Usage Heatmap', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 8, rowSpan: 6, columnSpan: 4 }, - configuration: { graphType: 'HEATMAP' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.PRODUCT_USER_ENGAGEMENT, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.PRODUCT_MAIN, - ), - title: 'User Engagement', - type: WidgetType.GRAPH, - gridPosition: { row: 6, column: 0, rowSpan: 4, columnSpan: 12 }, - configuration: { graphType: 'LINE' }, - objectMetadataId: null, - }, - - // Operations Main Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.OPERATIONS_EFFICIENCY_GAUGE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.OPERATIONS_MAIN, - ), - title: 'Efficiency Score', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 }, - configuration: { graphType: 'GAUGE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.OPERATIONS_COST_BREAKDOWN, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.OPERATIONS_MAIN, - ), - title: 'Cost Breakdown', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 4, rowSpan: 6, columnSpan: 4 }, - configuration: { graphType: 'PIE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.OPERATIONS_PROCESS_TIME, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.OPERATIONS_MAIN, - ), - title: 'Process Time', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 8, rowSpan: 6, columnSpan: 4 }, - configuration: { graphType: 'LINE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.OPERATIONS_ERROR_RATE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.OPERATIONS_MAIN, - ), - title: 'Error Rate', - type: WidgetType.GRAPH, - gridPosition: { row: 4, column: 0, rowSpan: 2, columnSpan: 4 }, - configuration: { graphType: 'NUMBER' }, - objectMetadataId: null, - }, - - // Finance Main Tab Widgets - { - id: generateSeedId(workspaceId, PAGE_LAYOUT_WIDGET_SEEDS.FINANCE_CASH_FLOW), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.FINANCE_MAIN, - ), - title: 'Cash Flow', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 6, columnSpan: 6 }, - configuration: { graphType: 'LINE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.FINANCE_EXPENSES_PIE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.FINANCE_MAIN, - ), - title: 'Expenses Distribution', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 6, rowSpan: 6, columnSpan: 6 }, - configuration: { graphType: 'PIE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.FINANCE_PROFIT_MARGIN, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.FINANCE_MAIN, - ), - title: 'Profit Margin', - type: WidgetType.GRAPH, - gridPosition: { row: 6, column: 0, rowSpan: 2, columnSpan: 3 }, - configuration: { graphType: 'NUMBER' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.FINANCE_BUDGET_VARIANCE, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.FINANCE_MAIN, - ), - title: 'Budget Variance', - type: WidgetType.GRAPH, - gridPosition: { row: 6, column: 3, rowSpan: 4, columnSpan: 9 }, - configuration: { graphType: 'BAR' }, - objectMetadataId: null, - }, - - // Executive Main Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.EXECUTIVE_KEY_METRICS, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.EXECUTIVE_MAIN, - ), - title: 'Key Business Metrics', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 3, columnSpan: 12 }, - configuration: { graphType: 'PIE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.EXECUTIVE_COMPANY_HEALTH, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.EXECUTIVE_MAIN, - ), - title: 'Company Health Score', - type: WidgetType.GRAPH, - gridPosition: { row: 3, column: 0, rowSpan: 4, columnSpan: 4 }, - configuration: { graphType: 'GAUGE' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.EXECUTIVE_QUARTERLY_REVIEW, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.EXECUTIVE_MAIN, - ), - title: 'Quarterly Performance', - type: WidgetType.GRAPH, - gridPosition: { row: 3, column: 4, rowSpan: 4, columnSpan: 4 }, - configuration: { graphType: 'BAR' }, - objectMetadataId: null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.EXECUTIVE_STRATEGIC_GOALS, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.EXECUTIVE_MAIN, - ), - title: 'Strategic Goals Progress', - type: WidgetType.GRAPH, - gridPosition: { row: 3, column: 8, rowSpan: 8, columnSpan: 4 }, - configuration: { graphType: 'LINE' }, - objectMetadataId: null, - }, -]; + // Team Metrics Tab Widgets + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.TEAM_CONTACT_ROLES, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS, + ), + title: 'Contact Roles', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 6 }, + configuration: { + graphType: 'PIE', + aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id, + aggregateOperation: AggregateOperations.COUNT, + groupByFieldMetadataId: PERSON_STANDARD_FIELD_IDS.jobTitle, + orderBy: 'VALUE_DESC', + }, + objectMetadataId: personObject?.id ?? null, + }, + { + id: generateSeedId(workspaceId, PAGE_LAYOUT_WIDGET_SEEDS.TEAM_OPEN_TASKS), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS, + ), + title: 'Open Tasks', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 6, rowSpan: 6, columnSpan: 6 }, + configuration: { + graphType: 'NUMBER', + aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id, + aggregateOperation: AggregateOperations.COUNT, + }, + objectMetadataId: taskObject?.id ?? null, + }, + ]; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util.ts index c6e559425a..11327b97db 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util.ts @@ -4,9 +4,6 @@ import { seedBillingSubscriptions } from 'src/engine/workspace-manager/dev-seede import { seedAgents } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util'; import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util'; import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util'; -import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util'; -import { seedPageLayoutTabs } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-tabs.util'; -import { seedPageLayoutWidgets } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util'; import { seedUserWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util'; import { seedUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util'; import { seedWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util'; @@ -17,7 +14,6 @@ type SeedCoreSchemaArgs = { appVersion: string | undefined; seedBilling?: boolean; seedFeatureFlags?: boolean; - seedPageLayouts?: boolean; }; export const seedCoreSchema = async ({ @@ -26,7 +22,6 @@ export const seedCoreSchema = async ({ workspaceId, seedBilling = true, seedFeatureFlags: shouldSeedFeatureFlags = true, - seedPageLayouts: shouldSeedPageLayouts = true, }: SeedCoreSchemaArgs) => { const schemaName = 'core'; @@ -50,10 +45,4 @@ export const seedCoreSchema = async ({ if (seedBilling) { await seedBillingSubscriptions(dataSource, schemaName, workspaceId); } - - if (shouldSeedPageLayouts) { - await seedPageLayouts(dataSource, schemaName, workspaceId); - await seedPageLayoutTabs(dataSource, schemaName, workspaceId); - await seedPageLayoutWidgets(dataSource, schemaName, workspaceId); - } }; 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 2effecdbcb..c0b0a28810 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,20 +1,33 @@ import { type DataSource } from 'typeorm'; +import { validateAndTransformWidgetConfiguration } from 'src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util'; +import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { getPageLayoutWidgetDataSeeds } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util'; export const seedPageLayoutWidgets = async ( dataSource: DataSource, schemaName: string, workspaceId: string, + objectMetadataItems: ObjectMetadataEntity[], ) => { - const pageLayoutWidgets = getPageLayoutWidgetDataSeeds(workspaceId).map( - (widget) => ({ + const pageLayoutWidgets = getPageLayoutWidgetDataSeeds( + workspaceId, + objectMetadataItems, + ).map((widget) => { + const validatedConfiguration = widget.configuration + ? validateAndTransformWidgetConfiguration( + widget.type, + widget.configuration, + ) + : null; + + return { ...widget, workspaceId, gridPosition: widget.gridPosition, - configuration: widget.configuration, - }), - ); + configuration: validatedConfiguration, + }; + }); if (pageLayoutWidgets.length > 0) { await dataSource diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/dashboard-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/dashboard-data-seeds.constant.ts index 573b0bc5bb..ca945f4496 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/dashboard-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/dashboard-data-seeds.constant.ts @@ -26,13 +26,6 @@ export const DASHBOARD_DATA_SEED_IDS = { SALES_OVERVIEW: '20202020-9e82-4342-91ef-c9e70f16a675', CUSTOMER_INSIGHTS: '20202020-d64e-4588-98cc-c56ba821247b', TEAM_PERFORMANCE: '20202020-b888-4c58-8975-76b4c2035d3a', - REVENUE_ANALYTICS: '20202020-842e-4cd0-a205-a5bfb1efdb1c', - MARKETING_METRICS: '20202020-7b87-4522-9fa8-7f97332c4fa7', - SUPPORT_DASHBOARD: '20202020-5404-49ba-a10a-7941205e577f', - PRODUCT_USAGE: '20202020-fce2-4c4c-b1dd-6db836141c11', - OPERATIONS_KPI: '20202020-c1e4-444c-ab36-4fb42d493eb3', - FINANCE_OVERVIEW: '20202020-b993-4601-945b-996ec668f9e9', - EXECUTIVE_SUMMARY: '20202020-d2e3-4304-9c80-b3db67903974', }; export const getDashboardDataSeeds = ( @@ -64,89 +57,11 @@ export const getDashboardDataSeeds = ( }, { id: DASHBOARD_DATA_SEED_IDS.TEAM_PERFORMANCE, - title: 'Team Performance', + title: 'Team & Activity', pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD), createdBySource: 'MANUAL', createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL, createdByName: 'Phil Schiller', position: 2, }, - { - id: DASHBOARD_DATA_SEED_IDS.REVENUE_ANALYTICS, - title: 'Revenue Analytics', - pageLayoutId: generateSeedId( - workspaceId, - PAGE_LAYOUT_SEEDS.REVENUE_ANALYTICS, - ), - createdBySource: 'MANUAL', - createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, - createdByName: 'Tim Apple', - position: 3, - }, - { - id: DASHBOARD_DATA_SEED_IDS.MARKETING_METRICS, - title: 'Marketing Metrics', - pageLayoutId: generateSeedId( - workspaceId, - PAGE_LAYOUT_SEEDS.MARKETING_METRICS, - ), - createdBySource: 'MANUAL', - createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY, - createdByName: 'Jony Ive', - position: 4, - }, - { - id: DASHBOARD_DATA_SEED_IDS.SUPPORT_DASHBOARD, - title: 'Support Dashboard', - pageLayoutId: generateSeedId( - workspaceId, - PAGE_LAYOUT_SEEDS.SUPPORT_DASHBOARD, - ), - createdBySource: 'MANUAL', - createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL, - createdByName: 'Phil Schiller', - position: 5, - }, - { - id: DASHBOARD_DATA_SEED_IDS.PRODUCT_USAGE, - title: 'Product Usage', - pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.PRODUCT_USAGE), - createdBySource: 'MANUAL', - createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, - createdByName: 'Tim Apple', - position: 6, - }, - { - id: DASHBOARD_DATA_SEED_IDS.OPERATIONS_KPI, - title: 'Operations KPI', - pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.OPERATIONS_KPI), - createdBySource: 'MANUAL', - createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY, - createdByName: 'Jony Ive', - position: 7, - }, - { - id: DASHBOARD_DATA_SEED_IDS.FINANCE_OVERVIEW, - title: 'Finance Overview', - pageLayoutId: generateSeedId( - workspaceId, - PAGE_LAYOUT_SEEDS.FINANCE_OVERVIEW, - ), - createdBySource: 'MANUAL', - createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL, - createdByName: 'Phil Schiller', - position: 8, - }, - { - id: DASHBOARD_DATA_SEED_IDS.EXECUTIVE_SUMMARY, - title: 'Executive Summary', - pageLayoutId: generateSeedId( - workspaceId, - PAGE_LAYOUT_SEEDS.EXECUTIVE_SUMMARY, - ), - createdBySource: 'MANUAL', - createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, - createdByName: 'Tim Apple', - position: 9, - }, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts index 6a5e9d68f1..b75fc326f1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts @@ -6,10 +6,14 @@ import { DataSource } from 'typeorm'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service'; import { seedCoreSchema } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util'; +import { seedPageLayoutTabs } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-tabs.util'; +import { seedPageLayoutWidgets } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util'; +import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util'; import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service'; import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service'; import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service'; @@ -73,6 +77,22 @@ export class DevSeederService { await this.devSeederPermissionsService.initPermissions(workspaceId); + await seedPageLayouts(this.coreDataSource, 'core', workspaceId); + await seedPageLayoutTabs(this.coreDataSource, 'core', workspaceId); + + const objectMetadataRepository = + this.coreDataSource.getRepository(ObjectMetadataEntity); + const objectMetadataItems = await objectMetadataRepository.find({ + where: { workspaceId }, + }); + + await seedPageLayoutWidgets( + this.coreDataSource, + 'core', + workspaceId, + objectMetadataItems, + ); + await this.devSeederDataService.seed({ schemaName: dataSourceMetadata.schema, workspaceId, diff --git a/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts b/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts index e0095fd7b1..c51ecf2194 100644 --- a/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts +++ b/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts @@ -18,6 +18,75 @@ export const PAGE_LAYOUT_TAB_GQL_FIELDS = ` deletedAt `; +export const PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS = ` + ... on IframeConfiguration { + url + } + ... on BarChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + groupByFieldMetadataIdX + orderByX + groupByFieldMetadataIdY + orderByY + omitNullValues + xAxisName + yAxisName + rangeMin + rangeMax + filter + color + description + } + ... on LineChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + groupByFieldMetadataIdX + orderByX + groupByFieldMetadataIdY + orderByY + omitNullValues + xAxisName + yAxisName + rangeMin + rangeMax + filter + color + description + } + ... on PieChartConfiguration { + graphType + groupByFieldMetadataId + aggregateFieldMetadataId + aggregateOperation + orderBy + filter + color + description + } + ... on NumberChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + description + filter + color + format + label + } + ... on GaugeChartConfiguration { + graphType + aggregateFieldMetadataId + aggregateOperation + aggregateOperationTotal + aggregateFieldMetadataIdTotal + description + filter + } +`; + export const PAGE_LAYOUT_WIDGET_GQL_FIELDS = ` id title @@ -30,7 +99,9 @@ export const PAGE_LAYOUT_WIDGET_GQL_FIELDS = ` rowSpan columnSpan } - configuration + configuration { + ${PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS} + } createdAt updatedAt deletedAt 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 new file mode 100644 index 0000000000..2cd1ef4278 --- /dev/null +++ b/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts @@ -0,0 +1,226 @@ +import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum'; +import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; + +export const TEST_FIELD_METADATA_ID_1 = '20202020-1111-4111-a111-111111111111'; +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 = { + url: 'https://example.com/dashboard', +}; + +export const TEST_IFRAME_CONFIG_ALTERNATIVE = { + url: 'https://app.twenty.com/analytics', +}; + +export const TEST_NUMBER_CHART_CONFIG = { + graphType: GraphType.NUMBER, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateOperation: AggregateOperations.COUNT, + label: 'Total Records', + description: 'Count of all records', + color: 'blue', + format: '0,0', +}; + +export const TEST_NUMBER_CHART_CONFIG_MINIMAL = { + graphType: GraphType.NUMBER, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateOperation: AggregateOperations.SUM, +}; + +export const TEST_BAR_CHART_CONFIG = { + graphType: GraphType.BAR, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateOperation: AggregateOperations.SUM, + groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2, + orderByX: GraphOrderBy.FIELD_ASC, + xAxisName: 'Month', + yAxisName: 'Revenue', + color: 'red', + description: 'Monthly revenue breakdown', + omitNullValues: true, + rangeMin: 0, + rangeMax: 100000, +}; + +export const TEST_BAR_CHART_CONFIG_MINIMAL = { + graphType: GraphType.BAR, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateOperation: AggregateOperations.COUNT, + groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2, + orderByX: GraphOrderBy.VALUE_DESC, +}; + +export const TEST_LINE_CHART_CONFIG = { + graphType: GraphType.LINE, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateOperation: AggregateOperations.AVG, + groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2, + orderByX: GraphOrderBy.FIELD_ASC, + groupByFieldMetadataIdY: TEST_FIELD_METADATA_ID_3, + orderByY: GraphOrderBy.FIELD_DESC, + xAxisName: 'Date', + yAxisName: 'Average Value', + color: 'cyan', + description: 'Trend over time', + omitNullValues: false, + rangeMin: -100, + rangeMax: 100, +}; + +export const TEST_LINE_CHART_CONFIG_MINIMAL = { + graphType: GraphType.LINE, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateOperation: AggregateOperations.MAX, + groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2, + orderByX: GraphOrderBy.VALUE_ASC, +}; + +export const TEST_PIE_CHART_CONFIG = { + graphType: GraphType.PIE, + groupByFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_2, + aggregateOperation: AggregateOperations.SUM, + orderBy: GraphOrderBy.VALUE_DESC, + color: 'yellow', + description: 'Distribution by category', + filter: { + and: [ + { + field: 'status', + operator: 'eq', + value: 'active', + }, + ], + }, +}; + +export const TEST_PIE_CHART_CONFIG_MINIMAL = { + graphType: GraphType.PIE, + groupByFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_2, + aggregateOperation: AggregateOperations.COUNT, + orderBy: GraphOrderBy.FIELD_ASC, +}; + +export const TEST_GAUGE_CHART_CONFIG = { + graphType: GraphType.GAUGE, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateOperation: AggregateOperations.SUM, + aggregateOperationTotal: AggregateOperations.COUNT, + aggregateFieldMetadataIdTotal: TEST_FIELD_METADATA_ID_2, + description: 'Completion percentage', + filter: { + or: [ + { + field: 'completed', + operator: 'eq', + value: true, + }, + ], + }, +}; + +export const TEST_GAUGE_CHART_CONFIG_MINIMAL = { + graphType: GraphType.GAUGE, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateOperation: AggregateOperations.COUNT_TRUE, + aggregateOperationTotal: AggregateOperations.COUNT, + aggregateFieldMetadataIdTotal: TEST_FIELD_METADATA_ID_2, +}; + +export const INVALID_IFRAME_CONFIG_BAD_URL = { + url: 'not-a-valid-url', +}; + +export const INVALID_IFRAME_CONFIG_MISSING_URL = {}; + +export const INVALID_IFRAME_CONFIG_EMPTY_URL = { + url: '', +}; + +export const INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS = { + graphType: GraphType.NUMBER, +}; + +export const INVALID_NUMBER_CHART_CONFIG_BAD_UUID = { + graphType: GraphType.NUMBER, + aggregateFieldMetadataId: 'not-a-uuid', + aggregateOperation: AggregateOperations.COUNT, +}; + +export const INVALID_NUMBER_CHART_CONFIG_INVALID_OPERATION = { + graphType: GraphType.NUMBER, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateOperation: 'INVALID_OP' as any, +}; + +export const INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY = { + graphType: GraphType.BAR, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateOperation: AggregateOperations.SUM, +}; + +export const INVALID_BAR_CHART_CONFIG_BAD_ORDER_BY = { + graphType: GraphType.BAR, + aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, + aggregateOperation: AggregateOperations.SUM, + groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2, + orderByX: 'INVALID_ORDER' as any, +}; + +export const CONFIG_TYPE_MISMATCH_IFRAME_WITH_GRAPH = { + graphType: GraphType.NUMBER, + url: 'https://example.com', +}; + +export const CONFIG_TYPE_MISMATCH_GRAPH_WITH_IFRAME = { + url: 'https://example.com', +}; + +export const ALL_VALID_GRAPH_CONFIGS = [ + TEST_NUMBER_CHART_CONFIG, + TEST_BAR_CHART_CONFIG, + TEST_LINE_CHART_CONFIG, + TEST_PIE_CHART_CONFIG, + TEST_GAUGE_CHART_CONFIG, +]; + +export const ALL_MINIMAL_GRAPH_CONFIGS = [ + TEST_NUMBER_CHART_CONFIG_MINIMAL, + TEST_BAR_CHART_CONFIG_MINIMAL, + TEST_LINE_CHART_CONFIG_MINIMAL, + TEST_PIE_CHART_CONFIG_MINIMAL, + TEST_GAUGE_CHART_CONFIG_MINIMAL, +]; + +export const ALL_INVALID_CONFIGS = [ + INVALID_IFRAME_CONFIG_BAD_URL, + INVALID_IFRAME_CONFIG_MISSING_URL, + INVALID_IFRAME_CONFIG_EMPTY_URL, + INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS, + INVALID_NUMBER_CHART_CONFIG_BAD_UUID, + INVALID_NUMBER_CHART_CONFIG_INVALID_OPERATION, + INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY, + INVALID_BAR_CHART_CONFIG_BAD_ORDER_BY, +]; + +export function getValidConfigForWidgetType(widgetType: string): any { + switch (widgetType) { + case 'IFRAME': + return TEST_IFRAME_CONFIG; + case 'GRAPH': { + const configs = ALL_VALID_GRAPH_CONFIGS; + + return configs[Math.floor(Math.random() * configs.length)]; + } + case 'VIEW': + case 'FIELDS': + return null; + default: + return null; + } +} diff --git a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts index 022ca36014..6d2fdf848d 100644 --- a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts @@ -1,3 +1,9 @@ +import { + TEST_BAR_CHART_CONFIG_MINIMAL, + TEST_IFRAME_CONFIG, + TEST_IFRAME_CONFIG_ALTERNATIVE, + TEST_NUMBER_CHART_CONFIG_MINIMAL, +} from 'test/integration/constants/widget-configuration-test-data.constants'; import { findPageLayoutOperationFactory } from 'test/integration/graphql/utils/find-page-layout-operation-factory.util'; import { assertGraphQLErrorResponse, @@ -104,13 +110,13 @@ describe('Page Layout Update With Tabs And Widgets Integration', () => { rowSpan: 2, columnSpan: 2, }, - configuration: { viewId: 'test-view-1' }, + configuration: null, }, { id: widgetToUpdateId, pageLayoutTabId: existingTabId, title: 'Widget To Update', - type: WidgetType.FIELDS, + type: WidgetType.GRAPH, objectMetadataId: testObjectMetadataId, gridPosition: { row: 0, @@ -118,7 +124,7 @@ describe('Page Layout Update With Tabs And Widgets Integration', () => { rowSpan: 1, columnSpan: 1, }, - configuration: { fields: ['id', 'name'] }, + configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL, }, { id: widgetToDeleteId, @@ -132,7 +138,7 @@ describe('Page Layout Update With Tabs And Widgets Integration', () => { rowSpan: 1, columnSpan: 1, }, - configuration: { url: 'https://example.com' }, + configuration: TEST_IFRAME_CONFIG, }, ], }, @@ -195,7 +201,7 @@ describe('Page Layout Update With Tabs And Widgets Integration', () => { rowSpan: 2, columnSpan: 2, }, - configuration: { viewId: 'test-view-1' }, + configuration: null, }, { id: widgetToUpdateId, @@ -209,7 +215,7 @@ describe('Page Layout Update With Tabs And Widgets Integration', () => { rowSpan: 2, columnSpan: 3, }, - configuration: { chartType: 'bar', metrics: ['count'] }, + configuration: TEST_BAR_CHART_CONFIG_MINIMAL, }, { id: newWidgetId, @@ -223,7 +229,7 @@ describe('Page Layout Update With Tabs And Widgets Integration', () => { rowSpan: 1, columnSpan: 2, }, - configuration: { fields: ['id', 'name', 'createdAt'] }, + configuration: null, }, ], }, @@ -244,7 +250,7 @@ describe('Page Layout Update With Tabs And Widgets Integration', () => { rowSpan: 1, columnSpan: 1, }, - configuration: { url: 'https://updated-example.com' }, + configuration: TEST_IFRAME_CONFIG_ALTERNATIVE, }, ], }, @@ -329,16 +335,10 @@ describe('Page Layout Update With Tabs And Widgets Integration', () => { expect(updatedWidget?.type).toBe(WidgetType.GRAPH); expect(updatedWidget?.gridPosition.rowSpan).toBe(2); expect(updatedWidget?.gridPosition.columnSpan).toBe(3); - expect(updatedWidget?.configuration?.chartType).toBe('bar'); expect(newWidget).toBeDefined(); expect(newWidget?.title).toBe('New Widget'); expect(newWidget?.type).toBe(WidgetType.FIELDS); - expect(newWidget?.configuration?.fields).toEqual([ - 'id', - 'name', - 'createdAt', - ]); expect(deletedWidget).toBeUndefined(); @@ -353,9 +353,6 @@ describe('Page Layout Update With Tabs And Widgets Integration', () => { expect(anotherNewWidget).toBeDefined(); expect(anotherNewWidget?.title).toBe('Another New Widget'); expect(anotherNewWidget?.type).toBe(WidgetType.IFRAME); - expect(anotherNewWidget?.configuration?.url).toBe( - 'https://updated-example.com', - ); expect(newTab?.widgets).toHaveLength(0); }); @@ -428,5 +425,152 @@ describe('Page Layout Update With Tabs And Widgets Integration', () => { assertGraphQLErrorResponse(response, ErrorCode.BAD_USER_INPUT); }); + + it('should reject invalid widget configurations', async () => { + const pageLayout = await createTestPageLayoutWithGraphQL({ + name: 'Test Invalid Config', + type: PageLayoutType.RECORD_PAGE, + objectMetadataId: testObjectMetadataId, + }); + + const invalidConfigInput = { + name: 'Layout with Invalid Widget Config', + type: PageLayoutType.DASHBOARD, + objectMetadataId: testObjectMetadataId, + tabs: [ + { + id: '20202020-1001-4001-a001-000000000001', + title: 'Tab with Invalid Widget', + position: 1, + widgets: [ + { + id: '20202020-1002-4002-a002-000000000002', + pageLayoutTabId: '20202020-1001-4001-a001-000000000001', + title: 'Invalid Iframe Widget', + type: WidgetType.IFRAME, + objectMetadataId: null, + gridPosition: { + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 1, + }, + configuration: { url: 'not-a-valid-url' }, + }, + ], + }, + ], + }; + + const operation = updatePageLayoutWithTabsOperationFactory({ + pageLayoutId: pageLayout.id, + data: invalidConfigInput, + }); + + const response = await makeGraphqlAPIRequest(operation); + + expect(response.body.errors).toBeDefined(); + expect(response.body.errors[0].message).toContain( + 'Invalid configuration for widget "Invalid Iframe Widget" of type IFRAME', + ); + expect(response.body.errors[0].message).toContain('url must be'); + }); + + it('should accept valid widget configurations for each type', async () => { + const pageLayout = await createTestPageLayoutWithGraphQL({ + name: 'Test Valid Configs', + type: PageLayoutType.RECORD_PAGE, + objectMetadataId: testObjectMetadataId, + }); + + const validConfigInput = { + name: 'Layout with Valid Widget Configs', + type: PageLayoutType.DASHBOARD, + objectMetadataId: testObjectMetadataId, + tabs: [ + { + id: '20202020-1010-4010-a010-101010101010', + title: 'Tab with Valid Widgets', + position: 1, + widgets: [ + { + id: '20202020-1011-4011-a011-111111111111', + pageLayoutTabId: '20202020-1010-4010-a010-101010101010', + title: 'Valid Iframe Widget', + type: WidgetType.IFRAME, + objectMetadataId: null, + gridPosition: { + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 1, + }, + configuration: TEST_IFRAME_CONFIG, + }, + { + id: '20202020-1012-4012-a012-121212121212', + pageLayoutTabId: '20202020-1010-4010-a010-101010101010', + title: 'Valid Graph Widget', + type: WidgetType.GRAPH, + objectMetadataId: testObjectMetadataId, + gridPosition: { + row: 1, + column: 0, + rowSpan: 2, + columnSpan: 2, + }, + configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL, + }, + { + id: '20202020-1013-4013-a013-131313131313', + pageLayoutTabId: '20202020-1010-4010-a010-101010101010', + title: 'Valid View Widget', + type: WidgetType.VIEW, + objectMetadataId: testObjectMetadataId, + gridPosition: { + row: 0, + column: 1, + rowSpan: 1, + columnSpan: 1, + }, + configuration: null, + }, + ], + }, + ], + }; + + const operation = updatePageLayoutWithTabsOperationFactory({ + pageLayoutId: pageLayout.id, + data: validConfigInput, + }); + + const response = await makeGraphqlAPIRequest(operation); + + assertGraphQLSuccessfulResponse(response); + + const result = response.body.data.updatePageLayoutWithTabsAndWidgets; + const widgets = result.tabs[0].widgets; + + expect(widgets).toHaveLength(3); + + const iframeWidget = widgets.find( + (w: any) => w.type === WidgetType.IFRAME, + ); + + expect(iframeWidget.configuration).toBeDefined(); + expect(iframeWidget.configuration.url).toBe(TEST_IFRAME_CONFIG.url); + + const graphWidget = widgets.find((w: any) => w.type === WidgetType.GRAPH); + + expect(graphWidget.configuration).toBeDefined(); + expect(graphWidget.configuration.graphType).toBe( + TEST_NUMBER_CHART_CONFIG_MINIMAL.graphType, + ); + + const viewWidget = widgets.find((w: any) => w.type === WidgetType.VIEW); + + expect(viewWidget.configuration).toBeNull(); + }); }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-widget-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-widget-resolver.integration-spec.ts index 1b3d7635a9..4396d7a137 100644 --- a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-widget-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-widget-resolver.integration-spec.ts @@ -186,7 +186,7 @@ describe('Page Layout Widget Resolver', () => { pageLayoutTabId: testPageLayoutTabId, objectMetadataId: testObjectMetadataId, gridPosition: { row: 1, column: 1, rowSpan: 1, columnSpan: 2 }, - configuration: {}, + configuration: null, }; const widget = await createTestPageLayoutWidgetWithGraphQL(input); @@ -212,7 +212,7 @@ describe('Page Layout Widget Resolver', () => { pageLayoutTabId: testPageLayoutTabId, objectMetadataId: testObjectMetadataId, gridPosition: { row: 2, column: 1, rowSpan: 3, columnSpan: 4 }, - configuration: { theme: 'dark', showBorders: true }, + configuration: null, }; const operation = createPageLayoutWidgetOperationFactory({ data: input }); @@ -257,7 +257,7 @@ describe('Page Layout Widget Resolver', () => { title: 'Updated Widget', type: WidgetType.IFRAME, gridPosition: { row: 1, column: 2, rowSpan: 2, columnSpan: 3 }, - configuration: { url: 'https://twenty.com' }, + configuration: null, }; const operation = updatePageLayoutWidgetOperationFactory({ @@ -414,7 +414,7 @@ describe('Page Layout Widget Resolver', () => { type: WidgetType.GRAPH, pageLayoutTabId: testPageLayoutTabId, gridPosition: { row: 2, column: 1, rowSpan: 1, columnSpan: 2 }, - configuration: {}, + configuration: null, }; const widget = await createTestPageLayoutWidgetWithGraphQL(input); @@ -477,7 +477,7 @@ describe('Page Layout Widget Resolver', () => { type: WidgetType.GRAPH, pageLayoutTabId: separateTab.id, gridPosition: { row: 1, column: 1, rowSpan: 1, columnSpan: 1 }, - configuration: {}, + configuration: null, }; const widget = await createTestPageLayoutWidgetWithGraphQL(input); diff --git a/packages/twenty-server/test/integration/graphql/utils/update-page-layout-with-tabs-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-page-layout-with-tabs-operation-factory.util.ts index f3bf6378fd..ed765619e5 100644 --- a/packages/twenty-server/test/integration/graphql/utils/update-page-layout-with-tabs-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/update-page-layout-with-tabs-operation-factory.util.ts @@ -1,5 +1,8 @@ import gql from 'graphql-tag'; -import { PAGE_LAYOUT_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; +import { + PAGE_LAYOUT_GQL_FIELDS, + PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS, +} from 'test/integration/constants/page-layout-gql-fields.constants'; import { type UpdatePageLayoutWithTabsInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input'; @@ -35,7 +38,9 @@ export const updatePageLayoutWithTabsOperationFactory = ({ rowSpan columnSpan } - configuration + configuration { + ${PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS} + } } } } diff --git a/packages/twenty-server/test/integration/rest/suites/page-layout-widget.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/page-layout-widget.integration-spec.ts index 768778d946..9524df11ab 100644 --- a/packages/twenty-server/test/integration/rest/suites/page-layout-widget.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/page-layout-widget.integration-spec.ts @@ -179,7 +179,7 @@ describe('Page Layout Widget REST API', () => { rowSpan: 2, columnSpan: 2, }, - configuration: { theme: 'dark', showLegend: true }, + configuration: null, }; const pageLayoutWidget = @@ -358,7 +358,7 @@ describe('Page Layout Widget REST API', () => { rowSpan: 2, columnSpan: 3, }, - configuration: { updated: true }, + configuration: null, }; const response = await makeRestAPIRequest({