Connect the bar chart to the group by resolver (#14885)
Closes https://github.com/twentyhq/core-team-issues/issues/1535 Video QA: https://github.com/user-attachments/assets/153fa3f1-08d9-4952-9456-635c602e1f57 https://github.com/user-attachments/assets/d3111afb-4c84-4f57-8a56-5cf666748c86 There are still some formatting and design issues (the labels which are on top of each other and the values which should be formatted) that will be fixed in a future PR --------- Co-authored-by: ehconitin <nitinkoche03@gmail.com>
This commit is contained in:
co-authored by
ehconitin
parent
973b4fef90
commit
875dc1a6b8
@@ -4432,7 +4432,7 @@ 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: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, orderBy: GraphOrderBy, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | 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: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, groupBySubFieldNameX?: string | null, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, groupBySubFieldNameX?: string | null, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupBySubFieldName?: string | null, orderBy: GraphOrderBy, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | null };
|
||||
|
||||
export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
|
||||
id: Scalars['String'];
|
||||
@@ -4440,7 +4440,7 @@ export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
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: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, orderBy: GraphOrderBy, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | null }> | 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: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, groupBySubFieldNameX?: string | null, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, groupBySubFieldNameX?: string | null, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupBySubFieldName?: string | null, orderBy: GraphOrderBy, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | null }> | null }> | null } };
|
||||
|
||||
export type OnDbEventSubscriptionVariables = Exact<{
|
||||
input: OnDbEventInput;
|
||||
@@ -4744,8 +4744,10 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
groupByFieldMetadataIdX
|
||||
groupBySubFieldNameX
|
||||
orderByX
|
||||
groupByFieldMetadataIdY
|
||||
groupBySubFieldNameY
|
||||
orderByY
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
@@ -4761,8 +4763,10 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
groupByFieldMetadataIdX
|
||||
groupBySubFieldNameX
|
||||
orderByX
|
||||
groupByFieldMetadataIdY
|
||||
groupBySubFieldNameY
|
||||
orderByY
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
@@ -4778,6 +4782,7 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
|
||||
groupByFieldMetadataId
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
groupBySubFieldName
|
||||
orderBy
|
||||
displayDataLabel
|
||||
color
|
||||
|
||||
+2
-2
@@ -58,8 +58,8 @@ export const ChartFieldSelectionForAggregateOperationDropdownContent = () => {
|
||||
items: sourceObjectMetadataItem?.fields || [],
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.label, item.name],
|
||||
// TODO: remove this once group by is supported for relation fields
|
||||
}).filter((field) => !isFieldRelation(field));
|
||||
// TODO: remove the relation filter once group by is supported for relation fields
|
||||
}).filter((field) => !isFieldRelation(field) && !field.isSystem);
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
|
||||
+2
-2
@@ -76,8 +76,8 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
|
||||
items: sourceObjectMetadataItem?.fields || [],
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.label, item.name],
|
||||
// TODO: remove this once group by is supported for relation fields
|
||||
}).filter((field) => !isFieldRelation(field)),
|
||||
// TODO: remove the relation filter once group by is supported for relation fields
|
||||
}).filter((field) => !isFieldRelation(field) && !field.isSystem),
|
||||
[sourceObjectMetadataItem?.fields, searchQuery],
|
||||
);
|
||||
|
||||
|
||||
+5
-1
@@ -31,6 +31,7 @@ export const computeAggregateValueAndLabel = ({
|
||||
timeZone,
|
||||
localeCatalog,
|
||||
formatNumberFn,
|
||||
formatShortNumberFn,
|
||||
}: {
|
||||
data: AggregateRecordsData;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
@@ -44,6 +45,7 @@ export const computeAggregateValueAndLabel = ({
|
||||
value: number,
|
||||
options?: Omit<FormatNumberOptions, 'format'>,
|
||||
) => string;
|
||||
formatShortNumberFn?: (value: number) => string | number;
|
||||
}) => {
|
||||
const formatNumber =
|
||||
formatNumberFn ??
|
||||
@@ -95,7 +97,9 @@ export const computeAggregateValueAndLabel = ({
|
||||
switch (field.type) {
|
||||
case FieldMetadataType.CURRENCY: {
|
||||
value = Number(aggregateValue);
|
||||
value = formatToShortNumber(value / 1_000_000);
|
||||
value = isDefined(formatShortNumberFn)
|
||||
? formatShortNumberFn(value / 1_000_000)
|
||||
: formatToShortNumber(value / 1_000_000);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
type PageLayoutInitializationQueryEffectProps = {
|
||||
pageLayoutId: string;
|
||||
onInitialized: (pageLayout: PageLayout) => void;
|
||||
onInitialized?: (pageLayout: PageLayout) => void;
|
||||
};
|
||||
|
||||
export const PageLayoutInitializationQueryEffect = ({
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'react-resizable/css/styles.css';
|
||||
|
||||
type PageLayoutRendererProps = {
|
||||
pageLayoutId: string;
|
||||
onInitialized: (pageLayout: PageLayout) => void;
|
||||
onInitialized?: (pageLayout: PageLayout) => void;
|
||||
};
|
||||
|
||||
export const PageLayoutRenderer = ({
|
||||
|
||||
+99
-14
@@ -1,21 +1,33 @@
|
||||
import { MockedProvider, type MockedResponse } from '@apollo/client/testing';
|
||||
import {
|
||||
type ApolloClient,
|
||||
type NormalizedCacheObject,
|
||||
useApolloClient,
|
||||
} from '@apollo/client';
|
||||
import { type MockedResponse } from '@apollo/client/testing';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLayout';
|
||||
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
|
||||
import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { generateGroupByQuery } from '@/page-layout/widgets/graph/utils/generateGroupByQuery';
|
||||
import {
|
||||
GraphOrderBy,
|
||||
GraphType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
AxisNameDisplay,
|
||||
type BarChartConfiguration,
|
||||
ExtendedAggregateOperations,
|
||||
PageLayoutType,
|
||||
type PageLayoutWidget,
|
||||
} from '~/generated/graphql';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
|
||||
|
||||
const mockPersonObjectMetadataItem = getMockObjectMetadataItemOrThrow('person');
|
||||
|
||||
const validatePageLayoutContent = async (canvasElement: HTMLElement) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -31,7 +43,7 @@ const mixedGraphsPageLayoutMocks = {
|
||||
id: 'mixed-graphs-layout',
|
||||
name: 'Mixed Graph Dashboard',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
objectMetadataId: mockPersonObjectMetadataItem.id,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
@@ -52,7 +64,7 @@ const mixedGraphsPageLayoutMocks = {
|
||||
pageLayoutTabId: 'mixed-tab',
|
||||
type: WidgetType.GRAPH,
|
||||
title: 'Revenue',
|
||||
objectMetadataId: null,
|
||||
objectMetadataId: mockPersonObjectMetadataItem.id,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 0,
|
||||
@@ -76,7 +88,7 @@ const mixedGraphsPageLayoutMocks = {
|
||||
pageLayoutTabId: 'mixed-tab',
|
||||
type: WidgetType.GRAPH,
|
||||
title: 'Goal Progress',
|
||||
objectMetadataId: null,
|
||||
objectMetadataId: mockPersonObjectMetadataItem.id,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 0,
|
||||
@@ -101,7 +113,7 @@ const mixedGraphsPageLayoutMocks = {
|
||||
pageLayoutTabId: 'mixed-tab',
|
||||
type: WidgetType.GRAPH,
|
||||
title: 'Revenue Sources',
|
||||
objectMetadataId: null,
|
||||
objectMetadataId: mockPersonObjectMetadataItem.id,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 0,
|
||||
@@ -127,7 +139,7 @@ const mixedGraphsPageLayoutMocks = {
|
||||
pageLayoutTabId: 'mixed-tab',
|
||||
type: WidgetType.GRAPH,
|
||||
title: 'Quarterly Comparison',
|
||||
objectMetadataId: null,
|
||||
objectMetadataId: mockPersonObjectMetadataItem.id,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 2,
|
||||
@@ -139,10 +151,16 @@ const mixedGraphsPageLayoutMocks = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
graphType: GraphType.BAR,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
groupByFieldMetadataIdX: 'createdAt',
|
||||
aggregateFieldMetadataId: mockPersonObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'name',
|
||||
)?.id,
|
||||
groupByFieldMetadataIdX: mockPersonObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'createdAt',
|
||||
)?.id,
|
||||
orderByX: GraphOrderBy.FIELD_ASC,
|
||||
},
|
||||
axisNameDisplay: AxisNameDisplay.BOTH,
|
||||
displayDataLabel: false,
|
||||
} satisfies BarChartConfiguration,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
@@ -152,6 +170,11 @@ const mixedGraphsPageLayoutMocks = {
|
||||
],
|
||||
};
|
||||
|
||||
const barChartGroupByQuery = generateGroupByQuery({
|
||||
objectMetadataItem: mockPersonObjectMetadataItem,
|
||||
aggregateOperations: ['totalCount'],
|
||||
});
|
||||
|
||||
const graphqlMocks: MockedResponse[] = [
|
||||
{
|
||||
request: {
|
||||
@@ -166,19 +189,81 @@ const graphqlMocks: MockedResponse[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: barChartGroupByQuery,
|
||||
variables: {
|
||||
groupBy: [
|
||||
{
|
||||
createdAt: {
|
||||
granularity: 'DAY',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
peopleGroupBy: [
|
||||
{
|
||||
groupByDimensionValues: ['2024-01-15T00:00:00.000Z'],
|
||||
totalCount: 12,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-02-15T00:00:00.000Z'],
|
||||
totalCount: 18,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-03-15T00:00:00.000Z'],
|
||||
totalCount: 25,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-04-15T00:00:00.000Z'],
|
||||
totalCount: 15,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-05-15T00:00:00.000Z'],
|
||||
totalCount: 22,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-06-15T00:00:00.000Z'],
|
||||
totalCount: 30,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const CoreClientProviderWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const apolloClient = useApolloClient() as ApolloClient<NormalizedCacheObject>;
|
||||
|
||||
return (
|
||||
<ApolloCoreClientContext.Provider value={apolloClient}>
|
||||
{children}
|
||||
</ApolloCoreClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: graphqlMocks,
|
||||
});
|
||||
|
||||
const meta: Meta<typeof PageLayoutRenderer> = {
|
||||
title: 'Modules/PageLayout/PageLayoutRenderer',
|
||||
component: PageLayoutRenderer,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<MemoryRouter>
|
||||
<MockedProvider mocks={graphqlMocks} addTypename={false}>
|
||||
<RecoilRoot>
|
||||
<JestMetadataAndApolloMocksWrapper>
|
||||
<CoreClientProviderWrapper>
|
||||
<Story />
|
||||
</RecoilRoot>
|
||||
</MockedProvider>
|
||||
</CoreClientProviderWrapper>
|
||||
</JestMetadataAndApolloMocksWrapper>
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
|
||||
+5
@@ -21,8 +21,10 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
groupByFieldMetadataIdX
|
||||
groupBySubFieldNameX
|
||||
orderByX
|
||||
groupByFieldMetadataIdY
|
||||
groupBySubFieldNameY
|
||||
orderByY
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
@@ -38,8 +40,10 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
groupByFieldMetadataIdX
|
||||
groupBySubFieldNameX
|
||||
orderByX
|
||||
groupByFieldMetadataIdY
|
||||
groupBySubFieldNameY
|
||||
orderByY
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
@@ -55,6 +59,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
groupByFieldMetadataId
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
groupBySubFieldName
|
||||
orderBy
|
||||
displayDataLabel
|
||||
color
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export const getGroupByQueryName = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
): string => {
|
||||
return `${objectMetadataItem.namePlural}GroupBy`;
|
||||
};
|
||||
+2
-23
@@ -1,17 +1,10 @@
|
||||
import { getDefaultWidgetData } from '@/page-layout/utils/getDefaultWidgetData';
|
||||
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
|
||||
import { GraphWidgetBarChartRenderer } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer';
|
||||
import { GraphWidgetNumberChart } from '@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { GraphType, type PageLayoutWidget } from '~/generated-metadata/graphql';
|
||||
|
||||
const GraphWidgetBarChart = lazy(() =>
|
||||
import(
|
||||
'@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart'
|
||||
).then((module) => ({
|
||||
default: module.GraphWidgetBarChart,
|
||||
})),
|
||||
);
|
||||
|
||||
const GraphWidgetLineChart = lazy(() =>
|
||||
import(
|
||||
'@/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart'
|
||||
@@ -100,21 +93,7 @@ export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => {
|
||||
);
|
||||
|
||||
case GraphType.BAR:
|
||||
return (
|
||||
<Suspense fallback={<ChartSkeletonLoader />}>
|
||||
<GraphWidgetBarChart
|
||||
data={data.items}
|
||||
indexBy={data.indexBy}
|
||||
keys={data.keys}
|
||||
seriesLabels={data.seriesLabels}
|
||||
layout={data.layout}
|
||||
showLegend
|
||||
showGrid
|
||||
displayType="number"
|
||||
id={`bar-chart-${widget.id}`}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
return <GraphWidgetBarChartRenderer widget={widget} />;
|
||||
|
||||
case GraphType.LINE:
|
||||
return (
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
|
||||
import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { GraphWidgetBarChart } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart';
|
||||
import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing';
|
||||
|
||||
const meta: Meta<typeof GraphWidgetBarChart> = {
|
||||
title: 'Modules/PageLayout/Widgets/GraphWidgetBarChart',
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const GRAPH_DEFAULT_AGGREGATE_VALUE = 0;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
|
||||
export const GRAPH_DEFAULT_COLOR: GraphColor = 'blue';
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
|
||||
// Default date granularity for grouping date fields in graphs
|
||||
// TODO: This will be configurable in the graph editor in the future
|
||||
export const GRAPH_DEFAULT_DATE_GRANULARITY =
|
||||
ObjectRecordGroupByDateGranularity.DAY;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const GRAPH_MAXIMUM_NUMBER_OF_GROUPS = 50;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
|
||||
import { useGraphBarChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type PageLayoutWidget,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const GraphWidgetBarChart = lazy(() =>
|
||||
import(
|
||||
'@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart'
|
||||
).then((module) => ({
|
||||
default: module.GraphWidgetBarChart,
|
||||
})),
|
||||
);
|
||||
|
||||
export const GraphWidgetBarChartRenderer = ({
|
||||
widget,
|
||||
}: {
|
||||
widget: PageLayoutWidget;
|
||||
}) => {
|
||||
const {
|
||||
data,
|
||||
indexBy,
|
||||
keys,
|
||||
series,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
showDataLabels,
|
||||
loading,
|
||||
error,
|
||||
} = useGraphBarChartWidgetData({
|
||||
objectMetadataItemId: widget.objectMetadataId,
|
||||
configuration: widget.configuration as BarChartConfiguration,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <ChartSkeletonLoader />;
|
||||
}
|
||||
|
||||
if (isDefined(error)) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<ChartSkeletonLoader />}>
|
||||
<GraphWidgetBarChart
|
||||
data={data}
|
||||
series={series}
|
||||
indexBy={indexBy}
|
||||
keys={keys}
|
||||
xAxisLabel={xAxisLabel}
|
||||
yAxisLabel={yAxisLabel}
|
||||
showValues={showDataLabels}
|
||||
id={widget.id}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
+1
@@ -36,6 +36,7 @@ export const useBarChartData = ({
|
||||
() => new Map<string, BarChartSeries>(series?.map((s) => [s.key, s]) || []),
|
||||
[series],
|
||||
);
|
||||
|
||||
const barConfigs = useMemo((): BarChartConfig[] => {
|
||||
return data.flatMap((dataPoint, dataIndex) => {
|
||||
const indexValue = dataPoint[indexBy];
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery';
|
||||
import { transformGroupByDataToBarChartData } from '@/page-layout/widgets/graph/utils/transformGroupByDataToBarChartData';
|
||||
import { useMemo } from 'react';
|
||||
import { type BarChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type UseGraphBarChartWidgetDataProps = {
|
||||
objectMetadataItemId: string;
|
||||
configuration: BarChartConfiguration;
|
||||
};
|
||||
|
||||
type UseGraphBarChartWidgetDataResult = {
|
||||
data: BarChartDataItem[];
|
||||
indexBy: string;
|
||||
keys: string[];
|
||||
series: BarChartSeries[];
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
showDataLabels: boolean;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
export const useGraphBarChartWidgetData = ({
|
||||
objectMetadataItemId,
|
||||
configuration,
|
||||
}: UseGraphBarChartWidgetDataProps): UseGraphBarChartWidgetDataResult => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
|
||||
const {
|
||||
data: groupByData,
|
||||
loading,
|
||||
error,
|
||||
aggregateOperation,
|
||||
} = useGraphWidgetGroupByQuery({
|
||||
objectMetadataItemId,
|
||||
configuration,
|
||||
});
|
||||
|
||||
const transformedData = useMemo(
|
||||
() =>
|
||||
transformGroupByDataToBarChartData({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
}),
|
||||
[groupByData, objectMetadataItem, configuration, aggregateOperation],
|
||||
);
|
||||
|
||||
return {
|
||||
...transformedData,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
|
||||
import { generateGroupByQuery } from '@/page-layout/widgets/graph/utils/generateGroupByQuery';
|
||||
import { generateGroupByQueryVariablesFromBarChartConfiguration } from '@/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type BarChartConfiguration } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGraphWidgetGroupByQuery = ({
|
||||
objectMetadataItemId,
|
||||
configuration,
|
||||
}: {
|
||||
objectMetadataItemId: string;
|
||||
configuration: BarChartConfiguration;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
|
||||
const availableAggregations = useMemo(
|
||||
() =>
|
||||
getAvailableAggregationsFromObjectFields(
|
||||
objectMetadataItem.readableFields,
|
||||
),
|
||||
[objectMetadataItem.readableFields],
|
||||
);
|
||||
|
||||
const aggregateFieldId = configuration.aggregateFieldMetadataId;
|
||||
|
||||
const aggregateField = objectMetadataItem.readableFields.find(
|
||||
(field) => field.id === aggregateFieldId,
|
||||
);
|
||||
|
||||
if (!isDefined(aggregateField)) {
|
||||
throw new Error('Aggregate field not found');
|
||||
}
|
||||
|
||||
const aggregateOperation =
|
||||
availableAggregations[aggregateField.name]?.[
|
||||
configuration.aggregateOperation
|
||||
];
|
||||
|
||||
if (!isDefined(aggregateOperation)) {
|
||||
throw new Error('Aggregate operation not found');
|
||||
}
|
||||
|
||||
const query = generateGroupByQuery({
|
||||
objectMetadataItem,
|
||||
aggregateOperations: [aggregateOperation],
|
||||
});
|
||||
|
||||
const variables = generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem,
|
||||
barChartConfiguration: configuration,
|
||||
});
|
||||
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { data, loading, error, refetch } = useQuery(query, {
|
||||
client: apolloCoreClient,
|
||||
variables,
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
variables,
|
||||
aggregateOperation,
|
||||
};
|
||||
};
|
||||
@@ -7,4 +7,5 @@ export type GraphColor =
|
||||
| 'yellow'
|
||||
| 'red'
|
||||
| 'green'
|
||||
| 'sky';
|
||||
| 'sky'
|
||||
| 'gray';
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export type GroupByRawResult = {
|
||||
groupByDimensionValues: unknown[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`formatDateByGranularity quarter calculations should calculate quarter for QUARTER with date 2024-01-15T00:00:00.000Z 1`] = `"Q1 2024"`;
|
||||
|
||||
exports[`formatDateByGranularity quarter calculations should calculate quarter for QUARTER with date 2024-04-15T00:00:00.000Z 1`] = `"Q2 2024"`;
|
||||
|
||||
exports[`formatDateByGranularity quarter calculations should calculate quarter for QUARTER with date 2024-07-15T00:00:00.000Z 1`] = `"Q3 2024"`;
|
||||
|
||||
exports[`formatDateByGranularity quarter calculations should calculate quarter for QUARTER with date 2024-10-15T00:00:00.000Z 1`] = `"Q4 2024"`;
|
||||
|
||||
exports[`formatDateByGranularity should format date for DAY granularity 1`] = `"Mar 20, 2024"`;
|
||||
|
||||
exports[`formatDateByGranularity should format date for MONTH granularity 1`] = `"March 2024"`;
|
||||
|
||||
exports[`formatDateByGranularity should format date for NONE granularity 1`] = `"3/20/2024"`;
|
||||
|
||||
exports[`formatDateByGranularity should format date for QUARTER granularity 1`] = `"Q1 2024"`;
|
||||
|
||||
exports[`formatDateByGranularity should format date for YEAR granularity 1`] = `"2024"`;
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`generateGroupByQuery should generate valid GraphQL query for empty aggregate operations 1`] = `
|
||||
"
|
||||
query PeopleGroupBy(
|
||||
$groupBy: [PersonGroupByInput!]
|
||||
$filter: PersonFilterInput
|
||||
$orderBy: [PersonOrderByWithGroupByInput!]
|
||||
$viewId: UUID
|
||||
) {
|
||||
peopleGroupBy(
|
||||
groupBy: $groupBy
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
viewId: $viewId
|
||||
) {
|
||||
groupByDimensionValues
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQuery should generate valid GraphQL query for multiple aggregate operations 1`] = `
|
||||
"
|
||||
query OpportunitiesGroupBy(
|
||||
$groupBy: [OpportunityGroupByInput!]
|
||||
$filter: OpportunityFilterInput
|
||||
$orderBy: [OpportunityOrderByWithGroupByInput!]
|
||||
$viewId: UUID
|
||||
) {
|
||||
opportunitiesGroupBy(
|
||||
groupBy: $groupBy
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
viewId: $viewId
|
||||
) {
|
||||
groupByDimensionValues
|
||||
totalCount
|
||||
sumAmountAmountMicros
|
||||
avgAmountAmountMicros
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQuery should generate valid GraphQL query for single aggregate operation 1`] = `
|
||||
"
|
||||
query OpportunitiesGroupBy(
|
||||
$groupBy: [OpportunityGroupByInput!]
|
||||
$filter: OpportunityFilterInput
|
||||
$orderBy: [OpportunityOrderByWithGroupByInput!]
|
||||
$viewId: UUID
|
||||
) {
|
||||
opportunitiesGroupBy(
|
||||
groupBy: $groupBy
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
viewId: $viewId
|
||||
) {
|
||||
groupByDimensionValues
|
||||
sumAmountAmountMicros
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromBarChartConfiguration should generate variables with composite field 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"name": {
|
||||
"firstName": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromBarChartConfiguration should generate variables with single groupBy field 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromBarChartConfiguration should generate variables with two groupBy fields 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
{
|
||||
"ownerId": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import { buildGroupByFieldObject } from '@/page-layout/widgets/graph/utils/buildGroupByFieldObject';
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
describe('buildGroupByFieldObject', () => {
|
||||
it('should return field with Id suffix for relation fields', () => {
|
||||
const field = {
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
} as any;
|
||||
|
||||
const result = buildGroupByFieldObject({ field });
|
||||
|
||||
expect(result).toEqual({ companyId: true });
|
||||
});
|
||||
|
||||
it('should return nested object for composite fields with subfield', () => {
|
||||
const field = {
|
||||
name: 'name',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
} as any;
|
||||
|
||||
const result = buildGroupByFieldObject({
|
||||
field,
|
||||
subFieldName: 'firstName',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
name: {
|
||||
firstName: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error for composite field without subfield', () => {
|
||||
const field = {
|
||||
name: 'name',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
} as any;
|
||||
|
||||
expect(() => buildGroupByFieldObject({ field })).toThrow(
|
||||
'Composite field name requires a subfield to be specified',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return field with default granularity for DATE field', () => {
|
||||
const field = {
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE,
|
||||
} as any;
|
||||
|
||||
const result = buildGroupByFieldObject({ field });
|
||||
|
||||
expect(result).toEqual({
|
||||
createdAt: {
|
||||
granularity: ObjectRecordGroupByDateGranularity.DAY,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return field with default granularity for DATE_TIME field', () => {
|
||||
const field = {
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
} as any;
|
||||
|
||||
const result = buildGroupByFieldObject({ field });
|
||||
|
||||
expect(result).toEqual({
|
||||
updatedAt: {
|
||||
granularity: ObjectRecordGroupByDateGranularity.DAY,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return field with custom granularity for date field', () => {
|
||||
const field = {
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE,
|
||||
} as any;
|
||||
|
||||
const result = buildGroupByFieldObject({
|
||||
field,
|
||||
dateGranularity: ObjectRecordGroupByDateGranularity.MONTH,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
createdAt: {
|
||||
granularity: ObjectRecordGroupByDateGranularity.MONTH,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return simple field object for regular fields', () => {
|
||||
const field = {
|
||||
name: 'status',
|
||||
type: FieldMetadataType.TEXT,
|
||||
} as any;
|
||||
|
||||
const result = buildGroupByFieldObject({ field });
|
||||
|
||||
expect(result).toEqual({ status: true });
|
||||
});
|
||||
});
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { computeAggregateNumericValueForGraph } from '../computeAggregateNumericValueForGraph';
|
||||
|
||||
describe('computeAggregateNumericValueForGraph', () => {
|
||||
it('should throw error for empty data', () => {
|
||||
expect(() =>
|
||||
computeAggregateNumericValueForGraph({
|
||||
data: {},
|
||||
objectMetadataItem: { fields: [] } as any,
|
||||
}),
|
||||
).toThrow('Empty aggregate records data');
|
||||
});
|
||||
|
||||
it('should return count value for COUNT operation', () => {
|
||||
const result = computeAggregateNumericValueForGraph({
|
||||
data: { status: { COUNT: 25 } },
|
||||
objectMetadataItem: {
|
||||
fields: [{ id: 'field-1', name: 'status' }],
|
||||
} as any,
|
||||
fieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
});
|
||||
|
||||
expect(result).toBe(25);
|
||||
});
|
||||
|
||||
it('should multiply by 100 for percentage operations', () => {
|
||||
const result = computeAggregateNumericValueForGraph({
|
||||
data: { status: { PERCENTAGE_EMPTY: 0.25 } },
|
||||
objectMetadataItem: {
|
||||
fields: [{ id: 'field-1', name: 'status' }],
|
||||
} as any,
|
||||
fieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.PERCENTAGE_EMPTY,
|
||||
});
|
||||
|
||||
expect(result).toBe(25);
|
||||
});
|
||||
|
||||
it('should return numeric value for NUMBER field', () => {
|
||||
const result = computeAggregateNumericValueForGraph({
|
||||
data: { amount: { SUM: 1000 } },
|
||||
objectMetadataItem: {
|
||||
fields: [
|
||||
{ id: 'field-1', name: 'amount', type: FieldMetadataType.NUMBER },
|
||||
],
|
||||
} as any,
|
||||
fieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
});
|
||||
|
||||
expect(result).toBe(1000);
|
||||
});
|
||||
|
||||
it('should return timestamp for DATE field', () => {
|
||||
const dateString = '2024-01-15';
|
||||
const result = computeAggregateNumericValueForGraph({
|
||||
data: { createdAt: { MIN: dateString } },
|
||||
objectMetadataItem: {
|
||||
fields: [
|
||||
{ id: 'field-1', name: 'createdAt', type: FieldMetadataType.DATE },
|
||||
],
|
||||
} as any,
|
||||
fieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.MIN,
|
||||
});
|
||||
|
||||
expect(result).toBe(new Date(dateString).getTime());
|
||||
});
|
||||
});
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { formatDateByGranularity } from '../formatDateByGranularity';
|
||||
|
||||
describe('formatDateByGranularity', () => {
|
||||
const testDate = new Date('2024-03-20T12:00:00Z');
|
||||
|
||||
beforeAll(() => {
|
||||
// Mock toLocaleDateString to avoid timezone/locale issues
|
||||
jest
|
||||
.spyOn(Date.prototype, 'toLocaleDateString')
|
||||
.mockImplementation((_locales, options) => {
|
||||
if (options?.weekday === 'long') return 'Wednesday';
|
||||
if (options?.month === 'long' && !isDefined(options?.year))
|
||||
return 'March';
|
||||
if (options?.month === 'long' && isDefined(options?.year))
|
||||
return 'March 2024';
|
||||
if (options?.month === 'short') return 'Mar 20, 2024';
|
||||
return '3/20/2024';
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const testCases: {
|
||||
granularity:
|
||||
| ObjectRecordGroupByDateGranularity.DAY
|
||||
| ObjectRecordGroupByDateGranularity.MONTH
|
||||
| ObjectRecordGroupByDateGranularity.QUARTER
|
||||
| ObjectRecordGroupByDateGranularity.YEAR
|
||||
| ObjectRecordGroupByDateGranularity.NONE;
|
||||
}[] = [
|
||||
{ granularity: ObjectRecordGroupByDateGranularity.DAY },
|
||||
{ granularity: ObjectRecordGroupByDateGranularity.MONTH },
|
||||
{ granularity: ObjectRecordGroupByDateGranularity.QUARTER },
|
||||
{ granularity: ObjectRecordGroupByDateGranularity.YEAR },
|
||||
{ granularity: ObjectRecordGroupByDateGranularity.NONE },
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
'should format date for $granularity granularity',
|
||||
({ granularity }) => {
|
||||
expect(formatDateByGranularity(testDate, granularity)).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
describe('quarter calculations', () => {
|
||||
const quarterTestCases: {
|
||||
date: Date;
|
||||
granularity: ObjectRecordGroupByDateGranularity.QUARTER;
|
||||
}[] = [
|
||||
{
|
||||
date: new Date('2024-01-15'),
|
||||
granularity: ObjectRecordGroupByDateGranularity.QUARTER,
|
||||
},
|
||||
{
|
||||
date: new Date('2024-04-15'),
|
||||
granularity: ObjectRecordGroupByDateGranularity.QUARTER,
|
||||
},
|
||||
{
|
||||
date: new Date('2024-07-15'),
|
||||
granularity: ObjectRecordGroupByDateGranularity.QUARTER,
|
||||
},
|
||||
{
|
||||
date: new Date('2024-10-15'),
|
||||
granularity: ObjectRecordGroupByDateGranularity.QUARTER,
|
||||
},
|
||||
];
|
||||
|
||||
it.each(quarterTestCases)(
|
||||
'should calculate quarter for $granularity with date $date',
|
||||
({ date, granularity }) => {
|
||||
expect(formatDateByGranularity(date, granularity)).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { generateGroupByQuery } from '../generateGroupByQuery';
|
||||
|
||||
describe('generateGroupByQuery', () => {
|
||||
const testCases = [
|
||||
{
|
||||
description: 'single aggregate operation',
|
||||
objectMetadataItem: {
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
},
|
||||
aggregateOperations: ['sumAmountAmountMicros'],
|
||||
},
|
||||
{
|
||||
description: 'multiple aggregate operations',
|
||||
objectMetadataItem: {
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
},
|
||||
aggregateOperations: [
|
||||
'totalCount',
|
||||
'sumAmountAmountMicros',
|
||||
'avgAmountAmountMicros',
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'empty aggregate operations',
|
||||
objectMetadataItem: {
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
aggregateOperations: [],
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
'should generate valid GraphQL query for $description',
|
||||
({ objectMetadataItem, aggregateOperations }) => {
|
||||
const result = generateGroupByQuery({
|
||||
objectMetadataItem: objectMetadataItem as ObjectMetadataItem,
|
||||
aggregateOperations,
|
||||
});
|
||||
|
||||
expect(result.loc?.source.body).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
FieldMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { generateGroupByQueryVariablesFromBarChartConfiguration } from '../generateGroupByQueryVariablesFromBarChartConfiguration';
|
||||
|
||||
describe('generateGroupByQueryVariablesFromBarChartConfiguration', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'obj-1',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [
|
||||
{
|
||||
id: 'field-1',
|
||||
name: 'stage',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
id: 'field-2',
|
||||
name: 'owner',
|
||||
type: FieldMetadataType.RELATION,
|
||||
},
|
||||
{
|
||||
id: 'field-3',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
},
|
||||
{
|
||||
id: 'field-4',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
},
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
it('should generate variables with single groupBy field', () => {
|
||||
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
barChartConfiguration: {
|
||||
groupByFieldMetadataIdX: 'field-1',
|
||||
groupBySubFieldNameX: null,
|
||||
} as BarChartConfiguration,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with two groupBy fields', () => {
|
||||
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
barChartConfiguration: {
|
||||
groupByFieldMetadataIdX: 'field-1',
|
||||
groupBySubFieldNameX: null,
|
||||
groupByFieldMetadataIdY: 'field-2',
|
||||
groupBySubFieldNameY: null,
|
||||
} as BarChartConfiguration,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with composite field', () => {
|
||||
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
barChartConfiguration: {
|
||||
groupByFieldMetadataIdX: 'field-4',
|
||||
groupBySubFieldNameX: 'firstName',
|
||||
} as BarChartConfiguration,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should throw error when groupBy field X not found', () => {
|
||||
expect(() =>
|
||||
generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
barChartConfiguration: {
|
||||
groupByFieldMetadataIdX: 'invalid-field',
|
||||
groupBySubFieldNameX: null,
|
||||
} as BarChartConfiguration,
|
||||
}),
|
||||
).toThrow('Field with id invalid-field not found in object metadata');
|
||||
});
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { getFieldKey } from '../getFieldKey';
|
||||
|
||||
describe('getFieldKey', () => {
|
||||
it('should return field name for non-composite field', () => {
|
||||
const field = {
|
||||
name: 'companyName',
|
||||
type: FieldMetadataType.TEXT,
|
||||
} as FieldMetadataItem;
|
||||
|
||||
expect(getFieldKey({ field })).toBe('companyName');
|
||||
});
|
||||
|
||||
it('should return composite key for composite field with subFieldName', () => {
|
||||
const field = {
|
||||
name: 'address',
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
} as FieldMetadataItem;
|
||||
|
||||
expect(getFieldKey({ field, subFieldName: 'city' })).toBe('address.city');
|
||||
});
|
||||
|
||||
it('should return field name when subFieldName is null', () => {
|
||||
const field = {
|
||||
name: 'address',
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
} as FieldMetadataItem;
|
||||
|
||||
expect(getFieldKey({ field, subFieldName: null })).toBe('address');
|
||||
});
|
||||
});
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
|
||||
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const buildGroupByFieldObject = ({
|
||||
field,
|
||||
subFieldName,
|
||||
dateGranularity,
|
||||
}: {
|
||||
field: FieldMetadataItem;
|
||||
subFieldName?: string | null;
|
||||
dateGranularity?: ObjectRecordGroupByDateGranularity;
|
||||
}): Record<string, boolean | Record<string, boolean | string>> => {
|
||||
const isRelation = isFieldRelation(field) || isFieldMorphRelation(field);
|
||||
const isComposite = isCompositeFieldType(field.type);
|
||||
const isDateField =
|
||||
field.type === FieldMetadataType.DATE ||
|
||||
field.type === FieldMetadataType.DATE_TIME;
|
||||
|
||||
if (isRelation) {
|
||||
return { [`${field.name}Id`]: true };
|
||||
}
|
||||
|
||||
if (isComposite) {
|
||||
if (!isDefined(subFieldName)) {
|
||||
throw new Error(
|
||||
`Composite field ${field.name} requires a subfield to be specified`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
[field.name]: {
|
||||
[subFieldName]: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isDateField) {
|
||||
return {
|
||||
[field.name]: {
|
||||
granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { [field.name]: true };
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords';
|
||||
import { type AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
||||
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { GRAPH_DEFAULT_AGGREGATE_VALUE } from '@/page-layout/widgets/graph/constants/GraphDefaultAggregateValue.constant';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const computeAggregateNumericValueForGraph = ({
|
||||
data,
|
||||
objectMetadataItem,
|
||||
fieldMetadataId,
|
||||
aggregateOperation,
|
||||
}: {
|
||||
data: AggregateRecordsData;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
fieldMetadataId?: string | null;
|
||||
aggregateOperation?: ExtendedAggregateOperations | null;
|
||||
}): number => {
|
||||
if (isEmpty(data)) {
|
||||
throw new Error('Empty aggregate records data');
|
||||
}
|
||||
|
||||
const field = objectMetadataItem.fields?.find(
|
||||
(field) => field.id === fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(field)) {
|
||||
throw new Error(`Field with id ${fieldMetadataId} not found`);
|
||||
}
|
||||
|
||||
if (!isDefined(aggregateOperation)) {
|
||||
throw new Error(`Missing aggregate operation for field ${field.name}`);
|
||||
}
|
||||
|
||||
const aggregateValue = data[field.name]?.[aggregateOperation];
|
||||
|
||||
if (
|
||||
COUNT_AGGREGATE_OPERATION_OPTIONS.includes(
|
||||
aggregateOperation as AggregateOperations,
|
||||
)
|
||||
) {
|
||||
return isDefined(aggregateValue)
|
||||
? Number(aggregateValue)
|
||||
: GRAPH_DEFAULT_AGGREGATE_VALUE;
|
||||
}
|
||||
|
||||
if (!isDefined(aggregateValue)) {
|
||||
return GRAPH_DEFAULT_AGGREGATE_VALUE;
|
||||
}
|
||||
|
||||
if (
|
||||
PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(
|
||||
aggregateOperation as AggregateOperations,
|
||||
)
|
||||
) {
|
||||
return Number(aggregateValue) * 100;
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case FieldMetadataType.CURRENCY:
|
||||
case FieldMetadataType.NUMBER: {
|
||||
return Number(aggregateValue);
|
||||
}
|
||||
|
||||
case FieldMetadataType.DATE_TIME:
|
||||
case FieldMetadataType.DATE: {
|
||||
return new Date(aggregateValue as string).getTime();
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported field type ${field.type}`);
|
||||
}
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { computeAggregateNumericValueForGraph } from '@/page-layout/widgets/graph/utils/computeAggregateNumericValueForGraph';
|
||||
|
||||
type GroupByRawResult = {
|
||||
groupByDimensionValues: unknown[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type ComputeAggregateFromGroupByResultParams = {
|
||||
rawResult: GroupByRawResult;
|
||||
aggregateField: FieldMetadataItem;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
aggregateOperationFromRawResult: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const computeAggregateValueFromGroupByResult = ({
|
||||
rawResult,
|
||||
aggregateField,
|
||||
aggregateOperation,
|
||||
aggregateOperationFromRawResult,
|
||||
objectMetadataItem,
|
||||
}: ComputeAggregateFromGroupByResultParams): number => {
|
||||
const transformedData = {
|
||||
[aggregateField.name]: {
|
||||
[aggregateOperation]: rawResult[aggregateOperationFromRawResult],
|
||||
},
|
||||
};
|
||||
|
||||
return computeAggregateNumericValueForGraph({
|
||||
data: transformedData,
|
||||
objectMetadataItem,
|
||||
fieldMetadataId: aggregateField.id,
|
||||
aggregateOperation: aggregateOperation,
|
||||
});
|
||||
};
|
||||
+8
@@ -80,4 +80,12 @@ export const createGraphColorRegistry = (
|
||||
},
|
||||
solid: theme.color.sky,
|
||||
},
|
||||
gray: {
|
||||
name: 'gray',
|
||||
gradient: {
|
||||
normal: [theme.adaptiveColors.gray1, theme.adaptiveColors.gray2],
|
||||
hover: [theme.adaptiveColors.gray3, theme.adaptiveColors.gray4],
|
||||
},
|
||||
solid: theme.color.gray,
|
||||
},
|
||||
});
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
|
||||
export const formatDateByGranularity = (
|
||||
date: Date,
|
||||
granularity:
|
||||
| ObjectRecordGroupByDateGranularity.DAY
|
||||
| ObjectRecordGroupByDateGranularity.MONTH
|
||||
| ObjectRecordGroupByDateGranularity.QUARTER
|
||||
| ObjectRecordGroupByDateGranularity.YEAR
|
||||
| ObjectRecordGroupByDateGranularity.NONE,
|
||||
): string => {
|
||||
switch (granularity) {
|
||||
case ObjectRecordGroupByDateGranularity.DAY:
|
||||
return date.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
case ObjectRecordGroupByDateGranularity.MONTH:
|
||||
return date.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
});
|
||||
case ObjectRecordGroupByDateGranularity.QUARTER:
|
||||
return `Q${Math.floor(date.getMonth() / 3) + 1} ${date.getFullYear()}`;
|
||||
case ObjectRecordGroupByDateGranularity.YEAR:
|
||||
return date.getFullYear().toString();
|
||||
case ObjectRecordGroupByDateGranularity.NONE:
|
||||
default:
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
};
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
|
||||
import { formatDateByGranularity } from '@/page-layout/widgets/graph/utils/formatDateByGranularity';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type FormatDimensionValueParams = {
|
||||
value: unknown;
|
||||
fieldMetadata: FieldMetadataItem;
|
||||
dateGranularity?: ObjectRecordGroupByDateGranularity;
|
||||
};
|
||||
|
||||
const normalizeMultiSelectValue = (value: unknown): unknown[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return [value];
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
const isPostgresArrayFormat =
|
||||
trimmed.startsWith('{') && trimmed.endsWith('}');
|
||||
|
||||
if (!isPostgresArrayFormat) {
|
||||
return [value];
|
||||
}
|
||||
|
||||
const content = trimmed.slice(1, -1);
|
||||
return content ? content.split(',') : [];
|
||||
};
|
||||
|
||||
export const formatDimensionValue = ({
|
||||
value,
|
||||
fieldMetadata,
|
||||
dateGranularity = GRAPH_DEFAULT_DATE_GRANULARITY as ObjectRecordGroupByDateGranularity,
|
||||
}: FormatDimensionValueParams): string => {
|
||||
if (!isDefined(value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.SELECT: {
|
||||
const selectedOption = fieldMetadata.options?.find(
|
||||
(option) => option.value === value,
|
||||
);
|
||||
return selectedOption?.label ?? String(value);
|
||||
}
|
||||
|
||||
case FieldMetadataType.MULTI_SELECT: {
|
||||
const values = normalizeMultiSelectValue(value);
|
||||
|
||||
return values
|
||||
.map((value) => {
|
||||
const option = fieldMetadata.options?.find(
|
||||
(option) => option.value === value,
|
||||
);
|
||||
return option?.label ?? String(value);
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
case FieldMetadataType.BOOLEAN: {
|
||||
return value === true ? t`Yes` : t`No`;
|
||||
}
|
||||
|
||||
case FieldMetadataType.DATE:
|
||||
case FieldMetadataType.DATE_TIME: {
|
||||
// TODO: granularity will be passed from the graph configuration when implemented
|
||||
if (
|
||||
dateGranularity ===
|
||||
ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK ||
|
||||
dateGranularity ===
|
||||
ObjectRecordGroupByDateGranularity.MONTH_OF_THE_YEAR ||
|
||||
dateGranularity ===
|
||||
ObjectRecordGroupByDateGranularity.QUARTER_OF_THE_YEAR
|
||||
) {
|
||||
return String(value);
|
||||
}
|
||||
return formatDateByGranularity(new Date(String(value)), dateGranularity);
|
||||
}
|
||||
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { getGroupByQueryName } from '../../../utils/getGroupByQueryName';
|
||||
|
||||
export const generateGroupByQuery = ({
|
||||
objectMetadataItem,
|
||||
aggregateOperations,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
aggregateOperations: string[];
|
||||
}) => {
|
||||
const capitalizedSingular = capitalize(objectMetadataItem.nameSingular);
|
||||
const queryName = `${capitalize(objectMetadataItem.namePlural)}GroupBy`;
|
||||
const queryFieldName = getGroupByQueryName(objectMetadataItem);
|
||||
|
||||
return gql`
|
||||
query ${queryName}(
|
||||
$groupBy: [${capitalizedSingular}GroupByInput!]
|
||||
$filter: ${capitalizedSingular}FilterInput
|
||||
$orderBy: [${capitalizedSingular}OrderByWithGroupByInput!]
|
||||
$viewId: UUID
|
||||
) {
|
||||
${queryFieldName}(
|
||||
groupBy: $groupBy
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
viewId: $viewId
|
||||
) {
|
||||
groupByDimensionValues${aggregateOperations.length > 0 ? `\n ${aggregateOperations.join('\n ')}` : ''}
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { type BarChartConfiguration, GraphOrderBy } from '~/generated/graphql';
|
||||
import { buildGroupByFieldObject } from './buildGroupByFieldObject';
|
||||
|
||||
const _mapOrderByToDirection = (orderByEnum: GraphOrderBy) => {
|
||||
switch (orderByEnum) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return 'AscNullsLast';
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return 'DescNullsLast';
|
||||
case GraphOrderBy.VALUE_ASC:
|
||||
return 'AscNullsLast';
|
||||
case GraphOrderBy.VALUE_DESC:
|
||||
return 'DescNullsLast';
|
||||
default:
|
||||
assertUnreachable(orderByEnum);
|
||||
}
|
||||
};
|
||||
|
||||
export const generateGroupByQueryVariablesFromBarChartConfiguration = ({
|
||||
objectMetadataItem,
|
||||
barChartConfiguration,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
barChartConfiguration: BarChartConfiguration;
|
||||
}) => {
|
||||
const groupByFieldX = objectMetadataItem.fields.find(
|
||||
(field) => field.id === barChartConfiguration.groupByFieldMetadataIdX,
|
||||
);
|
||||
|
||||
const groupByFieldY = objectMetadataItem.fields.find(
|
||||
(field) => field.id === barChartConfiguration.groupByFieldMetadataIdY,
|
||||
);
|
||||
|
||||
if (!isDefined(groupByFieldX)) {
|
||||
throw new Error(
|
||||
`Field with id ${barChartConfiguration.groupByFieldMetadataIdX} not found in object metadata`,
|
||||
);
|
||||
}
|
||||
|
||||
const groupBy: Array<
|
||||
Record<string, boolean | Record<string, boolean | string>>
|
||||
> = [];
|
||||
|
||||
groupBy.push(
|
||||
buildGroupByFieldObject({
|
||||
field: groupByFieldX,
|
||||
subFieldName: barChartConfiguration.groupBySubFieldNameX,
|
||||
}),
|
||||
);
|
||||
|
||||
if (isDefined(groupByFieldY)) {
|
||||
groupBy.push(
|
||||
buildGroupByFieldObject({
|
||||
field: groupByFieldY,
|
||||
subFieldName: barChartConfiguration.groupBySubFieldNameY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const orderBy: Array<Record<string, string>> = [];
|
||||
|
||||
// TODO: Add orderBy back in when the backend is ready
|
||||
// if (isDefined(barChartConfiguration.orderByX)) {
|
||||
// orderBy.push({
|
||||
// [groupByFieldX.name]: mapOrderByToDirection(
|
||||
// barChartConfiguration.orderByX,
|
||||
// ),
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (isDefined(groupByFieldY) && isDefined(barChartConfiguration.orderByY)) {
|
||||
// orderBy.push({
|
||||
// [groupByFieldY.name]: mapOrderByToDirection(
|
||||
// barChartConfiguration.orderByY,
|
||||
// ),
|
||||
// });
|
||||
// }
|
||||
|
||||
return {
|
||||
groupBy,
|
||||
// TODO: Add filters
|
||||
...(orderBy.length > 0 && { orderBy }),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type Maybe } from '~/generated/graphql';
|
||||
|
||||
type GetFieldKeyParams = {
|
||||
field: FieldMetadataItem;
|
||||
subFieldName?: Maybe<string>;
|
||||
};
|
||||
|
||||
export const getFieldKey = ({
|
||||
field,
|
||||
subFieldName,
|
||||
}: GetFieldKeyParams): string => {
|
||||
const isComposite = isCompositeFieldType(field.type);
|
||||
|
||||
return isComposite && isDefined(subFieldName)
|
||||
? `${field.name}.${subFieldName}`
|
||||
: field.name;
|
||||
};
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
|
||||
import { getGroupByQueryName } from '@/page-layout/utils/getGroupByQueryName';
|
||||
import { GRAPH_MAXIMUM_NUMBER_OF_GROUPS } from '@/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroups.constant';
|
||||
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
|
||||
import { transformOneDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData';
|
||||
import { transformTwoDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
AxisNameDisplay,
|
||||
type BarChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
type TransformGroupByDataToBarChartDataParams = {
|
||||
groupByData: Record<string, GroupByRawResult[]> | null | undefined;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
configuration: BarChartConfiguration;
|
||||
aggregateOperation: string;
|
||||
};
|
||||
|
||||
type TransformGroupByDataToBarChartDataResult = {
|
||||
data: BarChartDataItem[];
|
||||
indexBy: string;
|
||||
keys: string[];
|
||||
series: BarChartSeries[];
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
showDataLabels: boolean;
|
||||
};
|
||||
|
||||
const EMPTY_BAR_CHART_RESULT: TransformGroupByDataToBarChartDataResult = {
|
||||
data: [],
|
||||
indexBy: '',
|
||||
keys: [],
|
||||
series: [],
|
||||
xAxisLabel: undefined,
|
||||
yAxisLabel: undefined,
|
||||
showDataLabels: false,
|
||||
};
|
||||
|
||||
export const transformGroupByDataToBarChartData = ({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
}: TransformGroupByDataToBarChartDataParams): TransformGroupByDataToBarChartDataResult => {
|
||||
if (!isDefined(groupByData)) {
|
||||
return EMPTY_BAR_CHART_RESULT;
|
||||
}
|
||||
|
||||
const groupByFieldX = objectMetadataItem.fields.find(
|
||||
(field: FieldMetadataItem) =>
|
||||
field.id === configuration.groupByFieldMetadataIdX,
|
||||
);
|
||||
|
||||
const groupByFieldY = objectMetadataItem.fields.find(
|
||||
(field: FieldMetadataItem) =>
|
||||
field.id === configuration.groupByFieldMetadataIdY,
|
||||
);
|
||||
|
||||
const aggregateField = objectMetadataItem.fields.find(
|
||||
(field: FieldMetadataItem) =>
|
||||
field.id === configuration.aggregateFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(groupByFieldX) || !isDefined(aggregateField)) {
|
||||
return EMPTY_BAR_CHART_RESULT;
|
||||
}
|
||||
|
||||
const indexByKey = getFieldKey({
|
||||
field: groupByFieldX,
|
||||
subFieldName: configuration.groupBySubFieldNameX,
|
||||
});
|
||||
|
||||
const queryName = getGroupByQueryName(objectMetadataItem);
|
||||
const queryResults = groupByData[queryName];
|
||||
|
||||
if (!isDefined(queryResults) || !Array.isArray(queryResults)) {
|
||||
return {
|
||||
...EMPTY_BAR_CHART_RESULT,
|
||||
indexBy: indexByKey,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
|
||||
const rawResults = queryResults.slice(0, GRAPH_MAXIMUM_NUMBER_OF_GROUPS);
|
||||
|
||||
const showXAxis =
|
||||
configuration.axisNameDisplay === AxisNameDisplay.X ||
|
||||
configuration.axisNameDisplay === AxisNameDisplay.BOTH;
|
||||
|
||||
const showYAxis =
|
||||
configuration.axisNameDisplay === AxisNameDisplay.Y ||
|
||||
configuration.axisNameDisplay === AxisNameDisplay.BOTH;
|
||||
|
||||
const xAxisLabel = showXAxis ? groupByFieldX.label : undefined;
|
||||
|
||||
const yAxisLabel = showYAxis
|
||||
? `${getAggregateOperationLabel(configuration.aggregateOperation)} of ${aggregateField.label}`
|
||||
: undefined;
|
||||
|
||||
const showDataLabels = configuration.displayDataLabel;
|
||||
|
||||
const baseResult = isDefined(groupByFieldY)
|
||||
? transformTwoDimensionalGroupByToBarChartData({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
groupByFieldY,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
})
|
||||
: transformOneDimensionalGroupByToBarChartData({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return {
|
||||
...baseResult,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
showDataLabels,
|
||||
};
|
||||
};
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant';
|
||||
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type BarChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type TransformOneDimensionalGroupByToBarChartDataParams = {
|
||||
rawResults: GroupByRawResult[];
|
||||
groupByFieldX: FieldMetadataItem;
|
||||
aggregateField: FieldMetadataItem;
|
||||
configuration: BarChartConfiguration;
|
||||
aggregateOperation: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
type TransformOneDimensionalGroupByToBarChartDataResult = {
|
||||
data: BarChartDataItem[];
|
||||
indexBy: string;
|
||||
keys: string[];
|
||||
series: BarChartSeries[];
|
||||
};
|
||||
|
||||
export const transformOneDimensionalGroupByToBarChartData = ({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
}: TransformOneDimensionalGroupByToBarChartDataParams): TransformOneDimensionalGroupByToBarChartDataResult => {
|
||||
const indexByKey = getFieldKey({
|
||||
field: groupByFieldX,
|
||||
subFieldName: configuration.groupBySubFieldNameX,
|
||||
});
|
||||
|
||||
const data: BarChartDataItem[] = rawResults.map((result) => {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
|
||||
const xValue = isDefined(dimensionValues?.[0])
|
||||
? formatDimensionValue({
|
||||
value: dimensionValues[0],
|
||||
fieldMetadata: groupByFieldX,
|
||||
})
|
||||
: '';
|
||||
|
||||
const aggregateValue = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
aggregateField,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations,
|
||||
aggregateOperationFromRawResult: aggregateOperation,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return {
|
||||
[indexByKey]: xValue,
|
||||
[aggregateField.name]: aggregateValue,
|
||||
};
|
||||
});
|
||||
|
||||
const series: BarChartSeries[] = [
|
||||
{
|
||||
key: aggregateField.name,
|
||||
label: aggregateField.label,
|
||||
color: (configuration.color ?? GRAPH_DEFAULT_COLOR) as GraphColor,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
data,
|
||||
indexBy: indexByKey,
|
||||
keys: [aggregateField.name],
|
||||
series,
|
||||
};
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type BarChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type TransformTwoDimensionalGroupByToBarChartDataParams = {
|
||||
rawResults: GroupByRawResult[];
|
||||
groupByFieldX: FieldMetadataItem;
|
||||
groupByFieldY: FieldMetadataItem;
|
||||
aggregateField: FieldMetadataItem;
|
||||
configuration: BarChartConfiguration;
|
||||
aggregateOperation: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
type TransformTwoDimensionalGroupByToBarChartDataResult = {
|
||||
data: BarChartDataItem[];
|
||||
indexBy: string;
|
||||
keys: string[];
|
||||
series: BarChartSeries[];
|
||||
};
|
||||
|
||||
export const transformTwoDimensionalGroupByToBarChartData = ({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
groupByFieldY,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
}: TransformTwoDimensionalGroupByToBarChartDataParams): TransformTwoDimensionalGroupByToBarChartDataResult => {
|
||||
const indexByKey = getFieldKey({
|
||||
field: groupByFieldX,
|
||||
subFieldName: configuration.groupBySubFieldNameX,
|
||||
});
|
||||
|
||||
const dataMap = new Map<string, BarChartDataItem>();
|
||||
const yValues = new Set<string>();
|
||||
|
||||
rawResults.forEach((result) => {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
if (!isDefined(dimensionValues) || dimensionValues.length < 2) return;
|
||||
|
||||
const xValue = formatDimensionValue({
|
||||
value: dimensionValues[0],
|
||||
fieldMetadata: groupByFieldX,
|
||||
});
|
||||
const yValue = formatDimensionValue({
|
||||
value: dimensionValues[1],
|
||||
fieldMetadata: groupByFieldY,
|
||||
});
|
||||
|
||||
const aggregateValue = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
aggregateField,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations,
|
||||
aggregateOperationFromRawResult: aggregateOperation,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
yValues.add(yValue);
|
||||
|
||||
if (!dataMap.has(xValue)) {
|
||||
dataMap.set(xValue, {
|
||||
[indexByKey]: xValue,
|
||||
});
|
||||
}
|
||||
|
||||
const dataItem = dataMap.get(xValue)!;
|
||||
dataItem[yValue] = aggregateValue;
|
||||
});
|
||||
|
||||
const keys = Array.from(yValues);
|
||||
const series: BarChartSeries[] = keys.map((key) => ({
|
||||
key,
|
||||
label: key,
|
||||
}));
|
||||
|
||||
return {
|
||||
data: Array.from(dataMap.values()),
|
||||
indexBy: indexByKey,
|
||||
keys,
|
||||
series,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { type ObjectRecordGroupByDateGranularity } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
|
||||
+2
-4
@@ -1,9 +1,7 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
type ObjectRecord,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { type ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
|
||||
import { type IGroupByConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/group-by-connection.interface';
|
||||
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { ObjectRecordGroupByDateGranularity } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { type GroupByField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types';
|
||||
import { isGroupByDateField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field.util';
|
||||
|
||||
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
import { isDefined } from 'class-validator';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { ObjectRecordGroupByDateGranularity } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { type GroupByResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
|
||||
+2
-11
@@ -1,3 +1,5 @@
|
||||
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
|
||||
//TODO : Refacto-common - Should be moved to common api layer
|
||||
export interface ObjectRecord {
|
||||
id: string;
|
||||
@@ -40,17 +42,6 @@ export enum OrderByDirection {
|
||||
DescNullsLast = 'DescNullsLast',
|
||||
}
|
||||
|
||||
export enum ObjectRecordGroupByDateGranularity {
|
||||
DAY = 'DAY',
|
||||
MONTH = 'MONTH',
|
||||
QUARTER = 'QUARTER',
|
||||
YEAR = 'YEAR',
|
||||
DAY_OF_THE_WEEK = 'DAY_OF_THE_WEEK',
|
||||
QUARTER_OF_THE_YEAR = 'QUARTER_OF_THE_YEAR',
|
||||
MONTH_OF_THE_YEAR = 'MONTH_OF_THE_YEAR',
|
||||
NONE = 'NONE',
|
||||
}
|
||||
|
||||
export type ObjectRecordOrderBy = Array<
|
||||
ObjectRecordOrderByForScalarField | ObjectRecordOrderByForCompositeField
|
||||
>;
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLEnumType, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { ObjectRecordGroupByDateGranularity } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
|
||||
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export enum ObjectRecordGroupByDateGranularity {
|
||||
DAY = 'DAY',
|
||||
MONTH = 'MONTH',
|
||||
QUARTER = 'QUARTER',
|
||||
YEAR = 'YEAR',
|
||||
DAY_OF_THE_WEEK = 'DAY_OF_THE_WEEK',
|
||||
MONTH_OF_THE_YEAR = 'MONTH_OF_THE_YEAR',
|
||||
QUARTER_OF_THE_YEAR = 'QUARTER_OF_THE_YEAR',
|
||||
NONE = 'NONE',
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export type { Nullable } from './Nullable';
|
||||
export type { NullablePartial } from './NullablePartial';
|
||||
export type { ObjectPermissions } from './ObjectPermissions';
|
||||
export type { ObjectPermissionsDeprecated } from './ObjectPermissionsDeprecated';
|
||||
export { ObjectRecordGroupByDateGranularity } from './ObjectRecordGroupByDateGranularity';
|
||||
export type { ObjectsPermissions } from './ObjectsPermissions';
|
||||
export type { ObjectsPermissionsByRoleId } from './ObjectsPermissionsByRoleId';
|
||||
export type { ObjectsPermissionsByRoleIdDeprecated } from './ObjectsPermissionsByRoleIdDeprecated';
|
||||
|
||||
Reference in New Issue
Block a user