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:
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+8
-1
@@ -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);
|
||||
}
|
||||
|
||||
+4
@@ -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) => ({
|
||||
|
||||
+3
-12
@@ -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();
|
||||
|
||||
+3
-2
@@ -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}
|
||||
|
||||
+17
-8
@@ -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 '';
|
||||
}
|
||||
|
||||
+19
-10
@@ -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,
|
||||
|
||||
+36
-8
@@ -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 };
|
||||
};
|
||||
+9
-1
@@ -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,
|
||||
|
||||
+8
-7
@@ -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:
|
||||
|
||||
+25
@@ -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)}
|
||||
/>
|
||||
|
||||
+282
-42
@@ -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: {
|
||||
|
||||
+133
@@ -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;
|
||||
}
|
||||
};
|
||||
+17
-111
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+70
@@ -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;
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -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({
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+12
-9
@@ -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()
|
||||
|
||||
+3
-2
@@ -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()
|
||||
|
||||
+15
-9
@@ -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()
|
||||
|
||||
+3
-8
@@ -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()
|
||||
|
||||
+9
-6
@@ -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()
|
||||
|
||||
+28
-17
@@ -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
-5
@@ -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',
|
||||
}
|
||||
|
||||
+18
-1
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-38
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+32
@@ -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;
|
||||
};
|
||||
+30
-31
@@ -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
|
||||
}
|
||||
|
||||
-1
@@ -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,
|
||||
};
|
||||
|
||||
+18
-18
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+19
-19
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+17
-17
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
+2
-2
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user