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:
Raphaël Bosi
2025-10-03 10:25:45 +02:00
committed by GitHub
parent fc8b4f813c
commit d10ab5f67c
112 changed files with 3046 additions and 503 deletions
@@ -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'];
+57 -17
View File
@@ -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) {
@@ -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}
@@ -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();
},
};
@@ -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, {
@@ -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>
);
};
@@ -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>
);
};
@@ -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} />
</>
);
};
@@ -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}
/>
@@ -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>
);
};
@@ -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>
</>
);
};
@@ -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>
</>
);
};
@@ -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>
</>
);
};
@@ -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>
</>
);
};
@@ -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>
</>
);
};
@@ -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>
</>
);
};
@@ -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>
</>
);
};
@@ -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>
</>
);
};
@@ -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>
</>
);
};
@@ -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,
},
];
@@ -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],
},
];
@@ -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],
},
];
@@ -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,
},
};
@@ -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;
@@ -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],
},
];
@@ -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],
},
];
@@ -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],
},
];
@@ -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,
},
];
@@ -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,
};
@@ -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`,
};
@@ -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,
};
@@ -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,
};
@@ -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],
};
@@ -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],
};
@@ -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],
};
@@ -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,
};
@@ -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,
};
@@ -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],
};
@@ -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],
};
@@ -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,
};
@@ -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,
};
@@ -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,
};
};
@@ -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,
};
};
@@ -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,
};
};
@@ -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 };
};
@@ -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 };
};
@@ -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;
@@ -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;
@@ -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[];
};
@@ -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);
}
};
@@ -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>;
}
};
@@ -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];
};
@@ -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:
@@ -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,
@@ -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
}
@@ -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');
});
});
@@ -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(),
@@ -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],
);
@@ -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,
@@ -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',
@@ -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',
@@ -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',
});
@@ -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,
@@ -31,10 +31,7 @@ export const extractFieldMetadataIdsFromWidget = (
return [config.aggregateFieldMetadataId];
case 'GaugeChartConfiguration':
return [
config.aggregateFieldMetadataId,
config.aggregateFieldMetadataIdTotal,
];
return [config.aggregateFieldMetadataId];
case 'IframeConfiguration':
return [];
@@ -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: {
@@ -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}
@@ -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}
@@ -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;
@@ -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 });
}}
@@ -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}
@@ -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;
@@ -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;
@@ -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,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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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}
@@ -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;
@@ -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();
@@ -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}
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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),
);
});
};
@@ -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',
});
@@ -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()
@@ -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()
@@ -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()
@@ -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