Chart editor - Part 1 (#14820)
This PR is the first part of the creation of the Chart editor. https://github.com/user-attachments/assets/8b0af8ea-be41-4506-84cb-e40f5521cbf0 Done: - Bar chart settings (except filters) - Line chart settings (except filters) In progress: - Pie chart settings - Number chart settings - Gauge chart settings Left to do: - Loosen the backend validation to allow the user to save a partial configuration, validate the graph configuration in the frontend and display a error friendly message in the graph if the config is not completed yet - Implement the filter edition - Finish the other graph types settings
This commit is contained in:
@@ -270,12 +270,22 @@ export type AvailableWorkspacesAndAccessTokensOutput = {
|
||||
tokens: AuthTokenPair;
|
||||
};
|
||||
|
||||
/** Which axes should display labels */
|
||||
export enum AxisNameDisplay {
|
||||
BOTH = 'BOTH',
|
||||
NONE = 'NONE',
|
||||
X = 'X',
|
||||
Y = 'Y'
|
||||
}
|
||||
|
||||
export type BarChartConfiguration = {
|
||||
__typename?: 'BarChartConfiguration';
|
||||
aggregateFieldMetadataId: Scalars['UUID'];
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
axisNameDisplay: AxisNameDisplay;
|
||||
color?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
displayDataLabel: Scalars['Boolean'];
|
||||
filter?: Maybe<Scalars['JSON']>;
|
||||
graphType: GraphType;
|
||||
groupByFieldMetadataIdX: Scalars['UUID'];
|
||||
@@ -285,8 +295,6 @@ export type BarChartConfiguration = {
|
||||
orderByY?: Maybe<GraphOrderBy>;
|
||||
rangeMax?: Maybe<Scalars['Float']>;
|
||||
rangeMin?: Maybe<Scalars['Float']>;
|
||||
xAxisName?: Maybe<Scalars['String']>;
|
||||
yAxisName?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type Billing = {
|
||||
@@ -1151,6 +1159,23 @@ export type ExecuteServerlessFunctionInput = {
|
||||
version?: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum ExtendedAggregateOperations {
|
||||
AVG = 'AVG',
|
||||
COUNT = 'COUNT',
|
||||
COUNT_EMPTY = 'COUNT_EMPTY',
|
||||
COUNT_FALSE = 'COUNT_FALSE',
|
||||
COUNT_NOT_EMPTY = 'COUNT_NOT_EMPTY',
|
||||
COUNT_TRUE = 'COUNT_TRUE',
|
||||
COUNT_UNIQUE_VALUES = 'COUNT_UNIQUE_VALUES',
|
||||
EARLIEST = 'EARLIEST',
|
||||
LATEST = 'LATEST',
|
||||
MAX = 'MAX',
|
||||
MIN = 'MIN',
|
||||
PERCENTAGE_EMPTY = 'PERCENTAGE_EMPTY',
|
||||
PERCENTAGE_NOT_EMPTY = 'PERCENTAGE_NOT_EMPTY',
|
||||
SUM = 'SUM'
|
||||
}
|
||||
|
||||
export type FeatureFlag = {
|
||||
__typename?: 'FeatureFlag';
|
||||
id: Scalars['UUID'];
|
||||
@@ -1340,10 +1365,10 @@ export type FullName = {
|
||||
export type GaugeChartConfiguration = {
|
||||
__typename?: 'GaugeChartConfiguration';
|
||||
aggregateFieldMetadataId: Scalars['UUID'];
|
||||
aggregateFieldMetadataIdTotal: Scalars['UUID'];
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperationTotal: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
color?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
displayDataLabel: Scalars['Boolean'];
|
||||
filter?: Maybe<Scalars['JSON']>;
|
||||
graphType: GraphType;
|
||||
};
|
||||
@@ -1567,9 +1592,11 @@ export type InvalidatePassword = {
|
||||
export type LineChartConfiguration = {
|
||||
__typename?: 'LineChartConfiguration';
|
||||
aggregateFieldMetadataId: Scalars['UUID'];
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
axisNameDisplay: AxisNameDisplay;
|
||||
color?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
displayDataLabel: Scalars['Boolean'];
|
||||
filter?: Maybe<Scalars['JSON']>;
|
||||
graphType: GraphType;
|
||||
groupByFieldMetadataIdX: Scalars['UUID'];
|
||||
@@ -1579,8 +1606,6 @@ export type LineChartConfiguration = {
|
||||
orderByY?: Maybe<GraphOrderBy>;
|
||||
rangeMax?: Maybe<Scalars['Float']>;
|
||||
rangeMin?: Maybe<Scalars['Float']>;
|
||||
xAxisName?: Maybe<Scalars['String']>;
|
||||
yAxisName?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type LinkMetadata = {
|
||||
@@ -2675,9 +2700,10 @@ export type NativeModelCapabilities = {
|
||||
export type NumberChartConfiguration = {
|
||||
__typename?: 'NumberChartConfiguration';
|
||||
aggregateFieldMetadataId: Scalars['UUID'];
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
color?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
displayDataLabel: Scalars['Boolean'];
|
||||
filter?: Maybe<Scalars['JSON']>;
|
||||
format?: Maybe<Scalars['String']>;
|
||||
graphType: GraphType;
|
||||
@@ -2919,9 +2945,10 @@ export enum PermissionFlagType {
|
||||
export type PieChartConfiguration = {
|
||||
__typename?: 'PieChartConfiguration';
|
||||
aggregateFieldMetadataId: Scalars['UUID'];
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
color?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
displayDataLabel: Scalars['Boolean'];
|
||||
filter?: Maybe<Scalars['JSON']>;
|
||||
graphType: GraphType;
|
||||
groupByFieldMetadataId: Scalars['UUID'];
|
||||
|
||||
@@ -270,12 +270,22 @@ export type AvailableWorkspacesAndAccessTokensOutput = {
|
||||
tokens: AuthTokenPair;
|
||||
};
|
||||
|
||||
/** Which axes should display labels */
|
||||
export enum AxisNameDisplay {
|
||||
BOTH = 'BOTH',
|
||||
NONE = 'NONE',
|
||||
X = 'X',
|
||||
Y = 'Y'
|
||||
}
|
||||
|
||||
export type BarChartConfiguration = {
|
||||
__typename?: 'BarChartConfiguration';
|
||||
aggregateFieldMetadataId: Scalars['UUID'];
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
axisNameDisplay: AxisNameDisplay;
|
||||
color?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
displayDataLabel: Scalars['Boolean'];
|
||||
filter?: Maybe<Scalars['JSON']>;
|
||||
graphType: GraphType;
|
||||
groupByFieldMetadataIdX: Scalars['UUID'];
|
||||
@@ -285,8 +295,6 @@ export type BarChartConfiguration = {
|
||||
orderByY?: Maybe<GraphOrderBy>;
|
||||
rangeMax?: Maybe<Scalars['Float']>;
|
||||
rangeMin?: Maybe<Scalars['Float']>;
|
||||
xAxisName?: Maybe<Scalars['String']>;
|
||||
yAxisName?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type Billing = {
|
||||
@@ -1108,6 +1116,23 @@ export type ExecuteServerlessFunctionInput = {
|
||||
version?: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum ExtendedAggregateOperations {
|
||||
AVG = 'AVG',
|
||||
COUNT = 'COUNT',
|
||||
COUNT_EMPTY = 'COUNT_EMPTY',
|
||||
COUNT_FALSE = 'COUNT_FALSE',
|
||||
COUNT_NOT_EMPTY = 'COUNT_NOT_EMPTY',
|
||||
COUNT_TRUE = 'COUNT_TRUE',
|
||||
COUNT_UNIQUE_VALUES = 'COUNT_UNIQUE_VALUES',
|
||||
EARLIEST = 'EARLIEST',
|
||||
LATEST = 'LATEST',
|
||||
MAX = 'MAX',
|
||||
MIN = 'MIN',
|
||||
PERCENTAGE_EMPTY = 'PERCENTAGE_EMPTY',
|
||||
PERCENTAGE_NOT_EMPTY = 'PERCENTAGE_NOT_EMPTY',
|
||||
SUM = 'SUM'
|
||||
}
|
||||
|
||||
export type FeatureFlag = {
|
||||
__typename?: 'FeatureFlag';
|
||||
id: Scalars['UUID'];
|
||||
@@ -1290,10 +1315,10 @@ export type FullName = {
|
||||
export type GaugeChartConfiguration = {
|
||||
__typename?: 'GaugeChartConfiguration';
|
||||
aggregateFieldMetadataId: Scalars['UUID'];
|
||||
aggregateFieldMetadataIdTotal: Scalars['UUID'];
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperationTotal: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
color?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
displayDataLabel: Scalars['Boolean'];
|
||||
filter?: Maybe<Scalars['JSON']>;
|
||||
graphType: GraphType;
|
||||
};
|
||||
@@ -1517,9 +1542,11 @@ export type InvalidatePassword = {
|
||||
export type LineChartConfiguration = {
|
||||
__typename?: 'LineChartConfiguration';
|
||||
aggregateFieldMetadataId: Scalars['UUID'];
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
axisNameDisplay: AxisNameDisplay;
|
||||
color?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
displayDataLabel: Scalars['Boolean'];
|
||||
filter?: Maybe<Scalars['JSON']>;
|
||||
graphType: GraphType;
|
||||
groupByFieldMetadataIdX: Scalars['UUID'];
|
||||
@@ -1529,8 +1556,6 @@ export type LineChartConfiguration = {
|
||||
orderByY?: Maybe<GraphOrderBy>;
|
||||
rangeMax?: Maybe<Scalars['Float']>;
|
||||
rangeMin?: Maybe<Scalars['Float']>;
|
||||
xAxisName?: Maybe<Scalars['String']>;
|
||||
yAxisName?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type LinkMetadata = {
|
||||
@@ -2561,9 +2586,10 @@ export type NativeModelCapabilities = {
|
||||
export type NumberChartConfiguration = {
|
||||
__typename?: 'NumberChartConfiguration';
|
||||
aggregateFieldMetadataId: Scalars['UUID'];
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
color?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
displayDataLabel: Scalars['Boolean'];
|
||||
filter?: Maybe<Scalars['JSON']>;
|
||||
format?: Maybe<Scalars['String']>;
|
||||
graphType: GraphType;
|
||||
@@ -2805,9 +2831,10 @@ export enum PermissionFlagType {
|
||||
export type PieChartConfiguration = {
|
||||
__typename?: 'PieChartConfiguration';
|
||||
aggregateFieldMetadataId: Scalars['UUID'];
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
color?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
displayDataLabel: Scalars['Boolean'];
|
||||
filter?: Maybe<Scalars['JSON']>;
|
||||
graphType: GraphType;
|
||||
groupByFieldMetadataId: Scalars['UUID'];
|
||||
@@ -4348,7 +4375,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: AggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, xAxisName?: string | null, yAxisName?: string | null, rangeMin?: number | null, rangeMax?: number | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, aggregateOperationTotal: AggregateOperations, aggregateFieldMetadataIdTotal: any, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, orderBy: GraphOrderBy, filter?: any | null } | null };
|
||||
export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, orderBy: GraphOrderBy, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | null };
|
||||
|
||||
export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
|
||||
id: Scalars['String'];
|
||||
@@ -4356,7 +4383,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: AggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, xAxisName?: string | null, yAxisName?: string | null, rangeMin?: number | null, rangeMax?: number | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, aggregateOperationTotal: AggregateOperations, aggregateFieldMetadataIdTotal: any, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, orderBy: GraphOrderBy, filter?: any | null } | null }> | null }> | null } };
|
||||
export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, orderByX: GraphOrderBy, groupByFieldMetadataIdY?: any | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, orderBy: GraphOrderBy, displayDataLabel: boolean, color?: string | null, description?: string | null, filter?: any | null } | null }> | null }> | null } };
|
||||
|
||||
export type OnDbEventSubscriptionVariables = Exact<{
|
||||
input: OnDbEventInput;
|
||||
@@ -4664,10 +4691,12 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
|
||||
groupByFieldMetadataIdY
|
||||
orderByY
|
||||
omitNullValues
|
||||
xAxisName
|
||||
yAxisName
|
||||
axisNameDisplay
|
||||
displayDataLabel
|
||||
rangeMin
|
||||
rangeMax
|
||||
color
|
||||
description
|
||||
filter
|
||||
}
|
||||
... on LineChartConfiguration {
|
||||
@@ -4678,6 +4707,13 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
|
||||
orderByX
|
||||
groupByFieldMetadataIdY
|
||||
orderByY
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
displayDataLabel
|
||||
rangeMin
|
||||
rangeMax
|
||||
color
|
||||
description
|
||||
filter
|
||||
}
|
||||
... on PieChartConfiguration {
|
||||
@@ -4686,12 +4722,16 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
orderBy
|
||||
displayDataLabel
|
||||
color
|
||||
description
|
||||
filter
|
||||
}
|
||||
... on NumberChartConfiguration {
|
||||
graphType
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
displayDataLabel
|
||||
description
|
||||
filter
|
||||
}
|
||||
@@ -4699,8 +4739,8 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
|
||||
graphType
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
aggregateOperationTotal
|
||||
aggregateFieldMetadataIdTotal
|
||||
displayDataLabel
|
||||
color
|
||||
description
|
||||
filter
|
||||
}
|
||||
|
||||
@@ -16,17 +16,25 @@ export type CommandMenuItemProps = {
|
||||
Icon?: IconComponent;
|
||||
hotKeys?: string[];
|
||||
RightComponent?: ReactNode;
|
||||
contextualTextPosition?: 'left' | 'right';
|
||||
hasSubMenu?: boolean;
|
||||
isSubMenuOpened?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const CommandMenuItem = ({
|
||||
label,
|
||||
description,
|
||||
contextualTextPosition = 'left',
|
||||
to,
|
||||
id,
|
||||
onClick,
|
||||
Icon,
|
||||
hotKeys,
|
||||
RightComponent,
|
||||
hasSubMenu = false,
|
||||
isSubMenuOpened = false,
|
||||
disabled = false,
|
||||
}: CommandMenuItemProps) => {
|
||||
const { onItemClick } = useCommandMenuOnItemClick();
|
||||
|
||||
@@ -45,15 +53,22 @@ export const CommandMenuItem = ({
|
||||
LeftIcon={Icon}
|
||||
text={label}
|
||||
contextualText={description}
|
||||
contextualTextPosition={contextualTextPosition}
|
||||
hotKeys={hotKeys}
|
||||
onClick={() =>
|
||||
onItemClick({
|
||||
onClick,
|
||||
to,
|
||||
})
|
||||
onClick={
|
||||
onClick || to
|
||||
? () =>
|
||||
onItemClick({
|
||||
onClick,
|
||||
to,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
focused={isSelectedItemId}
|
||||
RightComponent={RightComponent}
|
||||
hasSubMenu={hasSubMenu}
|
||||
isSubMenuOpened={isSubMenuOpened}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
CommandMenuItem,
|
||||
type CommandMenuItemProps,
|
||||
} from '@/command-menu/components/CommandMenuItem';
|
||||
import {
|
||||
Dropdown,
|
||||
type DropdownProps,
|
||||
} from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
export type CommandMenuItemDropdownProps = CommandMenuItemProps &
|
||||
Pick<
|
||||
DropdownProps,
|
||||
'dropdownPlacement' | 'dropdownOffset' | 'dropdownId' | 'dropdownComponents'
|
||||
>;
|
||||
|
||||
export const CommandMenuItemDropdown = ({
|
||||
id,
|
||||
label,
|
||||
Icon,
|
||||
hotKeys,
|
||||
RightComponent,
|
||||
description,
|
||||
contextualTextPosition,
|
||||
dropdownComponents,
|
||||
dropdownPlacement,
|
||||
dropdownOffset,
|
||||
dropdownId,
|
||||
disabled = false,
|
||||
}: CommandMenuItemDropdownProps) => {
|
||||
const isDropdownOpen = useRecoilComponentValue(
|
||||
isDropdownOpenComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
clickableComponent={
|
||||
<CommandMenuItem
|
||||
id={id}
|
||||
label={label}
|
||||
description={description}
|
||||
contextualTextPosition={contextualTextPosition}
|
||||
Icon={Icon}
|
||||
hotKeys={hotKeys}
|
||||
RightComponent={RightComponent}
|
||||
hasSubMenu
|
||||
isSubMenuOpened={isDropdownOpen}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={dropdownComponents}
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement={dropdownPlacement}
|
||||
dropdownOffset={dropdownOffset}
|
||||
disableClickForClickableComponent={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { MenuItemToggle, type MenuItemToggleProps } from 'twenty-ui/navigation';
|
||||
|
||||
export type CommandMenuItemToggleProps = MenuItemToggleProps & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const CommandMenuItemToggle = (props: CommandMenuItemToggleProps) => {
|
||||
const isSelectedItemId = useRecoilComponentFamilyValue(
|
||||
isSelectedItemIdComponentFamilySelector,
|
||||
props.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuItemToggle
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
focused={isSelectedItemId}
|
||||
withIconContainer
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -30,7 +30,7 @@ const StyledInnerList = styled.div`
|
||||
);
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(4)});
|
||||
|
||||
@media (min-width: ${MOBILE_VIEWPORT}px) {
|
||||
|
||||
+5
-5
@@ -47,7 +47,7 @@ const StyledHeaderIconContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type WorkflowStepHeaderProps = {
|
||||
type SidePanelHeaderProps = {
|
||||
Icon: IconComponent;
|
||||
iconColor: string;
|
||||
initialTitle: string;
|
||||
@@ -63,14 +63,14 @@ type WorkflowStepHeaderProps = {
|
||||
}
|
||||
);
|
||||
|
||||
export const WorkflowStepHeader = ({
|
||||
export const SidePanelHeader = ({
|
||||
Icon,
|
||||
iconColor,
|
||||
initialTitle,
|
||||
headerType,
|
||||
disabled,
|
||||
onTitleChange,
|
||||
}: WorkflowStepHeaderProps) => {
|
||||
}: SidePanelHeaderProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [title, setTitle] = useState(initialTitle);
|
||||
@@ -90,7 +90,7 @@ export const WorkflowStepHeader = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledHeader data-testid="workflow-step-header">
|
||||
<StyledHeader data-testid="side-panel-header">
|
||||
<StyledHeaderIconContainer>
|
||||
<Icon
|
||||
color={iconColor}
|
||||
@@ -101,7 +101,7 @@ export const WorkflowStepHeader = ({
|
||||
<StyledHeaderInfo>
|
||||
<StyledHeaderTitle>
|
||||
<TitleInput
|
||||
instanceId="workflow-step-title-input"
|
||||
instanceId="side-panel-title-input"
|
||||
disabled={disabled}
|
||||
sizeVariant="md"
|
||||
value={title}
|
||||
+5
-15
@@ -3,11 +3,11 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { THEME_LIGHT } from 'twenty-ui/theme';
|
||||
import { WorkflowStepHeader } from '../WorkflowStepHeader';
|
||||
import { SidePanelHeader } from '../SidePanelHeader';
|
||||
|
||||
const meta: Meta<typeof WorkflowStepHeader> = {
|
||||
title: 'Modules/Workflow/WorkflowStepHeader',
|
||||
component: WorkflowStepHeader,
|
||||
const meta: Meta<typeof SidePanelHeader> = {
|
||||
title: 'Modules/CommandMenu/SidePanelHeader',
|
||||
component: SidePanelHeader,
|
||||
args: {
|
||||
onTitleChange: fn(),
|
||||
},
|
||||
@@ -20,7 +20,7 @@ const meta: Meta<typeof WorkflowStepHeader> = {
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof WorkflowStepHeader>;
|
||||
type Story = StoryObj<typeof SidePanelHeader>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
@@ -32,7 +32,6 @@ export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// TitleInput shows text in a div when not being edited
|
||||
expect(await canvas.findByText('Create Record')).toBeVisible();
|
||||
expect(await canvas.findByText('Action')).toBeVisible();
|
||||
},
|
||||
@@ -48,11 +47,9 @@ export const EditableTitle: Story = {
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
// First find the div with the text, then click it to activate the input
|
||||
const titleText = await canvas.findByText('Create Record');
|
||||
await userEvent.click(titleText);
|
||||
|
||||
// Now find the input that appears after clicking
|
||||
const titleInput = await canvas.findByDisplayValue('Create Record');
|
||||
|
||||
const NEW_TITLE = 'New Title';
|
||||
@@ -60,10 +57,8 @@ export const EditableTitle: Story = {
|
||||
await userEvent.clear(titleInput);
|
||||
await userEvent.type(titleInput, NEW_TITLE);
|
||||
|
||||
// Press Enter to submit the edit
|
||||
await userEvent.keyboard('{Enter}');
|
||||
|
||||
// Wait for the callback to be called
|
||||
await waitFor(() => {
|
||||
expect(args.onTitleChange).toHaveBeenCalledWith(NEW_TITLE);
|
||||
});
|
||||
@@ -82,20 +77,15 @@ export const Disabled: Story = {
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// When disabled, TitleInput just shows text in a div, not an input
|
||||
const titleText = await canvas.findByText('Create Record');
|
||||
|
||||
// Check if the element has the disabled styling (cursor: default)
|
||||
expect(window.getComputedStyle(titleText).cursor).toBe('default');
|
||||
|
||||
// Try to click it - nothing should happen
|
||||
await userEvent.click(titleText);
|
||||
|
||||
// Confirm there is no input field
|
||||
const titleInput = canvas.queryByDisplayValue('Create Record');
|
||||
expect(titleInput).not.toBeInTheDocument();
|
||||
|
||||
// Confirm the callback is not called
|
||||
expect(args.onTitleChange).not.toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
+42
@@ -12,7 +12,10 @@ import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelect
|
||||
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { emitSidePanelCloseEvent } from '@/ui/layout/right-drawer/utils/emitSidePanelCloseEvent';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
@@ -21,6 +24,7 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab
|
||||
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId';
|
||||
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useCommandMenuCloseAnimationCompleteCleanup = () => {
|
||||
const { resetSelectedItem } = useSelectableList('command-menu-list');
|
||||
@@ -37,6 +41,44 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => {
|
||||
resetContextStoreStates(COMMAND_MENU_COMPONENT_INSTANCE_ID);
|
||||
resetContextStoreStates(COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID);
|
||||
|
||||
const currentPage = snapshot
|
||||
.getLoadable(commandMenuPageState)
|
||||
.getValue();
|
||||
|
||||
const isPageLayoutEditingPage =
|
||||
currentPage === CommandMenuPages.PageLayoutWidgetTypeSelect ||
|
||||
currentPage === CommandMenuPages.PageLayoutGraphTypeSelect ||
|
||||
currentPage === CommandMenuPages.PageLayoutIframeConfig;
|
||||
|
||||
if (isPageLayoutEditingPage) {
|
||||
const targetedRecordsRule = snapshot
|
||||
.getLoadable(
|
||||
contextStoreTargetedRecordsRuleComponentState.atomFamily({
|
||||
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (
|
||||
targetedRecordsRule.mode === 'selection' &&
|
||||
targetedRecordsRule.selectedRecordIds.length === 1
|
||||
) {
|
||||
const recordId = targetedRecordsRule.selectedRecordIds[0];
|
||||
const record = snapshot
|
||||
.getLoadable(recordStoreFamilyState(recordId))
|
||||
.getValue();
|
||||
|
||||
if (isDefined(record) && isDefined(record.pageLayoutId)) {
|
||||
set(
|
||||
pageLayoutEditingWidgetIdComponentState.atomFamily({
|
||||
instanceId: record.pageLayoutId,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set(viewableRecordIdState, null);
|
||||
set(commandMenuPageState, CommandMenuPages.Root);
|
||||
set(commandMenuPageInfoState, {
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuItemDropdown';
|
||||
import { CommandMenuItemToggle } from '@/command-menu/components/CommandMenuItemToggle';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
|
||||
import { ChartTypeSelectionSection } from '@/command-menu/pages/page-layout/components/ChartTypeSelectionSection';
|
||||
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
|
||||
import { GRAPH_TYPE_TO_CONFIG_TYPENAME } from '@/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename';
|
||||
import { useChartSettingsValues } from '@/command-menu/pages/page-layout/hooks/useChartSettingsValues';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
|
||||
import {
|
||||
CHART_CONFIGURATION_SETTING_IDS,
|
||||
CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP,
|
||||
} from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { getChartSettingsDropdownContent } from '@/command-menu/pages/page-layout/utils/getChartSettingsDropdownContent';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { type GraphType, type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
|
||||
const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
|
||||
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
|
||||
if (widget.configuration?.__typename === 'IframeConfiguration') {
|
||||
throw new Error(t`IframeConfiguration is not supported`);
|
||||
}
|
||||
|
||||
const configuration = widget.configuration as ChartConfiguration;
|
||||
|
||||
const { getChartSettingsValues } = useChartSettingsValues({
|
||||
objectMetadataId: widget.objectMetadataId,
|
||||
configuration,
|
||||
});
|
||||
|
||||
const currentGraphType = configuration?.graphType;
|
||||
|
||||
const handleGraphTypeChange = (graphType: GraphType) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
__typename: GRAPH_TYPE_TO_CONFIG_TYPENAME[graphType],
|
||||
graphType,
|
||||
},
|
||||
});
|
||||
|
||||
updateCommandMenuPageInfo({
|
||||
pageIcon: GRAPH_TYPE_INFORMATION[graphType].icon,
|
||||
});
|
||||
};
|
||||
|
||||
const chartSettings = GRAPH_TYPE_INFORMATION[currentGraphType].settings;
|
||||
|
||||
return (
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={[
|
||||
...chartSettings.flatMap((group) => group.items.map((item) => item.id)),
|
||||
]}
|
||||
>
|
||||
<ChartTypeSelectionSection
|
||||
currentGraphType={currentGraphType}
|
||||
setCurrentGraphType={handleGraphTypeChange}
|
||||
/>
|
||||
{chartSettings.map((group) => (
|
||||
<CommandGroup key={group.heading} heading={group.heading}>
|
||||
{group.items.map((item) => {
|
||||
const isDisabled =
|
||||
!isNonEmptyString(widget.objectMetadataId) &&
|
||||
(item?.dependsOn?.includes(
|
||||
CHART_CONFIGURATION_SETTING_IDS.SOURCE,
|
||||
) ??
|
||||
false);
|
||||
|
||||
const handleToggleChange = () => {
|
||||
const configKey =
|
||||
item.id === CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS
|
||||
? CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP[
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS
|
||||
]
|
||||
: item.id;
|
||||
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
[configKey]: !getChartSettingsValues(item.id),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDropdownOpen = () => {
|
||||
openDropdown({
|
||||
dropdownComponentInstanceIdFromProps: item.id,
|
||||
});
|
||||
};
|
||||
|
||||
return item.isBoolean ? (
|
||||
<SelectableListItem
|
||||
key={item.id}
|
||||
itemId={item.id}
|
||||
onEnter={isDisabled ? undefined : handleToggleChange}
|
||||
>
|
||||
<CommandMenuItemToggle
|
||||
LeftIcon={item.Icon}
|
||||
text={t(item.label)}
|
||||
id={item.id}
|
||||
toggled={getChartSettingsValues(item.id) as boolean}
|
||||
onToggleChange={handleToggleChange}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
) : (
|
||||
<SelectableListItem
|
||||
key={item.id}
|
||||
itemId={item.id}
|
||||
onEnter={isDisabled ? undefined : handleDropdownOpen}
|
||||
>
|
||||
<CommandMenuItemDropdown
|
||||
Icon={item.Icon}
|
||||
label={t(item.label)}
|
||||
id={item.id}
|
||||
dropdownId={item.id}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
{getChartSettingsDropdownContent(item.id)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownPlacement="bottom-end"
|
||||
description={getChartSettingsValues(item.id) as string}
|
||||
contextualTextPosition={'right'}
|
||||
hasSubMenu
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandMenuList>
|
||||
);
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
|
||||
import styled from '@emotion/styled';
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { MenuPicker } from 'twenty-ui/navigation';
|
||||
|
||||
const graphTypeOptions = [
|
||||
GraphType.BAR,
|
||||
GraphType.PIE,
|
||||
GraphType.LINE,
|
||||
GraphType.NUMBER,
|
||||
GraphType.GAUGE,
|
||||
];
|
||||
|
||||
const StyledChartTypeSelectionContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type ChartTypeSelectionSectionProps = {
|
||||
currentGraphType: GraphType;
|
||||
setCurrentGraphType: (graphType: GraphType) => void;
|
||||
};
|
||||
|
||||
export const ChartTypeSelectionSection = ({
|
||||
currentGraphType,
|
||||
setCurrentGraphType,
|
||||
}: ChartTypeSelectionSectionProps) => {
|
||||
return (
|
||||
<StyledChartTypeSelectionContainer>
|
||||
{graphTypeOptions.map((graphType) => (
|
||||
<MenuPicker
|
||||
selected={currentGraphType === graphType}
|
||||
key={graphType}
|
||||
icon={GRAPH_TYPE_INFORMATION[graphType].icon}
|
||||
onClick={() => {
|
||||
setCurrentGraphType(graphType);
|
||||
}}
|
||||
label={t(GRAPH_TYPE_INFORMATION[graphType].label)}
|
||||
showLabel={false}
|
||||
/>
|
||||
))}
|
||||
</StyledChartTypeSelectionContainer>
|
||||
);
|
||||
};
|
||||
+42
-76
@@ -1,87 +1,53 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { ChartSettings } from '@/command-menu/pages/page-layout/components/ChartSettings';
|
||||
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
|
||||
import { GraphType, WidgetType } from '~/generated-metadata/graphql';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import {
|
||||
IconChartBar,
|
||||
IconChartLine,
|
||||
IconChartPie,
|
||||
IconGauge,
|
||||
IconNumber,
|
||||
} from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSectionTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const graphTypeOptions = [
|
||||
{
|
||||
type: GraphType.BAR,
|
||||
icon: IconChartBar,
|
||||
title: 'Bar Chart',
|
||||
},
|
||||
{
|
||||
type: GraphType.PIE,
|
||||
icon: IconChartPie,
|
||||
title: 'Pie Chart',
|
||||
},
|
||||
{
|
||||
type: GraphType.GAUGE,
|
||||
icon: IconGauge,
|
||||
title: 'Gauge',
|
||||
},
|
||||
{
|
||||
type: GraphType.NUMBER,
|
||||
icon: IconNumber,
|
||||
title: 'Number',
|
||||
},
|
||||
{
|
||||
type: GraphType.LINE,
|
||||
icon: IconChartLine,
|
||||
title: 'Line Chart',
|
||||
},
|
||||
];
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CommandMenuPageLayoutGraphTypeSelect = () => {
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
const { createPageLayoutWidget } =
|
||||
useCreatePageLayoutGraphWidget(pageLayoutId);
|
||||
const draftPageLayout = useRecoilComponentValue(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const handleSelectGraphType = (graphType: GraphType) => {
|
||||
createPageLayoutWidget(WidgetType.GRAPH, graphType);
|
||||
closeCommandMenu();
|
||||
};
|
||||
const pageLayoutEditingWidgetId = useRecoilComponentValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const widgetInEditMode = draftPageLayout.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((widget) => widget.id === pageLayoutEditingWidgetId);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
if (
|
||||
!isDefined(widgetInEditMode?.configuration) ||
|
||||
!('graphType' in widgetInEditMode.configuration)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentGraphType = widgetInEditMode.configuration.graphType;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSectionTitle>Graph type</StyledSectionTitle>
|
||||
<>
|
||||
<SidePanelHeader
|
||||
Icon={GRAPH_TYPE_INFORMATION[currentGraphType].icon}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={t`Chart`}
|
||||
headerType={t(GRAPH_TYPE_INFORMATION[currentGraphType].label)}
|
||||
onTitleChange={() => {}}
|
||||
/>
|
||||
|
||||
{graphTypeOptions.map((option) => (
|
||||
<MenuItem
|
||||
withIconContainer={true}
|
||||
key={option.type}
|
||||
LeftIcon={option.icon}
|
||||
text={option.title}
|
||||
onClick={() => handleSelectGraphType(option.type)}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
<ChartSettings widget={widgetInEditMode} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-3
@@ -8,6 +8,7 @@ import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pa
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { isValidUrl } from 'twenty-shared/utils';
|
||||
@@ -119,12 +120,12 @@ export const CommandMenuPageLayoutIframeConfig = () => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSectionTitle>
|
||||
{isEditMode ? 'Edit iFrame Widget' : 'Configure iFrame Widget'}
|
||||
{isEditMode ? t`Edit iFrame Widget` : t`Configure iFrame Widget`}
|
||||
</StyledSectionTitle>
|
||||
|
||||
<FormTextFieldInput
|
||||
label="Widget Title"
|
||||
placeholder="e.g., Analytics Dashboard"
|
||||
label={t`Widget Title`}
|
||||
placeholder={t`e.g., Analytics Dashboard`}
|
||||
defaultValue={title}
|
||||
onChange={setTitle}
|
||||
/>
|
||||
|
||||
+60
-91
@@ -1,109 +1,78 @@
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
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 { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChartPie, IconFrame, IconList } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSectionTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledDisabledMenuItem = styled.div`
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const widgetTypeOptions = [
|
||||
{
|
||||
type: WidgetType.GRAPH,
|
||||
icon: IconChartPie,
|
||||
title: 'Add a graph',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: WidgetType.VIEW,
|
||||
icon: IconList,
|
||||
title: 'Add a view',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
type: WidgetType.IFRAME,
|
||||
icon: IconFrame,
|
||||
title: 'Add an iframe',
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChartPie, IconFrame } from 'twenty-ui/display';
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const CommandMenuPageLayoutWidgetTypeSelect = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
const setPageLayoutDraggedArea = useSetRecoilComponentState(
|
||||
pageLayoutDraggedAreaComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
|
||||
|
||||
const handleSelectWidget = (widgetType: WidgetType) => {
|
||||
switch (widgetType) {
|
||||
case WidgetType.GRAPH: {
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage: CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
});
|
||||
const { createPageLayoutGraphWidget } =
|
||||
useCreatePageLayoutGraphWidget(pageLayoutId);
|
||||
|
||||
break;
|
||||
}
|
||||
case WidgetType.IFRAME: {
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage: CommandMenuPages.PageLayoutIframeConfig,
|
||||
});
|
||||
const [pageLayoutEditingWidgetId, setPageLayoutEditingWidgetId] =
|
||||
useRecoilComponentState(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
setPageLayoutDraggedArea(null);
|
||||
break;
|
||||
const handleNavigateToGraphTypeSelect = () => {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
const newWidget = createPageLayoutGraphWidget(GraphType.BAR);
|
||||
|
||||
setPageLayoutEditingWidgetId(newWidget.id);
|
||||
}
|
||||
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage: CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNavigateToIframeConfig = () => {
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage: CommandMenuPages.PageLayoutIframeConfig,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSectionTitle>Widget type</StyledSectionTitle>
|
||||
{widgetTypeOptions.map((option) => {
|
||||
const MenuItemComponent = (
|
||||
<MenuItem
|
||||
withIconContainer={true}
|
||||
key={option.type}
|
||||
LeftIcon={option.icon}
|
||||
text={option.title}
|
||||
onClick={() => handleSelectWidget(option.type)}
|
||||
<CommandMenuList commandGroups={[]} selectableItemIds={['chart', 'iframe']}>
|
||||
<CommandGroup heading={t`Widget type`}>
|
||||
<SelectableListItem
|
||||
itemId="chart"
|
||||
onEnter={handleNavigateToGraphTypeSelect}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconChartPie}
|
||||
label={t`Chart`}
|
||||
id="chart"
|
||||
onClick={handleNavigateToGraphTypeSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
return option.disabled ? (
|
||||
<StyledDisabledMenuItem key={option.type}>
|
||||
{MenuItemComponent}
|
||||
</StyledDisabledMenuItem>
|
||||
) : (
|
||||
MenuItemComponent
|
||||
);
|
||||
})}
|
||||
</StyledContainer>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId="iframe"
|
||||
onEnter={() => {
|
||||
handleNavigateToIframeConfig();
|
||||
}}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconFrame}
|
||||
label="iFrame"
|
||||
id="iframe"
|
||||
onClick={handleNavigateToIframeConfig}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</CommandGroup>
|
||||
</CommandMenuList>
|
||||
);
|
||||
};
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { mapToGraphQLExtendedAggregateOperation } from '@/command-menu/pages/page-layout/utils/mapToGraphQLExtendedAggregateOperation';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
|
||||
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartAggregateOperationSelectionDropdownContent = ({
|
||||
currentFieldMetadataId,
|
||||
setIsSubMenuOpen,
|
||||
}: {
|
||||
currentFieldMetadataId: string;
|
||||
setIsSubMenuOpen: (isSubMenuOpen: boolean) => void;
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentAggregateOperation =
|
||||
widgetInEditMode.configuration.aggregateOperation;
|
||||
|
||||
const sourceObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widgetInEditMode.objectMetadataId,
|
||||
);
|
||||
|
||||
const selectedField = sourceObjectMetadataItem?.fields.find(
|
||||
(field) => field.id === currentFieldMetadataId,
|
||||
);
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const availableAggregateOperations = selectedField
|
||||
? getAvailableAggregateOperationsForFieldMetadataType({
|
||||
fieldMetadataType: selectedField.type,
|
||||
})
|
||||
: [];
|
||||
|
||||
const aggregateOperationsWithLabels = availableAggregateOperations.map(
|
||||
(operation) => ({
|
||||
operation,
|
||||
label: getAggregateOperationLabel(operation),
|
||||
}),
|
||||
);
|
||||
|
||||
const filteredAggregateOperationsWithLabels = filterBySearchQuery({
|
||||
items: aggregateOperationsWithLabels,
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.label],
|
||||
});
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
if (!isDefined(sourceObjectMetadataItem) || !isDefined(selectedField)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelectAggregateOperation = (
|
||||
aggregateOperation: ExtendedAggregateOperations,
|
||||
) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
aggregateFieldMetadataId: currentFieldMetadataId,
|
||||
aggregateOperation:
|
||||
mapToGraphQLExtendedAggregateOperation(aggregateOperation),
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
StartComponent={
|
||||
<DropdownMenuHeaderLeftComponent
|
||||
onClick={() => setIsSubMenuOpen(false)}
|
||||
Icon={IconChevronLeft}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Trans>Y-Axis Aggregate Operation</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search operations`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={filteredAggregateOperationsWithLabels.map(
|
||||
(item) => item.operation,
|
||||
)}
|
||||
>
|
||||
{filteredAggregateOperationsWithLabels.map((item) => (
|
||||
<SelectableListItem
|
||||
key={item.operation}
|
||||
itemId={item.operation}
|
||||
onEnter={() => {
|
||||
handleSelectAggregateOperation(item.operation);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={item.label}
|
||||
selected={
|
||||
currentAggregateOperation ===
|
||||
mapToGraphQLExtendedAggregateOperation(item.operation)
|
||||
}
|
||||
focused={selectedItemId === item.operation}
|
||||
onClick={() => {
|
||||
handleSelectAggregateOperation(item.operation);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { getChartAxisNameDisplayOptions } from '@/command-menu/pages/page-layout/utils/getChartAxisNameDisplayOptions';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { AxisNameDisplay } from '~/generated/graphql';
|
||||
|
||||
export const ChartAxisNameSelectionDropdownContent = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentAxisNameDisplay = widgetInEditMode.configuration.axisNameDisplay;
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const axisOptions: AxisNameDisplay[] = [
|
||||
AxisNameDisplay.NONE,
|
||||
AxisNameDisplay.X,
|
||||
AxisNameDisplay.Y,
|
||||
AxisNameDisplay.BOTH,
|
||||
];
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const handleSelectAxisNameOption = (axisNameOption: AxisNameDisplay) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
axisNameDisplay: axisNameOption,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>Axis Name</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={axisOptions}
|
||||
>
|
||||
{axisOptions.map((option) => (
|
||||
<SelectableListItem
|
||||
key={option}
|
||||
itemId={option}
|
||||
onEnter={() => {
|
||||
handleSelectAxisNameOption(option);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={getChartAxisNameDisplayOptions(option)}
|
||||
selected={currentAxisNameDisplay?.toUpperCase() === option}
|
||||
focused={selectedItemId === option}
|
||||
onClick={() => {
|
||||
handleSelectAxisNameOption(option);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { ColorSample } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { MAIN_COLOR_NAMES, type ThemeColor } from 'twenty-ui/theme';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartColorSelectionDropdownContent = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (widgetInEditMode.configuration?.__typename === 'IframeConfiguration') {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const configuration = widgetInEditMode.configuration as ChartConfiguration;
|
||||
|
||||
const currentColor = configuration.color;
|
||||
|
||||
const colorOptions = MAIN_COLOR_NAMES.map((colorName) => ({
|
||||
id: colorName,
|
||||
name: capitalize(colorName),
|
||||
colorName: colorName,
|
||||
}));
|
||||
|
||||
const filteredColorOptions = filterBySearchQuery({
|
||||
items: colorOptions,
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.name],
|
||||
});
|
||||
|
||||
const handleSelectColor = (colorName: ThemeColor) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
color: colorName,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>Color</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search colors`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={filteredColorOptions.map(
|
||||
(colorOption) => colorOption.id,
|
||||
)}
|
||||
>
|
||||
{filteredColorOptions.map((colorOption) => (
|
||||
<SelectableListItem
|
||||
key={colorOption.id}
|
||||
itemId={colorOption.id}
|
||||
onEnter={() => {
|
||||
handleSelectColor(colorOption.colorName);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={colorOption.name}
|
||||
selected={currentColor === colorOption.id}
|
||||
focused={selectedItemId === colorOption.id}
|
||||
LeftIcon={() => (
|
||||
<ColorSample colorName={colorOption.colorName} />
|
||||
)}
|
||||
onClick={() => {
|
||||
handleSelectColor(colorOption.colorName);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartDataSourceDropdownContent = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const currentSource = widgetInEditMode?.objectMetadataId;
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const objectsWithReadAccess = objectMetadataItems.filter(
|
||||
(objectMetadataItem) => {
|
||||
const objectPermissions =
|
||||
objectPermissionsByObjectMetadataId[objectMetadataItem.id];
|
||||
|
||||
return (
|
||||
isDefined(objectPermissions) && objectPermissions.canReadObjectRecords
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const availableObjectMetadataItems = filterBySearchQuery({
|
||||
items: objectsWithReadAccess,
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.labelPlural, item.namePlural],
|
||||
});
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const handleSelectSource = (objectMetadataId: string) => {
|
||||
updateCurrentWidgetConfig({
|
||||
objectMetadataId,
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>Source</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search objects`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={availableObjectMetadataItems.map(
|
||||
(objectMetadataItem) => objectMetadataItem.id,
|
||||
)}
|
||||
>
|
||||
{availableObjectMetadataItems.map((objectMetadataItem) => (
|
||||
<SelectableListItem
|
||||
key={objectMetadataItem.id}
|
||||
itemId={objectMetadataItem.id}
|
||||
onEnter={() => {
|
||||
handleSelectSource(objectMetadataItem.id);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={objectMetadataItem.labelPlural}
|
||||
selected={currentSource === objectMetadataItem.id}
|
||||
focused={selectedItemId === objectMetadataItem.id}
|
||||
LeftIcon={getIcon(objectMetadataItem.icon)}
|
||||
onClick={() => {
|
||||
handleSelectSource(objectMetadataItem.id);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartFieldSelectionDropdownContent = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentXAxisFieldMetadataId =
|
||||
widgetInEditMode.configuration.groupByFieldMetadataIdX;
|
||||
|
||||
const sourceObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widgetInEditMode.objectMetadataId,
|
||||
);
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const availableFieldMetadataItems = filterBySearchQuery({
|
||||
items: sourceObjectMetadataItem?.fields || [],
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.label, item.name],
|
||||
});
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
if (!isDefined(sourceObjectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelectField = (fieldMetadataId: string) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
groupByFieldMetadataIdX: fieldMetadataId,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>X-Axis Field</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search fields`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={availableFieldMetadataItems.map(
|
||||
(item) => item.id,
|
||||
)}
|
||||
>
|
||||
{availableFieldMetadataItems.map((fieldMetadataItem) => (
|
||||
<SelectableListItem
|
||||
key={fieldMetadataItem.id}
|
||||
itemId={fieldMetadataItem.id}
|
||||
onEnter={() => {
|
||||
handleSelectField(fieldMetadataItem.id);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={fieldMetadataItem.label}
|
||||
selected={currentXAxisFieldMetadataId === fieldMetadataItem.id}
|
||||
focused={selectedItemId === fieldMetadataItem.id}
|
||||
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||
onClick={() => {
|
||||
handleSelectField(fieldMetadataItem.id);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { ChartAggregateOperationSelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartAggregateOperationSelectionDropdownContent';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartFieldSelectionForAggregateOperationDropdownContent = () => {
|
||||
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentFieldMetadataId =
|
||||
widgetInEditMode.configuration.groupByFieldMetadataIdY;
|
||||
|
||||
const [selectedFieldMetadataId, setSelectedFieldMetadataId] = useState(
|
||||
currentFieldMetadataId,
|
||||
);
|
||||
|
||||
const sourceObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widgetInEditMode.objectMetadataId,
|
||||
);
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const availableFieldMetadataItems = filterBySearchQuery({
|
||||
items: sourceObjectMetadataItem?.fields || [],
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.label, item.name],
|
||||
});
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
if (!isDefined(sourceObjectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isSubMenuOpen) {
|
||||
return (
|
||||
<ChartAggregateOperationSelectionDropdownContent
|
||||
currentFieldMetadataId={selectedFieldMetadataId}
|
||||
setIsSubMenuOpen={setIsSubMenuOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>Y-Axis Field</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search fields`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={availableFieldMetadataItems.map(
|
||||
(item) => item.id,
|
||||
)}
|
||||
>
|
||||
{availableFieldMetadataItems.map((fieldMetadataItem) => (
|
||||
<SelectableListItem
|
||||
key={fieldMetadataItem.id}
|
||||
itemId={fieldMetadataItem.id}
|
||||
onEnter={() => {
|
||||
setIsSubMenuOpen(true);
|
||||
setSelectedFieldMetadataId(fieldMetadataItem.id);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={fieldMetadataItem.label}
|
||||
selected={selectedFieldMetadataId === fieldMetadataItem.id}
|
||||
focused={selectedItemId === fieldMetadataItem.id}
|
||||
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||
hasSubMenu={true}
|
||||
onClick={() => {
|
||||
setIsSubMenuOpen(true);
|
||||
setSelectedFieldMetadataId(fieldMetadataItem.id);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartGroupByFieldSelectionDropdownContent = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentGroupByFieldMetadataId =
|
||||
widgetInEditMode.configuration.groupByFieldMetadataIdY;
|
||||
|
||||
const sourceObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widgetInEditMode.objectMetadataId,
|
||||
);
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const availableFieldMetadataItems = filterBySearchQuery({
|
||||
items: sourceObjectMetadataItem?.fields || [],
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.label, item.name],
|
||||
});
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
if (!isDefined(sourceObjectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelectField = (fieldMetadataId: string) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
groupByFieldMetadataIdY: fieldMetadataId,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>Y-Axis Group By Field</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search fields`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={availableFieldMetadataItems.map(
|
||||
(item) => item.id,
|
||||
)}
|
||||
>
|
||||
{availableFieldMetadataItems.map((fieldMetadataItem) => (
|
||||
<SelectableListItem
|
||||
key={fieldMetadataItem.id}
|
||||
itemId={fieldMetadataItem.id}
|
||||
onEnter={() => {
|
||||
handleSelectField(fieldMetadataItem.id);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={fieldMetadataItem.label}
|
||||
selected={
|
||||
currentGroupByFieldMetadataId === fieldMetadataItem.id
|
||||
}
|
||||
focused={selectedItemId === fieldMetadataItem.id}
|
||||
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||
onClick={() => {
|
||||
handleSelectField(fieldMetadataItem.id);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { AGGREGATE_SORT_BY_OPTIONS } from '@/command-menu/pages/page-layout/constants/AggregateSortByOptions';
|
||||
import { useGraphGroupBySortOptionLabels } from '@/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type GraphOrderBy,
|
||||
type LineChartConfiguration,
|
||||
type NumberChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
type ChartSortByGroupByFieldDropdownContentProps = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const ChartSortByGroupByFieldDropdownContent = ({
|
||||
title,
|
||||
}: ChartSortByGroupByFieldDropdownContentProps) => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const configuration = widgetInEditMode?.configuration as
|
||||
| BarChartConfiguration
|
||||
| LineChartConfiguration
|
||||
| NumberChartConfiguration;
|
||||
|
||||
const currentOrderBy =
|
||||
'orderByY' in configuration
|
||||
? configuration.orderByY
|
||||
: 'orderBy' in configuration
|
||||
? configuration.orderBy
|
||||
: undefined;
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const orderByKey = 'orderByY' in configuration ? 'orderByY' : 'orderBy';
|
||||
|
||||
const handleSelectSortOption = (orderBy: GraphOrderBy) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
[orderByKey]: orderBy,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const { getGroupBySortOptionLabel } = useGraphGroupBySortOptionLabels({
|
||||
objectMetadataId: widgetInEditMode?.objectMetadataId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>{title}</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={AGGREGATE_SORT_BY_OPTIONS.map(
|
||||
(option) => option.value,
|
||||
)}
|
||||
>
|
||||
{AGGREGATE_SORT_BY_OPTIONS.map((sortOption) => (
|
||||
<SelectableListItem
|
||||
key={sortOption.value}
|
||||
itemId={sortOption.value}
|
||||
onEnter={() => {
|
||||
handleSelectSortOption(sortOption.value);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={getGroupBySortOptionLabel({
|
||||
graphOrderBy: sortOption.value,
|
||||
groupByFieldMetadataId:
|
||||
'groupByFieldMetadataIdY' in configuration
|
||||
? configuration.groupByFieldMetadataIdY
|
||||
: 'groupByFieldMetadataId' in configuration
|
||||
? configuration.groupByFieldMetadataId
|
||||
: undefined,
|
||||
})}
|
||||
selected={currentOrderBy === sortOption.value}
|
||||
focused={selectedItemId === sortOption.value}
|
||||
LeftIcon={sortOption.icon}
|
||||
onClick={() => {
|
||||
handleSelectSortOption(sortOption.value);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import { X_SORT_BY_OPTIONS } from '@/command-menu/pages/page-layout/constants/XSortByOptions';
|
||||
import { useGraphXSortOptionLabels } from '@/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { type GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const ChartXAxisSortBySelectionDropdownContent = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const configuration = widgetInEditMode?.configuration;
|
||||
const currentOrderByX = configuration.orderByX;
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const handleSelectSortOption = (orderByX: GraphOrderBy) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
orderByX,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const { getXSortOptionLabel } = useGraphXSortOptionLabels({
|
||||
objectMetadataId: widgetInEditMode?.objectMetadataId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>X-Axis Sort By</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={X_SORT_BY_OPTIONS.map(
|
||||
(option) => option.value,
|
||||
)}
|
||||
>
|
||||
{X_SORT_BY_OPTIONS.map((sortOption) => (
|
||||
<SelectableListItem
|
||||
key={sortOption.value}
|
||||
itemId={sortOption.value}
|
||||
onEnter={() => {
|
||||
handleSelectSortOption(sortOption.value);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={getXSortOptionLabel({
|
||||
graphOrderBy: sortOption.value,
|
||||
groupByFieldMetadataIdX:
|
||||
configuration.groupByFieldMetadataIdX,
|
||||
aggregateFieldMetadataId:
|
||||
configuration.aggregateFieldMetadataId,
|
||||
aggregateOperation: configuration.aggregateOperation,
|
||||
})}
|
||||
selected={currentOrderByX === sortOption.value}
|
||||
focused={selectedItemId === sortOption.value}
|
||||
LeftIcon={sortOption.icon}
|
||||
onClick={() => {
|
||||
handleSelectSortOption(sortOption.value);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { IconArrowDown, IconArrowUp } from 'twenty-ui/display';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const AGGREGATE_SORT_BY_OPTIONS = [
|
||||
{
|
||||
value: GraphOrderBy.FIELD_ASC,
|
||||
icon: IconArrowUp,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.FIELD_DESC,
|
||||
icon: IconArrowDown,
|
||||
},
|
||||
];
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { AXIS_NAME_SETTING } from '@/command-menu/pages/page-layout/constants/settings/AxisNameSetting';
|
||||
import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting';
|
||||
import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ColorsSetting';
|
||||
import { DATA_DISPLAY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting';
|
||||
import { DATA_DISPLAY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting';
|
||||
import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting';
|
||||
import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting';
|
||||
import { GROUP_BY_SETTING } from '@/command-menu/pages/page-layout/constants/settings/GroupBySetting';
|
||||
import { SORT_BY_GROUP_BY_FIELD_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByGroupByFieldSetting';
|
||||
import { SORT_BY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByXSetting';
|
||||
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
|
||||
export const BAR_CHART_SETTINGS: ChartSettingsGroup[] = [
|
||||
{
|
||||
heading: 'Data',
|
||||
items: [CHART_DATA_SOURCE_SETTING, FILTER_SETTING],
|
||||
},
|
||||
{
|
||||
heading: 'X axis',
|
||||
items: [DATA_DISPLAY_X_SETTING, SORT_BY_X_SETTING],
|
||||
},
|
||||
{
|
||||
heading: 'Y axis',
|
||||
items: [
|
||||
DATA_DISPLAY_Y_SETTING,
|
||||
GROUP_BY_SETTING,
|
||||
SORT_BY_GROUP_BY_FIELD_SETTING,
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Style',
|
||||
items: [COLORS_SETTING, AXIS_NAME_SETTING, DATA_LABELS_SETTING],
|
||||
},
|
||||
];
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting';
|
||||
import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting';
|
||||
import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting';
|
||||
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
|
||||
export const GAUGE_CHART_SETTINGS: ChartSettingsGroup[] = [
|
||||
{
|
||||
heading: 'Data',
|
||||
items: [CHART_DATA_SOURCE_SETTING, FILTER_SETTING],
|
||||
},
|
||||
{
|
||||
heading: 'Style',
|
||||
items: [DATA_LABELS_SETTING],
|
||||
},
|
||||
];
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { BAR_CHART_SETTINGS } from '@/command-menu/pages/page-layout/constants/BarChartSettings';
|
||||
import { GAUGE_CHART_SETTINGS } from '@/command-menu/pages/page-layout/constants/GaugeChartSettings';
|
||||
import { LINE_CHART_SETTINGS } from '@/command-menu/pages/page-layout/constants/LineChartSettings';
|
||||
import { NUMBER_CHART_SETTINGS } from '@/command-menu/pages/page-layout/constants/NumberChartSettings';
|
||||
import { PIE_CHART_SETTINGS } from '@/command-menu/pages/page-layout/constants/PieChartSettings';
|
||||
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
IconChartBar,
|
||||
IconChartLine,
|
||||
IconChartPie,
|
||||
type IconComponent,
|
||||
IconGauge,
|
||||
IconNumber,
|
||||
} from 'twenty-ui/display';
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const GRAPH_TYPE_INFORMATION: Record<
|
||||
GraphType,
|
||||
{
|
||||
label: MessageDescriptor;
|
||||
icon: IconComponent;
|
||||
settings: ChartSettingsGroup[];
|
||||
}
|
||||
> = {
|
||||
[GraphType.BAR]: {
|
||||
label: msg`Bar Chart`,
|
||||
icon: IconChartBar,
|
||||
settings: BAR_CHART_SETTINGS,
|
||||
},
|
||||
[GraphType.PIE]: {
|
||||
label: msg`Pie Chart`,
|
||||
icon: IconChartPie,
|
||||
settings: PIE_CHART_SETTINGS,
|
||||
},
|
||||
[GraphType.LINE]: {
|
||||
label: msg`Line Chart`,
|
||||
icon: IconChartLine,
|
||||
settings: LINE_CHART_SETTINGS,
|
||||
},
|
||||
[GraphType.NUMBER]: {
|
||||
label: msg`Number Chart`,
|
||||
icon: IconNumber,
|
||||
settings: NUMBER_CHART_SETTINGS,
|
||||
},
|
||||
[GraphType.GAUGE]: {
|
||||
label: msg`Gauge Chart`,
|
||||
icon: IconGauge,
|
||||
settings: GAUGE_CHART_SETTINGS,
|
||||
},
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const GRAPH_TYPE_TO_CONFIG_TYPENAME = {
|
||||
[GraphType.BAR]: 'BarChartConfiguration',
|
||||
[GraphType.LINE]: 'LineChartConfiguration',
|
||||
[GraphType.PIE]: 'PieChartConfiguration',
|
||||
[GraphType.NUMBER]: 'NumberChartConfiguration',
|
||||
[GraphType.GAUGE]: 'GaugeChartConfiguration',
|
||||
} as const;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { AXIS_NAME_SETTING } from '@/command-menu/pages/page-layout/constants/settings/AxisNameSetting';
|
||||
import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting';
|
||||
import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ColorsSetting';
|
||||
import { DATA_DISPLAY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting';
|
||||
import { DATA_DISPLAY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting';
|
||||
import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting';
|
||||
import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting';
|
||||
import { GROUP_BY_SETTING } from '@/command-menu/pages/page-layout/constants/settings/GroupBySetting';
|
||||
import { SORT_BY_GROUP_BY_FIELD_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByGroupByFieldSetting';
|
||||
import { SORT_BY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByXSetting';
|
||||
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
|
||||
export const LINE_CHART_SETTINGS: ChartSettingsGroup[] = [
|
||||
{
|
||||
heading: 'Data',
|
||||
items: [CHART_DATA_SOURCE_SETTING, FILTER_SETTING],
|
||||
},
|
||||
{
|
||||
heading: 'X axis',
|
||||
items: [DATA_DISPLAY_X_SETTING, SORT_BY_X_SETTING],
|
||||
},
|
||||
{
|
||||
heading: 'Y axis',
|
||||
items: [
|
||||
DATA_DISPLAY_Y_SETTING,
|
||||
GROUP_BY_SETTING,
|
||||
SORT_BY_GROUP_BY_FIELD_SETTING,
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Style',
|
||||
items: [COLORS_SETTING, AXIS_NAME_SETTING, DATA_LABELS_SETTING],
|
||||
},
|
||||
];
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting';
|
||||
import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting';
|
||||
import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting';
|
||||
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
|
||||
export const NUMBER_CHART_SETTINGS: ChartSettingsGroup[] = [
|
||||
{
|
||||
heading: 'Data',
|
||||
items: [CHART_DATA_SOURCE_SETTING, FILTER_SETTING],
|
||||
},
|
||||
{
|
||||
heading: 'Style',
|
||||
items: [DATA_LABELS_SETTING],
|
||||
},
|
||||
];
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting';
|
||||
import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ColorsSetting';
|
||||
import { DATA_DISPLAY_PIE_CHART_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayPieChartSetting';
|
||||
import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting';
|
||||
import { EACH_SLICE_REPRESENTS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/EachSliceRepresentsSetting';
|
||||
import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting';
|
||||
import { SORT_BY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByXSetting';
|
||||
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
|
||||
export const PIE_CHART_SETTINGS: ChartSettingsGroup[] = [
|
||||
{
|
||||
heading: 'Data',
|
||||
items: [
|
||||
CHART_DATA_SOURCE_SETTING,
|
||||
FILTER_SETTING,
|
||||
DATA_DISPLAY_PIE_CHART_SETTING,
|
||||
EACH_SLICE_REPRESENTS_SETTING,
|
||||
SORT_BY_X_SETTING,
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Style',
|
||||
items: [COLORS_SETTING, DATA_LABELS_SETTING],
|
||||
},
|
||||
];
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
IconArrowDown,
|
||||
IconArrowUp,
|
||||
IconTrendingDown,
|
||||
IconTrendingUp,
|
||||
} from 'twenty-ui/display';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const X_SORT_BY_OPTIONS = [
|
||||
{
|
||||
value: GraphOrderBy.FIELD_ASC,
|
||||
icon: IconArrowUp,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.FIELD_DESC,
|
||||
icon: IconArrowDown,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.VALUE_ASC,
|
||||
icon: IconTrendingUp,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.VALUE_DESC,
|
||||
icon: IconTrendingDown,
|
||||
},
|
||||
];
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconGizmo } from 'twenty-ui/display';
|
||||
|
||||
export const AXIS_NAME_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: IconGizmo,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.AXIS_NAME,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.AXIS_NAME,
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
export const CHART_CONFIGURATION_SETTING_LABELS = {
|
||||
SOURCE: msg`Source`,
|
||||
DATA_LABELS: msg`Data labels`,
|
||||
COLORS: msg`Colors`,
|
||||
FILTER: msg`Filter`,
|
||||
GROUP_BY: msg`Group by`,
|
||||
SORT_BY_X: msg`Sort by`,
|
||||
SORT_BY_GROUP_BY_FIELD: msg`Sort by`,
|
||||
DATA_ON_DISPLAY_X: msg`Data on display`,
|
||||
DATA_ON_DISPLAY_Y: msg`Data on display`,
|
||||
DATA_ON_DISPLAY_PIE_CHART: msg`Data on display`,
|
||||
EACH_SLICE_REPRESENTS: msg`Each slice represents`,
|
||||
AXIS_NAME: msg`Axis name`,
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconDatabase } from 'twenty-ui/display';
|
||||
|
||||
export const CHART_DATA_SOURCE_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: IconDatabase,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.SOURCE,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.SOURCE,
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconColorSwatch } from 'twenty-ui/display';
|
||||
|
||||
export const COLORS_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: IconColorSwatch,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.COLORS,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.COLORS,
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconChartPie } from 'twenty-ui/display';
|
||||
|
||||
export const DATA_DISPLAY_PIE_CHART_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: IconChartPie,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.DATA_ON_DISPLAY_PIE_CHART,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_PIE_CHART,
|
||||
dependsOn: [CHART_CONFIGURATION_SETTING_IDS.SOURCE],
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconAxisX } from 'twenty-ui/display';
|
||||
|
||||
export const DATA_DISPLAY_X_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: IconAxisX,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.DATA_ON_DISPLAY_X,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
|
||||
dependsOn: [CHART_CONFIGURATION_SETTING_IDS.SOURCE],
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconAxisY } from 'twenty-ui/display';
|
||||
|
||||
export const DATA_DISPLAY_Y_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: IconAxisY,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.DATA_ON_DISPLAY_Y,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y,
|
||||
dependsOn: [CHART_CONFIGURATION_SETTING_IDS.SOURCE],
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconTag } from 'twenty-ui/display';
|
||||
|
||||
export const DATA_LABELS_SETTING: ChartSettingsItem = {
|
||||
isBoolean: true,
|
||||
Icon: IconTag,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.DATA_LABELS,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS,
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconChartPie } from 'twenty-ui/display';
|
||||
|
||||
export const EACH_SLICE_REPRESENTS_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: IconChartPie,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.EACH_SLICE_REPRESENTS,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.EACH_SLICE_REPRESENTS,
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconFilter } from 'twenty-ui/display';
|
||||
|
||||
export const FILTER_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: IconFilter,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.FILTER,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.FILTER,
|
||||
dependsOn: [CHART_CONFIGURATION_SETTING_IDS.SOURCE],
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconFilters } from 'twenty-ui/display';
|
||||
|
||||
export const GROUP_BY_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: IconFilters,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.GROUP_BY,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.GROUP_BY,
|
||||
dependsOn: [CHART_CONFIGURATION_SETTING_IDS.SOURCE],
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconArrowsSort } from 'twenty-ui/display';
|
||||
|
||||
export const SORT_BY_GROUP_BY_FIELD_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: IconArrowsSort,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.SORT_BY_GROUP_BY_FIELD,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.SORT_BY_GROUP_BY_FIELD,
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconArrowsSort } from 'twenty-ui/display';
|
||||
|
||||
export const SORT_BY_X_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: IconArrowsSort,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.SORT_BY_X,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.SORT_BY_X,
|
||||
};
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import { useGraphGroupBySortOptionLabels } from '@/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels';
|
||||
import { useGraphXSortOptionLabels } from '@/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels';
|
||||
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { getChartAxisNameDisplayOptions } from '@/command-menu/pages/page-layout/utils/getChartAxisNameDisplayOptions';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useChartSettingsValues = ({
|
||||
objectMetadataId,
|
||||
configuration,
|
||||
}: {
|
||||
objectMetadataId: string;
|
||||
configuration?: ChartConfiguration;
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.id === objectMetadataId,
|
||||
);
|
||||
|
||||
const { getXSortOptionLabel } = useGraphXSortOptionLabels({
|
||||
objectMetadataId,
|
||||
});
|
||||
|
||||
const { getGroupBySortOptionLabel } = useGraphGroupBySortOptionLabels({
|
||||
objectMetadataId,
|
||||
});
|
||||
|
||||
if (!configuration) {
|
||||
return {
|
||||
getChartSettingsValues: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const groupByFieldX =
|
||||
'groupByFieldMetadataIdX' in configuration
|
||||
? objectMetadataItem?.fields.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === configuration.groupByFieldMetadataIdX,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const aggregateField = objectMetadataItem?.fields.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === configuration.aggregateFieldMetadataId,
|
||||
);
|
||||
|
||||
const yAxisAggregateOperation = configuration.aggregateOperation;
|
||||
|
||||
const groupByFieldY =
|
||||
'groupByFieldMetadataIdY' in configuration
|
||||
? objectMetadataItem?.fields.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === configuration.groupByFieldMetadataIdY,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const xAxisOrderBy =
|
||||
'orderByX' in configuration ? configuration.orderByX : undefined;
|
||||
|
||||
const xAxisOrderByLabel =
|
||||
isDefined(xAxisOrderBy) && 'groupByFieldMetadataIdX' in configuration
|
||||
? getXSortOptionLabel({
|
||||
graphOrderBy: xAxisOrderBy,
|
||||
groupByFieldMetadataIdX: configuration.groupByFieldMetadataIdX,
|
||||
aggregateFieldMetadataId: configuration.aggregateFieldMetadataId,
|
||||
aggregateOperation: configuration.aggregateOperation,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const groupByOrderBy =
|
||||
'orderByY' in configuration
|
||||
? configuration.orderByY
|
||||
: 'orderBy' in configuration
|
||||
? configuration.orderBy
|
||||
: undefined;
|
||||
|
||||
const groupByOrderByLabel =
|
||||
isDefined(groupByOrderBy) &&
|
||||
getGroupBySortOptionLabel({
|
||||
graphOrderBy: groupByOrderBy,
|
||||
groupByFieldMetadataId:
|
||||
'groupByFieldMetadataIdY' in configuration
|
||||
? configuration.groupByFieldMetadataIdY
|
||||
: 'groupByFieldMetadataId' in configuration
|
||||
? configuration.groupByFieldMetadataId
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const getChartSettingsValues = (
|
||||
itemId: CHART_CONFIGURATION_SETTING_IDS,
|
||||
): boolean | string | undefined => {
|
||||
switch (itemId) {
|
||||
case CHART_CONFIGURATION_SETTING_IDS.SOURCE:
|
||||
return objectMetadataItem?.labelPlural;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X:
|
||||
return groupByFieldX?.label;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.COLORS:
|
||||
return isDefined(configuration.color) && 'color' in configuration
|
||||
? capitalize(configuration.color)
|
||||
: undefined;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y:
|
||||
return `${aggregateField?.label ?? ''}${aggregateField?.label ? ` (${getAggregateOperationLabel(yAxisAggregateOperation)})` : ''}`;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.GROUP_BY:
|
||||
return groupByFieldY?.label;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.AXIS_NAME:
|
||||
return 'axisNameDisplay' in configuration
|
||||
? getChartAxisNameDisplayOptions(configuration.axisNameDisplay)
|
||||
: undefined;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.SORT_BY_X:
|
||||
return xAxisOrderByLabel;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.SORT_BY_GROUP_BY_FIELD:
|
||||
return groupByOrderByLabel;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS:
|
||||
return configuration.displayDataLabel;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getChartSettingsValues,
|
||||
};
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const useGraphGroupBySortOptionLabels = ({
|
||||
objectMetadataId,
|
||||
}: {
|
||||
objectMetadataId: string;
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.id === objectMetadataId,
|
||||
);
|
||||
|
||||
const getGroupBySortOptionLabel = ({
|
||||
graphOrderBy,
|
||||
groupByFieldMetadataId,
|
||||
}: {
|
||||
graphOrderBy: GraphOrderBy;
|
||||
groupByFieldMetadataId: string;
|
||||
}): string => {
|
||||
const field = objectMetadataItem?.fields.find(
|
||||
(fieldMetadataItem) => fieldMetadataItem.id === groupByFieldMetadataId,
|
||||
);
|
||||
|
||||
const fieldLabel = field?.label || t`Field`;
|
||||
|
||||
switch (graphOrderBy) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return `${fieldLabel} ${t`Ascending`}`;
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return `${fieldLabel} ${t`Descending`}`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getGroupBySortOptionLabel,
|
||||
};
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type ExtendedAggregateOperations,
|
||||
GraphOrderBy,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const useGraphXSortOptionLabels = ({
|
||||
objectMetadataId,
|
||||
}: {
|
||||
objectMetadataId: string;
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.id === objectMetadataId,
|
||||
);
|
||||
|
||||
const getXSortOptionLabel = ({
|
||||
graphOrderBy,
|
||||
groupByFieldMetadataIdX,
|
||||
aggregateFieldMetadataId,
|
||||
aggregateOperation,
|
||||
}: {
|
||||
graphOrderBy: GraphOrderBy;
|
||||
groupByFieldMetadataIdX: string;
|
||||
aggregateFieldMetadataId?: string;
|
||||
aggregateOperation?: ExtendedAggregateOperations;
|
||||
}): string => {
|
||||
const groupByField = objectMetadataItem?.fields.find(
|
||||
(fieldMetadataItem) => fieldMetadataItem.id === groupByFieldMetadataIdX,
|
||||
);
|
||||
|
||||
const fieldLabel = groupByField?.label || t`Field`;
|
||||
|
||||
const aggregateField = objectMetadataItem?.fields.find(
|
||||
(fieldMetadataItem) =>
|
||||
isDefined(aggregateFieldMetadataId) &&
|
||||
fieldMetadataItem.id === aggregateFieldMetadataId,
|
||||
);
|
||||
|
||||
const valueLabel =
|
||||
aggregateField?.label && isDefined(aggregateOperation)
|
||||
? getAggregateOperationLabel(aggregateOperation)
|
||||
: t`Value`;
|
||||
|
||||
switch (graphOrderBy) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return `${fieldLabel} ${t`Ascending`}`;
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return `${fieldLabel} ${t`Descending`}`;
|
||||
case GraphOrderBy.VALUE_ASC:
|
||||
return `${valueLabel} ${t`Ascending`}`;
|
||||
case GraphOrderBy.VALUE_DESC:
|
||||
return `${valueLabel} ${t`Descending`}`;
|
||||
default:
|
||||
assertUnreachable(graphOrderBy);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getXSortOptionLabel,
|
||||
};
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import type { PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
export const useUpdateCurrentWidgetConfig = (pageLayoutIdFromProps: string) => {
|
||||
const pageLayoutDraftCallbackState = useRecoilComponentCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const currentlyEditingWidgetId = useRecoilComponentValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const updateCurrentWidgetConfig = useRecoilCallback(
|
||||
({ set }) =>
|
||||
({
|
||||
objectMetadataId,
|
||||
configToUpdate,
|
||||
}: {
|
||||
objectMetadataId?: string | null;
|
||||
configToUpdate?: Partial<PageLayoutWidget['configuration']>;
|
||||
}) => {
|
||||
set(pageLayoutDraftCallbackState, (prev) => ({
|
||||
...prev,
|
||||
tabs: prev.tabs.map((tab) => ({
|
||||
...tab,
|
||||
widgets: tab.widgets.map((widget) =>
|
||||
widget.id === currentlyEditingWidgetId
|
||||
? {
|
||||
...widget,
|
||||
objectMetadataId:
|
||||
objectMetadataId ?? widget.objectMetadataId,
|
||||
configuration: {
|
||||
...(widget.configuration ?? {}),
|
||||
...configToUpdate,
|
||||
} as PageLayoutWidget['configuration'],
|
||||
}
|
||||
: widget,
|
||||
),
|
||||
})),
|
||||
}));
|
||||
},
|
||||
[pageLayoutDraftCallbackState, currentlyEditingWidgetId],
|
||||
);
|
||||
|
||||
return { updateCurrentWidgetConfig };
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
export const useWidgetInEditMode = (pageLayoutId: string) => {
|
||||
const draftPageLayout = useRecoilComponentValue(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutEditingWidgetId = useRecoilComponentValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const widgetInEditMode = draftPageLayout.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((widget) => widget.id === pageLayoutEditingWidgetId);
|
||||
|
||||
return { widgetInEditMode };
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type GaugeChartConfiguration,
|
||||
type LineChartConfiguration,
|
||||
type NumberChartConfiguration,
|
||||
type PieChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export type ChartConfiguration =
|
||||
| BarChartConfiguration
|
||||
| LineChartConfiguration
|
||||
| PieChartConfiguration
|
||||
| NumberChartConfiguration
|
||||
| GaugeChartConfiguration;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
export enum CHART_CONFIGURATION_SETTING_IDS {
|
||||
SOURCE = 'SOURCE',
|
||||
DATA_LABELS = 'DATA_LABELS',
|
||||
COLORS = 'COLORS',
|
||||
FILTER = 'FILTER',
|
||||
GROUP_BY = 'GROUP_BY',
|
||||
SORT_BY_X = 'SORT_BY_X',
|
||||
SORT_BY_GROUP_BY_FIELD = 'SORT_BY_GROUP_BY_FIELD',
|
||||
SORT_BY = 'SORT_BY',
|
||||
DATA_ON_DISPLAY_X = 'DATA_ON_DISPLAY_X',
|
||||
DATA_ON_DISPLAY_Y = 'DATA_ON_DISPLAY_Y',
|
||||
DATA_ON_DISPLAY_PIE_CHART = 'DATA_ON_DISPLAY_PIE_CHART',
|
||||
EACH_SLICE_REPRESENTS = 'EACH_SLICE_REPRESENTS',
|
||||
AXIS_NAME = 'AXIS_NAME',
|
||||
}
|
||||
|
||||
export const CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP = {
|
||||
[CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS]: 'displayDataLabel',
|
||||
} as const;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { type CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
|
||||
export type ChartSettingsGroup = {
|
||||
heading: string;
|
||||
items: ChartSettingsItem[];
|
||||
};
|
||||
|
||||
export type ChartSettingsItem = {
|
||||
Icon: IconComponent;
|
||||
label: MessageDescriptor;
|
||||
id: CHART_CONFIGURATION_SETTING_IDS;
|
||||
description?: string;
|
||||
isBoolean: boolean;
|
||||
dependsOn?: CHART_CONFIGURATION_SETTING_IDS[];
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { AxisNameDisplay } from '~/generated/graphql';
|
||||
|
||||
export const getChartAxisNameDisplayOptions = (option: AxisNameDisplay) => {
|
||||
switch (option) {
|
||||
case AxisNameDisplay.NONE:
|
||||
return t`None`;
|
||||
case AxisNameDisplay.X:
|
||||
return t`X axis`;
|
||||
case AxisNameDisplay.Y:
|
||||
return t`Y axis`;
|
||||
case AxisNameDisplay.BOTH:
|
||||
return t`Both`;
|
||||
default:
|
||||
assertUnreachable(option);
|
||||
}
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { ChartAxisNameSelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartAxisNameSelectionDropdownContent';
|
||||
import { ChartColorSelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent';
|
||||
import { ChartDataSourceDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartDataSourceDropdownContent';
|
||||
import { ChartFieldSelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionDropdownContent';
|
||||
import { ChartFieldSelectionForAggregateOperationDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent';
|
||||
import { ChartGroupByFieldSelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContent';
|
||||
import { ChartSortByGroupByFieldDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartSortByGroupByFieldDropdownContent';
|
||||
import { ChartXAxisSortBySelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisSortBySelectionDropdownContent';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
export const getChartSettingsDropdownContent = (itemId: string) => {
|
||||
switch (itemId) {
|
||||
case CHART_CONFIGURATION_SETTING_IDS.SOURCE:
|
||||
return <ChartDataSourceDropdownContent />;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X:
|
||||
return <ChartFieldSelectionDropdownContent />;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.SORT_BY_X:
|
||||
return <ChartXAxisSortBySelectionDropdownContent />;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y:
|
||||
return <ChartFieldSelectionForAggregateOperationDropdownContent />;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.SORT_BY_GROUP_BY_FIELD:
|
||||
return (
|
||||
<ChartSortByGroupByFieldDropdownContent title={t`Y-Axis Sort By`} />
|
||||
);
|
||||
case CHART_CONFIGURATION_SETTING_IDS.GROUP_BY:
|
||||
return <ChartGroupByFieldSelectionDropdownContent />;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.COLORS:
|
||||
return <ChartColorSelectionDropdownContent />;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.AXIS_NAME:
|
||||
return <ChartAxisNameSelectionDropdownContent />;
|
||||
default:
|
||||
return <div>Configuration options will be implemented here</div>;
|
||||
}
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { DateAggregateOperations } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||
import { type ExtendedAggregateOperations as FrontendExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { ExtendedAggregateOperations as GraphQLExtendedAggregateOperations } from '~/generated-metadata/graphql';
|
||||
|
||||
const AGGREGATE_OPERATION_MAPPING: Record<
|
||||
FrontendExtendedAggregateOperations,
|
||||
GraphQLExtendedAggregateOperations
|
||||
> = {
|
||||
[AggregateOperations.MIN]: GraphQLExtendedAggregateOperations.MIN,
|
||||
[AggregateOperations.MAX]: GraphQLExtendedAggregateOperations.MAX,
|
||||
[AggregateOperations.AVG]: GraphQLExtendedAggregateOperations.AVG,
|
||||
[AggregateOperations.SUM]: GraphQLExtendedAggregateOperations.SUM,
|
||||
[AggregateOperations.COUNT]: GraphQLExtendedAggregateOperations.COUNT,
|
||||
[AggregateOperations.COUNT_EMPTY]:
|
||||
GraphQLExtendedAggregateOperations.COUNT_EMPTY,
|
||||
[AggregateOperations.COUNT_NOT_EMPTY]:
|
||||
GraphQLExtendedAggregateOperations.COUNT_NOT_EMPTY,
|
||||
[AggregateOperations.COUNT_UNIQUE_VALUES]:
|
||||
GraphQLExtendedAggregateOperations.COUNT_UNIQUE_VALUES,
|
||||
[AggregateOperations.PERCENTAGE_EMPTY]:
|
||||
GraphQLExtendedAggregateOperations.PERCENTAGE_EMPTY,
|
||||
[AggregateOperations.PERCENTAGE_NOT_EMPTY]:
|
||||
GraphQLExtendedAggregateOperations.PERCENTAGE_NOT_EMPTY,
|
||||
[AggregateOperations.COUNT_TRUE]:
|
||||
GraphQLExtendedAggregateOperations.COUNT_TRUE,
|
||||
[AggregateOperations.COUNT_FALSE]:
|
||||
GraphQLExtendedAggregateOperations.COUNT_FALSE,
|
||||
[DateAggregateOperations.EARLIEST]:
|
||||
GraphQLExtendedAggregateOperations.EARLIEST,
|
||||
[DateAggregateOperations.LATEST]: GraphQLExtendedAggregateOperations.LATEST,
|
||||
};
|
||||
|
||||
export const mapToGraphQLExtendedAggregateOperation = (
|
||||
operation: FrontendExtendedAggregateOperations,
|
||||
): GraphQLExtendedAggregateOperations => {
|
||||
return AGGREGATE_OPERATION_MAPPING[operation];
|
||||
};
|
||||
+2
-1
@@ -3,9 +3,10 @@ import { DateAggregateOperations } from '@/object-record/record-table/constants/
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { CustomError } from 'twenty-shared/utils';
|
||||
import type { ExtendedAggregateOperations as GqlExtendedAggregateOperations } from '~/generated/graphql';
|
||||
|
||||
export const getAggregateOperationLabel = (
|
||||
operation: ExtendedAggregateOperations,
|
||||
operation: ExtendedAggregateOperations | GqlExtendedAggregateOperations,
|
||||
) => {
|
||||
switch (operation) {
|
||||
case AggregateOperations.MIN:
|
||||
|
||||
+11
-9
@@ -4,15 +4,18 @@ import { expect, within } from '@storybook/test';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLayout';
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import {
|
||||
GraphOrderBy,
|
||||
GraphType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { PageLayoutType, type PageLayoutWidget } from '~/generated/graphql';
|
||||
import {
|
||||
ExtendedAggregateOperations,
|
||||
PageLayoutType,
|
||||
type PageLayoutWidget,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const validatePageLayoutContent = async (canvasElement: HTMLElement) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -60,7 +63,7 @@ const mixedGraphsPageLayoutMocks = {
|
||||
configuration: {
|
||||
__typename: 'NumberChartConfiguration',
|
||||
graphType: GraphType.NUMBER,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
@@ -84,10 +87,9 @@ const mixedGraphsPageLayoutMocks = {
|
||||
configuration: {
|
||||
__typename: 'GaugeChartConfiguration',
|
||||
graphType: GraphType.GAUGE,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
aggregateOperationTotal: AggregateOperations.COUNT,
|
||||
aggregateFieldMetadataIdTotal: 'id',
|
||||
displayDataLabel: false,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
@@ -110,7 +112,7 @@ const mixedGraphsPageLayoutMocks = {
|
||||
configuration: {
|
||||
__typename: 'PieChartConfiguration',
|
||||
graphType: GraphType.PIE,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
groupByFieldMetadataId: 'createdAt',
|
||||
orderBy: GraphOrderBy.VALUE_DESC,
|
||||
@@ -136,7 +138,7 @@ const mixedGraphsPageLayoutMocks = {
|
||||
configuration: {
|
||||
__typename: 'BarChartConfiguration',
|
||||
graphType: GraphType.BAR,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
groupByFieldMetadataIdX: 'createdAt',
|
||||
orderByX: GraphOrderBy.FIELD_ASC,
|
||||
|
||||
+17
-4
@@ -25,10 +25,12 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
groupByFieldMetadataIdY
|
||||
orderByY
|
||||
omitNullValues
|
||||
xAxisName
|
||||
yAxisName
|
||||
axisNameDisplay
|
||||
displayDataLabel
|
||||
rangeMin
|
||||
rangeMax
|
||||
color
|
||||
description
|
||||
filter
|
||||
}
|
||||
... on LineChartConfiguration {
|
||||
@@ -39,6 +41,13 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
orderByX
|
||||
groupByFieldMetadataIdY
|
||||
orderByY
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
displayDataLabel
|
||||
rangeMin
|
||||
rangeMax
|
||||
color
|
||||
description
|
||||
filter
|
||||
}
|
||||
... on PieChartConfiguration {
|
||||
@@ -47,12 +56,16 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
orderBy
|
||||
displayDataLabel
|
||||
color
|
||||
description
|
||||
filter
|
||||
}
|
||||
... on NumberChartConfiguration {
|
||||
graphType
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
displayDataLabel
|
||||
description
|
||||
filter
|
||||
}
|
||||
@@ -60,8 +73,8 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
graphType
|
||||
aggregateFieldMetadataId
|
||||
aggregateOperation
|
||||
aggregateOperationTotal
|
||||
aggregateFieldMetadataIdTotal
|
||||
displayDataLabel
|
||||
color
|
||||
description
|
||||
filter
|
||||
}
|
||||
|
||||
+8
-31
@@ -1,12 +1,12 @@
|
||||
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { GraphType, WidgetType } from '~/generated-metadata/graphql';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { GraphType, WidgetType } from '~/generated-metadata/graphql';
|
||||
import { PageLayoutType } from '~/generated/graphql';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
@@ -82,10 +82,7 @@ describe('useCreatePageLayoutGraphWidget', () => {
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
WidgetType.GRAPH,
|
||||
GraphType.BAR,
|
||||
);
|
||||
result.current.createWidget.createPageLayoutGraphWidget(GraphType.BAR);
|
||||
});
|
||||
|
||||
expect(result.current.allWidgets).toHaveLength(1);
|
||||
@@ -167,10 +164,7 @@ describe('useCreatePageLayoutGraphWidget', () => {
|
||||
|
||||
graphTypes.forEach((graphType) => {
|
||||
act(() => {
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
WidgetType.GRAPH,
|
||||
graphType,
|
||||
);
|
||||
result.current.createWidget.createPageLayoutGraphWidget(graphType);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,38 +193,21 @@ describe('useCreatePageLayoutGraphWidget', () => {
|
||||
expect(result.current.pageLayoutDraft.tabs[0].widgets).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should not create widget when activeTabId is null', () => {
|
||||
it('should throw an error when activeTabId is null', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const pageLayoutDraft = useRecoilComponentValue(
|
||||
pageLayoutDraftComponentState,
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
);
|
||||
const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets);
|
||||
const pageLayoutCurrentLayouts = useRecoilComponentValue(
|
||||
pageLayoutCurrentLayoutsComponentState,
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
);
|
||||
const createWidget = useCreatePageLayoutGraphWidget(
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
);
|
||||
return { allWidgets, pageLayoutCurrentLayouts, createWidget };
|
||||
return { createWidget };
|
||||
},
|
||||
{
|
||||
wrapper: PageLayoutTestWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
WidgetType.GRAPH,
|
||||
GraphType.BAR,
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.allWidgets).toHaveLength(0);
|
||||
expect(Object.keys(result.current.pageLayoutCurrentLayouts)).toHaveLength(
|
||||
0,
|
||||
);
|
||||
expect(() => {
|
||||
result.current.createWidget.createPageLayoutGraphWidget(GraphType.BAR);
|
||||
}).toThrow('A tab must be selected to create a new graph widget');
|
||||
});
|
||||
});
|
||||
|
||||
+7
-4
@@ -1,11 +1,13 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import {
|
||||
GraphOrderBy,
|
||||
GraphType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { PageLayoutType } from '~/generated/graphql';
|
||||
import {
|
||||
ExtendedAggregateOperations,
|
||||
PageLayoutType,
|
||||
} from '~/generated/graphql';
|
||||
import { usePageLayoutDraftState } from '../usePageLayoutDraftState';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
@@ -102,10 +104,11 @@ describe('usePageLayoutDraftState', () => {
|
||||
gridPosition: { row: 2, column: 2, rowSpan: 2, columnSpan: 2 },
|
||||
configuration: {
|
||||
graphType: GraphType.BAR,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
groupByFieldMetadataIdX: 'createdAt',
|
||||
orderByX: GraphOrderBy.FIELD_ASC,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
objectMetadataId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
|
||||
+18
-13
@@ -11,13 +11,14 @@ import {
|
||||
import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition';
|
||||
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
|
||||
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
|
||||
import { type GraphType } from '~/generated-metadata/graphql';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { type WidgetType } from '~/generated/graphql';
|
||||
import { type GraphType } from '~/generated-metadata/graphql';
|
||||
import { WidgetType, type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
export const useCreatePageLayoutGraphWidget = (
|
||||
pageLayoutIdFromProps?: string,
|
||||
@@ -49,13 +50,15 @@ export const useCreatePageLayoutGraphWidget = (
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const createPageLayoutWidget = useRecoilCallback(
|
||||
const createPageLayoutGraphWidget = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(widgetType: WidgetType, graphType: GraphType) => {
|
||||
(graphType: GraphType): PageLayoutWidget => {
|
||||
const activeTabId = snapshot.getLoadable(activeTabIdState).getValue();
|
||||
|
||||
if (!activeTabId) {
|
||||
return;
|
||||
if (!isDefined(activeTabId)) {
|
||||
throw new Error(
|
||||
'A tab must be selected to create a new graph widget',
|
||||
);
|
||||
}
|
||||
|
||||
const pageLayoutDraft = snapshot
|
||||
@@ -73,7 +76,7 @@ export const useCreatePageLayoutGraphWidget = (
|
||||
const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets);
|
||||
const existingWidgetCount = allWidgets.filter(
|
||||
(w) =>
|
||||
w.type === widgetType &&
|
||||
w.type === WidgetType.GRAPH &&
|
||||
w.configuration &&
|
||||
'graphType' in w.configuration &&
|
||||
w.configuration.graphType === graphType,
|
||||
@@ -87,18 +90,18 @@ export const useCreatePageLayoutGraphWidget = (
|
||||
defaultSize,
|
||||
);
|
||||
|
||||
const newWidget = createDefaultGraphWidget(
|
||||
widgetId,
|
||||
activeTabId,
|
||||
const newWidget = createDefaultGraphWidget({
|
||||
id: widgetId,
|
||||
pageLayoutTabId: activeTabId,
|
||||
title,
|
||||
graphType,
|
||||
{
|
||||
gridPosition: {
|
||||
row: position.y,
|
||||
column: position.x,
|
||||
rowSpan: position.h,
|
||||
columnSpan: position.w,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const newLayout = {
|
||||
i: widgetId,
|
||||
@@ -121,6 +124,8 @@ export const useCreatePageLayoutGraphWidget = (
|
||||
}));
|
||||
|
||||
set(pageLayoutDraggedAreaState, null);
|
||||
|
||||
return newWidget;
|
||||
},
|
||||
[
|
||||
activeTabIdState,
|
||||
@@ -130,5 +135,5 @@ export const useCreatePageLayoutGraphWidget = (
|
||||
],
|
||||
);
|
||||
|
||||
return { createPageLayoutWidget };
|
||||
return { createPageLayoutGraphWidget };
|
||||
};
|
||||
|
||||
@@ -39,6 +39,13 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
pageTitle: t`Edit iFrame`,
|
||||
});
|
||||
}
|
||||
|
||||
if (widgetType === WidgetType.GRAPH) {
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage: CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
pageTitle: t`Edit Graph`,
|
||||
});
|
||||
}
|
||||
},
|
||||
[setPageLayoutEditingWidgetId, navigatePageLayoutCommandMenu],
|
||||
);
|
||||
|
||||
+6
-3
@@ -1,7 +1,9 @@
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { GraphType, WidgetType } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
ExtendedAggregateOperations,
|
||||
type PageLayoutWidget,
|
||||
} from '~/generated/graphql';
|
||||
import { type PageLayoutTab } from '../../types/PageLayoutTab';
|
||||
import { type PageLayoutWidget } from '~/generated/graphql';
|
||||
import { addWidgetToTab } from '../addWidgetToTab';
|
||||
|
||||
describe('addWidgetToTab', () => {
|
||||
@@ -13,8 +15,9 @@ describe('addWidgetToTab', () => {
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: {
|
||||
graphType: GraphType.NUMBER,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
displayDataLabel: false,
|
||||
},
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
|
||||
objectMetadataId: null,
|
||||
|
||||
+5
-3
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
ExtendedAggregateOperations,
|
||||
GraphOrderBy,
|
||||
GraphType,
|
||||
WidgetType,
|
||||
type PageLayoutWidget,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { convertLayoutsToWidgets } from '../convertLayoutsToWidgets';
|
||||
|
||||
describe('convertLayoutsToWidgets', () => {
|
||||
@@ -23,8 +23,9 @@ describe('convertLayoutsToWidgets', () => {
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphType.NUMBER,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
displayDataLabel: false,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
@@ -44,10 +45,11 @@ describe('convertLayoutsToWidgets', () => {
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphType.PIE,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
groupByFieldMetadataId: 'status',
|
||||
orderBy: GraphOrderBy.VALUE_DESC,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
|
||||
+5
-3
@@ -1,7 +1,7 @@
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
|
||||
import {
|
||||
ExtendedAggregateOperations,
|
||||
GraphOrderBy,
|
||||
GraphType,
|
||||
PageLayoutType,
|
||||
@@ -30,8 +30,9 @@ describe('convertPageLayoutToTabLayouts', () => {
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: {
|
||||
graphType: GraphType.NUMBER,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
displayDataLabel: false,
|
||||
},
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
|
||||
objectMetadataId: 'object-metadata-1',
|
||||
@@ -47,10 +48,11 @@ describe('convertPageLayoutToTabLayouts', () => {
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: {
|
||||
graphType: GraphType.PIE,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
groupByFieldMetadataId: 'status',
|
||||
orderBy: GraphOrderBy.VALUE_DESC,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
gridPosition: { row: 2, column: 0, rowSpan: 2, columnSpan: 2 },
|
||||
objectMetadataId: 'object-metadata-1',
|
||||
|
||||
+21
-12
@@ -3,7 +3,8 @@ import {
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
AggregateOperations,
|
||||
AxisNameDisplay,
|
||||
ExtendedAggregateOperations,
|
||||
GraphOrderBy,
|
||||
GraphType,
|
||||
} from '~/generated/graphql';
|
||||
@@ -51,9 +52,11 @@ describe('extractFieldMetadataIdsFromWidget', () => {
|
||||
__typename: 'BarChartConfiguration' as const,
|
||||
graphType: GraphType.BAR,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: 'field-2',
|
||||
orderByX: GraphOrderBy.FIELD_ASC,
|
||||
axisNameDisplay: AxisNameDisplay.BOTH,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
@@ -71,10 +74,12 @@ describe('extractFieldMetadataIdsFromWidget', () => {
|
||||
__typename: 'BarChartConfiguration' as const,
|
||||
graphType: GraphType.BAR,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: 'field-2',
|
||||
groupByFieldMetadataIdY: 'field-3',
|
||||
orderByX: GraphOrderBy.FIELD_ASC,
|
||||
axisNameDisplay: AxisNameDisplay.BOTH,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
@@ -93,9 +98,11 @@ describe('extractFieldMetadataIdsFromWidget', () => {
|
||||
__typename: 'LineChartConfiguration' as const,
|
||||
graphType: GraphType.LINE,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateOperation: ExtendedAggregateOperations.SUM,
|
||||
groupByFieldMetadataIdX: 'field-2',
|
||||
orderByX: GraphOrderBy.FIELD_DESC,
|
||||
axisNameDisplay: AxisNameDisplay.BOTH,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
@@ -113,9 +120,10 @@ describe('extractFieldMetadataIdsFromWidget', () => {
|
||||
__typename: 'PieChartConfiguration' as const,
|
||||
graphType: GraphType.PIE,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
groupByFieldMetadataId: 'field-2',
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
@@ -133,7 +141,8 @@ describe('extractFieldMetadataIdsFromWidget', () => {
|
||||
__typename: 'NumberChartConfiguration' as const,
|
||||
graphType: GraphType.NUMBER,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.AVG,
|
||||
aggregateOperation: ExtendedAggregateOperations.AVG,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
@@ -150,18 +159,16 @@ describe('extractFieldMetadataIdsFromWidget', () => {
|
||||
__typename: 'GaugeChartConfiguration' as const,
|
||||
graphType: GraphType.GAUGE,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateFieldMetadataIdTotal: 'field-2',
|
||||
aggregateOperationTotal: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.SUM,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
|
||||
const result = extractFieldMetadataIdsFromWidget(widget);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContain('field-1');
|
||||
expect(result).toContain('field-2');
|
||||
});
|
||||
|
||||
it('should not include undefined optional fields', () => {
|
||||
@@ -170,10 +177,12 @@ describe('extractFieldMetadataIdsFromWidget', () => {
|
||||
__typename: 'BarChartConfiguration' as const,
|
||||
graphType: GraphType.BAR,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: 'field-2',
|
||||
groupByFieldMetadataIdY: undefined,
|
||||
orderByX: GraphOrderBy.FIELD_ASC,
|
||||
axisNameDisplay: AxisNameDisplay.BOTH,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
|
||||
+5
-3
@@ -1,5 +1,5 @@
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import {
|
||||
ExtendedAggregateOperations,
|
||||
GraphOrderBy,
|
||||
GraphType,
|
||||
WidgetType,
|
||||
@@ -23,8 +23,9 @@ describe('removeWidgetFromTab', () => {
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: {
|
||||
graphType: GraphType.NUMBER,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
displayDataLabel: false,
|
||||
},
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
|
||||
objectMetadataId: null,
|
||||
@@ -40,10 +41,11 @@ describe('removeWidgetFromTab', () => {
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: {
|
||||
graphType: GraphType.PIE,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
groupByFieldMetadataId: 'status',
|
||||
orderBy: GraphOrderBy.VALUE_DESC,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
gridPosition: { row: 2, column: 0, rowSpan: 2, columnSpan: 2 },
|
||||
objectMetadataId: null,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { assertUnreachable } 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 {
|
||||
AxisNameDisplay,
|
||||
ExtendedAggregateOperations,
|
||||
type GridPosition,
|
||||
type PageLayoutWidget,
|
||||
type WidgetConfiguration,
|
||||
@@ -17,64 +20,81 @@ const createDefaultGraphConfiguration = (
|
||||
switch (graphType) {
|
||||
case GraphType.NUMBER:
|
||||
return {
|
||||
__typename: 'NumberChartConfiguration',
|
||||
graphType: GraphType.NUMBER,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: placeholderFieldId1,
|
||||
displayDataLabel: false,
|
||||
};
|
||||
|
||||
case GraphType.PIE:
|
||||
return {
|
||||
__typename: 'PieChartConfiguration',
|
||||
graphType: GraphType.PIE,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: placeholderFieldId1,
|
||||
groupByFieldMetadataId: placeholderFieldId2,
|
||||
orderBy: GraphOrderBy.VALUE_DESC,
|
||||
displayDataLabel: false,
|
||||
color: 'blue' satisfies ThemeColor,
|
||||
};
|
||||
|
||||
case GraphType.BAR:
|
||||
return {
|
||||
__typename: 'BarChartConfiguration',
|
||||
graphType: GraphType.BAR,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: placeholderFieldId1,
|
||||
groupByFieldMetadataIdX: placeholderFieldId2,
|
||||
orderByX: GraphOrderBy.FIELD_ASC,
|
||||
displayDataLabel: false,
|
||||
axisNameDisplay: AxisNameDisplay.BOTH,
|
||||
color: 'blue' satisfies ThemeColor,
|
||||
};
|
||||
|
||||
case GraphType.LINE:
|
||||
return {
|
||||
__typename: 'LineChartConfiguration',
|
||||
graphType: GraphType.LINE,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: placeholderFieldId1,
|
||||
groupByFieldMetadataIdX: placeholderFieldId2,
|
||||
orderByX: GraphOrderBy.FIELD_ASC,
|
||||
displayDataLabel: false,
|
||||
axisNameDisplay: AxisNameDisplay.BOTH,
|
||||
color: 'blue' satisfies ThemeColor,
|
||||
};
|
||||
|
||||
case GraphType.GAUGE:
|
||||
return {
|
||||
__typename: 'GaugeChartConfiguration',
|
||||
graphType: GraphType.GAUGE,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateOperation: ExtendedAggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: placeholderFieldId1,
|
||||
aggregateOperationTotal: AggregateOperations.COUNT,
|
||||
aggregateFieldMetadataIdTotal: placeholderFieldId2,
|
||||
displayDataLabel: false,
|
||||
color: 'blue' satisfies ThemeColor,
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
graphType: GraphType.NUMBER,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: placeholderFieldId1,
|
||||
};
|
||||
assertUnreachable(graphType);
|
||||
}
|
||||
};
|
||||
|
||||
export const createDefaultGraphWidget = (
|
||||
id: string,
|
||||
pageLayoutTabId: string,
|
||||
title: string,
|
||||
graphType: GraphType,
|
||||
gridPosition: GridPosition,
|
||||
objectMetadataId?: string | null,
|
||||
): PageLayoutWidget => {
|
||||
export const createDefaultGraphWidget = ({
|
||||
id,
|
||||
pageLayoutTabId,
|
||||
title,
|
||||
graphType,
|
||||
gridPosition,
|
||||
objectMetadataId,
|
||||
}: {
|
||||
id: string;
|
||||
pageLayoutTabId: string;
|
||||
title: string;
|
||||
graphType: GraphType;
|
||||
gridPosition: GridPosition;
|
||||
objectMetadataId?: string | null;
|
||||
}): PageLayoutWidget => {
|
||||
return {
|
||||
__typename: 'PageLayoutWidget',
|
||||
id,
|
||||
|
||||
+1
-4
@@ -31,10 +31,7 @@ export const extractFieldMetadataIdsFromWidget = (
|
||||
return [config.aggregateFieldMetadataId];
|
||||
|
||||
case 'GaugeChartConfiguration':
|
||||
return [
|
||||
config.aggregateFieldMetadataId,
|
||||
config.aggregateFieldMetadataIdTotal,
|
||||
];
|
||||
return [config.aggregateFieldMetadataId];
|
||||
|
||||
case 'IframeConfiguration':
|
||||
return [];
|
||||
|
||||
+57
-57
@@ -1,9 +1,9 @@
|
||||
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
|
||||
import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget';
|
||||
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
|
||||
const meta: Meta<typeof WidgetRenderer> = {
|
||||
title: 'Modules/PageLayout/Widgets/WidgetRenderer',
|
||||
@@ -30,18 +30,18 @@ type Story = StoryObj<typeof WidgetRenderer>;
|
||||
|
||||
export const WithNumberChart: Story = {
|
||||
args: {
|
||||
widget: createDefaultGraphWidget(
|
||||
'widget-1',
|
||||
'tab-overview',
|
||||
'Sales Pipeline',
|
||||
GraphType.NUMBER,
|
||||
{
|
||||
widget: createDefaultGraphWidget({
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-overview',
|
||||
title: 'Sales Pipeline',
|
||||
graphType: GraphType.NUMBER,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 2,
|
||||
columnSpan: 3,
|
||||
},
|
||||
),
|
||||
}),
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '100px' }}>
|
||||
@@ -52,18 +52,18 @@ export const WithNumberChart: Story = {
|
||||
|
||||
export const WithGaugeChart: Story = {
|
||||
args: {
|
||||
widget: createDefaultGraphWidget(
|
||||
'widget-1',
|
||||
'tab-overview',
|
||||
'Conversion Rate',
|
||||
GraphType.GAUGE,
|
||||
{
|
||||
widget: createDefaultGraphWidget({
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-overview',
|
||||
title: 'Conversion Rate',
|
||||
graphType: GraphType.GAUGE,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 5,
|
||||
columnSpan: 3,
|
||||
},
|
||||
),
|
||||
}),
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '400px' }}>
|
||||
@@ -74,18 +74,18 @@ export const WithGaugeChart: Story = {
|
||||
|
||||
export const WithPieChart: Story = {
|
||||
args: {
|
||||
widget: createDefaultGraphWidget(
|
||||
'widget-1',
|
||||
'tab-overview',
|
||||
'Lead Distribution',
|
||||
GraphType.PIE,
|
||||
{
|
||||
widget: createDefaultGraphWidget({
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-overview',
|
||||
title: 'Lead Distribution',
|
||||
graphType: GraphType.PIE,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 5,
|
||||
columnSpan: 3,
|
||||
},
|
||||
),
|
||||
}),
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '500px' }}>
|
||||
@@ -96,18 +96,18 @@ export const WithPieChart: Story = {
|
||||
|
||||
export const SmallWidget: Story = {
|
||||
args: {
|
||||
widget: createDefaultGraphWidget(
|
||||
'widget-1',
|
||||
'tab-overview',
|
||||
'Small Widget (2x2 grid)',
|
||||
GraphType.NUMBER,
|
||||
{
|
||||
widget: createDefaultGraphWidget({
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-overview',
|
||||
title: 'Small Widget (2x2 grid)',
|
||||
graphType: GraphType.NUMBER,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 2,
|
||||
columnSpan: 2,
|
||||
},
|
||||
),
|
||||
}),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
@@ -125,18 +125,18 @@ export const SmallWidget: Story = {
|
||||
|
||||
export const MediumWidget: Story = {
|
||||
args: {
|
||||
widget: createDefaultGraphWidget(
|
||||
'widget-1',
|
||||
'tab-overview',
|
||||
'Medium Widget (4x3 grid)',
|
||||
GraphType.GAUGE,
|
||||
{
|
||||
widget: createDefaultGraphWidget({
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-overview',
|
||||
title: 'Medium Widget (4x3 grid)',
|
||||
graphType: GraphType.GAUGE,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 3,
|
||||
columnSpan: 4,
|
||||
},
|
||||
),
|
||||
}),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
@@ -154,18 +154,18 @@ export const MediumWidget: Story = {
|
||||
|
||||
export const LargeWidget: Story = {
|
||||
args: {
|
||||
widget: createDefaultGraphWidget(
|
||||
'widget-1',
|
||||
'tab-overview',
|
||||
'Large Widget (6x4 grid)',
|
||||
GraphType.PIE,
|
||||
{
|
||||
widget: createDefaultGraphWidget({
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-overview',
|
||||
title: 'Large Widget (6x4 grid)',
|
||||
graphType: GraphType.PIE,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 4,
|
||||
columnSpan: 6,
|
||||
},
|
||||
),
|
||||
}),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
@@ -183,18 +183,18 @@ export const LargeWidget: Story = {
|
||||
|
||||
export const WideWidget: Story = {
|
||||
args: {
|
||||
widget: createDefaultGraphWidget(
|
||||
'widget-1',
|
||||
'tab-overview',
|
||||
'Wide Widget (8x2 grid)',
|
||||
GraphType.NUMBER,
|
||||
{
|
||||
widget: createDefaultGraphWidget({
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-overview',
|
||||
title: 'Wide Widget (8x2 grid)',
|
||||
graphType: GraphType.NUMBER,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 2,
|
||||
columnSpan: 8,
|
||||
},
|
||||
),
|
||||
}),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
@@ -212,18 +212,18 @@ export const WideWidget: Story = {
|
||||
|
||||
export const TallWidget: Story = {
|
||||
args: {
|
||||
widget: createDefaultGraphWidget(
|
||||
'widget-1',
|
||||
'tab-overview',
|
||||
'Tall Widget (3x6 grid)',
|
||||
GraphType.GAUGE,
|
||||
{
|
||||
widget: createDefaultGraphWidget({
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-overview',
|
||||
title: 'Tall Widget (3x6 grid)',
|
||||
graphType: GraphType.GAUGE,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 6,
|
||||
columnSpan: 3,
|
||||
},
|
||||
),
|
||||
}),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { workflowRunIteratorSubStepIterationIndexComponentState } from '@/command-menu/pages/workflow/step/view-run/states/workflowRunIteratorSubStepIterationIndexComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { getIsDescendantOfIterator } from '@/workflow/workflow-steps/utils/getIsDescendantOfIterator';
|
||||
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
|
||||
import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
|
||||
@@ -115,7 +115,7 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
disabled
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowRunStepInfo } from '@/workflow/workflow-steps/hooks/useWorkflowRunStepInfo';
|
||||
import { getWorkflowRunStepInfoToDisplayAsOutput } from '@/workflow/workflow-steps/utils/getWorkflowRunStepInfoToDisplayAsOutput';
|
||||
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
|
||||
@@ -83,7 +83,7 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
disabled
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { useAiAgentOutputSchema } from '@/ai/hooks/useAiAgentOutputSchema';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { type WorkflowAiAgentAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { type BaseOutputSchemaDeprecated } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2';
|
||||
@@ -99,7 +99,7 @@ export const WorkflowEditActionAiAgent = ({
|
||||
<RightDrawerSkeletonLoader />
|
||||
) : (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
||||
@@ -7,7 +8,6 @@ import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUp
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { type WorkflowCodeAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/setNestedValue';
|
||||
|
||||
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
|
||||
@@ -414,7 +414,7 @@ export const WorkflowEditActionServerlessFunction = ({
|
||||
WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID
|
||||
}
|
||||
/>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
updateAction({ name: newName });
|
||||
}}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { type WorkflowCodeAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
|
||||
import { INDEX_FILE_NAME } from '@/serverless-functions/constants/IndexFileName';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
@@ -71,7 +71,7 @@ export const WorkflowReadonlyActionServerlessFunction = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
@@ -9,7 +10,6 @@ import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOr
|
||||
import { type WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
@@ -203,7 +203,7 @@ export const WorkflowEditActionCreateRecord = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { type WorkflowDeleteRecordAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
@@ -123,7 +123,7 @@ export const WorkflowEditActionDeleteRecord = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { type WorkflowEmptyAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
@@ -30,7 +30,7 @@ export const WorkflowEditActionEmpty = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -2,6 +2,7 @@ import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope';
|
||||
import { MICROSOFT_SEND_SCOPE } from '@/accounts/constants/MicrosoftSendScope';
|
||||
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
@@ -13,7 +14,6 @@ import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/wo
|
||||
import { type WorkflowSendEmailAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { WorkflowSendEmailBody } from '@/workflow/workflow-steps/workflow-actions/email-action/components/WorkflowSendEmailBody';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
@@ -225,7 +225,7 @@ export const WorkflowEditActionSendEmail = ({
|
||||
return (
|
||||
!loading && (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -3,6 +3,7 @@ import { Select } from '@/ui/input/components/Select';
|
||||
import { type WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
|
||||
@@ -11,7 +12,6 @@ import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/Gene
|
||||
import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
@@ -159,7 +159,7 @@ export const WorkflowEditActionUpdateRecord = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { type WorkflowFilterAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { WorkflowEditActionFilterBody } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody';
|
||||
import { WorkflowEditActionFilterBodyEffect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBodyEffect';
|
||||
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
|
||||
@@ -39,7 +39,7 @@ export const WorkflowEditActionFilter = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { type WorkflowFindRecordsAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
@@ -135,7 +135,7 @@ export const WorkflowEditActionFindRecords = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
|
||||
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
|
||||
import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer';
|
||||
@@ -8,7 +9,6 @@ import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableLi
|
||||
import { type WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { WorkflowEditActionFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings';
|
||||
import { WorkflowFormEmptyMessage } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormEmptyMessage';
|
||||
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||
@@ -223,7 +223,7 @@ export const WorkflowEditActionFormBuilder = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -1,4 +1,5 @@
|
||||
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
|
||||
@@ -7,7 +8,6 @@ import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDraw
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { type WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useUpdateWorkflowRunStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowRunStep';
|
||||
import { WorkflowFormFieldInput } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowFormFieldInput';
|
||||
import { useSubmitFormStep } from '@/workflow/workflow-steps/workflow-actions/form-action/hooks/useSubmitFormStep';
|
||||
@@ -106,7 +106,7 @@ export const WorkflowEditActionFormFiller = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { FormRawJsonFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRawJsonFieldInput';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { type WorkflowHttpRequestAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
|
||||
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
|
||||
@@ -140,7 +140,7 @@ export const WorkflowEditActionHttpRequest = ({
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={WORKFLOW_HTTP_REQUEST_TAB_LIST_COMPONENT_ID}
|
||||
/>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ export const Configured: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const header = await canvas.findByTestId('workflow-step-header');
|
||||
const header = await canvas.findByTestId('side-panel-header');
|
||||
const headerCanvas = within(header);
|
||||
expect(await headerCanvas.findByText('API Call')).toBeVisible();
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { type WorkflowIteratorAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -80,7 +80,7 @@ export const WorkflowEditActionIterator = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={handleTitleChange}
|
||||
Icon={getIcon(headerIcon || 'IconRepeat')}
|
||||
iconColor={headerIconColor}
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { type WorkflowCronTrigger } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { CronExpressionHelperLazy } from '@/workflow/workflow-trigger/components/CronExpressionHelperLazy';
|
||||
import { CRON_TRIGGER_INTERVAL_OPTIONS } from '@/workflow/workflow-trigger/constants/CronTriggerIntervalOptions';
|
||||
import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings';
|
||||
@@ -64,7 +64,7 @@ export const WorkflowEditTriggerCronForm = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { type FieldMultiSelectValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
@@ -14,7 +15,6 @@ import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpd
|
||||
import { type WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
|
||||
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
@@ -172,7 +172,7 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
@@ -5,7 +6,6 @@ import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { type WorkflowManualTrigger } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { MANUAL_TRIGGER_AVAILABILITY_TYPE_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerAvailabilityTypeOptions';
|
||||
import { MANUAL_TRIGGER_IS_PINNED_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerIsPinnedOptions';
|
||||
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
|
||||
@@ -86,7 +86,7 @@ export const WorkflowEditTriggerManual = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
type WorkflowManualTriggerAvailability,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerAvailabilityOptions';
|
||||
import { MANUAL_TRIGGER_IS_PINNED_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerIsPinnedOptions';
|
||||
import { getManualTriggerDefaultSettingsDeprecated } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettingsDeprecated';
|
||||
@@ -92,7 +92,7 @@ export const WorkflowEditTriggerManualDeprecated = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
+2
-2
@@ -1,4 +1,5 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { FormRawJsonFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRawJsonFieldInput';
|
||||
import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctionOutputSchema';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
@@ -9,7 +10,6 @@ import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/wo
|
||||
import { type WorkflowWebhookTrigger } from '@/workflow/types/Workflow';
|
||||
import { parseAndValidateVariableFriendlyStringifiedJson } from '@/workflow/utils/parseAndValidateVariableFriendlyStringifiedJson';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { WEBHOOK_TRIGGER_AUTHENTICATION_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerAuthenticationOptions';
|
||||
import { WEBHOOK_TRIGGER_HTTP_METHOD_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerHttpMethodOptions';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
@@ -84,7 +84,7 @@ export const WorkflowEditTriggerWebhookForm = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { filterBySearchQuery } from '../filterBySearchQuery';
|
||||
|
||||
describe('filterBySearchQuery', () => {
|
||||
type TestUser = {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
const users: TestUser[] = [
|
||||
{
|
||||
id: '1',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john@example.com',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
email: 'jane@example.com',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
firstName: 'José',
|
||||
lastName: 'García',
|
||||
email: 'jose@example.com',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
firstName: 'François',
|
||||
lastName: 'Müller',
|
||||
email: 'francois@example.com',
|
||||
},
|
||||
];
|
||||
|
||||
it('should return all items when search query is empty', () => {
|
||||
const result = filterBySearchQuery({
|
||||
items: users,
|
||||
searchQuery: '',
|
||||
getSearchableValues: (user) => [user.firstName, user.lastName],
|
||||
});
|
||||
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
|
||||
it('should return empty array when search query is only whitespace', () => {
|
||||
const result = filterBySearchQuery({
|
||||
items: users,
|
||||
searchQuery: ' ',
|
||||
getSearchableValues: (user) => [user.firstName, user.lastName],
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle partial matches', () => {
|
||||
const result = filterBySearchQuery({
|
||||
items: users,
|
||||
searchQuery: 'jan',
|
||||
getSearchableValues: (user) => [user.firstName],
|
||||
});
|
||||
|
||||
expect(result).toEqual([users[1]]);
|
||||
});
|
||||
|
||||
it('should normalize accents when searching', () => {
|
||||
const result = filterBySearchQuery({
|
||||
items: users,
|
||||
searchQuery: 'jose',
|
||||
getSearchableValues: (user) => [user.firstName],
|
||||
});
|
||||
|
||||
expect(result).toEqual([users[2]]);
|
||||
});
|
||||
|
||||
it('should normalize special characters when searching', () => {
|
||||
const result = filterBySearchQuery({
|
||||
items: users,
|
||||
searchQuery: 'muller',
|
||||
getSearchableValues: (user) => [user.lastName],
|
||||
});
|
||||
|
||||
expect(result).toEqual([users[3]]);
|
||||
});
|
||||
|
||||
it('should handle search with accents matching normalized text', () => {
|
||||
const result = filterBySearchQuery({
|
||||
items: users,
|
||||
searchQuery: 'José',
|
||||
getSearchableValues: (user) => [user.firstName],
|
||||
});
|
||||
|
||||
expect(result).toEqual([users[2]]);
|
||||
});
|
||||
|
||||
it('should return empty array when no matches found', () => {
|
||||
const result = filterBySearchQuery({
|
||||
items: users,
|
||||
searchQuery: 'nonexistent',
|
||||
getSearchableValues: (user) => [user.firstName, user.lastName],
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle empty items array', () => {
|
||||
const result = filterBySearchQuery({
|
||||
items: [],
|
||||
searchQuery: 'test',
|
||||
getSearchableValues: (user: TestUser) => [user.firstName],
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should be case-insensitive for both query and values', () => {
|
||||
const result = filterBySearchQuery({
|
||||
items: users,
|
||||
searchQuery: 'JOHN',
|
||||
getSearchableValues: (user) => [user.firstName.toUpperCase()],
|
||||
});
|
||||
|
||||
expect(result).toEqual([users[0]]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { normalizeSearchText } from './normalizeSearchText';
|
||||
|
||||
type FilterBySearchQueryParams<T> = {
|
||||
items: T[];
|
||||
searchQuery: string;
|
||||
getSearchableValues: (item: T) => string[];
|
||||
};
|
||||
|
||||
export const filterBySearchQuery = <T>({
|
||||
items,
|
||||
searchQuery,
|
||||
getSearchableValues,
|
||||
}: FilterBySearchQueryParams<T>): T[] => {
|
||||
if (!isNonEmptyString(searchQuery)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const normalizedQuery = normalizeSearchText(searchQuery);
|
||||
|
||||
return items.filter((item) => {
|
||||
const searchableValues = getSearchableValues(item);
|
||||
return searchableValues.some((value) =>
|
||||
normalizeSearchText(value).includes(normalizedQuery),
|
||||
);
|
||||
});
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { AggregateOperations } from './aggregate-operations.constant';
|
||||
|
||||
export enum ExtendedAggregateOperations {
|
||||
MIN = AggregateOperations.MIN,
|
||||
MAX = AggregateOperations.MAX,
|
||||
AVG = AggregateOperations.AVG,
|
||||
SUM = AggregateOperations.SUM,
|
||||
COUNT = AggregateOperations.COUNT,
|
||||
COUNT_UNIQUE_VALUES = AggregateOperations.COUNT_UNIQUE_VALUES,
|
||||
COUNT_EMPTY = AggregateOperations.COUNT_EMPTY,
|
||||
COUNT_NOT_EMPTY = AggregateOperations.COUNT_NOT_EMPTY,
|
||||
COUNT_TRUE = AggregateOperations.COUNT_TRUE,
|
||||
COUNT_FALSE = AggregateOperations.COUNT_FALSE,
|
||||
PERCENTAGE_EMPTY = AggregateOperations.PERCENTAGE_EMPTY,
|
||||
PERCENTAGE_NOT_EMPTY = AggregateOperations.PERCENTAGE_NOT_EMPTY,
|
||||
EARLIEST = 'EARLIEST',
|
||||
LATEST = 'LATEST',
|
||||
}
|
||||
|
||||
registerEnumType(ExtendedAggregateOperations, {
|
||||
name: 'ExtendedAggregateOperations',
|
||||
});
|
||||
+15
-14
@@ -14,10 +14,11 @@ import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum';
|
||||
import { ExtendedAggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/extended-aggregate-operations.constant';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { AxisNameDisplay } from 'src/engine/core-modules/page-layout/enums/axis-name-display.enum';
|
||||
import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum';
|
||||
import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
|
||||
@ObjectType('BarChartConfiguration')
|
||||
export class BarChartConfigurationDTO {
|
||||
@@ -31,10 +32,10 @@ export class BarChartConfigurationDTO {
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
@Field(() => ExtendedAggregateOperations)
|
||||
@IsEnum(ExtendedAggregateOperations)
|
||||
@IsNotEmpty()
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@@ -61,15 +62,15 @@ export class BarChartConfigurationDTO {
|
||||
@IsOptional()
|
||||
omitNullValues?: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
xAxisName?: string;
|
||||
@Field(() => AxisNameDisplay)
|
||||
@IsEnum(AxisNameDisplay)
|
||||
@IsNotEmpty()
|
||||
axisNameDisplay: AxisNameDisplay;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
yAxisName?: string;
|
||||
@Field(() => Boolean)
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
displayDataLabel: boolean;
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
@IsNumber()
|
||||
|
||||
+13
-13
@@ -1,6 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsObject,
|
||||
@@ -12,9 +13,9 @@ import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
import { ExtendedAggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/extended-aggregate-operations.constant';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
|
||||
@ObjectType('GaugeChartConfiguration')
|
||||
export class GaugeChartConfigurationDTO {
|
||||
@@ -28,20 +29,19 @@ export class GaugeChartConfigurationDTO {
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
@Field(() => ExtendedAggregateOperations)
|
||||
@IsEnum(ExtendedAggregateOperations)
|
||||
@IsNotEmpty()
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
@IsNotEmpty()
|
||||
aggregateOperationTotal: AggregateOperations;
|
||||
@Field(() => Boolean)
|
||||
@IsBoolean()
|
||||
displayDataLabel: boolean;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataIdTotal: string;
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
color?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
|
||||
+13
-12
@@ -14,8 +14,9 @@ import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { ExtendedAggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/extended-aggregate-operations.constant';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { AxisNameDisplay } from 'src/engine/core-modules/page-layout/enums/axis-name-display.enum';
|
||||
import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum';
|
||||
import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
|
||||
@@ -31,10 +32,10 @@ export class LineChartConfigurationDTO {
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
@Field(() => ExtendedAggregateOperations)
|
||||
@IsEnum(ExtendedAggregateOperations)
|
||||
@IsNotEmpty()
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@@ -61,15 +62,15 @@ export class LineChartConfigurationDTO {
|
||||
@IsOptional()
|
||||
omitNullValues?: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
xAxisName?: string;
|
||||
@Field(() => AxisNameDisplay)
|
||||
@IsEnum(AxisNameDisplay)
|
||||
@IsNotEmpty()
|
||||
axisNameDisplay: AxisNameDisplay;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
yAxisName?: string;
|
||||
@Field(() => Boolean)
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
displayDataLabel: boolean;
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
@IsNumber()
|
||||
|
||||
+11
-5
@@ -1,6 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsObject,
|
||||
@@ -12,9 +13,9 @@ import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
import { ExtendedAggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/extended-aggregate-operations.constant';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
|
||||
@ObjectType('NumberChartConfiguration')
|
||||
export class NumberChartConfigurationDTO {
|
||||
@@ -28,16 +29,21 @@ export class NumberChartConfigurationDTO {
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
@Field(() => ExtendedAggregateOperations)
|
||||
@IsEnum(ExtendedAggregateOperations)
|
||||
@IsNotEmpty()
|
||||
aggregateOperation: AggregateOperations;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
label?: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
displayDataLabel: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user