Tidy up validation for configuration (#14939)

closes https://github.com/twentyhq/core-team-issues/issues/1605

TODO:  
~~- add stories~~
~~- add base graph on companies when creating a new graph widget~~
This commit is contained in:
nitin
2025-10-10 02:04:51 +05:30
committed by GitHub
parent 5adc9fe9b2
commit 44f97058ac
44 changed files with 1015 additions and 475 deletions
@@ -281,10 +281,10 @@ export type BarChartConfiguration = {
__typename?: 'BarChartConfiguration';
aggregateFieldMetadataId: Scalars['UUID'];
aggregateOperation: ExtendedAggregateOperations;
axisNameDisplay: AxisNameDisplay;
axisNameDisplay?: Maybe<AxisNameDisplay>;
color?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
displayDataLabel: Scalars['Boolean'];
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
groupByFieldMetadataIdX: Scalars['UUID'];
@@ -292,7 +292,7 @@ export type BarChartConfiguration = {
groupBySubFieldNameX?: Maybe<Scalars['String']>;
groupBySubFieldNameY?: Maybe<Scalars['String']>;
omitNullValues?: Maybe<Scalars['Boolean']>;
orderByX: GraphOrderBy;
orderByX?: Maybe<GraphOrderBy>;
orderByY?: Maybe<GraphOrderBy>;
rangeMax?: Maybe<Scalars['Float']>;
rangeMin?: Maybe<Scalars['Float']>;
@@ -1368,7 +1368,7 @@ export type GaugeChartConfiguration = {
aggregateOperation: ExtendedAggregateOperations;
color?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
displayDataLabel: Scalars['Boolean'];
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
};
@@ -1593,10 +1593,10 @@ export type LineChartConfiguration = {
__typename?: 'LineChartConfiguration';
aggregateFieldMetadataId: Scalars['UUID'];
aggregateOperation: ExtendedAggregateOperations;
axisNameDisplay: AxisNameDisplay;
axisNameDisplay?: Maybe<AxisNameDisplay>;
color?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
displayDataLabel: Scalars['Boolean'];
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
groupByFieldMetadataIdX: Scalars['UUID'];
@@ -1604,7 +1604,7 @@ export type LineChartConfiguration = {
groupBySubFieldNameX?: Maybe<Scalars['String']>;
groupBySubFieldNameY?: Maybe<Scalars['String']>;
omitNullValues?: Maybe<Scalars['Boolean']>;
orderByX: GraphOrderBy;
orderByX?: Maybe<GraphOrderBy>;
orderByY?: Maybe<GraphOrderBy>;
rangeMax?: Maybe<Scalars['Float']>;
rangeMin?: Maybe<Scalars['Float']>;
@@ -2709,9 +2709,8 @@ export type NumberChartConfiguration = {
__typename?: 'NumberChartConfiguration';
aggregateFieldMetadataId: Scalars['UUID'];
aggregateOperation: ExtendedAggregateOperations;
color?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
displayDataLabel: Scalars['Boolean'];
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
format?: Maybe<Scalars['String']>;
graphType: GraphType;
@@ -2956,12 +2955,12 @@ export type PieChartConfiguration = {
aggregateOperation: ExtendedAggregateOperations;
color?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
displayDataLabel: Scalars['Boolean'];
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
groupByFieldMetadataId: Scalars['UUID'];
groupBySubFieldName?: Maybe<Scalars['String']>;
orderBy: GraphOrderBy;
orderBy?: Maybe<GraphOrderBy>;
};
export type PlaceDetailsResultDto = {
+12 -13
View File
@@ -281,10 +281,10 @@ export type BarChartConfiguration = {
__typename?: 'BarChartConfiguration';
aggregateFieldMetadataId: Scalars['UUID'];
aggregateOperation: ExtendedAggregateOperations;
axisNameDisplay: AxisNameDisplay;
axisNameDisplay?: Maybe<AxisNameDisplay>;
color?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
displayDataLabel: Scalars['Boolean'];
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
groupByFieldMetadataIdX: Scalars['UUID'];
@@ -292,7 +292,7 @@ export type BarChartConfiguration = {
groupBySubFieldNameX?: Maybe<Scalars['String']>;
groupBySubFieldNameY?: Maybe<Scalars['String']>;
omitNullValues?: Maybe<Scalars['Boolean']>;
orderByX: GraphOrderBy;
orderByX?: Maybe<GraphOrderBy>;
orderByY?: Maybe<GraphOrderBy>;
rangeMax?: Maybe<Scalars['Float']>;
rangeMin?: Maybe<Scalars['Float']>;
@@ -1325,7 +1325,7 @@ export type GaugeChartConfiguration = {
aggregateOperation: ExtendedAggregateOperations;
color?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
displayDataLabel: Scalars['Boolean'];
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
};
@@ -1550,10 +1550,10 @@ export type LineChartConfiguration = {
__typename?: 'LineChartConfiguration';
aggregateFieldMetadataId: Scalars['UUID'];
aggregateOperation: ExtendedAggregateOperations;
axisNameDisplay: AxisNameDisplay;
axisNameDisplay?: Maybe<AxisNameDisplay>;
color?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
displayDataLabel: Scalars['Boolean'];
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
groupByFieldMetadataIdX: Scalars['UUID'];
@@ -1561,7 +1561,7 @@ export type LineChartConfiguration = {
groupBySubFieldNameX?: Maybe<Scalars['String']>;
groupBySubFieldNameY?: Maybe<Scalars['String']>;
omitNullValues?: Maybe<Scalars['Boolean']>;
orderByX: GraphOrderBy;
orderByX?: Maybe<GraphOrderBy>;
orderByY?: Maybe<GraphOrderBy>;
rangeMax?: Maybe<Scalars['Float']>;
rangeMin?: Maybe<Scalars['Float']>;
@@ -2620,9 +2620,8 @@ export type NumberChartConfiguration = {
__typename?: 'NumberChartConfiguration';
aggregateFieldMetadataId: Scalars['UUID'];
aggregateOperation: ExtendedAggregateOperations;
color?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
displayDataLabel: Scalars['Boolean'];
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
format?: Maybe<Scalars['String']>;
graphType: GraphType;
@@ -2867,12 +2866,12 @@ export type PieChartConfiguration = {
aggregateOperation: ExtendedAggregateOperations;
color?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
displayDataLabel: Scalars['Boolean'];
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
groupByFieldMetadataId: Scalars['UUID'];
groupBySubFieldName?: Maybe<Scalars['String']>;
orderBy: GraphOrderBy;
orderBy?: Maybe<GraphOrderBy>;
};
export type PlaceDetailsResultDto = {
@@ -4432,7 +4431,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, 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 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 | null, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, 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 | null, 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 | null, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, 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 | null, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupBySubFieldName?: string | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | null };
export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
id: Scalars['String'];
@@ -4440,7 +4439,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, 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 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 | null, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, 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 | null, 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 | null, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, 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 | null, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupBySubFieldName?: string | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | null }> | null }> | null } };
export type OnDbEventSubscriptionVariables = Exact<{
input: OnDbEventInput;
@@ -4,6 +4,7 @@ import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
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 { useCompanyDefaultChartConfig } from '@/page-layout/hooks/useCompanyDefaultChartConfig';
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
@@ -18,6 +19,8 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
const { buildBarChartFieldSelection } = useCompanyDefaultChartConfig();
const { createPageLayoutGraphWidget } =
useCreatePageLayoutGraphWidget(pageLayoutId);
@@ -29,7 +32,11 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
const handleNavigateToGraphTypeSelect = () => {
if (!isDefined(pageLayoutEditingWidgetId)) {
const newWidget = createPageLayoutGraphWidget(GraphType.BAR);
const fieldSelection = buildBarChartFieldSelection();
const newWidget = createPageLayoutGraphWidget({
graphType: GraphType.BAR,
fieldSelection,
});
setPageLayoutEditingWidgetId(newWidget.id);
}
@@ -48,6 +48,10 @@ export const ChartColorSelectionDropdownContent = () => {
const configuration = widgetInEditMode.configuration as ChartConfiguration;
if (!('color' in configuration)) {
return null;
}
const currentColor = configuration.color;
const colorOptions = MAIN_COLOR_NAMES.map((colorName) => ({
@@ -17,7 +17,6 @@ import {
type BarChartConfiguration,
type GraphOrderBy,
type LineChartConfiguration,
type NumberChartConfiguration,
} from '~/generated/graphql';
export const ChartSortByGroupByFieldDropdownContent = () => {
@@ -26,15 +25,9 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
const configuration = widgetInEditMode?.configuration as
| BarChartConfiguration
| LineChartConfiguration
| NumberChartConfiguration;
| LineChartConfiguration;
const currentOrderBy =
'orderByY' in configuration
? configuration.orderByY
: 'orderBy' in configuration
? configuration.orderBy
: undefined;
const currentOrderBy = configuration.orderByY;
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
@@ -50,12 +43,10 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
const { closeDropdown } = useCloseDropdown();
const orderByKey = 'orderByY' in configuration ? 'orderByY' : 'orderBy';
const handleSelectSortOption = (orderBy: GraphOrderBy) => {
updateCurrentWidgetConfig({
configToUpdate: {
[orderByKey]: orderBy,
orderByY: orderBy,
},
});
closeDropdown();
@@ -76,8 +76,9 @@ export const ChartXAxisSortBySelectionDropdownContent = () => {
graphOrderBy: sortOption.value,
groupByFieldMetadataIdX: configuration.groupByFieldMetadataIdX,
aggregateFieldMetadataId:
configuration.aggregateFieldMetadataId,
aggregateOperation: configuration.aggregateOperation,
configuration.aggregateFieldMetadataId ?? undefined,
aggregateOperation:
configuration.aggregateOperation ?? undefined,
})}
selected={currentOrderByX === sortOption.value}
focused={selectedItemId === sortOption.value}
@@ -80,7 +80,7 @@ export const useChartSettingsValues = ({
groupBySubFieldNameX:
configuration.groupBySubFieldNameX as CompositeFieldSubFieldName,
aggregateFieldMetadataId: configuration.aggregateFieldMetadataId,
aggregateOperation: configuration.aggregateOperation,
aggregateOperation: configuration.aggregateOperation ?? undefined,
})
: undefined;
@@ -116,17 +116,26 @@ export const useChartSettingsValues = ({
case CHART_CONFIGURATION_SETTING_IDS.SOURCE:
return objectMetadataItem?.labelPlural;
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X:
return groupBySubFieldNameXLabel;
return groupBySubFieldNameXLabel ?? groupByFieldX?.label;
case CHART_CONFIGURATION_SETTING_IDS.COLORS:
return isDefined(configuration.color) && 'color' in configuration
? capitalize(configuration.color)
return 'color' in configuration && isDefined(configuration.color)
? capitalize(configuration.color as string)
: undefined;
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y:
return `${aggregateField?.label ?? ''}${aggregateField?.label ? ` (${getAggregateOperationLabel(yAxisAggregateOperation)})` : ''}`;
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y: {
const hasAggregateLabel = isDefined(aggregateField?.label);
const hasAggregateOperation = isDefined(yAxisAggregateOperation);
return `${aggregateField?.label ?? ''}${
hasAggregateLabel && hasAggregateOperation
? ` (${getAggregateOperationLabel(yAxisAggregateOperation)})`
: ''
}`;
}
case CHART_CONFIGURATION_SETTING_IDS.GROUP_BY:
return groupByFieldY?.label;
case CHART_CONFIGURATION_SETTING_IDS.AXIS_NAME:
return 'axisNameDisplay' in configuration
return 'axisNameDisplay' in configuration &&
isDefined(configuration.axisNameDisplay)
? getChartAxisNameDisplayOptions(configuration.axisNameDisplay)
: undefined;
case CHART_CONFIGURATION_SETTING_IDS.SORT_BY_X:
@@ -134,7 +143,7 @@ export const useChartSettingsValues = ({
case CHART_CONFIGURATION_SETTING_IDS.SORT_BY_GROUP_BY_FIELD:
return groupByOrderByLabel;
case CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS:
return configuration.displayDataLabel;
return configuration.displayDataLabel ?? undefined;
default:
return '';
}
@@ -25,9 +25,22 @@ import {
type PageLayoutWidget,
} from '~/generated/graphql';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
const mockPersonObjectMetadataItem = getMockObjectMetadataItemOrThrow('person');
const idField = getMockFieldMetadataItemOrThrow({
objectMetadataItem: mockPersonObjectMetadataItem,
fieldName: 'id',
});
const nameField = getMockFieldMetadataItemOrThrow({
objectMetadataItem: mockPersonObjectMetadataItem,
fieldName: 'name',
});
const createdAtField = getMockFieldMetadataItemOrThrow({
objectMetadataItem: mockPersonObjectMetadataItem,
fieldName: 'createdAt',
});
const validatePageLayoutContent = async (canvasElement: HTMLElement) => {
const canvas = within(canvasElement);
@@ -76,7 +89,7 @@ const mixedGraphsPageLayoutMocks = {
__typename: 'NumberChartConfiguration',
graphType: GraphType.NUMBER,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
aggregateFieldMetadataId: idField.id,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
@@ -100,7 +113,7 @@ const mixedGraphsPageLayoutMocks = {
__typename: 'GaugeChartConfiguration',
graphType: GraphType.GAUGE,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
aggregateFieldMetadataId: idField.id,
displayDataLabel: false,
},
createdAt: '2024-01-01T00:00:00Z',
@@ -125,8 +138,8 @@ const mixedGraphsPageLayoutMocks = {
__typename: 'PieChartConfiguration',
graphType: GraphType.PIE,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
groupByFieldMetadataId: 'createdAt',
aggregateFieldMetadataId: idField.id,
groupByFieldMetadataId: createdAtField.id,
orderBy: GraphOrderBy.VALUE_DESC,
},
createdAt: '2024-01-01T00:00:00Z',
@@ -151,12 +164,8 @@ const mixedGraphsPageLayoutMocks = {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: mockPersonObjectMetadataItem.fields.find(
(field) => field.name === 'name',
)?.id,
groupByFieldMetadataIdX: mockPersonObjectMetadataItem.fields.find(
(field) => field.name === 'createdAt',
)?.id,
aggregateFieldMetadataId: nameField.id,
groupByFieldMetadataIdX: createdAtField.id,
orderByX: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
@@ -6,12 +6,14 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { act, renderHook } from '@testing-library/react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { GraphType, WidgetType } from '~/generated-metadata/graphql';
import { PageLayoutType } from '~/generated/graphql';
import {
PAGE_LAYOUT_TEST_INSTANCE_ID,
PageLayoutTestWrapper,
} from './PageLayoutTestWrapper';
import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection';
jest.mock('uuid', () => ({
v4: jest.fn(() => 'mock-uuid'),
@@ -82,7 +84,9 @@ describe('useCreatePageLayoutGraphWidget', () => {
});
act(() => {
result.current.createWidget.createPageLayoutGraphWidget(GraphType.BAR);
result.current.createWidget.createPageLayoutGraphWidget({
graphType: GraphType.BAR,
});
});
expect(result.current.allWidgets).toHaveLength(1);
@@ -162,9 +166,28 @@ describe('useCreatePageLayoutGraphWidget', () => {
GraphType.BAR,
];
const mockFieldSelections: Partial<
Record<GraphType, GraphWidgetFieldSelection>
> = {
[GraphType.NUMBER]: {
objectMetadataId: 'test-object-id',
aggregateFieldMetadataId: 'test-aggregate-field-id',
},
[GraphType.BAR]: {
objectMetadataId: 'test-object-id',
groupByFieldMetadataIdX: 'test-groupby-field-id',
aggregateFieldMetadataId: 'test-aggregate-field-id',
},
};
graphTypes.forEach((graphType) => {
act(() => {
result.current.createWidget.createPageLayoutGraphWidget(graphType);
const fieldSelection =
mockFieldSelections[graphType as keyof typeof mockFieldSelections];
result.current.createWidget.createPageLayoutGraphWidget({
graphType,
fieldSelection,
});
});
});
@@ -174,11 +197,14 @@ describe('useCreatePageLayoutGraphWidget', () => {
const widget = result.current.allWidgets[index];
expect(widget.type).toBe(WidgetType.GRAPH);
expect(widget.pageLayoutTabId).toBe('tab-1');
expect(
widget.configuration && 'graphType' in widget.configuration
? widget.configuration.graphType
: null,
).toBe(graphType);
if (
isDefined(widget.configuration) &&
'graphType' in widget.configuration
) {
expect(widget.configuration.graphType).toBe(graphType);
}
expect(widget.id).toBe('mock-uuid');
});
@@ -207,7 +233,9 @@ describe('useCreatePageLayoutGraphWidget', () => {
);
expect(() => {
result.current.createWidget.createPageLayoutGraphWidget(GraphType.BAR);
result.current.createWidget.createPageLayoutGraphWidget({
graphType: GraphType.BAR,
});
}).toThrow('A tab must be selected to create a new graph widget');
});
});
@@ -0,0 +1,76 @@
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection';
export const useCompanyDefaultChartConfig = () => {
const companyObjectMetadata = useRecoilValue(
objectMetadataItemFamilySelector({
objectName: 'company',
objectNameType: 'singular',
}),
);
const buildBarChartFieldSelection = ():
| GraphWidgetFieldSelection
| undefined => {
if (!isDefined(companyObjectMetadata)) {
return;
}
const labelIdentifierField = companyObjectMetadata.fields.find(
(field) =>
field.id === companyObjectMetadata.labelIdentifierFieldMetadataId,
);
const employeesField = companyObjectMetadata.fields.find(
(field) => field.name === 'employees',
);
const arrField = companyObjectMetadata.fields.find(
(field) => field.name === 'annualRecurringRevenue',
);
const aggregateField = employeesField ?? arrField;
if (!isDefined(labelIdentifierField) || !isDefined(aggregateField)) {
return;
}
return {
objectMetadataId: companyObjectMetadata.id,
groupByFieldMetadataIdX: labelIdentifierField.id,
aggregateFieldMetadataId: aggregateField.id,
};
};
const buildNumberChartFieldSelection = ():
| GraphWidgetFieldSelection
| undefined => {
if (!isDefined(companyObjectMetadata)) {
return;
}
const employeesField = companyObjectMetadata.fields.find(
(field) => field.name === 'employees',
);
const arrField = companyObjectMetadata.fields.find(
(field) => field.name === 'annualRecurringRevenue',
);
const aggregateField = employeesField ?? arrField;
if (!isDefined(aggregateField)) {
return;
}
return {
objectMetadataId: companyObjectMetadata.id,
aggregateFieldMetadataId: aggregateField.id,
};
};
return { buildBarChartFieldSelection, buildNumberChartFieldSelection };
};
@@ -2,6 +2,7 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget';
import {
@@ -52,7 +53,13 @@ export const useCreatePageLayoutGraphWidget = (
const createPageLayoutGraphWidget = useRecoilCallback(
({ snapshot, set }) =>
(graphType: GraphType): PageLayoutWidget => {
({
graphType,
fieldSelection,
}: {
graphType: GraphType;
fieldSelection?: GraphWidgetFieldSelection;
}): PageLayoutWidget => {
const activeTabId = snapshot.getLoadable(activeTabIdState).getValue();
if (!isDefined(activeTabId)) {
@@ -101,6 +108,7 @@ export const useCreatePageLayoutGraphWidget = (
rowSpan: position.h,
columnSpan: position.w,
},
fieldSelection,
});
const newLayout = {
@@ -0,0 +1,6 @@
export type GraphWidgetFieldSelection = {
objectMetadataId?: string;
aggregateFieldMetadataId?: string;
groupByFieldMetadataId?: string;
groupByFieldMetadataIdX?: string;
};
@@ -1,85 +1,80 @@
import { assertUnreachable } from 'twenty-shared/utils';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { type ThemeColor } from 'twenty-ui/theme';
import { v4 as uuidv4 } from 'uuid';
import { GraphOrderBy, GraphType } from '~/generated-metadata/graphql';
import {
ExtendedAggregateOperations,
GraphOrderBy,
GraphType,
} from '~/generated-metadata/graphql';
import {
AxisNameDisplay,
ExtendedAggregateOperations,
type GridPosition,
type PageLayoutWidget,
type WidgetConfiguration,
WidgetType,
} from '~/generated/graphql';
import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection';
const createDefaultGraphConfiguration = (
graphType: GraphType,
): WidgetConfiguration => {
const placeholderFieldId1 = uuidv4();
const placeholderFieldId2 = uuidv4();
fieldSelection?: GraphWidgetFieldSelection,
): WidgetConfiguration | null => {
switch (graphType) {
case GraphType.NUMBER:
if (!isDefined(fieldSelection?.aggregateFieldMetadataId)) {
return null;
}
return {
__typename: 'NumberChartConfiguration',
graphType: GraphType.NUMBER,
aggregateFieldMetadataId: fieldSelection.aggregateFieldMetadataId,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: placeholderFieldId1,
displayDataLabel: false,
displayDataLabel: true,
};
case GraphType.PIE:
return {
__typename: 'PieChartConfiguration',
graphType: GraphType.PIE,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: placeholderFieldId1,
groupByFieldMetadataId: placeholderFieldId2,
orderBy: GraphOrderBy.VALUE_DESC,
displayDataLabel: false,
color: 'blue' satisfies ThemeColor,
};
return null;
case GraphType.BAR:
if (
!isDefined(fieldSelection?.aggregateFieldMetadataId) ||
!isDefined(fieldSelection?.groupByFieldMetadataIdX)
) {
return null;
}
return {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: placeholderFieldId1,
groupByFieldMetadataIdX: placeholderFieldId2,
orderByX: GraphOrderBy.FIELD_ASC,
displayDataLabel: false,
axisNameDisplay: AxisNameDisplay.BOTH,
color: 'blue' satisfies ThemeColor,
groupByFieldMetadataIdX: fieldSelection.groupByFieldMetadataIdX,
aggregateFieldMetadataId: fieldSelection.aggregateFieldMetadataId,
aggregateOperation: ExtendedAggregateOperations.SUM,
orderByX: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
};
case GraphType.LINE:
return {
__typename: 'LineChartConfiguration',
graphType: GraphType.LINE,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: placeholderFieldId1,
groupByFieldMetadataIdX: placeholderFieldId2,
orderByX: GraphOrderBy.FIELD_ASC,
displayDataLabel: false,
axisNameDisplay: AxisNameDisplay.BOTH,
color: 'blue' satisfies ThemeColor,
};
return null;
case GraphType.GAUGE:
return {
__typename: 'GaugeChartConfiguration',
graphType: GraphType.GAUGE,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: placeholderFieldId1,
displayDataLabel: false,
color: 'blue' satisfies ThemeColor,
};
return null;
default:
assertUnreachable(graphType);
}
};
type CreateDefaultGraphWidgetParams = {
id: string;
pageLayoutTabId: string;
title: string;
graphType: GraphType;
gridPosition: GridPosition;
objectMetadataId?: string | null;
fieldSelection?: GraphWidgetFieldSelection;
};
export const createDefaultGraphWidget = ({
id,
pageLayoutTabId,
@@ -87,23 +82,25 @@ export const createDefaultGraphWidget = ({
graphType,
gridPosition,
objectMetadataId,
}: {
id: string;
pageLayoutTabId: string;
title: string;
graphType: GraphType;
gridPosition: GridPosition;
objectMetadataId?: string | null;
}): PageLayoutWidget => {
fieldSelection,
}: CreateDefaultGraphWidgetParams): PageLayoutWidget => {
const resolvedObjectMetadataId =
fieldSelection?.objectMetadataId ?? objectMetadataId ?? null;
const configuration = createDefaultGraphConfiguration(
graphType,
fieldSelection,
);
return {
__typename: 'PageLayoutWidget',
id,
pageLayoutTabId,
title,
type: WidgetType.GRAPH,
configuration: createDefaultGraphConfiguration(graphType),
configuration,
gridPosition,
objectMetadataId: objectMetadataId ?? null,
objectMetadataId: resolvedObjectMetadataId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
@@ -19,19 +19,20 @@ export const extractFieldMetadataIdsFromWidget = (
return [
config.aggregateFieldMetadataId,
config.groupByFieldMetadataIdX,
...(isDefined(config.groupByFieldMetadataIdY)
? [config.groupByFieldMetadataIdY]
: []),
];
config.groupByFieldMetadataIdY,
].filter(isDefined);
case 'PieChartConfiguration':
return [config.aggregateFieldMetadataId, config.groupByFieldMetadataId];
return [
config.aggregateFieldMetadataId,
config.groupByFieldMetadataId,
].filter(isDefined);
case 'NumberChartConfiguration':
return [config.aggregateFieldMetadataId];
return [config.aggregateFieldMetadataId].filter(isDefined);
case 'GaugeChartConfiguration':
return [config.aggregateFieldMetadataId];
return [config.aggregateFieldMetadataId].filter(isDefined);
case 'IframeConfiguration':
return [];
@@ -135,7 +135,7 @@ export const getWidgetSize = (graphType: GraphType) => {
case GraphType.PIE:
return { w: 4, h: 4 };
case GraphType.BAR:
return { w: 6, h: 4 };
return { w: 6, h: 6 };
case GraphType.LINE:
return { w: 6, h: 10 };
default:
@@ -0,0 +1,25 @@
import { t } from '@lingui/core/macro';
import { AppTooltip, Status } from 'twenty-ui/display';
type PageLayoutWidgetNoDataDisplayProps = {
widgetId: string;
};
export const PageLayoutWidgetNoDataDisplay = ({
widgetId,
}: PageLayoutWidgetNoDataDisplayProps) => {
const tooltipId = `widget-incomplete-tooltip-${widgetId}`;
return (
<>
<div id={tooltipId}>
<Status color="red" text={t`No Data`} />
</div>
<AppTooltip
anchorSelect={`#${tooltipId}`}
content={t`No data available. Click edit to configure this widget.`}
place="top"
/>
</>
);
};
@@ -25,19 +25,23 @@ const StyledContent = styled.div`
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
const { handleEditWidget } = useEditPageLayoutWidget();
const { hasAccess, restriction } = useWidgetPermissions(widget);
const isPageLayoutInEditMode = useRecoilComponentValue(
isPageLayoutInEditModeComponentState,
);
const { hasAccess, restriction } = useWidgetPermissions(widget);
return (
<WidgetContainer isRestricted={!hasAccess}>
<WidgetHeader
isInEditMode={isPageLayoutInEditMode}
title={widget.title}
onEdit={() =>
handleEditWidget({ widgetId: widget.id, widgetType: widget.type })
handleEditWidget({
widgetId: widget.id,
widgetType: widget.type,
})
}
onRemove={() => deletePageLayoutWidget(widget.id)}
/>
@@ -1,18 +1,142 @@
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import {
type ApolloClient,
type NormalizedCacheObject,
useApolloClient,
} from '@apollo/client';
import { type MockedResponse } from '@apollo/client/testing';
import { type Meta, type StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui/testing';
import { GraphType } from '~/generated-metadata/graphql';
import { type MutableSnapshot } from 'recoil';
import { MemoryRouter } from 'react-router-dom';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import { generateGroupByQuery } from '@/page-layout/widgets/graph/utils/generateGroupByQuery';
import {
GraphOrderBy,
GraphType,
WidgetType,
} from '~/generated-metadata/graphql';
import {
AxisNameDisplay,
ExtendedAggregateOperations,
type PageLayoutWidget,
} from '~/generated/graphql';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
const companyObjectMetadataItem = getMockObjectMetadataItemOrThrow(
CoreObjectNameSingular.Company,
);
const idField = getMockFieldMetadataItemOrThrow({
objectMetadataItem: companyObjectMetadataItem,
fieldName: 'id',
});
const createdAtField = getMockFieldMetadataItemOrThrow({
objectMetadataItem: companyObjectMetadataItem,
fieldName: 'createdAt',
});
const barChartGroupByQuery = generateGroupByQuery({
objectMetadataItem: companyObjectMetadataItem,
aggregateOperations: ['totalCount'],
});
const graphqlMocks: MockedResponse[] = [
{
request: {
query: barChartGroupByQuery,
variables: {
groupBy: [
{
createdAt: {
granularity: 'DAY',
},
},
],
},
},
result: {
data: {
companiesGroupBy: [
{
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 WidgetRenderer> = {
title: 'Modules/PageLayout/Widgets/WidgetRenderer',
component: WidgetRenderer,
decorators: [
(Story, context) => (
<PageLayoutTestWrapper>{Story(context)}</PageLayoutTestWrapper>
),
ComponentDecorator,
(Story) => {
const initializeState = (snapshot: MutableSnapshot) => {
snapshot.set(
objectMetadataItemsState,
generatedMockObjectMetadataItems,
);
snapshot.set(isAppWaitingForFreshObjectMetadataState, false);
};
return (
<MemoryRouter>
<JestMetadataAndApolloMocksWrapper>
<CoreClientProviderWrapper>
<PageLayoutTestWrapper initializeState={initializeState}>
<Story />
</PageLayoutTestWrapper>
</CoreClientProviderWrapper>
</JestMetadataAndApolloMocksWrapper>
</MemoryRouter>
);
},
],
parameters: {
layout: 'centered',
@@ -30,18 +154,31 @@ type Story = StoryObj<typeof WidgetRenderer>;
export const WithNumberChart: Story = {
args: {
widget: createDefaultGraphWidget({
widget: {
__typename: 'PageLayoutWidget',
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
type: WidgetType.GRAPH,
title: 'Sales Pipeline',
graphType: GraphType.NUMBER,
objectMetadataId: companyObjectMetadataItem.id,
gridPosition: {
__typename: 'GridPosition',
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 3,
},
}),
configuration: {
__typename: 'NumberChartConfiguration',
graphType: GraphType.NUMBER,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
displayDataLabel: true,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidget,
},
render: (args) => (
<div style={{ width: '300px', height: '100px' }}>
@@ -52,18 +189,31 @@ export const WithNumberChart: Story = {
export const WithGaugeChart: Story = {
args: {
widget: createDefaultGraphWidget({
id: 'widget-1',
widget: {
__typename: 'PageLayoutWidget',
id: 'widget-2',
pageLayoutTabId: 'tab-overview',
type: WidgetType.GRAPH,
title: 'Conversion Rate',
graphType: GraphType.GAUGE,
objectMetadataId: companyObjectMetadataItem.id,
gridPosition: {
__typename: 'GridPosition',
row: 0,
column: 0,
rowSpan: 5,
columnSpan: 3,
},
}),
configuration: {
__typename: 'GaugeChartConfiguration',
graphType: GraphType.GAUGE,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
displayDataLabel: false,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidget,
},
render: (args) => (
<div style={{ width: '300px', height: '400px' }}>
@@ -72,20 +222,36 @@ export const WithGaugeChart: Story = {
),
};
export const WithPieChart: Story = {
export const WithBarChart: Story = {
args: {
widget: createDefaultGraphWidget({
id: 'widget-1',
widget: {
__typename: 'PageLayoutWidget',
id: 'widget-3',
pageLayoutTabId: 'tab-overview',
title: 'Lead Distribution',
graphType: GraphType.PIE,
type: WidgetType.GRAPH,
title: 'Monthly Trends',
objectMetadataId: companyObjectMetadataItem.id,
gridPosition: {
__typename: 'GridPosition',
row: 0,
column: 0,
rowSpan: 5,
columnSpan: 3,
},
}),
configuration: {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
groupByFieldMetadataIdX: createdAtField.id,
orderByX: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidget,
},
render: (args) => (
<div style={{ width: '300px', height: '500px' }}>
@@ -96,18 +262,31 @@ export const WithPieChart: Story = {
export const SmallWidget: Story = {
args: {
widget: createDefaultGraphWidget({
id: 'widget-1',
widget: {
__typename: 'PageLayoutWidget',
id: 'widget-4',
pageLayoutTabId: 'tab-overview',
type: WidgetType.GRAPH,
title: 'Small Widget (2x2 grid)',
graphType: GraphType.NUMBER,
objectMetadataId: companyObjectMetadataItem.id,
gridPosition: {
__typename: 'GridPosition',
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 2,
},
}),
configuration: {
__typename: 'NumberChartConfiguration',
graphType: GraphType.NUMBER,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
displayDataLabel: true,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidget,
},
parameters: {
docs: {
@@ -125,18 +304,34 @@ export const SmallWidget: Story = {
export const MediumWidget: Story = {
args: {
widget: createDefaultGraphWidget({
id: 'widget-1',
widget: {
__typename: 'PageLayoutWidget',
id: 'widget-5',
pageLayoutTabId: 'tab-overview',
type: WidgetType.GRAPH,
title: 'Medium Widget (4x3 grid)',
graphType: GraphType.GAUGE,
objectMetadataId: companyObjectMetadataItem.id,
gridPosition: {
__typename: 'GridPosition',
row: 0,
column: 0,
rowSpan: 3,
columnSpan: 4,
},
}),
configuration: {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
groupByFieldMetadataIdX: createdAtField.id,
orderByX: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidget,
},
parameters: {
docs: {
@@ -154,18 +349,34 @@ export const MediumWidget: Story = {
export const LargeWidget: Story = {
args: {
widget: createDefaultGraphWidget({
id: 'widget-1',
widget: {
__typename: 'PageLayoutWidget',
id: 'widget-6',
pageLayoutTabId: 'tab-overview',
type: WidgetType.GRAPH,
title: 'Large Widget (6x4 grid)',
graphType: GraphType.PIE,
objectMetadataId: companyObjectMetadataItem.id,
gridPosition: {
__typename: 'GridPosition',
row: 0,
column: 0,
rowSpan: 4,
columnSpan: 6,
},
}),
configuration: {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
groupByFieldMetadataIdX: createdAtField.id,
orderByX: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidget,
},
parameters: {
docs: {
@@ -183,18 +394,31 @@ export const LargeWidget: Story = {
export const WideWidget: Story = {
args: {
widget: createDefaultGraphWidget({
id: 'widget-1',
widget: {
__typename: 'PageLayoutWidget',
id: 'widget-7',
pageLayoutTabId: 'tab-overview',
type: WidgetType.GRAPH,
title: 'Wide Widget (8x2 grid)',
graphType: GraphType.NUMBER,
objectMetadataId: companyObjectMetadataItem.id,
gridPosition: {
__typename: 'GridPosition',
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 8,
},
}),
configuration: {
__typename: 'NumberChartConfiguration',
graphType: GraphType.NUMBER,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
displayDataLabel: true,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidget,
},
parameters: {
docs: {
@@ -212,18 +436,34 @@ export const WideWidget: Story = {
export const TallWidget: Story = {
args: {
widget: createDefaultGraphWidget({
id: 'widget-1',
widget: {
__typename: 'PageLayoutWidget',
id: 'widget-8',
pageLayoutTabId: 'tab-overview',
type: WidgetType.GRAPH,
title: 'Tall Widget (3x6 grid)',
graphType: GraphType.GAUGE,
objectMetadataId: companyObjectMetadataItem.id,
gridPosition: {
__typename: 'GridPosition',
row: 0,
column: 0,
rowSpan: 6,
columnSpan: 3,
},
}),
configuration: {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
groupByFieldMetadataIdX: createdAtField.id,
orderByX: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidget,
},
parameters: {
docs: {
@@ -0,0 +1,133 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { getDefaultWidgetData } from '@/page-layout/utils/getDefaultWidgetData';
import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay';
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 { areChartConfigurationFieldsValidForQuery } from '@/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery';
import { lazy, Suspense } from 'react';
import { GraphType, type PageLayoutWidget } from '~/generated/graphql';
const GraphWidgetLineChart = lazy(() =>
import(
'@/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart'
).then((module) => ({
default: module.GraphWidgetLineChart,
})),
);
const GraphWidgetPieChart = lazy(() =>
import(
'@/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart'
).then((module) => ({
default: module.GraphWidgetPieChart,
})),
);
const GraphWidgetGaugeChart = lazy(() =>
import(
'@/page-layout/widgets/graph/graphWidgetGaugeChart/components/GraphWidgetGaugeChart'
).then((module) => ({
default: module.GraphWidgetGaugeChart,
})),
);
export type GraphWidgetProps = {
widget: PageLayoutWidget;
objectMetadataId: string;
graphType: GraphType;
};
export const GraphWidget = ({
widget,
objectMetadataId,
graphType,
}: GraphWidgetProps) => {
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataId,
});
const hasValidConfiguration = areChartConfigurationFieldsValidForQuery(
widget.configuration,
objectMetadataItem,
);
if (!hasValidConfiguration) {
return <PageLayoutWidgetNoDataDisplay widgetId={widget.id} />;
}
const data: any = getDefaultWidgetData(graphType);
if (!data) {
return null;
}
switch (graphType) {
case GraphType.NUMBER:
return (
<GraphWidgetNumberChart
value={data.value}
trendPercentage={data.trendPercentage}
/>
);
case GraphType.GAUGE:
return (
<Suspense fallback={<ChartSkeletonLoader />}>
<GraphWidgetGaugeChart
data={{
value: data.value,
min: data.min,
max: data.max,
label: data.label,
}}
displayType="percentage"
showValue
id={`gauge-chart-${widget.id}`}
/>
</Suspense>
);
case GraphType.PIE:
return (
<Suspense fallback={<ChartSkeletonLoader />}>
<GraphWidgetPieChart
data={data.items}
showLegend
displayType="percentage"
id={`pie-chart-${widget.id}`}
/>
</Suspense>
);
case GraphType.BAR:
return <GraphWidgetBarChartRenderer widget={widget} />;
case GraphType.LINE:
return (
<Suspense fallback={<ChartSkeletonLoader />}>
<GraphWidgetLineChart
id={`line-chart-${widget.id}`}
data={data.series}
enableArea={data.enableArea}
showLegend={data.showLegend}
showGrid={data.showGrid}
enablePoints={data.enablePoints}
xAxisLabel={data.xAxisLabel}
yAxisLabel={data.yAxisLabel}
displayType={data.displayType}
prefix={data.prefix}
suffix={data.suffix}
xScale={data.xScale}
yScale={data.yScale}
curve={data.curve}
stackedArea={data.stackedArea}
enableSlices={data.enableSlices}
/>
</Suspense>
);
default:
return null;
}
};
@@ -1,33 +1,7 @@
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 GraphWidgetLineChart = lazy(() =>
import(
'@/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart'
).then((module) => ({
default: module.GraphWidgetLineChart,
})),
);
const GraphWidgetPieChart = lazy(() =>
import(
'@/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart'
).then((module) => ({
default: module.GraphWidgetPieChart,
})),
);
const GraphWidgetGaugeChart = lazy(() =>
import(
'@/page-layout/widgets/graph/graphWidgetGaugeChart/components/GraphWidgetGaugeChart'
).then((module) => ({
default: module.GraphWidgetGaugeChart,
})),
);
import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay';
import { GraphWidget } from '@/page-layout/widgets/graph/components/GraphWidget';
import { isDefined } from 'twenty-shared/utils';
import { GraphType, type PageLayoutWidget } from '~/generated/graphql';
type GraphWidgetRendererProps = {
widget: PageLayoutWidget;
@@ -35,91 +9,23 @@ type GraphWidgetRendererProps = {
export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => {
if (!widget.configuration || !('graphType' in widget.configuration)) {
throw new Error(
`Invalid configuration for widget ${widget.id}: missing graphType`,
);
return <PageLayoutWidgetNoDataDisplay widgetId={widget.id} />;
}
const graphType = widget.configuration.graphType;
if (!Object.values(GraphType).includes(graphType)) {
throw new Error(
`Unsupported graph type ${graphType} for widget ${widget.id}`,
);
if (
!Object.values(GraphType).includes(graphType) ||
!isDefined(widget.objectMetadataId)
) {
return <PageLayoutWidgetNoDataDisplay widgetId={widget.id} />;
}
const data: any = getDefaultWidgetData(graphType);
if (!data) {
return null;
}
switch (graphType) {
case GraphType.NUMBER:
return (
<GraphWidgetNumberChart
value={data.value}
trendPercentage={data.trendPercentage}
/>
);
case GraphType.GAUGE:
return (
<Suspense fallback={<ChartSkeletonLoader />}>
<GraphWidgetGaugeChart
data={{
value: data.value,
min: data.min,
max: data.max,
label: data.label,
}}
displayType="percentage"
showValue
id={`gauge-chart-${widget.id}`}
/>
</Suspense>
);
case GraphType.PIE:
return (
<Suspense fallback={<ChartSkeletonLoader />}>
<GraphWidgetPieChart
data={data.items}
showLegend
displayType="percentage"
id={`pie-chart-${widget.id}`}
/>
</Suspense>
);
case GraphType.BAR:
return <GraphWidgetBarChartRenderer widget={widget} />;
case GraphType.LINE:
return (
<Suspense fallback={<ChartSkeletonLoader />}>
<GraphWidgetLineChart
id={`line-chart-${widget.id}`}
data={data.series}
enableArea={data.enableArea}
showLegend={data.showLegend}
showGrid={data.showGrid}
enablePoints={data.enablePoints}
xAxisLabel={data.xAxisLabel}
yAxisLabel={data.yAxisLabel}
displayType={data.displayType}
prefix={data.prefix}
suffix={data.suffix}
xScale={data.xScale}
yScale={data.yScale}
curve={data.curve}
stackedArea={data.stackedArea}
enableSlices={data.enableSlices}
/>
</Suspense>
);
default:
return null;
}
return (
<GraphWidget
widget={widget}
objectMetadataId={widget.objectMetadataId}
graphType={graphType}
/>
);
};
@@ -0,0 +1,70 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isDefined } from 'twenty-shared/utils';
import { type PageLayoutWidget } from '~/generated/graphql';
const fieldExists = (
fieldId: string | undefined | null,
objectMetadataItem: ObjectMetadataItem,
): boolean => {
if (!isDefined(fieldId)) {
return false;
}
return objectMetadataItem.fields.some((field) => field.id === fieldId);
};
export const areChartConfigurationFieldsValidForQuery = (
configuration: PageLayoutWidget['configuration'],
objectMetadataItem?: ObjectMetadataItem | null,
): boolean => {
if (!isDefined(configuration)) {
return false;
}
if (!isDefined(objectMetadataItem) || !isDefined(objectMetadataItem.fields)) {
return false;
}
switch (configuration.__typename) {
case 'BarChartConfiguration':
case 'LineChartConfiguration': {
const hasRequiredXFields =
fieldExists(
configuration.aggregateFieldMetadataId,
objectMetadataItem,
) &&
fieldExists(configuration.groupByFieldMetadataIdX, objectMetadataItem);
const hasValidYField =
!isDefined(configuration.groupByFieldMetadataIdY) ||
fieldExists(configuration.groupByFieldMetadataIdY, objectMetadataItem);
return hasRequiredXFields && hasValidYField;
}
case 'PieChartConfiguration':
return (
fieldExists(
configuration.aggregateFieldMetadataId,
objectMetadataItem,
) &&
fieldExists(configuration.groupByFieldMetadataId, objectMetadataItem)
);
case 'NumberChartConfiguration':
case 'GaugeChartConfiguration':
return fieldExists(
configuration.aggregateFieldMetadataId,
objectMetadataItem,
);
case 'IframeConfiguration':
return (
typeof configuration.url === 'string' &&
configuration.url.trim().length > 0
);
default:
return false;
}
};
@@ -103,7 +103,7 @@ export const transformGroupByDataToBarChartData = ({
? `${getAggregateOperationLabel(configuration.aggregateOperation)} of ${aggregateField.label}`
: undefined;
const showDataLabels = configuration.displayDataLabel;
const showDataLabels = configuration.displayDataLabel ?? false;
const baseResult = isDefined(groupByFieldY)
? transformTwoDimensionalGroupByToBarChartData({
@@ -28,7 +28,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/page-layout-tabs')
@Controller('rest/metadata/pageLayoutTabs')
@UseGuards(WorkspaceAuthGuard)
@UseFilters(PageLayoutTabRestApiExceptionFilter)
export class PageLayoutTabController {
@@ -28,7 +28,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/page-layout-widgets')
@Controller('rest/metadata/pageLayoutWidgets')
@UseGuards(WorkspaceAuthGuard)
@UseFilters(PageLayoutWidgetRestApiExceptionFilter)
export class PageLayoutWidgetController {
@@ -23,7 +23,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/page-layouts')
@Controller('rest/metadata/pageLayouts')
@UseGuards(WorkspaceAuthGuard)
@UseFilters(PageLayoutRestApiExceptionFilter)
export class PageLayoutController {
@@ -47,10 +47,10 @@ export class BarChartConfigurationDTO {
@IsOptional()
groupBySubFieldNameX?: string;
@Field(() => GraphOrderBy)
@Field(() => GraphOrderBy, { nullable: true })
@IsEnum(GraphOrderBy)
@IsNotEmpty()
orderByX: GraphOrderBy;
@IsOptional()
orderByX?: GraphOrderBy;
@Field(() => UUIDScalarType, { nullable: true })
@IsUUID()
@@ -72,15 +72,18 @@ export class BarChartConfigurationDTO {
@IsOptional()
omitNullValues?: boolean;
@Field(() => AxisNameDisplay)
@Field(() => AxisNameDisplay, {
nullable: true,
defaultValue: AxisNameDisplay.BOTH,
})
@IsEnum(AxisNameDisplay)
@IsNotEmpty()
axisNameDisplay: AxisNameDisplay;
@IsOptional()
axisNameDisplay?: AxisNameDisplay;
@Field(() => Boolean)
@Field(() => Boolean, { nullable: true, defaultValue: false })
@IsBoolean()
@IsNotEmpty()
displayDataLabel: boolean;
@IsOptional()
displayDataLabel?: boolean;
@Field(() => Number, { nullable: true })
@IsNumber()
@@ -34,9 +34,10 @@ export class GaugeChartConfigurationDTO {
@IsNotEmpty()
aggregateOperation: ExtendedAggregateOperations;
@Field(() => Boolean)
@Field(() => Boolean, { nullable: true, defaultValue: false })
@IsBoolean()
displayDataLabel: boolean;
@IsOptional()
displayDataLabel?: boolean;
@Field(() => String, { nullable: true })
@IsString()
@@ -47,10 +47,13 @@ export class LineChartConfigurationDTO {
@IsOptional()
groupBySubFieldNameX?: string;
@Field(() => GraphOrderBy)
@Field(() => GraphOrderBy, {
nullable: true,
defaultValue: GraphOrderBy.FIELD_ASC,
})
@IsEnum(GraphOrderBy)
@IsNotEmpty()
orderByX: GraphOrderBy;
@IsOptional()
orderByX?: GraphOrderBy;
@Field(() => UUIDScalarType, { nullable: true })
@IsUUID()
@@ -72,15 +75,18 @@ export class LineChartConfigurationDTO {
@IsOptional()
omitNullValues?: boolean;
@Field(() => AxisNameDisplay)
@Field(() => AxisNameDisplay, {
nullable: true,
defaultValue: AxisNameDisplay.BOTH,
})
@IsEnum(AxisNameDisplay)
@IsNotEmpty()
axisNameDisplay: AxisNameDisplay;
@IsOptional()
axisNameDisplay?: AxisNameDisplay;
@Field(() => Boolean)
@Field(() => Boolean, { nullable: true, defaultValue: false })
@IsBoolean()
@IsNotEmpty()
displayDataLabel: boolean;
@IsOptional()
displayDataLabel?: boolean;
@Field(() => Number, { nullable: true })
@IsNumber()
@@ -39,10 +39,10 @@ export class NumberChartConfigurationDTO {
@IsOptional()
label?: string;
@Field(() => Boolean)
@Field(() => Boolean, { nullable: true, defaultValue: false })
@IsBoolean()
@IsNotEmpty()
displayDataLabel: boolean;
@IsOptional()
displayDataLabel?: boolean;
@Field(() => String, { nullable: true })
@IsString()
@@ -54,11 +54,6 @@ export class NumberChartConfigurationDTO {
@IsOptional()
description?: string;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
color?: string;
@Field(() => GraphQLJSON, { nullable: true })
@IsObject()
@IsOptional()
@@ -45,15 +45,18 @@ export class PieChartConfigurationDTO {
@IsOptional()
groupBySubFieldName?: string;
@Field(() => GraphOrderBy)
@Field(() => GraphOrderBy, {
nullable: true,
defaultValue: GraphOrderBy.VALUE_DESC,
})
@IsEnum(GraphOrderBy)
@IsNotEmpty()
orderBy: GraphOrderBy;
@IsOptional()
orderBy?: GraphOrderBy;
@Field(() => Boolean)
@Field(() => Boolean, { nullable: true, defaultValue: false })
@IsBoolean()
@IsNotEmpty()
displayDataLabel: boolean;
@IsOptional()
displayDataLabel?: boolean;
@Field(() => String, { nullable: true })
@IsString()
@@ -6,6 +6,7 @@ import { IframeConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos
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 { 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';
export const WidgetConfiguration = createUnionType({
@@ -25,24 +26,34 @@ export const WidgetConfiguration = createUnionType({
);
}
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}`,
);
if (
configuration.configurationType === WidgetConfigurationType.CHART_CONFIG
) {
switch (configuration.graphType) {
case GraphType.BAR:
return BarChartConfigurationDTO;
case GraphType.LINE:
return LineChartConfigurationDTO;
case GraphType.PIE:
return PieChartConfigurationDTO;
case GraphType.NUMBER:
return NumberChartConfigurationDTO;
case GraphType.GAUGE:
return GaugeChartConfigurationDTO;
default:
throw new Error(`Unknown graph type: ${configuration.graphType}`);
}
}
if (
configuration.configurationType === WidgetConfigurationType.IFRAME_CONFIG
) {
return IframeConfigurationDTO;
}
throw new Error(
`Unknown widget configuration type: ${configuration.configurationType}`,
);
},
});
@@ -1,8 +1,4 @@
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',
CHART_CONFIG = 'CHART_CONFIG',
IFRAME_CONFIG = 'IFRAME_CONFIG',
}
@@ -1,11 +1,20 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import {
Args,
Mutation,
Parent,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
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 { PageLayoutWidgetDTO } from 'src/engine/core-modules/page-layout/dtos/page-layout-widget.dto';
import { WidgetConfiguration } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface';
import { PageLayoutWidgetService } from 'src/engine/core-modules/page-layout/services/page-layout-widget.service';
import { injectWidgetConfigurationDiscriminator } from 'src/engine/core-modules/page-layout/utils/inject-widget-configuration-discriminator.util';
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/core-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
@@ -79,4 +88,12 @@ export class PageLayoutWidgetResolver {
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.restore(id, workspace.id);
}
@ResolveField(() => WidgetConfiguration, { nullable: true })
configuration(@Parent() widget: PageLayoutWidgetDTO) {
return injectWidgetConfigurationDiscriminator(
widget.type,
widget.configuration,
);
}
}
@@ -18,7 +18,6 @@ import {
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';
@@ -31,9 +30,6 @@ describe('validateAndTransformWidgetConfiguration', () => {
);
expect(result).toMatchObject(TEST_IFRAME_CONFIG);
expect((result as any)?.configurationType).toBe(
WidgetConfigurationType.IFRAME_CONFIG,
);
});
it('should throw error for invalid URL', () => {
@@ -73,9 +69,6 @@ describe('validateAndTransformWidgetConfiguration', () => {
);
expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG);
expect((result as any)?.configurationType).toBe(
WidgetConfigurationType.NUMBER_CHART_CONFIG,
);
});
it('should validate minimal number graph configuration', () => {
@@ -85,18 +78,15 @@ describe('validateAndTransformWidgetConfiguration', () => {
);
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', () => {
it('should throw error for partial number graph configuration with missing required fields', () => {
expect(() =>
validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
),
).toThrow(/aggregateFieldMetadataId should not be empty/);
).toThrow(/aggregateFieldMetadataId.*aggregateOperation/);
});
it('should throw error for invalid UUID', () => {
@@ -117,9 +107,6 @@ describe('validateAndTransformWidgetConfiguration', () => {
);
expect(result).toMatchObject(TEST_BAR_CHART_CONFIG);
expect((result as any)?.configurationType).toBe(
WidgetConfigurationType.BAR_CHART_CONFIG,
);
});
it('should validate minimal bar graph configuration', () => {
@@ -129,18 +116,15 @@ describe('validateAndTransformWidgetConfiguration', () => {
);
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', () => {
it('should throw error for partial bar graph configuration with missing required fields', () => {
expect(() =>
validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY,
),
).toThrow(/groupByFieldMetadataIdX should not be empty/);
).toThrow(/groupByFieldMetadataIdX/);
});
});
@@ -152,9 +136,6 @@ describe('validateAndTransformWidgetConfiguration', () => {
);
expect(result).toMatchObject(TEST_LINE_CHART_CONFIG);
expect((result as any)?.configurationType).toBe(
WidgetConfigurationType.LINE_CHART_CONFIG,
);
});
it('should validate minimal line graph configuration', () => {
@@ -164,9 +145,6 @@ describe('validateAndTransformWidgetConfiguration', () => {
);
expect(result).toMatchObject(TEST_LINE_CHART_CONFIG_MINIMAL);
expect((result as any)?.configurationType).toBe(
WidgetConfigurationType.LINE_CHART_CONFIG,
);
});
});
@@ -178,9 +156,6 @@ describe('validateAndTransformWidgetConfiguration', () => {
);
expect(result).toMatchObject(TEST_PIE_CHART_CONFIG);
expect((result as any)?.configurationType).toBe(
WidgetConfigurationType.PIE_CHART_CONFIG,
);
});
it('should validate minimal pie graph configuration', () => {
@@ -190,9 +165,6 @@ describe('validateAndTransformWidgetConfiguration', () => {
);
expect(result).toMatchObject(TEST_PIE_CHART_CONFIG_MINIMAL);
expect((result as any)?.configurationType).toBe(
WidgetConfigurationType.PIE_CHART_CONFIG,
);
});
});
@@ -204,9 +176,6 @@ describe('validateAndTransformWidgetConfiguration', () => {
);
expect(result).toMatchObject(TEST_GAUGE_CHART_CONFIG);
expect((result as any)?.configurationType).toBe(
WidgetConfigurationType.GAUGE_CHART_CONFIG,
);
});
it('should validate minimal gauge graph configuration', () => {
@@ -216,9 +185,6 @@ describe('validateAndTransformWidgetConfiguration', () => {
);
expect(result).toMatchObject(TEST_GAUGE_CHART_CONFIG_MINIMAL);
expect((result as any)?.configurationType).toBe(
WidgetConfigurationType.GAUGE_CHART_CONFIG,
);
});
});
@@ -0,0 +1,32 @@
import { type WidgetConfigurationInterface } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface';
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';
type ConfigurationWithDiscriminator = WidgetConfigurationInterface & {
configurationType: WidgetConfigurationType;
};
export const injectWidgetConfigurationDiscriminator = (
widgetType: WidgetType,
configuration: WidgetConfigurationInterface | null,
): ConfigurationWithDiscriminator | null => {
if (!configuration) {
return null;
}
if (widgetType === WidgetType.IFRAME) {
return {
...configuration,
configurationType: WidgetConfigurationType.IFRAME_CONFIG,
} as ConfigurationWithDiscriminator;
}
if (widgetType === WidgetType.GRAPH && 'graphType' in configuration) {
return {
...configuration,
configurationType: WidgetConfigurationType.CHART_CONFIG,
} as ConfigurationWithDiscriminator;
}
return configuration as ConfigurationWithDiscriminator;
};
@@ -9,7 +9,6 @@ import { NumberChartConfigurationDTO } from 'src/engine/core-modules/page-layout
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 => {
@@ -37,16 +36,16 @@ const validateGraphConfiguration = (
case GraphType.BAR: {
const instance = plainToInstance(BarChartConfigurationDTO, configuration);
const errors = validateSync(instance);
const errors = validateSync(instance, {
whitelist: true,
forbidUnknownValues: true,
});
if (errors.length > 0) {
throw errors;
}
return {
...instance,
configurationType: WidgetConfigurationType.BAR_CHART_CONFIG,
} as WidgetConfigurationInterface;
return instance;
}
case GraphType.LINE: {
const instance = plainToInstance(
@@ -54,30 +53,30 @@ const validateGraphConfiguration = (
configuration,
);
const errors = validateSync(instance);
const errors = validateSync(instance, {
whitelist: true,
forbidUnknownValues: true,
});
if (errors.length > 0) {
throw errors;
}
return {
...instance,
configurationType: WidgetConfigurationType.LINE_CHART_CONFIG,
} as WidgetConfigurationInterface;
return instance;
}
case GraphType.PIE: {
const instance = plainToInstance(PieChartConfigurationDTO, configuration);
const errors = validateSync(instance);
const errors = validateSync(instance, {
whitelist: true,
forbidUnknownValues: true,
});
if (errors.length > 0) {
throw errors;
}
return {
...instance,
configurationType: WidgetConfigurationType.PIE_CHART_CONFIG,
} as WidgetConfigurationInterface;
return instance;
}
case GraphType.NUMBER: {
const instance = plainToInstance(
@@ -85,16 +84,16 @@ const validateGraphConfiguration = (
configuration,
);
const errors = validateSync(instance);
const errors = validateSync(instance, {
whitelist: true,
forbidUnknownValues: true,
});
if (errors.length > 0) {
throw errors;
}
return {
...instance,
configurationType: WidgetConfigurationType.NUMBER_CHART_CONFIG,
} as WidgetConfigurationInterface;
return instance;
}
case GraphType.GAUGE: {
const instance = plainToInstance(
@@ -102,16 +101,16 @@ const validateGraphConfiguration = (
configuration,
);
const errors = validateSync(instance);
const errors = validateSync(instance, {
whitelist: true,
forbidUnknownValues: true,
});
if (errors.length > 0) {
throw errors;
}
return {
...instance,
configurationType: WidgetConfigurationType.GAUGE_CHART_CONFIG,
} as WidgetConfigurationInterface;
return instance;
}
default:
return null;
@@ -123,16 +122,16 @@ const validateIframeConfiguration = (
): WidgetConfigurationInterface | null => {
const instance = plainToInstance(IframeConfigurationDTO, configuration);
const errors = validateSync(instance);
const errors = validateSync(instance, {
whitelist: true,
forbidUnknownValues: true,
});
if (errors.length > 0) {
throw errors;
}
return {
...instance,
configurationType: WidgetConfigurationType.IFRAME_CONFIG,
} as WidgetConfigurationInterface;
return instance;
};
export const validateAndTransformWidgetConfiguration = (
@@ -73,7 +73,6 @@ export const PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS = `
aggregateOperation
description
filter
color
format
label
}
@@ -22,7 +22,6 @@ export const TEST_NUMBER_CHART_CONFIG = {
aggregateOperation: AggregateOperations.COUNT,
label: 'Total Records',
description: 'Count of all records',
color: 'blue',
format: '0,0',
displayDataLabel: true,
};
@@ -78,7 +78,7 @@ describe('Page Layout Tab REST API', () => {
await cleanupPageLayoutTabRecords();
});
describe('GET /rest/metadata/page-layout-tabs', () => {
describe('GET /rest/metadata/pageLayoutTabs', () => {
it('should return page layout tabs filtered by pageLayoutId', async () => {
const input1 = {
title: 'Tab 1',
@@ -96,7 +96,7 @@ describe('Page Layout Tab REST API', () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layout-tabs?pageLayoutId=${testPageLayoutId}`,
path: `/metadata/pageLayoutTabs?pageLayoutId=${testPageLayoutId}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -111,7 +111,7 @@ describe('Page Layout Tab REST API', () => {
it('should return empty array when no page layout tabs match pageLayoutId', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layout-tabs?pageLayoutId=${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`,
path: `/metadata/pageLayoutTabs?pageLayoutId=${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -123,7 +123,7 @@ describe('Page Layout Tab REST API', () => {
it('should return error when pageLayoutId is missing', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: '/metadata/page-layout-tabs',
path: '/metadata/pageLayoutTabs',
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -137,7 +137,7 @@ describe('Page Layout Tab REST API', () => {
});
});
describe('POST /rest/metadata/page-layout-tabs', () => {
describe('POST /rest/metadata/pageLayoutTabs', () => {
it('should create a new page layout tab with all properties', async () => {
const input = {
title: 'Test Tab',
@@ -180,7 +180,7 @@ describe('Page Layout Tab REST API', () => {
const response = await makeRestAPIRequest({
method: 'post',
path: '/metadata/page-layout-tabs',
path: '/metadata/pageLayoutTabs',
body: pageLayoutTabData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -202,7 +202,7 @@ describe('Page Layout Tab REST API', () => {
const response = await makeRestAPIRequest({
method: 'post',
path: '/metadata/page-layout-tabs',
path: '/metadata/pageLayoutTabs',
body: pageLayoutTabData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -211,7 +211,7 @@ describe('Page Layout Tab REST API', () => {
});
});
describe('GET /rest/metadata/page-layout-tabs/:id', () => {
describe('GET /rest/metadata/pageLayoutTabs/:id', () => {
it('should return a page layout tab by id', async () => {
const input = {
title: 'Tab',
@@ -222,7 +222,7 @@ describe('Page Layout Tab REST API', () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layout-tabs/${pageLayoutTab.id}`,
path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -238,7 +238,7 @@ describe('Page Layout Tab REST API', () => {
it('should return error for non-existent page layout tab', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layout-tabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`,
path: `/metadata/pageLayoutTabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -253,7 +253,7 @@ describe('Page Layout Tab REST API', () => {
});
});
describe('PATCH /rest/metadata/page-layout-tabs/:id', () => {
describe('PATCH /rest/metadata/pageLayoutTabs/:id', () => {
it('should update an existing page layout tab', async () => {
const input = {
title: 'Test Tab for Update',
@@ -269,7 +269,7 @@ describe('Page Layout Tab REST API', () => {
const response = await makeRestAPIRequest({
method: 'patch',
path: `/metadata/page-layout-tabs/${pageLayoutTab.id}`,
path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`,
body: updateData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -299,7 +299,7 @@ describe('Page Layout Tab REST API', () => {
const response = await makeRestAPIRequest({
method: 'patch',
path: `/metadata/page-layout-tabs/${pageLayoutTab.id}`,
path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`,
body: updateData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -323,7 +323,7 @@ describe('Page Layout Tab REST API', () => {
const response = await makeRestAPIRequest({
method: 'patch',
path: `/metadata/page-layout-tabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`,
path: `/metadata/pageLayoutTabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`,
body: updateData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -339,7 +339,7 @@ describe('Page Layout Tab REST API', () => {
});
});
describe('DELETE /rest/metadata/page-layout-tabs/:id', () => {
describe('DELETE /rest/metadata/pageLayoutTabs/:id', () => {
it('should delete an existing page layout tab', async () => {
const pageLayoutTabTitle = generateRecordName('Test Tab for Delete');
const pageLayoutTab = await createTestPageLayoutTabWithRestApi({
@@ -350,7 +350,7 @@ describe('Page Layout Tab REST API', () => {
const deleteResponse = await makeRestAPIRequest({
method: 'delete',
path: `/metadata/page-layout-tabs/${pageLayoutTab.id}`,
path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -359,7 +359,7 @@ describe('Page Layout Tab REST API', () => {
const getResponse = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layout-tabs/${pageLayoutTab.id}`,
path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -376,7 +376,7 @@ describe('Page Layout Tab REST API', () => {
it('should return error when deleting non-existent page layout tab', async () => {
const response = await makeRestAPIRequest({
method: 'delete',
path: `/metadata/page-layout-tabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`,
path: `/metadata/pageLayoutTabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -93,7 +93,7 @@ describe('Page Layout Widget REST API', () => {
await cleanupPageLayoutWidgetRecords();
});
describe('GET /rest/metadata/page-layout-widgets', () => {
describe('GET /rest/metadata/pageLayoutWidgets', () => {
it('should return page layout widgets filtered by pageLayoutTabId', async () => {
const input1 = {
title: 'Widget 1',
@@ -123,7 +123,7 @@ describe('Page Layout Widget REST API', () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layout-widgets?pageLayoutTabId=${testPageLayoutTabId}`,
path: `/metadata/pageLayoutWidgets?pageLayoutTabId=${testPageLayoutTabId}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -140,7 +140,7 @@ describe('Page Layout Widget REST API', () => {
it('should return empty array when no page layout widgets match pageLayoutTabId', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layout-widgets?pageLayoutTabId=${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`,
path: `/metadata/pageLayoutWidgets?pageLayoutTabId=${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -152,7 +152,7 @@ describe('Page Layout Widget REST API', () => {
it('should return error when pageLayoutTabId is missing', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: '/metadata/page-layout-widgets',
path: '/metadata/pageLayoutWidgets',
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -166,7 +166,7 @@ describe('Page Layout Widget REST API', () => {
});
});
describe('POST /rest/metadata/page-layout-widgets', () => {
describe('POST /rest/metadata/pageLayoutWidgets', () => {
it('should create a new page layout widget with all properties', async () => {
const input = {
title: 'Test Widget',
@@ -232,7 +232,7 @@ describe('Page Layout Widget REST API', () => {
const response = await makeRestAPIRequest({
method: 'post',
path: '/metadata/page-layout-widgets',
path: '/metadata/pageLayoutWidgets',
body: pageLayoutWidgetData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -259,7 +259,7 @@ describe('Page Layout Widget REST API', () => {
const response = await makeRestAPIRequest({
method: 'post',
path: '/metadata/page-layout-widgets',
path: '/metadata/pageLayoutWidgets',
body: pageLayoutWidgetData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -275,7 +275,7 @@ describe('Page Layout Widget REST API', () => {
const response = await makeRestAPIRequest({
method: 'post',
path: '/metadata/page-layout-widgets',
path: '/metadata/pageLayoutWidgets',
body: pageLayoutWidgetData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -284,7 +284,7 @@ describe('Page Layout Widget REST API', () => {
});
});
describe('GET /rest/metadata/page-layout-widgets/:id', () => {
describe('GET /rest/metadata/pageLayoutWidgets/:id', () => {
it('should return a page layout widget by id', async () => {
const input = {
title: 'Widget For Get',
@@ -302,7 +302,7 @@ describe('Page Layout Widget REST API', () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layout-widgets/${pageLayoutWidget.id}`,
path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -318,7 +318,7 @@ describe('Page Layout Widget REST API', () => {
it('should return error for non-existent page layout widget', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layout-widgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`,
path: `/metadata/pageLayoutWidgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -333,7 +333,7 @@ describe('Page Layout Widget REST API', () => {
});
});
describe('PATCH /rest/metadata/page-layout-widgets/:id', () => {
describe('PATCH /rest/metadata/pageLayoutWidgets/:id', () => {
it('should update an existing page layout widget', async () => {
const input = {
title: 'Test Widget for Update',
@@ -363,7 +363,7 @@ describe('Page Layout Widget REST API', () => {
const response = await makeRestAPIRequest({
method: 'patch',
path: `/metadata/page-layout-widgets/${pageLayoutWidget.id}`,
path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`,
body: updateData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -400,7 +400,7 @@ describe('Page Layout Widget REST API', () => {
const response = await makeRestAPIRequest({
method: 'patch',
path: `/metadata/page-layout-widgets/${pageLayoutWidget.id}`,
path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`,
body: updateData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -425,7 +425,7 @@ describe('Page Layout Widget REST API', () => {
const response = await makeRestAPIRequest({
method: 'patch',
path: `/metadata/page-layout-widgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`,
path: `/metadata/pageLayoutWidgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`,
body: updateData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -441,7 +441,7 @@ describe('Page Layout Widget REST API', () => {
});
});
describe('DELETE /rest/metadata/page-layout-widgets/:id', () => {
describe('DELETE /rest/metadata/pageLayoutWidgets/:id', () => {
it('should delete an existing page layout widget', async () => {
const pageLayoutWidgetTitle = generateRecordName(
'Test Widget for Delete',
@@ -460,7 +460,7 @@ describe('Page Layout Widget REST API', () => {
const deleteResponse = await makeRestAPIRequest({
method: 'delete',
path: `/metadata/page-layout-widgets/${pageLayoutWidget.id}`,
path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -469,7 +469,7 @@ describe('Page Layout Widget REST API', () => {
const getResponse = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layout-widgets/${pageLayoutWidget.id}`,
path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -486,7 +486,7 @@ describe('Page Layout Widget REST API', () => {
it('should return error when deleting non-existent page layout widget', async () => {
const response = await makeRestAPIRequest({
method: 'delete',
path: `/metadata/page-layout-widgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`,
path: `/metadata/pageLayoutWidgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -63,11 +63,11 @@ describe('Page Layout REST API', () => {
await cleanupPageLayoutRecords();
});
describe('GET /rest/metadata/page-layouts', () => {
describe('GET /rest/metadata/pageLayouts', () => {
it('should return all page layouts for workspace', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: '/metadata/page-layouts',
path: '/metadata/pageLayouts',
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -84,7 +84,7 @@ describe('Page Layout REST API', () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layouts?objectMetadataId=${testObjectMetadataId}`,
path: `/metadata/pageLayouts?objectMetadataId=${testObjectMetadataId}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -100,7 +100,7 @@ describe('Page Layout REST API', () => {
it('should return empty array when no page layouts match objectMetadataId', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layouts?objectMetadataId=${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`,
path: `/metadata/pageLayouts?objectMetadataId=${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -110,7 +110,7 @@ describe('Page Layout REST API', () => {
});
});
describe('POST /rest/metadata/page-layouts', () => {
describe('POST /rest/metadata/pageLayouts', () => {
it('should create a new page layout with all properties', async () => {
const pageLayoutName = generateRecordName('Dashboard Page Layout');
const pageLayout = await createTestPageLayoutWithRestApi({
@@ -143,7 +143,7 @@ describe('Page Layout REST API', () => {
await deleteTestPageLayoutWithRestApi(pageLayout.id);
});
describe('GET /rest/metadata/page-layouts/:id', () => {
describe('GET /rest/metadata/pageLayouts/:id', () => {
it('should return a page layout by id', async () => {
const pageLayoutName = generateRecordName('Test Page Layout for Get');
const pageLayout = await createTestPageLayoutWithRestApi({
@@ -154,7 +154,7 @@ describe('Page Layout REST API', () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layouts/${pageLayout.id}`,
path: `/metadata/pageLayouts/${pageLayout.id}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -172,7 +172,7 @@ describe('Page Layout REST API', () => {
it('should return {} for non-existent page layout', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`,
path: `/metadata/pageLayouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -187,7 +187,7 @@ describe('Page Layout REST API', () => {
});
});
describe('PATCH /rest/metadata/page-layouts/:id', () => {
describe('PATCH /rest/metadata/pageLayouts/:id', () => {
it('should update an existing page layout', async () => {
const pageLayoutName = generateRecordName(
'Test Page Layout for Update',
@@ -206,7 +206,7 @@ describe('Page Layout REST API', () => {
const response = await makeRestAPIRequest({
method: 'patch',
path: `/metadata/page-layouts/${pageLayout.id}`,
path: `/metadata/pageLayouts/${pageLayout.id}`,
body: updateData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -237,7 +237,7 @@ describe('Page Layout REST API', () => {
const response = await makeRestAPIRequest({
method: 'patch',
path: `/metadata/page-layouts/${pageLayout.id}`,
path: `/metadata/pageLayouts/${pageLayout.id}`,
body: updateData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -261,7 +261,7 @@ describe('Page Layout REST API', () => {
const response = await makeRestAPIRequest({
method: 'patch',
path: `/metadata/page-layouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`,
path: `/metadata/pageLayouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`,
body: updateData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -277,7 +277,7 @@ describe('Page Layout REST API', () => {
});
});
describe('DELETE /rest/metadata/page-layouts/:id', () => {
describe('DELETE /rest/metadata/pageLayouts/:id', () => {
it('should delete an existing page layout', async () => {
const pageLayoutName = generateRecordName(
'Test Page Layout for Delete',
@@ -290,7 +290,7 @@ describe('Page Layout REST API', () => {
const deleteResponse = await makeRestAPIRequest({
method: 'delete',
path: `/metadata/page-layouts/${pageLayout.id}`,
path: `/metadata/pageLayouts/${pageLayout.id}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -299,7 +299,7 @@ describe('Page Layout REST API', () => {
const getResponse = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layouts/${pageLayout.id}`,
path: `/metadata/pageLayouts/${pageLayout.id}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -316,7 +316,7 @@ describe('Page Layout REST API', () => {
it('should return 404 error when deleting non-existent page layout', async () => {
const response = await makeRestAPIRequest({
method: 'delete',
path: `/metadata/page-layouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`,
path: `/metadata/pageLayouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -347,7 +347,7 @@ describe('Page Layout REST API', () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/page-layouts?objectMetadataId=${testObjectMetadataId}`,
path: `/metadata/pageLayouts?objectMetadataId=${testObjectMetadataId}`,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -16,7 +16,7 @@ export const createTestPageLayoutWithRestApi = async (
const response = await makeRestAPIRequest({
method: 'post',
path: '/metadata/page-layouts',
path: '/metadata/pageLayouts',
body: pageLayoutData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -35,7 +35,7 @@ export const deleteTestPageLayoutWithRestApi = async (
): Promise<void> => {
await makeRestAPIRequest({
method: 'delete',
path: `/metadata/page-layouts/${pageLayoutId}`,
path: `/metadata/pageLayouts/${pageLayoutId}`,
bearer: API_KEY_ACCESS_TOKEN,
}).catch(() => {});
};
@@ -14,7 +14,7 @@ export const createTestPageLayoutTabWithRestApi = async (
const response = await makeRestAPIRequest({
method: 'post',
path: '/metadata/page-layout-tabs',
path: '/metadata/pageLayoutTabs',
body: pageLayoutTabData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -33,7 +33,7 @@ export const deleteTestPageLayoutTabWithRestApi = async (
): Promise<void> => {
await makeRestAPIRequest({
method: 'delete',
path: `/metadata/page-layout-tabs/${pageLayoutTabId}`,
path: `/metadata/pageLayoutTabs/${pageLayoutTabId}`,
bearer: API_KEY_ACCESS_TOKEN,
});
};
@@ -21,7 +21,7 @@ export const createTestPageLayoutWidgetWithRestApi = async (
const response = await makeRestAPIRequest({
method: 'post',
path: '/metadata/page-layout-widgets',
path: '/metadata/pageLayoutWidgets',
body: pageLayoutWidgetData,
bearer: API_KEY_ACCESS_TOKEN,
});
@@ -40,7 +40,7 @@ export const deleteTestPageLayoutWidgetWithRestApi = async (
): Promise<void> => {
await makeRestAPIRequest({
method: 'delete',
path: `/metadata/page-layout-widgets/${pageLayoutWidgetId}`,
path: `/metadata/pageLayoutWidgets/${pageLayoutWidgetId}`,
bearer: API_KEY_ACCESS_TOKEN,
});
};